
简介这份Python实现的MADDPG多智能体博弈对抗算法源码主要面向强化学习与人工智能方向的高校学生、科研工作者及算法开发者尤其适合计算机、自动化、电子信息等相关专业用于多智能体协作、竞争与混合博弈场景的算法研究与实验复现。压缩包共91个文件以76个Python脚本为核心涵盖MADDPG主算法、神经网络结构、经验回放机制、环境交互逻辑与测试模块另有10个GIF动态演示、1个Jupyter Notebook示例、1个配置文件及说明文档整体仅3.26MB下载后可快速部署运行。目前已有92人学习下载。源码附有完整的训练与测试流程可直接运行复现对抗实验结果也便于修改奖励函数或网络结构扩展至追逃博弈、多智能体战斗等自定义场景资源内附带配置文件、说明文档与GIF演示有助于理解环境搭建和复现细节可作为课程设计、毕业设计或项目前期的算法验证基础也适合初学者结合示例逐步理解MADDPG原理。1. MADDPG 多智能体博弈对抗算法为什么它成了多智能体强化学习的默认起点做过多智能体强化学习的人第一次跑通 MADDPG 的感觉大概率是“拍大腿”——原来让一群智能体学会配合或者互坑不需要复杂的通信协议只要在训练阶段把所有人的动作和观测拼在一起喂给 Critic 就够了。这个算法由 OpenAI 在 2017 年提出全称是 Multi-Agent Deep Deterministic Policy Gradient专治“一个智能体在环境里单打独斗其他智能体一动就把环境变成非平稳”的痛点。它适合三类人你在做机器人编队、博弈对抗推演、或者研究多智能体决策的入门级复现这份源码和实验结果就是你判断算法到底有没有用的最快路径。2. 从 DDPG 到 MADDPG集中训练分散执行到底改了谁的策略2.1 单智能体 DDPG 的局限环境一变梯度的根基就歪了DDPG 当年在连续控制任务里表现很好核心思路是让 Actor 网络根据当前状态输出连续动作让 Critic 网络估计该状态下这个动作的 Q 值。训练时Critic 通过时序差分误差去逼近真实的 Q 函数Actor 则沿着 Q 值上升的方向更新自己的策略梯度。这套逻辑建立在“环境的状态转移是稳定的”这一潜在假设上——也就是说下一个时刻的状态只取决于当前状态和当前动作背后的转移概率不会变。但在多智能体环境中这个假设撑不住了。以经典的两个机器人对抗场景为例智能体 A 的策略在更新但智能体 B 也在同步更新于是环境对 A 来说每时每秒都在变。上一轮梯度告诉 A “向左走能得分”但 B 的策略学得快一点下一轮变成“向左走会被撞开”A 的 Q 值估计就被污染了。DDPG 在训练过程中看到的经验回放样本因为环境非平稳不再能代表真实的动态分布这也就是多智能体强化学习最常说的“非平稳性”问题。MADDPG 解决这个问题的关键词就 8 个字集中训练、分散执行。训练时每个智能体的 Critic 可以获得全局信息包括所有智能体的观测和动作执行时Actor 只需要自己的局部观测就能决策。这样 Critic 对 Q 值的估计是站在全局视角做的把其他智能体策略变化带来的“干扰”显式当作输入梯度自然就准了。2.2 Critic 的输入拼接这是 MADDPG 最核心的改动实现层面看MADDPG 对 DDPG 的改动并不大核心在 Critic 网络的输入形状上。单智能体 DDPG 的 Critic 输入是 (state, action) 拼接MADDPG 的 Critic 输入换成 (obs_1, obs_2, ..., obs_n, action_1, action_2, ..., action_n) 的拼接。import torch import torch.nn as nn import torch.nn.functional as F class MADDPGCritic(nn.Module): def __init__(self, num_agents, obs_dim, action_dim, hidden_dim128): super().__init__() self.num_agents num_agents self.obs_dim obs_dim self.action_dim action_dim # 关键输入维度是所有智能体的观测拼接 所有智能体的动作拼接 input_dim num_agents * (obs_dim action_dim) self.fc1 nn.Linear(input_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.q nn.Linear(hidden_dim, 1) def forward(self, all_obs, all_actions): # all_obs 形状: [batch, num_agents, obs_dim] # all_actions 形状: [batch, num_agents, action_dim] batch_size all_obs.size(0) obs_flat all_obs.view(batch_size, -1) action_flat all_actions.view(batch_size, -1) x torch.cat([obs_flat, action_flat], dim1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) q_value self.q(x) return q_value这段代码里有两个细节要注意。第一个是view(batch_size, -1)的用法它把所有智能体的观测沿特征维拉平保证 Critic 看到的输入顺序是固定且一致的第二个是激活函数选择中间层用 ReLU 是通用做法最后输出 Q 值不加激活因为 Q 值可以是任意实数不需要限定在某个区间。如果你的环境里智能体数量固定这个实现直接可用如果智能体数量会变化比如对抗中某一方中途退出你就得考虑用 masking 或者上限智能体数量来固定输入维度。2.3 Actor 与 Critic 的分工决定了博弈对抗的稳定性Actor 网络仍然只输入自己的观测输出动作结构上和单智能体 DDPG 完全一致。区别在于训练时Actor 的梯度来自 Critic 对自己动作的偏导而此时 Critic 已经把其他智能体的动作当作固定输入了所以 Actor 学习到的策略隐含了“在对手当前策略下如何做得最好”这个含义。这比单纯地在一堆历史样本里做回归更贴近博弈对抗的本质因为你的策略要动态应对对手而不是对着一个静态环境练死招。另一个值得注意的设计是目标网络。MADDPG 延续了 DDPG 的 soft update 方式也就是目标网络参数不是定期从在线网络硬拷贝而是每次更新时按比例 τ 向在线网络参数靠近。这个比例一般取 0.01你会发现如果设到 0.1训练初期很容易因为目标参数跟着在线参数剧烈变动而疯狂震荡。原因在于多智能体场景里每个智能体的策略都在变目标网络的平滑作用比单智能体场景更重要。3. 把源码跑起来Python 环境准备与最小训练命令3.1 拿到“源码及实验结果.zip”之后先检查什么解压这个 zip 之后我一般不会急着运行先按目录结构确认里面有没有三样东西算法模块、实验脚本、以及实验结果文件。常见的排列方式是maddpg.py或algorithms/目录放算法主体main.py或train.py是入口脚本results/或logs/目录下放训练日志、reward 曲线图、模型权重文件。优先级从train.py开始看因为入口脚本能告诉你这个项目默认跑哪个环境、训练多少轮、评估频率是多少。有的源码包会把模型参数和 reward 曲线图一起打包但如果里面只有“实验结果.txt”或 CSV 日志你最终还是要自己跑一遍训练才能对比。确认环境依赖之前建议先看一下有没有requirements.txt没有就用模块文件头的 import 列表来推断依赖。3.2 最小训练命令从 Python 安装到第一步训练MADDPG 项目最常见的复现环境是 Python 3.8 及以上配 PyTorch 和 OpenAI 的 Multi-Agent Particle Environment也就是 MPE。MPE 在 PyPI 上有社区维护的 mpe 包也可以从源码安装。我的建议是不要在这个环节追求最新版本能跑通最重要。# 1. 创建虚拟环境避免污染系统 Python python -m venv maddpg_env source maddpg_env/bin/activate # Windows 下用 maddpg_env\Scripts\activate # 2. 安装核心依赖 pip install torch --index-url https://download.pytorch.org/whl/cpu pip install numpy matplotlib pip install mpe # 3. 跑最小训练先把轮数调小验证环境 python train.py --scenario simple_adversary --max-episodes 100 --episode-len 25这里每个命令都有取舍。--index-url指定 CPU 版 PyTorch是因为 MADDPG 的训练通常不需要 GPUMPE 环境本身是 Python 实现的控制器瓶颈在环境仿真而非张量计算如果你本机有 NVIDIA 显卡且想跑大规模实验去掉这个参数装默认版本就行。--scenario simple_adversary是 MPE 里最经典的对抗环境两个对手、一个要被争夺的目标点很适合验证算法正确性。--max-episodes 100是我强烈建议你在第一次跑的时候设置的值因为完整训练常常要跑几千轮不要浪费一晚上在环境报错上。3.3 训练日志和实验结果文件怎么判断这次跑成功了跑完这 100 个 episode 之后打开results/目录下生成的 CSV你能看到每轮的平均 reward。此刻先别急着看曲线高低看三个异常信号日志里有没有 NaNreward 是否有任何上升趋势程序有没有在回合中途异常退出。如果 100 轮 reward 从 -50 慢慢爬升到 -20哪怕绝对值仍然是负的也说明梯度在正常流动整套环境、模型、回放缓冲区的链路是通的。这 100 轮只是抽烟测试真正要复现源码包里实验结果的完整训练建议训练 2000~3000 episode评估间隔设成 100。MADDPG 在 MPE 的小地图上对算力要求很低纯 CPU 跑一个场景也就四五个小时这个开销远低于大多数 CV 训练任务也正因此它成为多智能体强化学习入门复现的首选算法。4. 核心实现解读每个智能体如何“欺骗”对手4.1 动作空间的分岔路连续动作与离散动作的处理MADDPG 原论文的理论推导建立在连续动作空间上Actor 的输出层通常用 tanh 把动作限制在 [-1, 1]。但 MPE 里的大多数场景动作空间其实是离散的比如上下左右和不动五个动作这就造成了一个经典的动手坑直接把离散动作输入 Critic梯度回传时离散动作没有连续导数训练就会中断。常见做法是两种。第一种是把离散动作转成 one-hot 再拼给 Critic同时 Actor 输出的连续值转成概率分布用 Gumbel-Softmax 做可微采样第二种更粗暴Actor 照常输出连续值实际环境执行时按阈值映射成离散动作训练时仍然用连续值。我倾向于推荐你写第一种因为梯度路径更干净。import torch import torch.nn.functional as F from torch.distributions import Categorical def discrete_action_sample(logits, tau1.0, hardTrue): # logits: [batch, num_discrete_actions] 每个智能体独立预测的动作概率 # 用 Gumbel-Softmax 让离散采样过程可微 gumbels -torch.empty_like(logits).exponential_().log() gumbels (logits gumbels) / tau y_soft F.softmax(gumbels, dim-1) if hard: index y_soft.argmax(dim-1) y_hard torch.zeros_like(logits) y_hard.scatter_(1, index.unsqueeze(-1), 1.0) # 直通估计前向用硬 one-hot反向用 soft 的梯度 return (y_hard - y_soft).detach() y_soft else: return y_soft两个参数值得进一步说。tau是温度系数控制样本的随机程度训练初期可以设 1.0 让探索更充分训练后期降到 0.1 左右让策略更果断hard设为 True 时前向传播拿到的是硬 one-hot 动作但反向传播时梯度经过的是 soft 版本这就是直通估计的主意也就是这些年许多离散动作强化学习实现里都能看到的手法。你在阅读源码时如果发现 Actor 输出层之后接了F.softmax又做了detach八成就是这种处理。4.2 目标网络与经验回放MADDPG 的“后悔药”和“黑匣子”没有经验回放多智能体强化学习根本练不动。理由很简单环境非平稳如果你只在当前轨迹上做梯度更新那么智能体 A 的一个动作导致智能体 B 的行为变化这种影响没有任何机制被记录下来。经验回放缓冲区把 (所有智能体观测, 所有智能体动作, 奖励, 下一帧所有智能体观测) 的元组存起来训练时随机抽取一批样本相当于把前一刻的“当下环境”冻结成历史标本让 Critic 在相对稳定的数据分布上拟合。缓冲区大小一般设 100000批次大小设 1024。实际项目中缓冲区大小对显存影响不大因为 MPE 的单条经验很短批次大小反而更敏感设太大会导致每步更新太慢设太小梯度噪声太大。经验回放相当于是这个算法的“后悔药”训练跑崩了之后你可以缩小探索噪声重新初始化但不必从头开始攒经验。4.3 训练循环里的 4 个关键超参数怎么定我每次复现 MADDPG都会把超参数表打印出来贴在终端旁边。固定要调的是这四个Actor 学习率 1e-4Critic 学习率 1e-3这个比例关系不是随意定的Critic 收敛速度需要快于 Actor否则 Q 值还没拟合好Actor 就被带偏了折扣因子 gamma 取 0.95MPE 单回合步数短取太接近 1 会让价值估计在回合结束时拖泥带水soft update 的 tau 取 0.01作用前面已经说过了最后是探索噪声如果你用高斯噪声初始标准差 0.3 是个安全值训练到中段逐步衰减到 0.05。# 训练循环核心片段省略了环境交互部分 for episode in range(max_episodes): obs_n env.reset() # obs_n 是 list每个元素是一个智能体的观测 episode_reward [0 for _ in range(num_agents)] for step in range(episode_len): # 探索连续动作加噪声离散动作提高温度 actions_n [] for i, agent in enumerate(agents): obs_tensor torch.FloatTensor(obs_n[i]).unsqueeze(0) action agent.actor(obs_tensor) if continuous: action torch.randn_like(action) * exploration_rate else: action discrete_action_sample(action, tauexploration_rate) actions_n.append(action.detach()) # 环境步进拿奖励和下一帧观测 next_obs_n, rewards_n, done_n env.step(actions_n) # 存入 replay buffer必须存多智能体全量信息 replay_buffer.add(obs_n, actions_n, rewards_n, next_obs_n, done_n) # 每 100 步做一次真正的参数更新 if replay_buffer.size() batch_size and step % 100 0: batch_obs, batch_act, batch_rew, batch_next_obs, batch_done replay_buffer.sample(batch_size) # 计算 target Q 时需要用目标网络输出下一时刻所有智能体的动作 with torch.no_grad(): next_actions_n [] for i, agent in enumerate(agents): next_actions_n.append(agent.target_actor(batch_next_obs[i])) target_q agent.target_critic(batch_next_obs, next_actions_n) target_q batch_rew gamma * target_q * (1 - batch_done) # Critic 损失预测 Q 与 target Q 的 MSE current_q agent.critic(batch_obs, batch_act) critic_loss F.mse_loss(current_q, target_q) # Actor 损失最大化当前 Critic 对自己动作的估值 current_actions [agent.actor(batch_obs[0]) for agent in agents] actor_loss -agent.critic(batch_obs, current_actions).mean() # 更新网络 critic_optimizer.zero_grad() critic_loss.backward() critic_optimizer.step() actor_optimizer.zero_grad() actor_loss.backward() actor_optimizer.step() # soft update 目标网络 for target_param, param in zip(agent.target_actor.parameters(), agent.actor.parameters()): target_param.data.copy_(tau * param.data (1 - tau) * target_param.data)这段代码是 MADDPG 训练循环的全貌我用 python 伪代码的形式把省略的环境交互留在注释里。两个关键点需要展开。第一next_actions_n必须用目标 Actor 计算而不是在线 Actor否则 target Q 的计算会被当前策略的噪声污染违背了时序差分里“target 应该是对固定策略的期望”这一原则。第二Actor 的损失函数是对 Critic 输出取负均值这是 DDPG 系列一脉相承的操作Critic 对这个动作的打分越高Actor 的损失越小从而推动 Actor 朝高分方向演进。超参数的初始值我通常按照上述数值设置然后观察前 500 episode 的 reward 曲线走向。如果曲线一路向下先调学习率尤其是 Critic 的 1e-3如果曲线不动检查探索噪声是否过早衰减到 0如果曲线剧烈震荡把 tau 调小到 0.005并增大 batch size 到 2048。这套排查顺序在多个场景下都管用。5. 跑不通、不收敛、震荡MADDPG 训练常见的 5 个坑5.1 训练 500 episode reward 还在负值是算法坏了还是奖励函数问题现象reward 曲线平得像一条直线或者在负值区间内抖动但毫无上升趋势许多人在这一步就放弃了。原因有两个概率最高第一探索噪声太大Actor 输出的动作被噪声淹没Critic 学到的 Q 值是对一堆随机动作的平均策略信号微弱第二MPE 的 sparse reward 环境里奖励本来就稀疏比如只有最后撞到目标点才给 10训练中大部分时间 reward 为 0 或负的小惩罚曲线自然难看。解决先降探索噪声把初始标准差从 0.3 降到 0.1观察 200 episode如果还没有变化就在环境交互阶段加 reward shaping比如靠近目标点给一个小奖励梯度帮助 Critic 建立中间状态的价值估计。注意 reward shaping 只用于训练评估时要关掉。5.2 离散动作环境直接报错ValueError 的根源现象程序跑在simple_tag这类连续动作场景时一切正常但换到simple_adversary离散动作就报ValueError: shape mismatch或者 loss 直接变成 NaN。原因很直接MPE 里动作空间定义成 Discrete(5)而 MADDPG 的 Actor 输出是 tanh 激活的连续值长度也不是 5拼给 Critic 后维度不匹配。解决按 4.1 节 Gumbel-Softmax 的方法处理把 Actor 输出维度改成跟离散动作数一致用温度参数控制探索。如果项目已经有可用的行动模块也可以把离散动作转为 one-hot 向量再拼接到 Critic 输入这样 Critic 的输入维度恒定为 num_agents * (obs_dim num_discrete_actions)。5.3 同一套参数在合作场景稳定、在对抗场景震荡现象跑合作场景simple_spread收敛得很漂亮切换成对抗场景simple_adversary之后 reward 曲线来回大力震荡看上去像两个智能体在互相比谁先“翻车”。原因是竞争场景里面两个智能体的目标天然冲突Critic 输入里拼接的对手动作在高频变化导致 Q 值估计方差变大。这是 MADDPG 本身的一个已知软肋论文里虽然有对抗实验但收敛速度明显慢于合作场景。解决把 batch size 从 1024 提到 2048提升目标 Q 值估计的置信度再加长目标网络 soft update 的周期比如 tau 从 0.01 降到 0.005给目标 Q 值更大的惯性。还有一个容易忽略的点对抗场景的评估频率要降低。合作场景每 100 episode 评估一次就够对抗场景最好等到收敛迹象出现后再评估否则容易把中间阶段的策略误判为失败。5.4 保存和加载智能体权重时维度错位现象训练结束保存模型重新加载想继续训练时出现size mismatch for fc1.weight: copying a param with shape torch.Size([128, 6]) from checkpoint。原因很常见你保存的是agent.actor.state_dict()但加载时头脑一热用了torch.load(model.pth)直接赋值给新模型而新模型的 obs_dim 或智能体数量跟训练时不一致。解决保存时用一个包装字典把网络名、智能体索引、维度信息全部记下来checkpoint { actor_i0: agents[0].actor.state_dict(), critic_i0: agents[0].critic.state_dict(), obs_dim: 4, action_dim: 5, num_agents: 2, episode: episode_count } torch.save(checkpoint, maddpg_checkpoint.pt)加载时先校验维度再load_state_dict校验不通过就打印提示而不是直接报错。多智能体项目里这个习惯能帮你省下大量调试时间尤其是当你改了环境配置又想用旧权重接着训练时。5.5 训练速度慢到怀疑人生现象CPU 占用拉满但每 100 episode 要跑十几分钟日志刷新像是挤牙膏。原因有两个一是没有利用 MPE 环境本身是轻量仿真这个特点没必要每次采样都重新reset()整个场景二是训练代码里可能不小心在环境交互循环内做了模型前向传播的grad记录浪费大量计算。解决把采样阶段包在torch.no_grad()里只在真正训练更新时开梯度。另一个有效手段是把 MPE 环境版本换成 vectorized 实现比如用ray做并行 rollout这样四个环境同时采样训练速度基本能翻倍。我自己的经验是先在单环境把小规模参数跑通再上并行不要一上来就并行排查问题会复杂得多。6. 如何验证“源码及实验结果”里的结论从 reward 曲线到胜率6.1 把 reward 曲线分为三个区间来看拿到实验结果文件里的 reward 曲线后不要只看最终数值。我会把曲线纵向切成三段前 20% 是探索期曲线应该在波动中缓慢爬升中间 60% 是学习期曲线会出现台阶式上升因为智能体偶尔发现一个新策略后Critic 暂时高估了新策略的价值随后回摆到合理位置最后 20% 是收敛期曲线在某个水平附近小幅波动。如果最后阶段不是波动而是单调上升说明还没收敛需要更多 episode。6.2 对抗场景的硬指标胜率与平均奖励统计reward 曲线看趋势胜率看结论。GitHub 上大多数 MADDPG 源码在simple_adversary里会用“对抗双方的胜率”作为主指标但实现方式五花八门。最可靠的做法是在评估阶段固定对手为随机策略让待评估智能体连续对战 100 局统计胜率。这样做能排除训练时的探索噪声得到一个近似真实性能的数字。另外一份实验结果的 reward 表哪怕平均值写得再漂亮也要看标准差。标准差大于平均值的均值是在向你发警告训练不稳定重跑几次结果可能完全不同。6.3 定制你自己的实验结果修改环境参数复现实验想真正吃透这个源码包最后一步是修改环境参数跑一次对比实验。比如把simple_adversary环境中的N_GOOD 1改成 2让两个好智能体合作对抗一个坏智能体你就能直观理解智能体数量变化对 Critic 输入维度的影响。改完这组对比实验你再回头理解“集中训练”这四个字感受会完全不同——你会发现 Critic 的输入维度成倍膨胀而 Actor 的输入维度一点没变这正是分散执行的意义所在。这几年我在多智能体方向反复折腾最大的教训是MADDPG 的高门槛从来不在公式而在环境、动作空间和参数之间的隐性耦合。只要你遇到离奇报错时不急着怀疑算法本身而是先打印维度、检查动作空间是否连续、再看噪声参数多半能在半小时内定位问题。希望这篇笔记能帮你在跑通自己的“源码及实验结果”时少走一点我当时走过的弯路把时间花在真正有趣的对抗策略设计上。本文还有配套的精品资源点击获取