简介:这份资源是面向毕业设计与强化学习入门者的MADDPG多智能体博弈对抗算法Python实现,适合具备一定机器学习、深度学习基础,希望理解多智能体连续动作空间协作与对抗机制的学生和研究人员。压缩包共13个文件,以10个py源码文件为主,辅以txt说明、md文档与cfg配置文件,整体约12KB,涵盖环境构建、智能体初始化、Actor-Critic网络搭建、训练流程与结果展示等模块,代码中配有详细注释,便于对照理解算法细节。目前已有122人学习下载。读者可据此复现MADDPG算法,掌握中心评论家与分散演员的协作机制,并在此基础上修改网络结构、调整参数或迁移到多车道路控制、多人游戏策略学习等场景,是开展多智能体博弈方向毕业设计与课题研究的实用起点。
1. 从一份 MADDPG 源码说起:多智能体博弈对抗到底在解决什么问题
如果你正在做毕业设计,选题落在「多智能体博弈对抗」上,大概率会遇到一个尴尬局面:单智能体的 DQN、PPO 代码一抓一大把,但一旦把环境里塞进两个以上会互相影响的智能体,训练曲线立刻变成玄学——奖励不升反降,loss 震荡,两个智能体要么一起摆烂,要么一个碾压另一个导致另一方学不到东西。这份基于 MADDPG 的多智能体博弈对抗算法 Python 源码,解决的正是这个核心痛点:让多个智能体在共享环境里各自决策、互相博弈,同时还能稳定收敛。
MADDPG(Multi-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_dim;Critic 的输入维度是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), dtype=np.float32) self.act_buf = np.zeros((capacity, n_agents, action_dim), dtype=np.float32) self.rew_buf = np.zeros((capacity, n_agents, 1), dtype=np.float32) self.next_obs_buf = np.zeros((capacity, n_agents, obs_dim), dtype=np.float32) self.done_buf = np.zeros((capacity, n_agents, 1), dtype=np.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, size=batch_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, explore=True) 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 numpy==1.21.6 pip install torch==1.10.0 pip install gym==0.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 git+https://github.com/openai/multiagent-particle-envs.git这个包提供了 simple_tag、simple_spread 等经典博弈对抗场景。装完后用import multiagent测试一下,不报错就说明环境通了。
3.2 关键超参数配置表与含义说明
源码里arguments.py或main.py开头会有一堆超参数,新手最容易随便改然后训练崩掉。下表列出核心参数和建议范围:
| 参数名 | 含义 | 建议值 | 调整影响 |
|---|---|---|---|
lr_actor | Actor 学习率 | 1e-3 ~ 1e-4 | 太大策略震荡,太小收敛慢 |
lr_critic | Critic 学习率 | 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 | 太大动作乱跳,太小探索不足 |
这些参数不是拍脑袋定的。gamma=0.95在博弈对抗里比较常用,因为对抗场景的回报周期通常不长。tau=0.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 和 action,Critic 看到的全局信息不完整,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 里一起看。别只盯着 reward,reward 会骗人,行为指标不会。
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(),宁可多写一行也别省。
希望帮到你。
本文还有配套的精品资源,点击获取