
简介Python实现的MADDPG多智能体博弈对抗算法工程包面向机器学习、强化学习方向的在校学生、科研工作者及算法爱好者用于多智能体协作与对抗场景的算法复现、实验对比与二次开发。压缩包共91个文件以76个Python脚本为主体涵盖MADDPG主程序、DDPG基线、神经网络结构、经验回放缓冲及强化学习常用工具函数等核心模块另含10个gif实验可视化、ipynb交互式示例、环境配置文件、README说明及项目文档整体仅3.26MB轻量便携。资源已有92人学习浏览代码经严格测试可稳定运行。内容除算法源码外还内置ma-gym多智能体实验环境覆盖捕食者-猎物、战斗对抗、交通路口等典型博弈任务并配有实验结果动图与测试脚本便于对照观察智能体策略演化过程。读者可基于现有框架快速掌握MADDPG参数更新与训练流程也能扩展改进用于课程设计、毕业设计或初期科研探索是兼顾入门学习与进阶实验的高质量参考。1. 从单智能体到MADDPG多智能体博弈对抗的关键一跳MADDPGMulti-Agent Deep Deterministic Policy Gradient解决的并不是“多个智能体各自跑DDPG”这个简单问题而是多智能体环境下训练不稳定的非平稳性难题。单智能体强化学习里环境转移只由自己的动作决定多智能体博弈里每个智能体的奖励和下一个状态都受其他智能体动作影响环境对单个智能体而言变成了“移动靶”。MADDPG用集中训练、分布式执行CTDECentralized Training with Decentralized Execution的思路在训练时让每个Actor看到全局观测和所有智能体动作执行时只依赖自身观测从而在博弈对抗场景中获得稳定梯度。这个Python源码包把MADDPG核心算法、完整的ma-gym环境库和实验脚本打包在一起适合做多智能体对抗研究的复现起点也能直接作为课程设计和论文实验的基座。2. 源码逐模块拆解从经验回放到梯度更新的完整链路这个项目源码的核心文件不多buffer.py、network.py、DDPG.py、MADDPG.py、mag.py、main.py构成了一条清晰的数据流环境产生交互样本 → 存入经验回放 → 采样更新Critic → 用策略梯度更新Actor。下面按数据流动的顺序拆解每个文件。2.1 buffer.py多智能体经验回放的两种实现经验回放Experience Replay在MADDPG里不只是存状态转换还要把每个智能体的动作、奖励一起存下来。这个项目的buffer.py同时提供了两个回放类分别对应不同交互模式。# buffer.py 核心逻辑结构还原 import numpy as np from collections import deque import random class ReplayBuffer: 单智能体经验回放兼容DDPG使用 def __init__(self, capacity): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) state, action, reward, next_state, done map(np.stack, zip(*batch)) return state, action, reward, next_state, done def __len__(self): return len(self.buffer) class MaReplayBuffer: 多智能体经验回放每个transition包含n_agents份obs/action/reward def __init__(self, capacity, n_agents): self.capacity capacity self.n_agents n_agents self.buffer deque(maxlencapacity) def push(self, obs_n, action_n, reward_n, obs_next_n, done_n): # obs_n: list of arrays, 每个元素是单个智能体的观测 self.buffer.append((obs_n, action_n, reward_n, obs_next_n, done_n)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) # 分别按智能体维度重组返回 (n_agents, batch_size, obs_dim) 形状 obs_n [[] for _ in range(self.n_agents)] act_n [[] for _ in range(self.n_agents)] rew_n [[] for _ in range(self.n_agents)] obs_next_n [[] for _ in range(self.n_agents)] done_n [[] for _ in range(self.n_agents)] for transition in batch: for i in range(self.n_agents): obs_n[i].append(transition[0][i]) act_n[i].append(transition[1][i]) rew_n[i].append(transition[2][i]) obs_next_n[i].append(transition[3][i]) done_n[i].append(transition[4][i]) # 每个智能体单独stack成ndarray obs_n [np.stack(o) for o in obs_n] act_n [np.stack(a) for a in act_n] rew_n [np.stack(r) for r in rew_n] obs_next_n [np.stack(o) for o in obs_next_n] done_n [np.stack(d) for d in done_n] return obs_n, act_n, rew_n, obs_next_n, done_nMaReplayBuffer.sample()返回的是按智能体拆分的列表而不是拼接成一个大矩阵。这样设计的原因是MADDPG更新时每个智能体的Critic输入不同——有的Critic要拼接所有智能体的动作有的只拼相邻智能体按智能体维度预先拆好能避免在损失函数里反复做切片索引。capacity参数控制经验容量我一般设1e6量级但注意多智能体场景下每条经验包含n_agents份数据内存占用要按倍数预估。如果训练时发现采样到的transition里done全部为False说明回放池还没存满此时梯度方向偏差会比较大。2.2 network.pyActor-Critic的网络结构与激活函数细节network.py里定义了Actor和Critic两个网络类核心是用Relu激活函数的全连接网络输出层用了不同处理。Actor输出层接tanh将动作限制在[-1, 1]Critic则直接输出实数价值。# network.py 核心结构结构还原 import torch import torch.nn as nn import torch.nn.functional as F class Relu(nn.Module): 带inplace优化的ReLU封装加速前向传播 def __init__(self, inplaceTrue): super().__init__() self.inplace inplace def forward(self, x): return F.relu(x, inplaceself.inplace) class MLP(nn.Module): 多层感知机基类隐藏层统一用ReLU输出层可配置 def __init__(self, input_dim, hidden_dim, output_dim, hidden_layers2): super().__init__() layers [] dims [input_dim] [hidden_dim] * hidden_layers [output_dim] for i in range(len(dims) - 1): layers.append(nn.Linear(dims[i], dims[i 1])) if i len(dims) - 2: layers.append(Relu()) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x) class DDPGActor(nn.Module): DDPG/MADDPG共享的Actor输出连续动作tanh压缩到[-1,1] def __init__(self, obs_dim, action_dim, hidden_dim64): super().__init__() self.mlp MLP(obs_dim, hidden_dim, action_dim) def forward(self, obs): return torch.tanh(self.mlp(obs)) class DDPGCritic(nn.Module): DDPG/MADDPG共享的CriticQ(s,a) def __init__(self, obs_dim, action_dim, hidden_dim64): super().__init__() self.mlp MLP(obs_dim action_dim, hidden_dim, 1) def forward(self, obs, action): return self.mlp(torch.cat([obs, action], dim-1))注意Relu(inplaceTrue)这个细节。inplace操作会直接修改输入张量减少显存分配频率。多智能体场景下每个Actor带一个目标网络一个Critic带一个目标网络网络数量是3 * n_agents显存压力比单智能体大得多这个inplaceTrue能省5%到10%的显存占用。torch.tanh把Actor输出限制在[-1, 1]环境侧拿到动作后要自己映射到真实动作范围比如traffic_junction里速度是[0,1]连续值就要做(a 1) / 2的线性缩放。2.3 DDPG.py与MADDPG.py单智能体基座到多智能体扩展这个项目的层次设计很实用先实现了标准DDPG类MADDPG类在DDPG基础上扩展出多智能体版本。两者共享network.py中的Actor和Critic结构区别在于Critic的输入组成和梯度更新时是否需要全局信息。# MADDPG.py 核心更新逻辑结构还原 class MADDPG: def __init__(self, n_agents, obs_dims, action_dims, gamma0.95, tau0.01, lr_actor1e-4, lr_critic1e-3): self.n_agents n_agents self.gamma gamma self.tau tau # 每个智能体独立维护 actor/actor_target/critic/critic_target self.actors [DDPGActor(obs_dims[i], action_dims[i]) for i in range(n_agents)] self.critics [DDPGCritic(sum(obs_dims) sum(action_dims), 1) for i in range(n_agents)] self.actor_targets [DDPGActor(obs_dims[i], action_dims[i]) for i in range(n_agents)] self.critic_targets [DDPGCritic(sum(obs_dims) sum(action_dims), 1) for i in range(n_agents)] def update(self, sample, i_agent): obs_n, act_n, rew_n, obs_next_n, done_n sample # 当前Q值第i个Critic输入所有智能体的观测动作 obs_all torch.cat(obs_n, dim-1) act_all torch.cat(act_n, dim-1) q_current self.critics[i_agent](obs_all, act_all) # 目标Q值用目标Actor输出的下一时刻动作 with torch.no_grad(): act_next_n [self.actor_targets[i](obs_next_n[i]) for i in range(self.n_agents)] act_next_all torch.cat(act_next_n, dim-1) q_next self.critics[i_agent](torch.cat(obs_next_n, dim-1), act_next_all) q_target rew_n[i_agent].unsqueeze(-1) self.gamma * (1 - done_n[i_agent].unsqueeze(-1)) * q_next # 更新CriticMSE损失 critic_loss F.mse_loss(q_current, q_target) self.critic_optimizers[i_agent].zero_grad() critic_loss.backward() self.critic_optimizers[i_agent].step() # 更新Actor最大化Q值 act_n[i_agent] self.actors[i_agent](obs_n[i_agent]) act_all_new torch.cat(act_n, dim-1) actor_loss -self.critics[i_agent](torch.cat(obs_n, dim-1), act_all_new).mean() self.actor_optimizers[i_agent].zero_grad() actor_loss.backward() self.actor_optimizers[i_agent].step() # 软更新目标网络 for target, source in zip(self.actor_targets, self.actors): for tp, sp in zip(target.parameters(), source.parameters()): tp.data.copy_(self.tau * sp.data (1 - self.tau) * tp.data) # critic_target同理这里最关键的代码是把obs_n用torch.cat(obs_n, dim-1)拼成整体输入Critic。每个智能体有独立的Critic但所有Critic看到的全局观测和全局动作是一样的——这就是集中训练。执行时Actor只接受obs_n[i]不接收其他智能体的信息这就是分布式执行。实现上的坑在于done_n在ma-gym里是一个列表每个元素是标量更新时要先unsqueeze(-1)对齐q_next的形状否则会触发广播错误。tau设为0.01时目标网络更新较慢适合博弈环境如果环境是非平稳性极强比如对手策略频繁切换可以调大到0.05但可能出现目标Q值抖动。3. ma-gym环境库predator_prey与traffic_junction实验配置只跑算法不换环境很难验证MADDPG的泛化能力。这个源码包内置了完整的ma-gym环境库覆盖竞争、合作、混合博弈三类场景。下面说环境怎么装、接口怎么用、哪些参数影响训练。3.1 环境安装与接口约定ma-gym是OpenAI Gym风格的自定义环境接口和Gym保持一致但step()返回的观测是一个列表每个元素对应一个智能体。# 本地安装ma-gym cd ma-gym-master pip install -e .安装完成后所有环境的创建方式统一为gym.make(ma_gym:PredictPrey-v0)。注意环境ID要带ma_gym:前缀否则Gym注册表找不到。交互时每个智能体的动作是离散索引MADDPG输出连续动作后要通过概率采样或argmax转成离散动作。源码包里的interactive_agent.py和random_agent.py就是两个现成的交互示例前者通过键盘控制单个智能体后者让所有智能体随机动作可以用它们快速验证环境是否安装成功。3.2 predator_prey捕食者-猎物环境的核心参数predator_prey是MADDPG论文里最常用的验证环境这个项目里叫PredictPrey-v0。环境里多个捕食者协作捕捉一个或多个猎物捕食者共享成功捕捉的奖励属于完全合作博弈。# 查看环境参数 python -c import gym; import ma_gym; env gym.make(ma_gym:PredictPrey-v0); print(env.observation_space, env.action_space) # 输出示例 # Tuple(Tuple(Discrete(13), Discrete(13), Discrete(2)), ...) # 每个智能体观测为(网格x, 网格y, 是否携带) # Tuple(Discrete(5), ...) # 动作上/下/左/右/静止环境默认grid_shape10x10捕食者数量可以通过n_agents参数调整猎物数量由n_preys控制。观测是离散的网格坐标MADDPG的Critic要求输入观测维度固定如果一个捕食者已经捕捉到猎物携带状态观测维度会变化常见做法是在外层包一层FlattenObservation自定义wrapper把变长观测pad到固定长度。这个坑在switch和checkers环境里同样存在建议写环境适配层时统一处理。3.3 combat与traffic_junction混合博弈与竞争环境的差异combat是两个阵营的多智能体对抗智能体属于不同team团队内部协作、团队之间竞争适合观察MADDPG在混合博弈中是否出现策略振荡。traffic_junction是交通调度场景多辆车在十字路口协同通过车辆之间不能通信只能通过观测相互位置来避免碰撞属于纯合作但强约束场景。实际用下来traffic_junction比predator_prey更难收敛因为稀疏奖励只有通过路口才有正奖励加上车辆数量增多导致联合动作空间爆炸。在这个环境里建议把Critic的隐藏层维度从64提高到128并且减小batch_size到128以下否则Critic对联合Q值的估计方差会相当大。lumberjacks和pong_duel则更适合观察竞争博弈pong_duel是一个简化版乒乓球对抗两个智能体各自控制球拍MADDPG在这个环境里能比较快地学到“压制对方回球角度”的策略。4. 实验复现与调参让MADDPG在默认设置下跑出结果拿到源码后第一步是跑通main.py确认训练循环、环境交互、模型更新三个环节没有断裂。这章给出完整的启动方式、核心参数表以及训练曲线失败时的排查路径。4.1 main.py训练脚本的启动方式main.py是统一的训练入口通过命令行参数指定环境名和关键超参数。启动命令如下# 训练predator_prey环境3个智能体训练5000轮 python main.py --env PredictPrey-v0 --n_agents 3 --max_episodes 5000 --batch_size 256 # 训练combat环境4个智能体2v2启用模型保存 python main.py --env Combat-v0 --n_agents 4 --max_episodes 8000 --save_dir ./checkpoints训练循环里每轮(episode)先调用env.reset()拿到初始观测列表然后循环执行step()直到done为True。done在ma-gym里是一个布尔值但多智能体环境中部分智能体可能提前结束比如在combat中被淘汰此时需要自己维护一个alive_mask掩码只让存活的智能体继续执行动作。常见做法是每步都取所有智能体的动作但将死亡智能体的动作固定为no-op通常动作索引0奖励设为0。启动前先检查Python版本兼容性。这个源码包是基于较早的Gym接口写的如果本地是Gym 0.26环境返回的done和info结构变了直接跑会报错。我一般固定用gym0.21和torch1.9确保env.step()返回四元组结构。4.2 核心超参数表与负载均衡策略MADDPG的超参数比DDPG多一层每个智能体有独立的Actor、Critic优化器还要统一管理目标网络更新频率。下表是这套代码里可调的关键参数和推荐区间。参数推荐值调整方向lr_actor1e-4训练震荡时降到1e-5收敛慢可调到3e-4lr_critic1e-3Critic发散就降一般比actor大5到10倍gamma0.95奖励稀疏场景traffic_junction调高到0.99tau0.01对手策略突变时调大到0.05加快目标网络跟随batch_size256联合动作空间大时降到128减少Q值估计方差buffer_capacity1e6内存充足可以开到2e6提高样本多样性noise_std0.1探索不足时调大到0.3训练后期降到0.01还有一个隐蔽参数是每个episode更新模型的次数update_freq。这个项目默认是每步都做一次梯度更新也就是update_freq1。如果环境单episode步数很长比如traffic_junction能跑到200步每步更新会导致样本利用率低我一般改成每5步更新一次训练速度提升明显且最终收敛值差不多。另一个实践是把所有智能体的梯度累积起来统一调用一次optimizer.step()这样可以避免多智能体之间因为各自更新导致的目标网络不同步问题但实现上需要手动管理梯度累计变量。4.3 训练曲线常见失败形态与应对MADDPG训练失败的规律比其他多智能体算法更明显下面三种是我在复现时最常遇到的。第一种是所有智能体奖励都围绕0小幅波动说明探索噪声太大Actor策略一直随机。处理办法是先固定noise_std0.3跑200个episode等Critic的Q值有区分度后再降噪或者在main.py里加一个线性衰减scheduler。第二种是部分智能体奖励上升另一个智能体奖励急剧下降这是典型的策略振荡常见于combat环境。缓解办法是把tau调小到0.005并且降低失败一方的lr_actor让优势方不要更新太快。第三种是Critic loss不收敛且在某个值附近周期性抖动通常是目标网络软更新太慢追赶不上当前网络的Q值变化。这时检查tau是否过小或者buffer_capacity是否太小导致经验分布不均匀。调试时不要只看reward曲线我习惯同时打印每个智能体的actor_loss和critic_loss。如果actor_loss持续降低但reward不变说明Critic对动作的梯度方向失效了需要检查Critic输入是否拼接正确。如果critic_loss一直不降比如始终在0.5以上大概率是target Q计算里done的处理有误导致目标值反复跳动。5. 验证算法收敛的三种手段与MADDPG的边界模型训完不能只看训练奖励曲线还要在固定随机种子下做多次回放验证并对比随机策略的基线水平。最后这部分给出实际可用的验证技巧以及MADDPG在哪些场景下会失效。# 用test_env.py思路做一次固定种子的策略回放验证 import gym import ma_gym import torch from MADDPG import MADDPG def evaluate(env_name, maddpg, n_episodes10, seed42): env gym.make(env_name) total_rewards [0.0 for _ in range(maddpg.n_agents)] for ep in range(n_episodes): obs_n env.reset() if seed is not None: env.seed(seed * 100 ep) # 每个episode不同但可复现 done False while not done: actions [] with torch.no_grad(): for i, obs in enumerate(obs_n): obs_t torch.FloatTensor(obs).unsqueeze(0) action maddpg.actors[i](obs_t).squeeze(0).numpy() # 连续动作转离散采样概率分布 action softmax(action / temperature) actions.append(action.argmax()) obs_n, reward_n, done, info env.step(actions) for i in range(maddpg.n_agents): total_rewards[i] reward_n[i] return [r / n_episodes for r in total_rewards]验证时注意把temperature设低比如0.1让动作选择接近确定性策略否则评估结果会被探索噪声污染。多跑几个不同的seed取平均值能剔除环境随机性带来的波动。MADDPG的算法边界要明确它假设每个智能体的Critic在训练时能看到全局观测和动作一旦智能体数量超过10个Critic输入维度会迅速膨胀训练复杂度呈指数增长这时需要改用MAPPO或QMIX这类更适合大规模智能体的算法。另一个边界是动作空间——MADDPG本质是DDPG的多智能体扩展输出连续动作如果任务本身就是离散动作空间直接用DQN的变体或离散化MADDPG可能更高效。最后MADDPG对手博弈的均衡收敛不保证唯一不同随机种子可能收敛到不同纳什均衡评估时至少要跑5次独立训练取中位数不要用单次实验的最好结果下结论。本文还有配套的精品资源点击获取