MADDPG多智能体博弈对抗源码解析:从算法原理到训练避坑 简介这份资源是面向毕业设计与强化学习入门者的MADDPG多智能体博弈对抗算法Python实现适合具备一定机器学习、深度学习基础希望理解多智能体连续动作空间协作与对抗机制的学生和研究人员。压缩包共13个文件以10个py源码文件为主辅以txt说明、md文档与cfg配置文件整体约12KB涵盖环境构建、智能体初始化、Actor-Critic网络搭建、训练流程与结果展示等模块代码中配有详细注释便于对照理解算法细节。目前已有122人学习下载。读者可据此复现MADDPG算法掌握中心评论家与分散演员的协作机制并在此基础上修改网络结构、调整参数或迁移到多车道路控制、多人游戏策略学习等场景是开展多智能体博弈方向毕业设计与课题研究的实用起点。1. 从一份 MADDPG 源码说起多智能体博弈对抗到底在解决什么问题如果你正在做毕业设计选题落在「多智能体博弈对抗」上大概率会遇到一个尴尬局面单智能体的 DQN、PPO 代码一抓一大把但一旦把环境里塞进两个以上会互相影响的智能体训练曲线立刻变成玄学——奖励不升反降loss 震荡两个智能体要么一起摆烂要么一个碾压另一个导致另一方学不到东西。这份基于 MADDPG 的多智能体博弈对抗算法 Python 源码解决的正是这个核心痛点让多个智能体在共享环境里各自决策、互相博弈同时还能稳定收敛。MADDPGMulti-Agent Deep Deterministic Policy Gradient是 UC Berkeley 团队在 2017 年提出的经典多智能体强化学习算法属于 Actor-Critic 架构下的集中训练、分散执行范式。它最值钱的地方在于训练时 Critic 能看到所有智能体的观测和动作执行时每个 Actor 只依赖自己的局部观测。这个设计直接绕开了多智能体环境非平稳性的死结——当其他智能体策略在变你的环境转移概率就在变普通单智能体算法根本稳不住。这份源码适合三类人一是毕业设计需要完整可跑通的多智能体项目二是想从单智能体 RL 进阶到多智能体 RL 的工程师三是需要拿博弈对抗场景做算法对比实验的研究生。接下来我会把这份源码背后的算法逻辑、环境搭建、参数调优和踩坑记录拆开讲清楚让你拿到手能跑、跑完能改、改完能写进论文。2. MADDPG 算法拆解集中训练分散执行到底怎么落地2.1 Actor-Critic 在多智能体下的角色分工单智能体的 DDPG 里一个 Actor 负责输出动作一个 Critic 负责评估动作价值。到了 MADDPG每个智能体都有自己的 Actor 和 Critic但 Critic 的输入维度变了——它接收的是所有智能体的联合观测和联合动作。这就是「集中训练」的含义Critic 在训练阶段拥有全局信息能准确评估当前联合动作的好坏。执行阶段则完全不同。每个 Actor 只拿自己的局部观测obs_i输出动作a_i。这意味着部署时不需要全局通信每个智能体独立决策。这个「训练时开天眼、执行时闭眼跑」的设计是 MADDPG 能在博弈对抗场景里 work 的根本原因。源码里通常会有两个网络类Actor和Critic。Actor 的输入维度是obs_dim输出维度是action_dimCritic 的输入维度是obs_dim * n_agents action_dim * n_agents输出是一个标量 Q 值。这个维度变化是新手最容易写错的地方后面避坑章节会细说。2.2 源码目录结构与核心模块定位拿到一份 MADDPG 源码先别急着跑花五分钟把目录结构看清楚能省掉后面两小时的瞎折腾。典型结构如下MADDPG/ ├── main.py # 训练入口解析参数、创建环境、启动训练循环 ├── maddpg.py # MADDPG 核心类管理多个 Agent ├── agent.py # 单个 Agent 的 Actor/Critic 定义与更新逻辑 ├── replay_buffer.py # 经验回放池存储 (obs, actions, rewards, next_obs, done) ├── envs/ # 环境封装常见有 simple_tag、simple_spread 等 │ └── multiagent_env.py ├── utils.py # 噪声生成、参数初始化、日志工具 └── arguments.py # 超参数配置main.py是入口负责把环境、MADDPG 类、回放池串起来。maddpg.py里通常有一个MADDPG类内部维护一个agents列表每个元素是一个DDPGAgent。agent.py是核心Actor 和 Critic 的网络结构、前向传播、损失计算都在这里。提示不同版本的源码目录名可能略有差异但核心文件一定包含 agent 定义、回放池和训练入口这三块。如果缺了回放池说明这份源码可能不完整。2.3 经验回放池的存储与采样逻辑多智能体经验回放池和单智能体最大的区别在于存的是联合信息不是单个智能体的转移。每条经验通常包含obs所有智能体当前观测形状(n_agents, obs_dim)actions所有智能体当前动作形状(n_agents, action_dim)rewards所有智能体奖励形状(n_agents, 1)next_obs所有智能体下一时刻观测done是否终止采样时从池子里随机抽一批batch_size的数据然后按智能体维度拆开分别喂给每个智能体的 Critic。这里有个关键点每个智能体的 Critic 更新时用的都是同一批联合数据但只优化自己的 Q 值。class ReplayBuffer: def __init__(self, capacity, n_agents, obs_dim, action_dim): self.capacity capacity self.n_agents n_agents self.obs_buf np.zeros((capacity, n_agents, obs_dim), dtypenp.float32) self.act_buf np.zeros((capacity, n_agents, action_dim), dtypenp.float32) self.rew_buf np.zeros((capacity, n_agents, 1), dtypenp.float32) self.next_obs_buf np.zeros((capacity, n_agents, obs_dim), dtypenp.float32) self.done_buf np.zeros((capacity, n_agents, 1), dtypenp.float32) self.ptr, self.size 0, 0 def store(self, obs, act, rew, next_obs, done): # 按指针位置写入满了就覆盖最旧的数据 self.obs_buf[self.ptr] obs self.act_buf[self.ptr] act self.rew_buf[self.ptr] rew self.next_obs_buf[self.ptr] next_obs self.done_buf[self.ptr] done self.ptr (self.ptr 1) % self.capacity self.size min(self.size 1, self.capacity) def sample(self, batch_size): # 随机采样返回的是按智能体维度组织好的联合数据 idxs np.random.randint(0, self.size, sizebatch_size) return (self.obs_buf[idxs], self.act_buf[idxs], self.rew_buf[idxs], self.next_obs_buf[idxs], self.done_buf[idxs])这段代码里capacity一般设 100 万batch_size设 1024 或 512。n_agents根据环境定simple_tag 里通常是 2 到 4 个。注意done的维度是(n_agents, 1)因为每个智能体可能在不同时刻终止不能只存一个全局 done。2.4 训练主循环从环境重置到参数更新训练主循环的逻辑是重置环境 → 每个智能体根据局部观测选动作 → 环境执行联合动作 → 存储经验 → 采样更新。伪代码如下for episode in range(max_episodes): obs env.reset() # obs shape: (n_agents, obs_dim) for step in range(max_steps): actions maddpg.select_actions(obs, exploreTrue) next_obs, rewards, dones, info env.step(actions) buffer.store(obs, actions, rewards, next_obs, dones) obs next_obs if buffer.size batch_size: maddpg.update(buffer, batch_size) if np.any(dones): breakselect_actions里每个智能体的 Actor 输出动作后会加上高斯噪声或 OU 噪声来探索。update里先更新 Critic再更新 Actor最后软更新目标网络。这个顺序不能乱Critic 没更新好就更新 Actor策略梯度方向会偏。注意dones的判断用np.any还是np.all取决于环境设计。如果任一智能体终止就结束整个 episode用np.any如果所有智能体都终止才结束用np.all。这个细节写错训练数据分布会完全不对。3. 环境搭建与源码运行从零跑通第一个对抗场景3.1 Python 环境与依赖安装的确定性步骤这份源码依赖的库不多但版本兼容性有坑。推荐用 Python 3.8 或 3.9太新的版本某些 RL 库还没适配。安装步骤如下# 创建虚拟环境避免污染全局包 python -m venv maddpg_env source maddpg_env/bin/activate # Linux/Mac # maddpg_env\Scripts\activate # Windows # 安装核心依赖 pip install numpy1.21.6 pip install torch1.10.0 pip install gym0.21.0 pip install tensorboardnumpy别装太新的版本1.24 以后某些旧代码里的np.float会报错。torch用 1.10 左右比较稳CUDA 版本根据自己显卡选。gym0.21 是很多多智能体环境的依赖版本0.26 以后 API 变了step返回值从 4 个变成 5 个直接跑会报解包错误。如果源码里用了multiagent-particle-envs还需要额外安装pip install githttps://github.com/openai/multiagent-particle-envs.git这个包提供了 simple_tag、simple_spread 等经典博弈对抗场景。装完后用import multiagent测试一下不报错就说明环境通了。3.2 关键超参数配置表与含义说明源码里arguments.py或main.py开头会有一堆超参数新手最容易随便改然后训练崩掉。下表列出核心参数和建议范围参数名含义建议值调整影响lr_actorActor 学习率1e-3 ~ 1e-4太大策略震荡太小收敛慢lr_criticCritic 学习率1e-3 ~ 1e-4通常比 Actor 略大gamma折扣因子0.95 ~ 0.99越大越看重长期回报tau软更新系数0.01太大目标网络不稳batch_size采样批量512 ~ 1024太小梯度噪声大buffer_size回放池容量1e6太小经验重复率高noise_std探索噪声标准差0.1 ~ 0.3太大动作乱跳太小探索不足这些参数不是拍脑袋定的。gamma0.95在博弈对抗里比较常用因为对抗场景的回报周期通常不长。tau0.01是 DDPG 系列的标准值软更新目标网络时每次只靠近 1%。noise_std建议从 0.3 开始训练稳定后逐步降到 0.1。3.3 启动训练与日志观察配置好参数后直接运行入口脚本python main.py --env simple_tag --n_agents 4 --max_episodes 20000训练启动后终端会打印每个 episode 的奖励和 loss。同时 TensorBoard 日志会写到runs/目录下用以下命令查看tensorboard --logdir runs/浏览器打开localhost:6006重点看三条曲线agent_i/actor_loss、agent_i/critic_loss、agent_i/reward。Critic loss 应该整体下降然后在一个区间内波动如果一直上升说明 Critic 过拟合或学习率太大。Reward 曲线在对抗场景里通常不是单调上升的因为对手也在变强能维持在一个水平不崩就是好现象。提示simple_tag 场景里追捕者adversary和逃跑者good agent的奖励是零和的。如果你看到追捕者奖励上升、逃跑者奖励下降说明对抗在正常进行。如果两者一起下降大概率是探索噪声太大或者 Critic 没学好。4. 避坑与排查MADDPG 训练中最容易翻车的五个地方4.1 现象训练一开始奖励就爆炸或变成 NaN原因最常见的是 Critic 输入维度拼错了。MADDPG 的 Critic 输入应该是所有智能体的 obs 拼接 所有智能体的 action 拼接维度是n_agents * obs_dim n_agents * action_dim。如果只拼了当前智能体的 obs 和 actionCritic 看到的全局信息不完整Q 值估计会严重偏差梯度爆炸后直接 NaN。解决在agent.py里检查 Critic 的forward函数确认输入拼接逻辑def forward(self, obs_all, act_all): # obs_all: (batch, n_agents * obs_dim) # act_all: (batch, n_agents * action_dim) x torch.cat([obs_all, act_all], dim-1) return self.net(x)同时检查maddpg.py里传给 Critic 的数据是否已经按n_agents维度展平。展平用obs.reshape(batch_size, -1)不要用obs.view搞错维度顺序。4.2 现象Actor loss 一直不降策略没有学到东西原因Actor 的更新依赖于 Critic 给出的 Q 值。如果 Critic 还没学好就频繁更新 Actor策略梯度方向就是错的。源码里如果update函数先更新 Actor 再更新 Critic或者两者更新频率一样但 Critic 学习率太低都会导致这个问题。解决调整更新顺序和频率。常见做法是每个 step 更新一次 Critic每两个 step 更新一次 Actor。或者把lr_critic设得比lr_actor大 2 到 3 倍让 Critic 先收敛。另外检查 Actor 的损失函数是不是-critic_q.mean()负号漏了会导致策略往反方向优化。4.3 现象多个智能体行为趋同失去对抗性原因这是多智能体里典型的「策略同质化」。如果所有智能体共享同一个 Actor 网络或者噪声设置一样、初始参数一样它们会学到几乎相同的策略。在博弈对抗场景里这意味着追捕者和逃跑者可能一起摆烂或者一起往同一个方向跑。解决确保每个智能体有独立的 Actor 和 Critic 网络不要共享参数。初始化时用不同的随机种子。噪声方面不同智能体可以设不同的noise_std比如追捕者 0.3、逃跑者 0.2增加策略多样性。如果环境支持给不同智能体设置不同的奖励函数权重也能逼出差异化策略。4.4 现象训练后期奖励突然崩掉之前学好的策略失效原因这是「灾难性遗忘」在多智能体里的表现。回放池满了以后新经验覆盖旧经验如果新经验分布和旧经验差异太大网络会快速适应新数据而忘记旧策略。在对抗场景里对手策略一变你的最优响应也变旧经验就过时了。解决把buffer_size设大一点至少 1e6让旧经验保留更久。或者用优先经验回放Prioritized Experience Replay给 TD 误差大的样本更高采样概率。源码里如果没实现优先回放可以手动加一个简单的权重采样逻辑。另外降低lr_actor和lr_critic在训练后期的值让网络更新更保守。4.5 现象环境 step 返回值解包报错原因gym版本不匹配。0.21 及以前env.step()返回(obs, reward, done, info)四个值0.26 以后返回(obs, reward, terminated, truncated, info)五个值。源码如果是按旧版写的在新版 gym 上跑就会报ValueError: too many values to unpack。解决要么降级 gym 到 0.21要么改源码里的解包逻辑# 旧版写法 next_obs, rewards, dones, info env.step(actions) # 新版兼容写法 result env.step(actions) if len(result) 5: next_obs, rewards, terminated, truncated, info result dones np.logical_or(terminated, truncated) else: next_obs, rewards, dones, info result改完后确认dones的维度和类型多智能体环境里dones通常是列表或数组不是单个布尔值。5. 进阶技巧用参数共享与课程学习把对抗效果再拉一档5.1 参数共享的适用边界与实现方式MADDPG 原版每个智能体独立网络参数量随智能体数量线性增长。如果你有 10 个以上智能体显存和训练时间都会吃不消。参数共享Parameter Sharing是常见的优化手段所有同类型智能体共享一套 Actor 和 Critic 参数输入里加上智能体 ID 的 one-hot 编码来区分。实现上把agent.py里的网络定义改成全局一份每个智能体维护一个agent_id前向传播时把 ID 拼到观测后面class SharedActor(nn.Module): def __init__(self, obs_dim, action_dim, n_agents): super().__init__() self.n_agents n_agents self.net nn.Sequential( nn.Linear(obs_dim n_agents, 64), # 拼接 one-hot ID nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, action_dim), nn.Tanh() ) def forward(self, obs, agent_id): id_onehot F.one_hot(agent_id, self.n_agents).float() x torch.cat([obs, id_onehot], dim-1) return self.net(x)参数共享的边界是同类型智能体才能共享。追捕者和逃跑者的观测空间和动作空间可能不同强行共享会出问题。另外共享后策略多样性会下降需要靠噪声或奖励设计来弥补。5.2 课程学习从简单场景逐步过渡到复杂对抗直接在一个复杂对抗场景里训练 MADDPG收敛难度很大。课程学习Curriculum Learning的思路是先让智能体在简单场景里学会基本动作再逐步增加对手数量或环境复杂度。具体操作上可以分三个阶段阶段场景配置训练目标切换条件第一阶段1 个追捕者 vs 1 个逃跑者学会追击/逃跑平均奖励稳定 50 episode第二阶段2 个追捕者 vs 1 个逃跑者学会协作围堵胜率超过 60%第三阶段3 个追捕者 vs 2 个逃跑者复杂博弈对抗训练到收敛每个阶段训练时加载上一阶段的模型参数作为初始化而不是从头开始。这样能省掉大量重复探索时间。切换条件不要设得太死如果第二阶段一直卡住可以回退到第一阶段再训几千 episode。5.3 验证训练效果除了看奖励曲线还能看什么奖励曲线只能反映整体趋势要验证策略真的学到了东西还得看行为层面的指标。我一般会记录三个额外指标动作熵统计每个智能体输出动作的分布熵。熵太低说明策略过早收敛到固定动作探索不足熵太高说明策略还在乱试。碰撞率/成功率在 simple_tag 里追捕者碰到逃跑者的次数、逃跑者成功躲避的次数比奖励更直观。策略距离计算不同智能体策略输出的 KL 散度或欧氏距离。距离太小说明策略同质化对抗性不足。这些指标可以在main.py的训练循环里加几行统计代码写到 TensorBoard 里一起看。别只盯着 rewardreward 会骗人行为指标不会。5.4 一个我踩过的坑软更新目标网络时忘了 detach最后说一个血泪教训。MADDPG 更新 Critic 时目标 Q 值是用目标网络算的计算目标 Q 值的那部分计算图必须detach否则梯度会回传到目标网络导致目标网络和在线网络一起被更新软更新就失去意义了。# 正确写法 with torch.no_grad(): next_actions target_actor(next_obs) target_q target_critic(next_obs, next_actions) target_q rewards gamma * (1 - dones) * target_q # 错误写法没有 no_grad 或 detach next_actions target_actor(next_obs) # 梯度会回传 target_q target_critic(next_obs, next_actions)这个坑很隐蔽因为代码能跑通loss 也在降但训练效果就是比预期差一截。我当初排查了两天才定位到。后来养成习惯凡是目标网络相关的计算一律包在torch.no_grad()里不确定就加detach()宁可多写一行也别省。希望帮到你。本文还有配套的精品资源点击获取