
简介这是一份基于MADDPG的多智能体博弈对抗算法Python实现项目源码适合计算机相关专业正在准备课程设计、期末大作业的学生以及需要强化学习实战练习的开发者。项目为个人98分期末大作业代码完整且经过调试下载后可直接运行。压缩包共13个文件以Python源码为主10个py文件涵盖MADDPG.py、DDPG.py、network.py等核心算法模块以及buffer.py、rl_utils.py等工具脚本另含配置文件与环境说明结构清晰便于二次开发。资源包仅15KB轻量易用。目前已有385人学习下载项目演示了多智能体在对抗环境中的训练与决策流程可作为理解MADDPG原理、动手实践多智能体博弈算法的参考范例。1. 拿到这份MADDPG源码别急着点运行它解决什么问题、适合谁、坑在哪晚上十一点你把刚下载的MADDPG源码包解压按README装了依赖敲下训练命令。二十分钟后你盯着终端里滚动的loss和reward发现智能体像无头苍蝇一样在环境里乱撞于是开始怀疑人生——这不是代码坏了是你还没搞明白maddpg算法的脾气。这份基于MADDPG的多智能体博弈对抗算法python实现核心解决的是多智能体强化学习里最头疼的非平稳性问题每个智能体都在变环境在别人眼里就成了移动靶。它特别适合做对抗博弈场景追击-躲避、两队对抗、合作-竞争混合模型的课程设计、算法对比实验和论文基线。如果你手里已经有一份这样的源码包这篇笔记会帮你找到入口文件、调通环境、看懂核心更新逻辑并把常见的翻车点提前排掉。2. 跑通前先把原理对齐MADDPG的核心机制、项目结构与运行入口想跑通一份多智能体源码最难的不是语法而是理解作者当初怎么想的。MADDPG的全称是Multi-Agent Deep Deterministic Policy Gradient它站在DDPG的肩膀上解决的是“多个智能体同时学习时环境不平稳”的问题。你单拿DDPG跑一个智能体没问题但两个智能体互相把对方当成环境的一部分各自更新策略时对彼此来说环境都在变Q值估计就会失真。MADDPG的思路非常直接既然你们互相干扰那就让每个智能体的Critic在训练时看到所有智能体的观测和动作而Actor执行时只看自己的观测。这就是常说的“中心化训练、去中心化执行”CTDE也是MADDPG能在博弈对抗场景站稳脚跟的根本原因。2.1 博弈对抗里为什么非要用CTDE中心化Critic和去中心化Actor的分工在多智能体对抗里你的策略好不好很大程度上取决于对手怎么动。如果你只在训练时把自己蒙在鼓里不看对手在干什么你的价值函数就是在一片迷雾里做估计梯度自然抖得厉害。MADDPG的策略是让每个智能体i都拥有自己的Actor µi和Critic Qi但Qi的输入是所有智能体的观测 {o1, o2, ..., oN} 和动作 {a1, a2, ..., aN}。这个设计让每个Critic都能“站在上帝视角”评估当前联合状态-动作的价值。测试推理时每个Actor只吃自己的观测oi决策延迟被压到最低。这样的好处是博弈对手的策略变化会被Critic感知到你的Actor更新方向会动态适应对手行为而不是在对手变化后直接崩溃。还有一个细节值得注意MADDPG在每个智能体的Critic之外还维护了其他所有智能体的策略近似模型µ_j用来在计算目标Q值时估计其他智能体的下一步动作。这就在不要求对手暴露真实策略的前提下构建了一个“预期的对手行为模型”。你可以把这一层理解为多智能体系统里最常见的博弈建模手段我不需要知道你脑子里在想什么但我训练一个对你行为的预测器照样能把你的意图纳入我的决策。2.2 解压源码包先看哪几个文件一分钟定位项目结构的检查顺序常见做法是MADDPG源码包会包含几个固定角色环境目录、算法目录、工具脚本和主入口。我拿到一份陌生源码的第一件事不是打开main.py而是先看目录树按以下顺序确认结构。# 1. 查看项目顶层结构 tree -L 2 -d # 2. 看README里声明的依赖和运行命令 head -80 README.md # 3. 看main或train入口文件是否存在 ls *.py # 4. 查看环境目录下有几套场景确认是否包含你需要的对抗场景 ls envs/这套检查顺序的核心逻辑是先搞清楚“这份代码把算法和环境分别放在哪里”再确认“作者用哪个文件作为启动入口”。很多源码包的问题在于入口文件不叫main.py而是叫train.py、run.py或者experiment.py。用tree命令把目录结构打出来再配合README里的运行命令基本能在一分钟内定位。不会先翻代码细节先找入口是所有多智能体项目的默认动作。2.3 多智能体环境的配置细节conda环境、Python版本与依赖冲突的取舍MADDPG是老牌算法公开实现大多是两三年前写的用的还是gym 0.x的API。你如果直接装最新的gymnasium大概率会在环境交互层报错。我一般会先建一个独立conda环境把版本锁死在作者声明过的范围内。conda create -n maddpg python3.8 conda activate maddpg # 按README声明安装核心依赖torch装CPU版就够训练小场景 pip install torch1.13.0 --index-url https://download.pytorch.org/whl/cpu pip install gym0.15.4 pip install numpy matplotlib tensorboard这里的版本选择要说明Python 3.8是兼容性最稳的选择因为gym 0.15和numpy 1.x系列在3.8上不会有接口报错。torch 1.13是最后一个对老代码兼容极好的版本训练这类小规模多智能体场景用CPU版就够了不必为课程设计去配CUDA。gym 0.15.4的Env接口是reset()返回obs、step()返回(obs, reward, done, info)和现在gymnasium的reset返回(obs, info)规则不一样如果你用了新版gym代码里所有环境交互位置都要改。这一步做对了后面所有训练脚本才能稳定运行这也是多智能体强化学习入门时最常被忽略的一环。3. 下载即用是起点不是终点最小复现命令、训练参数解读与日志判读源码包到手后第一个目标是在二十分钟以内跑出一组可看的训练曲线而不是直接调自己的场景。先把自带的示例环境跑通确认算法实现没有问题再去换场景。这个顺序能为你省下大量排查算法bug的时间。本节的核心是敲哪条命令能跑起来、训练参数都代表着什么、曲线怎么算正常。3.1 最小复现命令从启动训练到打开TensorBoard的完整流程MADDPG的公开实现通常支持多种场景比如simple_adversary对抗、simple_tag追击-躲避、simple_spread协作覆盖。训练入口一般会接收scenario参数和训练轮数参数。# 训练入口跑simple_adversary场景共20000个episode每500轮记录一次模型 python main.py --scenario simple_adversary --n_episodes 20000 --save_interval 500 # 训练过程输出到log目录另开终端查看训练曲线 tensorboard --logdir ./log --port 6006 # 浏览器打开 http://localhost:6006参数里的scenario指定了环境名称MADDPG源码包通常在envs目录里为每个场景准备了独立的env.py和参数配置。n_episodes是训练的总回合数20000是一个经验值太小学不出稳定策略太大会产生过拟合。save_interval控制每多少轮保存一次模型权重做课程设计时建议每500轮存一次这样后面做消融实验时有后悔药可以吃。训练完成后模型权重会保存成.pt或.pkl文件TensorBoard里的Scalar页面会显示每个智能体的平均reward曲线。3.2 训练参数解读学习率、折扣因子、软更新系数与噪声衰减如果README没写明参数含义那么你在main.py或arguments.py里大概率会看到一组hyperparameters。这里挑四个影响最直接的参数把它们的取值逻辑说清楚。参数名常见取值参数作用调整方向lr_actor1e-4 ~ 1e-2Actor的策略网络学习率训练震荡就调小学不动就调大lr_critic1e-3 ~ 1e-2Critic的价值网络学习率一般比Actor大1~10倍gamma0.9 ~ 0.99折扣因子控制远期奖励权重任务越长程gamma越接近1tau0.001 ~ 0.01目标网络软更新系数调小更稳定调大收敛更快在博弈对抗场景里gamma建议从0.95起步。如果设成0.99智能体倾向于为远期目标牺牲眼前利益但在对抗任务里近期的位置优势往往比远期收益更关键。tau的默认值0.01在多数实现里够用不建议初学者一上来就改它。判断学习率是否合适的粗暴标准是训练前500个episode如果reward曲线出现剧烈震荡且无下降趋势优先把lr_actor调到1e-4以下。多智能体强化学习对学习率的敏感度比单智能体高得多因为每个智能体的策略更新都在互相影响。3.3 训练日志怎么读reward曲线、梯度范数和策略熵的联动判读训练过程中的日志输出通常包括每轮平均reward、当前episode长度、以及每个智能体的exploration noise取值。Tensoboard里最该盯的曲线不是reward而是策略熵和梯度范数。如果reward曲线在5000轮内持续上升说明CTDE机制正常运作。如果reward卡在某条水平线上不动同时梯度范数曲线也在零附近多半是Critic的梯度传不到Actor。这时候优先检查是否在计算Actor loss时漏掉了负号——Actor的损失是负的Q值均值加错正负号会导致策略往价值低的方向进化这是多智能体强化学习里最经典的逻辑错位。噪声值随时间从0.5降到0.1是正常的如果噪声不衰减智能体永远在探索永远学不出稳定的博弈策略。学好判读日志等于给自己装了一双看清训练过程的眼睛。4. 核心代码逐段拆解经验回放、Critic中心化评估与Actor梯度回传很多拿到源码包的人会跳过算法文件直接改环境结果策略完全学不动回过头来还得重读算法。MADDPG的代码量不大核心更新逻辑集中在几个类里。本节按训练循环的先后顺序把经验回放、Critic更新、Actor更新和目标网络软更新四段核心代码拆开每段都配参数说明保证你改得动。4.1 经验回放采样多智能体经验怎样组织才能让训练稳定多智能体的经验回放与单智能体最大的区别在于每条经验必须包含所有智能体的观测和动作。你采样的时候得保证各智能体的数据在时间上对齐否则Critic在中心化评估时看到的就是错位的联合状态。# 经验回放缓冲区每条经验是一个大tuple # 包含N个智能体的观测、动作、奖励、下一观测和done标志 class ReplayBuffer: def __init__(self, buffer_size, n_agents): self.buffer_size buffer_size self.n_agents n_agents self.buffer [] self.position 0 def push(self, obs_n, act_n, rew_n, next_obs_n, done_n): # obs_n的shape: (n_agents, obs_dim) # act_n的shape: (n_agents, act_dim) if len(self.buffer) self.buffer_size: self.buffer.append(None) self.buffer[self.position] (obs_n, act_n, rew_n, next_obs_n, done_n) self.position (self.position 1) % self.buffer_size def sample(self, batch_size): # 随机采样batch_size条经验 batch random.sample(self.buffer, batch_size) # 把每条经验按“时间步”堆叠成batch obs_n torch.tensor(np.array([exp[0] for exp in batch]), dtypetorch.float32) act_n torch.tensor(np.array([exp[1] for exp in batch]), dtypetorch.float32) rew_n torch.tensor(np.array([exp[2] for exp in batch]), dtypetorch.float32) next_obs_n torch.tensor(np.array([exp[3] for exp in batch]), dtypetorch.float32) done_n torch.tensor(np.array([exp[4] for exp in batch]), dtypetorch.float32) return obs_n, act_n, rew_n, next_obs_n, done_n这里的关键点是把obs_n组织成shape为(batch_size, n_agents, obs_dim)的张量而不是把单个智能体的数据混在一起。后面的Critic网络会把这个三维张量flatten后作为输入所以保持第一维是batch、第二维是智能体编号的顺序很重要。如果你在改造这份源码时遇到了“维度对不上”的报错先检查这里的数据组织方式。buffer_size一般取1e6batch_size取1024太大的batch会让更新变慢太小的batch会让梯度噪声增大。4.2 Critic的中心化评估为什么Q值要吞下所有人的观测和动作每个智能体的Critic网络负责估计Q_i(o1, a1, o2, a2, ..., oN, aN)也就是在给定全局状态信息的情况下评估智能体i当前策略的价值。这个Q值会同时用于训练Actor和更新自身。# 智能体i的Critic更新 def update_critic(self, replay_buffer, agents, batch_size): obs_n, act_n, rew_n, next_obs_n, done_n replay_buffer.sample(batch_size) # 目标Q值用目标网络计算 with torch.no_grad(): # 先让所有智能体的target_actor根据next_obs输出下一时刻动作 next_act_n [agents[j].target_actor(next_obs_n[:, j, :]) for j in range(self.n_agents)] # 拼接成与act_n相同形状 next_act_n torch.cat(next_act_n, dim1) # target_critic吃全局next_obs和全局next_act target_q self.target_critic(next_obs_n.view(batch_size, -1), next_act_n) # 计算目标价值done为1时不再考虑未来奖励 target_q rew_n[:, self.agent_id].unsqueeze(1) self.gamma * target_q * (1 - done_n[:, self.agent_id].unsqueeze(1)) # 当前Q值当前critic吃当前全局观测和全局动作 current_q self.critic(obs_n.view(batch_size, -1), act_n) # MSE损失反向传播更新critic参数 critic_loss nn.MSELoss()(current_q, target_q) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step()这段代码是MADDPG的核心主干它完成了中心化Critic的训练闭环。计算target_q时用target_actor和target_critic是为了让目标值尽量稳定用done标志把终止状态后的未来奖励置零是强化学习的标准套路。特别要注意的是在计算next_act_n时每个target_actor只吃自己的next_obs拼接后再交给target_critic这个流程体现了“去中心化执行、中心化评估”的分工。如果你把next_act_n用当前actor算出来会引入非平稳目标训练大概率会发散。4.3 Actor的策略梯度梯度从Critic反传到Actor的链路拆解Actor的目标是让Critic给自己打的分尽可能高。它不需要直接计算奖励而是借助Critic的Q值作为导引信号更新自己的策略参数。这是DDPG家族算法的精髓也是多智能体强化学习最容易理解出错的地方。# 智能体i的Actor更新 def update_actor(self, replay_buffer, agents, batch_size): obs_n, act_n, rew_n, next_obs_n, done_n replay_buffer.sample(batch_size) # 当前智能体的actor基于自己的观测输出动作 curr_act self.actor(obs_n[:, self.agent_id, :]) # 拼接其他智能体用buffer里的历史动作本智能体用actor新输出的动作 # 这是因为要计算Q对“自己动作”的梯度 act_n_for_critic act_n.clone() act_n_for_critic[:, self.agent_id, :] curr_act # 让critic对拼接后的联合动作打分 policy_q self.critic(obs_n.view(batch_size, -1), act_n_for_critic) # Actor的损失是负Q值均值让梯度上升方向指向Q变大 actor_loss -policy_q.mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step()这个更新过程的关键在于“只替换自己的动作保留别人的动作”这个操作。在计算Q对自身动作的梯度时必须保证其他智能体的动作是固定值否则梯度会顺着别人的动作参数传播导致更新方向混乱。act_n_for_critic就是为这一步服务的。如果actor_loss的符号写反策略会反向进化表现为累计奖励快速下降。这段代码在后续做多智能体系统改造时也最常被修改——比如你想让两个智能体共享Critic时改动点就在这里的拼接逻辑。4.4 目标网络软更新tau参数在多智能体环境里的微妙作用目标网络的作用是给Q值更新提供一个稳定靶子MADDPG里对每个智能体额外维护一套target_actor和target_critic。软更新的实现极其简单但对训练稳定性的影响远超直觉。# 软更新target网络参数tau是软更新系数 def update_target(self, tau): for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data) for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data)tau取0.01时每个训练步目标网络只向当前网络移动1%需要大约几百步才能完成一次显著偏移。这种缓慢追随的特性在多智能体对抗里尤其重要因为所有智能体的target网络都在同步移动如果tau设得太大目标Q值会跟着策略抖动训练直接变成布朗运动。常见的调参经验是如果你把训练循环改为多线程异步采集数据tau要降到0.001或更低否则数据分布的非平稳性会成倍放大。软更新代码虽然只有三行却是整个算法稳定性的压舱石。5. 训练不收敛、策略退化这五个高频坑与排查顺序再优秀的MADDPG实现落到自己的机器上都有可能翻车。这里整理了我见过最多的五个问题全部按“现象 → 原因 → 解决”的结构来写方便你对号入座。5.1 现象Critic的loss在下降但Actor的reward纹丝不动这是多智能体强化学习里最迷惑人的场景损失函数正常下降智能体却没有变聪明。原因通常是Critic产生了“过于乐观的估计”它给Actor打分的标准在自我强化中失真导致Actor即使按照错误方向更新Q值仍然在下降。解决办法是优先检查reward是否做了归一化处理。如果reward量级超过10Critic很容易在初始阶段就拟合出偏大的Q值然后带着Actor往错误方向走。我一般会把reward除以一个常数让单步奖励落在[-1, 1]区间内再重新训练效果立竿见影。5.2 现象训练刚开始几百个episodereward就跌到历史最低点这种早期崩溃常见于经验回放buffer刚被填满的时刻。当第一条经验还在被反复采样时如果buffer里的经验里存在大量“极端状态”Critic会瞬间过拟合然后给所有状态打低分策略直接退化。解决办法是把随机探索的噪声初始值调大比如0.5让智能体前期尽量多探索不同状态保证buffer里的数据有足够多样性。还有一招是把batch_size调大让每次更新看到的样本更丰富减少单条极端样本的影响力。5.3 现象训练时reward曲线正常上涨一关闭探索噪声就“装死”测试阶段去掉噪声后智能体站在原地不动或走直线这叫“策略退化”。原因是训练时依赖噪声探索Actor自身输出的动作分布太窄去掉噪声后无法应对观测中的微小偏移。解决方法是训练最后阶段让噪声线性衰减到接近0迫使Actor学会在没有噪声的情况下也能给出合理动作。同时检查Actor输出层是否用了tanh激活MADDPG的连续动作空间通常要求输出落在[-1,1]tanh激活能保证动作边界稳定。5.4 现象换了一个环境场景训练曲线完全学不上去多智能体系统的环境差异极大simple_adversary能收敛的参数换到simple_tag追击-躲避里可能完全失效。原因多半是动作空间和观测空间维度变了但网络结构和超参数没跟着调。检查方法很简单打印一下新环境的obs_dim和act_dim如果维度比原先大一倍以上需要把Actor和Critic的隐藏层节点数从64提升到128或256。正常情况下MADDPG对环境的适应能力体现在“机制不变调参即可”你要是跨任务还指望同一套参数直接出结果大概率是在赌运气。5.5 现象两个智能体训练时各自都能学到策略放一起就互相干扰这是博弈对抗场景的典型问题单独训练一个智能体没问题放进多智能体环境就失效。原因极为常见训练时没有固定随机种子或者不同智能体的经验回放buffer共享导致数据串扰。解决方法是所有智能体各自维护独立的ReplayBuffer并在训练脚本里给环境、模型初始化、采样器都设好统一的seed。还要检查内存共享问题如果多个智能体共用一份buffer的引用而不是复制后一个智能体更新参数时会无意间篡改前一个智能体的训练数据。这个坑很隐蔽但检查起来并不难打印一下各智能体buffer的id是否一致就能确认。6. 从跑通到改出你自己的博弈场景环境替换三步法和一个验证指标现在你已经拥有了一套能跑通的MADDPG实现接下来最有可能的需求是把自带场景换成你自己的博弈对抗环境。这里有一套屡试不爽的三步替换法能帮你把翻车概率降到最低。第一步写一个与原有环境同构的自定义环境类。你需要实现reset()返回所有智能体的初始观测列表step(actions)返回新的观测列表、奖励列表和done列表。这些方法的输入输出格式必须与原环境完全一致否则训练脚本里的所有环境交互代码都要改。第二步改场景参数文件。MADDPG源码包里通常有一个场景配置文件或环境构造参数你要把观测维度、动作维度、智能体数量、最大步数都改成新环境的真实值。第三步先关掉所有智能体的学习能力只让随机策略跑100个episode确认环境能正常交互、reward不会报错再打开学习开关跑完整训练。验证指标我用的是“对抗随机策略的胜率曲线”每训练500个episode就让当前策略去跟一个随机策略、一个固定规则策略各打50场计算胜率。胜率曲线比reward曲线更能反映博弈能力因为reward容易被奖励塑形中的小漏洞欺骗而胜率直接告诉你策略在实际对抗中赢没赢。训练20000个episode后如果胜率稳定在85%以上说明你的多智能体博弈对抗算法已经具备实战意义。按这套流程改完三个环境你会发现自己对MADDPG的理解比刷十遍论文都扎实。我个人踩过最深的坑是把全部精力花在调超参数上最后才发现是环境返回的观测里混进了不该有的未来信息导致智能体在训练时“作弊”成功、测试时原形毕露。所以我的习惯是拿到任何新环境先打印一步完整交互数据自己盯着看一眼观测里到底有什么再决定奖励函数怎么写。这份多智能体项目源码只是起点真正值钱的是你把算法迁移到新场景时踩过的那一圈坑。希望这篇笔记能帮你少走几步弯路。本文还有配套的精品资源点击获取