简介:这份资源是面向计算机相关专业学生与强化学习入门者的多智能体博弈对抗实战项目,基于MADDPG算法用Python实现,可直接用于课程设计、期末大作业或项目实战练习。压缩包共13个文件,以10个py源码文件为主,另含1个txt说明、1个cfg环境配置与1个gitignore,整体约15KB,体积轻便。源码按模块拆分,涵盖DDPG网络、MADDPG核心逻辑、经验回放缓冲区、神经网络构建、强化学习工具函数以及多智能体环境与测试脚本,并配有代码注释,便于理解算法流程与调试思路。该资源为个人98分高分项目,代码完整、经过严格调试,下载即用,能帮助读者快速跑通多智能体博弈对抗实验,掌握集中训练分散执行的实现细节。目前已有423人学习下载,适合需要完整参考方案与排错思路的学习者。
1. 从一份 98 分课设说起:MADDPG 多智能体博弈对抗到底能跑出什么
如果你正在做多智能体强化学习的课程设计,大概率会遇到一个尴尬局面:单智能体的 DQN、DDPG 代码一搜一大把,但一旦把环境里塞进两个以上会互相影响的智能体,训练曲线立刻变成玄学——奖励不涨、loss 震荡、智能体学会摆烂。这份基于 MADDPG 的多智能体博弈对抗算法 Python 实现源码,就是冲着这个痛点来的。它把多智能体博弈对抗算法拆成了可直接运行的工程:MADDPG.py是算法主体,DDPG.py是单智能体基座,network.py定义 Actor/Critic 网络,buffer.py管经验回放,main.py串起训练循环,rl_utils.py收口工具函数。适合两类人:一是赶期末大作业、需要一份能跑通、有注释、结构清晰的参考实现;二是想真正搞懂 MADDPG 里「集中训练、分散执行」到底怎么落到代码上的学习者。下面我按「这是什么 → 怎么跑起来 → 参数怎么调 → 坑在哪」的顺序,把这份源码拆开讲。
2. MADDPG 的算法骨架:集中式 Critic 与分散式 Actor 怎么在代码里落地
MADDPG 的核心思想一句话能说清:每个智能体的 Actor 只看自己的局部观测,但 Critic 在训练时能看到所有智能体的观测和动作。这个设计解决了多智能体环境非平稳的问题——因为每个智能体都在学习,环境对单个智能体来说一直在变。源码里MADDPG.py和DDPG.py的分工,正好对应这个思想。
2.1 从 DDPG 到 MADDPG:为什么不能直接复制单智能体代码
DDPG 是确定性策略梯度算法,适合连续动作空间。它的结构是 Actor 输出确定性动作,Critic 评估这个动作的 Q 值。单智能体场景下,Critic 只需要看自己的状态和动作就够了。但多智能体场景下,如果每个智能体的 Critic 也只盯着自己,就会出问题:智能体 A 的策略一变,智能体 B 面对的环境就变了,B 的 Q 值估计立刻失准,训练进入「你变我也变」的死循环。
MADDPG 的解法是给 Critic 开天眼。训练时,第 i 个智能体的 Critic 输入是全局状态(所有智能体的观测拼接)和全局动作(所有智能体的动作拼接),输出一个标量 Q 值。这样 Critic 的评估基准是稳定的全局信息,不会因为某个智能体偷偷改策略就崩掉。而 Actor 依然只拿局部观测,保证执行时不需要全局通信。这就是「集中训练、分散执行」的字面含义。
源码里DDPG.py大概率是单智能体的基类实现,包含 Actor 网络、Critic 网络、目标网络软更新、经验回放这些通用组件。MADDPG.py则在此基础上做扩展:为每个智能体维护一套 Actor 和 Critic,Critic 的输入维度从「局部观测+局部动作」变成「全局观测+全局动作」。network.py里应该定义了多层感知机结构,buffer.py负责存储(obs, action, reward, next_obs, done)五元组,其中 obs 和 next_obs 都是全局的。
2.2 网络结构与经验回放:network.py和buffer.py的关键参数
network.py通常包含两个类:Actor 和 Critic。Actor 的输入维度是单个智能体的观测维度,输出维度是动作维度,中间用两层全连接加 ReLU 激活,最后一层用 tanh 把动作压到[-1, 1]。Critic 的输入维度是「全局观测维度 + 全局动作维度」,输出是一个标量 Q 值,中间同样两层全连接。这里有个容易翻车的点:Critic 的输入拼接顺序必须和训练时保持一致,否则 Q 值会莫名其妙地偏。
buffer.py实现经验回放池。常见做法是用一个固定大小的队列或列表,存满后按先进先出替换。采样时随机抽一批,返回 numpy 数组或 tensor。关键参数是capacity,太小会导致样本相关性高、训练不稳;太大则占用内存且早期旧策略的样本会拖后腿。对于课设规模的环境,capacity设在 100000 到 1000000 之间比较常见。
# buffer.py 经验回放池的典型实现 import numpy as np class ReplayBuffer: def __init__(self, capacity): self.capacity = capacity self.buffer = [] self.position = 0 def push(self, state, action, reward, next_state, done): # state/action 均为全局拼接后的数组 if len(self.buffer) < self.capacity: self.buffer.append(None) self.buffer[self.position] = (state, action, reward, next_state, done) self.position = (self.position + 1) % self.capacity def sample(self, batch_size): # 随机采样,打破时间相关性 batch = np.random.choice(len(self.buffer), batch_size, replace=False) state, action, reward, next_state, done = zip(*[self.buffer[i] for i in batch]) return (np.array(state), np.array(action), np.array(reward), np.array(next_state), np.array(done)) def __len__(self): return len(self.buffer)这段代码的逻辑很直白:push负责存,sample负责随机取。参数capacity控制池子上限,batch_size控制每次训练抽多少条。注意done标志位在计算目标 Q 值时用来决定是否截断,如果环境是分幕的,done 为 True 时目标 Q 值只取即时奖励,不再加折扣后的下一状态价值。
2.3 训练主循环:main.py里每个 episode 到底在做什么
main.py是整份源码的入口。典型流程是:初始化环境、初始化 MADDPG 智能体集合、初始化回放池,然后进入双层循环——外层是 episode,内层是 timestep。每个 timestep 里,每个智能体根据自己的 Actor 选动作,环境执行联合动作后返回各自的奖励和下一个全局观测,把这些数据塞进回放池。当回放池样本足够时,每个智能体从池里采样一批,计算 Critic 损失和 Actor 损失,反向传播更新网络,最后软更新目标网络。
# main.py 训练循环骨架 for episode in range(num_episodes): obs = env.reset() for t in range(max_steps): actions = [agent.select_action(o) for agent, o in zip(agents, obs)] next_obs, rewards, dones, _ = env.step(actions) # 拼接全局状态和全局动作 global_obs = np.concatenate(obs) global_next_obs = np.concatenate(next_obs) global_actions = np.concatenate(actions) buffer.push(global_obs, global_actions, rewards, global_next_obs, dones) if len(buffer) > batch_size: for agent in agents: agent.update(buffer, batch_size) obs = next_obs if all(dones): break这里有几个参数值得盯住:max_steps控制单幕最大步数,太小会导致智能体来不及学到有效策略;batch_size影响梯度估计的方差;软更新系数tau通常设 0.01 左右,控制目标网络跟随当前网络的速度。gamma是折扣因子,博弈对抗场景下一般设 0.95 到 0.99。
3. 把源码跑起来:环境依赖、入口选择与训练曲线观察
拿到一份源码,最怕的是「下载即用」变成「下载即报错」。这一章按实际复现顺序走一遍,从 Python 环境到跑通训练,再到怎么看结果。
3.1 环境准备:Python 版本、依赖库与pyvenv.cfg的暗示
源码包里有个pyvenv.cfg,说明作者用的是 Python 虚拟环境。常见做法是 Python 3.8 或 3.9,太新的版本可能和某些老库不兼容。依赖库大概率包括numpy、torch、gym或gymnasium,可能还有matplotlib用于画训练曲线。安装命令如下:
# 创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy torch gym matplotlib如果gym版本较新,注意env.step()的返回值从 4 个变成了 5 个(多了truncated),源码里如果按 4 个解包会报错。解决办法是降级gym到 0.21 左右,或者手动改解包逻辑。这是多智能体代码里最常见的翻车点之一。
3.2 入口文件辨析:main.py、test.py、ceshi.py分别干什么
源码里有三个疑似入口:main.py、test.py、ceshi.py。按命名习惯,main.py是训练主入口,test.py是测试或评估入口,ceshi.py是「测试」的拼音,可能是作者调试时留下的脚本。test_env.py大概率是自定义环境或环境测试文件。mag.py可能是某个具体博弈场景的实现,比如追逃、对抗或合作任务。
建议的复现顺序是:先跑test_env.py确认环境能正常 reset 和 step,再跑main.py看训练是否启动,最后用test.py加载训练好的模型做评估。如果main.py里直接调用了mag.py里的环境类,那mag.py就是核心场景文件,值得重点读。
3.3 训练过程监控:奖励曲线、loss 与「不涨」的排查思路
训练启动后,最直观的监控指标是每个 episode 的累计奖励。多智能体博弈对抗场景下,奖励曲线通常不会像单智能体那样平滑上升,而是呈现对抗性震荡——一方涨另一方跌,或者双方在某个水平附近拉锯。这是正常的,因为博弈的本质就是策略互相压制。
如果奖励完全不涨,按以下顺序排查:第一,检查动作是否真的作用到了环境,有些环境对动作范围有要求,Actor 输出的[-1,1]可能需要缩放;第二,检查 Critic 的输入是否真的拼接了全局信息,如果只拼了局部,那就退化成独立 DDPG,非平稳问题会直接压垮训练;第三,检查回放池是否在样本足够前就开始训练,早期样本太少会导致过拟合;第四,看 loss 是否爆炸,如果 Critic loss 冲到几百,多半是学习率太大或奖励尺度没归一化。
# 训练曲线绘制的常见做法 import matplotlib.pyplot as plt rewards_history = [] # 每个 episode 的累计奖励 plt.plot(rewards_history) plt.xlabel('Episode') plt.ylabel('Cumulative Reward') plt.title('MADDPG Training Curve') plt.savefig('training_curve.png')这段代码不复杂,但关键在rewards_history怎么记。常见做法是记录所有智能体奖励之和,或者分别记录每个智能体的奖励画多条线。博弈对抗场景下,分开画更能看出谁占优。
4. 参数调优与避坑:学习率、软更新与那些让训练崩掉的细节
这一章集中讲踩坑。MADDPG 的论文看起来清晰,但落到代码里,参数之间的耦合比单智能体复杂得多。以下五条是我在复现这类源码时反复遇到的。
4.1 学习率与网络更新频率:Actor 和 Critic 不能一刀切
现象:训练初期奖励短暂上升后迅速崩塌,loss 剧烈震荡。原因:Actor 和 Critic 用了同一个学习率,且 Critic 更新太频繁。Critic 需要先学到一个相对准确的 Q 值估计,Actor 才能根据它更新策略。如果两者同步猛更新,Critic 还没站稳,Actor 就被带偏了。
解决:Critic 的学习率通常比 Actor 大一个量级,比如 Critic 用1e-3,Actor 用1e-4。另外可以延迟 Actor 的更新,比如 Critic 每更新两次,Actor 更新一次。源码里如果没做这个区分,可以手动在update函数里加一个计数器。
4.2 软更新系数tau的取值:太大目标网络形同虚设
现象:训练不稳定,Q 值估计和目标 Q 值差距始终很大。原因:tau设得太大,比如 0.1 甚至 0.5,目标网络几乎在复制当前网络,失去了「稳定靶子」的作用。MADDPG 原文建议tau取 0.01。
解决:把tau降到 0.01 或 0.005,观察 Q 值是否逐渐收敛。如果环境特别简单,可以适当放大到 0.02,但不要超过 0.05。软更新公式是target_param = tau * param + (1 - tau) * target_param,tau越小,目标网络越稳。
4.3 奖励尺度与归一化:博弈对抗里奖励不对称会怎样
现象:一方智能体奖励始终为正,另一方始终为负,训练后期弱势方直接摆烂。原因:博弈环境的奖励设计本身可能不对称,比如追逃任务里追捕者每步都有惩罚,逃跑者每步都有奖励。如果不做归一化,Critic 的 Q 值范围会差出几个量级。
解决:常见做法是对奖励做 running mean 标准化,或者手动把奖励缩放到[-1, 1]。另外可以给每个智能体单独维护一个奖励缩放系数,避免强势方主导梯度。源码里如果没做这一步,可以在buffer.push之前加一层奖励处理。
4.4 观测拼接顺序与维度对齐:一个下标错位就白训练
现象:代码能跑,loss 也降,但智能体学不到任何有效策略。原因:全局观测拼接时,智能体的顺序和 Critic 输入的顺序不一致,或者next_obs拼接时漏了某个智能体。这种 bug 最隐蔽,因为不会报错,只是静默地让 Critic 学到错误关联。
解决:在拼接全局观测和全局动作时,固定智能体顺序,并在代码里加断言检查维度。比如assert global_obs.shape[0] == sum(obs_dims)。每次改完环境或智能体数量,都重新跑一遍维度检查。
4.5 环境重置与 done 标志:分幕任务里最容易忽略的截断
现象:训练到后期,智能体在某一幕里表现很好,但下一幕立刻归零。原因:done标志没有正确传递,或者环境 reset 后没有重新初始化隐藏状态。有些环境在 done 为 True 时还会返回一个无效的next_obs,如果直接塞进回放池,会污染训练数据。
解决:在push之前判断done,如果为 True,把next_obs置为零向量或直接跳过存储。另外,如果环境有状态延续(比如 RNN 类策略),reset 时要同步清空隐藏状态。源码里test_env.py可以用来验证 reset 和 step 的返回值是否符合预期。
5. 从跑通到改出花:换环境、加智能体与验证训练是否真的收敛
跑通源码只是第一步。课设拿高分的关键,往往在于你能不能在这个基础上做一点自己的东西。这一章讲两个进阶方向和一个验证技巧。
5.1 替换自定义环境:mag.py的接口约定与最小改动
mag.py大概率是作者自定义的博弈环境。如果你想换成自己的场景,需要对齐几个接口:reset()返回所有智能体的初始观测列表;step(actions)接收联合动作列表,返回next_obs列表、rewards列表、dones列表和额外信息;observation_space和action_space要能被智能体读取以确定网络输入输出维度。
最小改动路径是:复制mag.py改名为你的环境文件,保留类名和方法签名,只改内部逻辑。然后在main.py里把环境实例化那一行换成你的类。注意观测维度和动作维度变了之后,network.py里的输入输出维度要同步改,否则第一层全连接就会报维度不匹配。
5.2 增加智能体数量:Critic 输入维度膨胀后的内存与显存问题
从 2 个智能体加到 4 个,Critic 的输入维度会翻倍。如果每个智能体观测维度是 10,动作维度是 2,2 个智能体时 Critic 输入是(10*2 + 2*2) = 24,4 个智能体时变成(10*4 + 2*4) = 48。网络参数变多,回放池里每条样本也变大。常见做法是适当减小capacity,或者把 batch_size 从 256 降到 128,避免显存爆掉。
另外,智能体数量增加后,非平稳性更强,可能需要把tau再调小一点,或者增加 Critic 的更新频率。如果训练实在不收敛,可以考虑给每个智能体单独设奖励缩放,避免某个智能体的梯度主导全局。
5.3 验证收敛:固定随机种子跑三次,看奖励方差
判断训练是否真的收敛,不能只看一次运行的曲线。常见做法是固定随机种子,跑三次,比较最终奖励的均值和方差。如果三次的最终奖励差距在 10% 以内,说明策略相对稳定;如果差距很大,说明训练对初始化敏感,需要调参或增加训练轮数。
# 固定随机种子的做法 import torch import numpy as np import random def set_seed(seed): torch.manual_seed(seed) np.random.seed(seed) random.seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) # 在 main.py 开头调用 set_seed(42)这段代码放在训练脚本最前面,保证每次运行的初始网络参数、环境随机性和采样顺序一致。注意有些环境内部有自己的随机数生成器,可能需要单独设种子。如果源码里没有这个函数,加上去再跑三次,对比结果会清晰很多。
5.4 一个具体技巧:用test.py加载模型做确定性评估
训练时的动作通常带探索噪声,评估时要关掉噪声,用 Actor 的确定性输出。test.py大概率就是干这个的。常见做法是加载训练好的模型参数,把select_action里的噪声项去掉,跑几个 episode 看平均奖励。如果评估奖励远低于训练奖励,说明策略过拟合了训练时的噪声,或者 Critic 过估计了 Q 值。
我一般会在test.py里加一个render开关,把环境可视化出来,肉眼确认智能体行为是否符合预期。有一次训练曲线很漂亮,但渲染出来发现两个智能体在原地转圈,奖励是靠环境 bug 刷出来的。从那以后我每次评估都强制走一遍渲染,确认行为合理再去看数字。希望这份拆解能帮你少走点弯路,把这份源码真正用起来。
本文还有配套的精品资源,点击获取