
简介基于MADDPG的多智能体博弈对抗算法Python实现项目源码是一份面向计算机相关专业学生的毕业设计、期末大作业优质参考项目也适合希望进行项目实战练习的强化学习开发者。内容覆盖多智能体强化学习核心模块包括MADDPG与DDPG算法实现、经验回放与网络结构、多智能体交互环境工具包以及完整训练与测试脚本结构清晰可帮助读者快速搭建属于自己的博弈对抗实验框架。资源包共14个文件以10个Python源码文件为主辅以配置文件、环境压缩包与说明文档整体仅1.6MB便于下载和本地运行。这一项目经导师指导并认可评审获得98分编写规范性好注释清晰能提供完整的课程设计或毕业设计参考。目前已有189人学习下载既可作为期末大作业也可作为毕业设计的基础源码尤其适合正在完成多智能体课题的同学通过阅读代码可掌握多智能体强化学习的训练流程、超参数调整与结果分析思路。1. 基于MADDPG的多智能体博弈对抗算法Python项目源码先理解为什么是这个框架多智能体博弈对抗算法的Python实现项目源码绕不开MADDPG。两个智能体在同一个环境里互相博弈时如果用单智能体DDPG各自训练大概率会看到一方刚学会一点策略另一方策略一变之前学到的全作废。MADDPG的核心思路是Centralized Training with Decentralized Execution训练时每个智能体的评价网络能看到所有人的观测和动作执行时每个智能体只用自己的观测做决策。这样一来环境对单个智能体不再像一个随时变化的黑匣子。适合的场景比如无人机攻防、追逃博弈、双人对抗游戏都需要这类多智能体连续动作空间算法。这篇文章从原理、环境搭建、源码组织、参数调节到踩坑按一个能实际跑出对抗结果的项目来拆。2. MADDPG核心机制拆解CTDE、经验回放和目标网络怎么配合2.1 CTDE架构为什么能让多智能体收敛评论家看全局演员只看自己MADDPG的每一个智能体都由一个actor网络和一个critic网络组成。actor只接收自己的观测输出连续动作critic在训练阶段接收所有智能体的观测和动作输出当前智能体的Q值。关键点在于critic拥有全局信息所以它估计出的价值函数不随其他智能体策略改变而成为一个移动靶这就是非平稳性问题被缓解的原因。用追逃博弈举例子。追击者和逃逸者分别有自己的actor和critic。训练追击者的actor时它的梯度来自自己的critic这个critic的输入是所有智能体的观测和动作因此追击者actor的更新会显式考虑逃逸者当前动作的影响。如果改成单智能体DDPG追击者只能用自己的观测和动作估计Q值逃逸者策略一变Q值含义就变了训练自然不稳定。实现这个结构时最常见做法是每个智能体独立保存actor、critic、目标actor、目标critic四个网络。但项目里也有简化版本两个智能体共享同一个critic因为对抗博弈的奖励往往是零和。我一般不建议默认共享critic。只要奖励不是严格对称的共享critic会让某个智能体的价值估计被另一个智能体的局部特征污染。具体到网络输入维度critic的输入长度是sum(obs_dim) sum(act_dim)两个智能体各有4维观测、2维动作时critic输入就是12维。别小看这个拼接后面源码里很多崩溃问题都出在维度对不上。2.2 经验回放与目标网络三个必须一起调的部件MADDPG延续了DDPG的经验回放机制。但多智能体的经验样本不是一个agent单独存的tuple而是所有agent在同一时刻的状态转移元组(obs_all, actions_all, rewards_all, next_obs_all, done)。每个agent的训练从同一个batch里取数据不同agent只是用其中属于自己的奖励和观测。回放缓冲区大小直接影响收敛。开太大样本里混入太旧策略的transitionagent会同时拟合多个策略下的价值开太小样本相关性过强训练容易震荡。做双人对抗时我一般设buffer_size100000如果任务更复杂再往上加但不会超过500000。目标网络的更新方式也是必调项。MADDPG原版用soft updatetarget_param tau * source_param (1 - tau) * target_param。tau不能拍脑袋设。设0.01目标网络基本稳定适合大部分博弈任务设到0.1目标网络跟踪太快等价于没有目标网络critic loss容易炸设到0.001目标网络太迟钝训练后期会发现当前Q值不断上升但评估结果不动。还有一个容易被忽略的动作噪声。DDPG原版用OU噪声MADDPG实践里很多源码直接用高斯噪声因为连续动作空间下高斯噪声更简单。噪声标准差早期设0.2左右后期衰减到0.05以下。如果噪声一直过大actor学到的策略会被随机扰动掩盖评估时看到的是策略抖动而不是稳定对抗行为。2.3 博弈对抗中的奖励设计奖励是灵魂不只是打分同一套MADDPG代码换一个奖励函数对抗行为完全不同。追逃博弈里有三种常见奖励设计。第一种是零和追击者碰到逃逸者得1逃逸者得-1逃逸者跑出边界则反过来。这种设计让双方最大化自身收益对抗性强。第二种是稀疏大奖励加稠密小惩罚。比如追击者每一步都有一点小的负奖励-0.01促使它不要原地转圈。第三种是混合奖励追击者不仅因为碰撞得1还因为离逃逸者更近获得0.1的距离差奖励。第三种设计训练更快但要注意不要让稠密距离奖励主导行为否则追击者只会追直线不会学习包抄。在这类Python项目里我习惯把奖励计算单独放到环境类的_get_reward方法中而不是塞在训练脚本里。因为奖励调整频率很高放在环境里方便换配置跑对照实验。MADDPG对奖励尺度很敏感奖励值最好保持在[-1, 1]区间附近否则critic的Q值目标会很大回归容易发散。3. 动手搭建博弈对抗环境追逃场景的完整Python实现3.1 环境代码连续动作空间、观测空间与奖励函数开始写环境前先确认你的Python环境能跑起numpy和gymnasium。新手经常卡在import gymnasium报错大概率是没装库在vscode里配置python环境时注意选对虚拟环境否则pip安装的包和你运行脚本的解释器不是一个。装依赖直接执行pip install numpy gymnasium matplotlib torch如果网络慢可以换国内镜像源但这属于环境问题不影响算法本身。下面是一个适合MADDPG训练的追逃博弈环境动作是连续加速度控制观测是相对位置和速度import numpy as np from gymnasium.spaces import Box class PursuitEvasionEnv: def __init__(self, world_size5.0, max_steps100): self.world_size world_size self.max_steps max_steps self.collision_dist 0.2 self.max_accel 0.5 # 两个智能体动作都是二维连续量x方向和y方向的加速度 self.action_space [ Box(low-1.0, high1.0, shape(2,), dtypenp.float32) for _ in range(2) ] # 观测相对位置(2维) 自身速度(2维) self.observation_space [ Box(low-np.inf, highnp.inf, shape(4,), dtypenp.float32) for _ in range(2) ] self.step_count 0 def reset(self): self.step_count 0 self.pursuer_pos np.array([-4.0, 0.0], dtypenp.float32) self.evader_pos np.array([4.0, 0.0], dtypenp.float32) self.pursuer_vel np.zeros(2, dtypenp.float32) self.evader_vel np.zeros(2, dtypenp.float32) return self._get_obs(), {} def step(self, actions): p_act np.clip(actions[0], -1.0, 1.0) e_act np.clip(actions[1], -1.0, 1.0) # 加速度控制速度速度再做简单摩擦衰减 self.pursuer_vel p_act * self.max_accel self.evader_vel e_act * self.max_accel self.pursuer_vel * 0.9 self.evader_vel * 0.9 self.pursuer_pos self.pursuer_vel self.evader_pos self.evader_vel self.step_count 1 dist np.linalg.norm(self.pursuer_pos - self.evader_pos) reward [0.0, 0.0] done False info {dist: dist} if dist self.collision_dist: # 追击成功零和奖励 reward[0] 1.0 reward[1] - 1.0 done True elif self.step_count self.max_steps: # 超时没有人抓住逃逸者算小胜 reward[0] - 0.1 reward[1] 0.1 done True elif np.abs(self.evader_pos[0]) self.world_size or np.abs(self.evader_pos[1]) self.world_size: # 逃逸者跑出边界 reward[0] - 1.0 reward[1] 1.0 done True return self._get_obs(), reward, done, info def _get_obs(self): # 追击者观测逃逸者相对自己位置 自己速度 obs0 np.concatenate([ self.evader_pos - self.pursuer_pos, self.pursuer_vel ]).astype(np.float32) # 逃逸者观测追击者相对自己位置 自己速度 obs1 np.concatenate([ self.pursuer_pos - self.evader_pos, self.evader_vel ]).astype(np.float32) return [obs0, obs1]这段代码有几个参数对训练影响很大。max_accel0.5控制智能体能多快改变速度设太小会让追击者看起来反应迟钝设太大则动作输出稍有不慎就会冲过头训练更难。vel * 0.9是速度衰减相当于摩擦阻力没有这个衰减智能体会一直加速到一个极端值动作空间的存在意义会被削弱。world_size5.0也需要和初始距离匹配初始位置在-4和4正好留出一段可周旋空间。奖励设计上这里用的是零和博弈追击者赢逃逸者输。注意没有给“接近目标”给额外奖励因此这是纯稀疏奖励。MADDPG能跑通但收敛会慢。如果你想训练更快可以在_get_reward里加一段距离差奖励但要控制系数。3.2 训练主循环从交互采样到经验入库环境就绪后训练主循环要做四件事选动作、环境交互、样本入库、策略更新。选动作时要用带噪声的策略噪声来自于actor输出的确定性动作叠加高斯噪声。import numpy as np import torch def select_action(agent, obs, noise_scale): obs_t torch.tensor(obs, dtypetorch.float32).unsqueeze(0) action agent.actor(obs_t).detach().numpy()[0] noise noise_scale * np.random.randn(*action.shape) return np.clip(action noise, -1.0, 1.0) for episode in range(max_episodes): obs, _ env.reset() episode_reward np.array([0.0, 0.0]) noise_scale max(0.05, initial_noise * (noise_decay ** episode)) for t in range(env.max_steps): actions [ select_action(agents[i], obs[i], noise_scale) for i in range(env.action_space.__len__()) ] next_obs, rewards, done, info env.step(actions) # 多智能体经验必须是同步的整体样本 replay_buffer.add((obs, actions, rewards, next_obs, done)) obs next_obs episode_reward np.array(rewards) if len(replay_buffer) batch_size: for _ in range(update_steps): update_all(agents, replay_buffer, batch_size) if done: breakselect_action里的noise_scale每一轮都会衰减但设置了max(0.05, ...)下限保证后期不会完全变成确定性策略。经验回放时传入的obs和actions都是列表每个元素对应一个智能体在回放缓冲区里要把它们转换成整批张量维度为(batch_size, num_agents, obs_dim)。训练主循环中的update_steps通常设为1到10。如果每步只更新一次训练稳定但慢多更新几次能更高效利用样本但critic和actor的更新次数不平衡会导致价值估计震荡。先用1跑通再逐步加大。3.3 为什么用底层层层实现而不是直接套现成引擎很多人在Python项目里直接用PettingZoo或Multi-Agent Particle Environment确实省事。但自己做博弈对抗研究或毕业设计源码时我建议自己写环境。原因有两点。第一现成环境的奖励和观测接口是固定的你想换成追击者视野受限、障碍物遮挡这些博弈要素改动成本反而高。第二MADDPG训练过程需要频繁可视化调试自建环境可以随时把智能体位置、速度、碰撞距离打印出来现成环境不好插手。当然环境接口设计要兼容类似PettingZoo的约定reset返回初始观测step返回(obs, reward, done, info)。这个约定PyTorch强化学习生态都认后续如果要换用其他多智能体算法比如MAPPO环境可以不用改。4. MADDPG项目源码的组织方式模块划分、配置参数与更新逻辑4.1 项目文件结构每个文件负责什么一个好的MADDPG Python项目源码不会把所有代码塞进一个训练脚本。下面这个结构是我常用的组织方式适合扩展到更多智能体和更复杂的博弈环境maddpg_project/ ├── config.py # 超参数集中管理 ├── envs/ │ ├── __init__.py │ └── pursuit_evasion.py # 博弈环境 ├── agents/ │ ├── __init__.py │ ├── actor.py # actor网络定义 │ ├── critic.py # critic网络定义 │ └── maddpg_agent.py # 单个智能体封装 ├── replay_buffer.py # 经验回放缓冲区 ├── train.py # 训练入口 └── evaluate.py # 评估入口config.py集中管理所有超参数这是减少翻车的关键。不要在每个模块里直接写死参数否则调参时要改多个文件。actor网络和critic网络单独成文件便于替换网络结构比如把全连接层改成LSTM或加注意力机制。maddpg_agent.py封装一个智能体内部持有actor、critic以及它们各自的目标网络。train.py负责训练主循环evaluate.py独立出来保证评估代码不会被训练逻辑干扰。4.2 更新逻辑代码actor和critic的交替更新update_all是MADDPG源码里最需要写对的地方。每个智能体的critic用自己的目标网络计算目标Q值并更新然后actor利用当前critic对自身动作的梯度更新。import torch import torch.nn.functional as F def update_all(agents, replay_buffer, batch_size, gamma, tau): obs, actions, rewards, next_obs, done replay_buffer.sample(batch_size) # shape: [batch, num_agents, obs_dim] for i, agent in enumerate(agents): # 1. 所有智能体基于next_obs生成目标动作 with torch.no_grad(): next_actions torch.stack([ a.target_actor(next_obs[:, j]) for j, a in enumerate(agents) ], dim1) target_q rewards[:, i:i1] gamma * (1 - done) * agent.target_critic(next_obs, next_actions) # 2. 更新critic current_q agent.critic(obs, actions) critic_loss F.mse_loss(current_q, target_q.detach()) agent.critic_opt.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(agent.critic.parameters(), 0.5) agent.critic_opt.step() # 3. 更新actor注意只替换第i个智能体的动作 new_actions actions.clone().detach() new_actions[:, i] agent.actor(obs[:, i]) actor_loss -agent.critic(obs, new_actions).mean() agent.actor_opt.zero_grad() actor_loss.backward() agent.actor_opt.step() # 4. 软更新目标网络 soft_update(agent.target_critic, agent.critic, tau) soft_update(agent.target_actor, agent.actor, tau) def soft_update(target, source, tau): for tp, sp in zip(target.parameters(), source.parameters()): tp.data.copy_(tau * sp.data (1.0 - tau) * tp.data)这里最容易被忽略的是new_actions[:, i] agent.actor(obs[:, i])。如果直接把actions里所有智能体的动作参与actor梯度反传当前actor的梯度会穿过其他智能体的动作导致每个智能体都在调整别人动作对自身价值的影响。正确做法是只让第i个智能体自己的动作可微其他动作全部detach这样actor更新方向才完全由自己的策略决定。clip_grad_norm_这行是实战经验。MADDPG的critic输入维度高梯度很容易爆炸不裁剪的话训练到一半loss就飞了。先固定用0.5如果训练不稳再降到0.1。4.3 必调参数表不同参数在实战里的表现和边界MADDPG参数众多但真正决定项目源码能不能出结果的集中在这几个。参数推荐范围实战说明actor_lr1e-4 ~ 3e-4调大容易看到前期reward波动但也会让actor在critic不准时乱跳critic_lr1e-3 ~ 3e-3通常比actor_lr大几倍critic需要先收敛出稳定的价值估计gamma0.95 ~ 0.99追逃博弈0.95够用超时惩罚存在时不需要太长远视野tau0.01 ~ 0.050.01最稳0.05会加快目标网络跟踪但增加发散风险batch_size256 ~ 2048小batch适合探索大batch训练稳定但critic输入维度高时显存压力大noise_std0.1 ~ 0.3越大越探索但过大导致样本全是随机动作buffer_size50000 ~ 500000太大样本过旧太小样本相关性过强update_steps1 ~ 10每步交互后的更新次数先设1稳定后慢慢加gamma在带惩罚的任务里不要太高。如果逃逸者每拖延一步都有小负奖励gamma过高会让策略只盯远期结果忽略当前被抓住前的逃脱效率出现“宁可超时也不冒险”的诡异行为。actor_lr和critic_lr的比值也很关键。我见过很多项目崩溃在actor_lr和critic_lr设成一样大这样actor更新速度快于环境反馈训练曲线在高频震荡但loss还在下降最迷惑人。建议先跑一遍固定随机种子对比不同lr组合的loss曲线。5. 实战踩坑与排查MADDPG不收敛时先查这5个地方5.1 现象actor loss在下降但对抗胜率完全不动这种情况最容易骗人。训练日志里actor loss逐渐走低看起来策略在优化但用固定规则对手评估时胜率一直停在初始水平。原因通常是评估时没有去掉探索噪声或者评估的对手策略本身就是随机策略导致actor学到的针对性动作被随机性掩盖。另一个原因是奖励太稀疏actor loss下降只是因为它学会了把动作输出到某个固定范围而不是真正学会了追捕。解决时我一般会写一个单独的评估脚本固定torch.manual_seed评估时把噪声设为0使用actor输出直接作为动作。对手要固定要么用写死的规则策略要么用一个训练了一半的“冻结”模型。绝不能一边训练一边评估同一个正在更新的agent否则每次评估的对手都在变胜率永远是噪声。5.2 现象critic loss爆炸数值一下到几百MADDPG源码里最常翻车的点。critic的输入是全智能体观测和动作拼接Q值函数是一个高维回归问题reward一旦不在稳定尺度内target_q很容易被推到一个极端值。比如奖励写成了10、-10并且没有对target_q做裁剪critic要拟合的target会越来越大形成正反馈。解决思路分三步。第一步把奖励缩放到[-1, 1]范围。第二步在update_all里加梯度裁剪clip_grad_norm_(critic.parameters(), 0.5)。第三步是把tau调到0.005或0.01避免target网络追踪太快。还有一个血泪经验不要忘了target_q用with torch.no_grad()包住。如果混进当前计算图每次更新都会把梯度和历史纠缠在一起几个小时训练直接报废。5.3 现象样本同质化所有transition都像复制粘贴当两个智能体初始位置固定、噪声较小、探索不足时replay buffer里存下的样本高度相似。训练出来的actor只会应对一种局面一旦改变初始位置就完全失效。多智能体对抗项目源码里这个问题很隐蔽因为训练时reward在涨评估时一换起点就崩。解决把追逃初始位置随机化比如追击者从[-4, 0]附近随机漂移0.5逃逸者同理。同时噪声下限不要设成0保留一点随机性让buffer里的状态覆盖更广。你也可以尝试每1000个episode评估一次多种初始位置下的胜率胜率方差大就是样本多样性不足最直接的表现。5.4 现象噪声太大训练前期全是随机碰撞一开始把高斯噪声标准差设成0.5甚至1.0actor的输出完全被噪声覆盖环境里全是随机动作。这种情况等同于用随机策略采样策略梯度算出来的方向是残缺的。更麻烦的是replay buffer里沉积了大量随机样本要花很多step把旧样本稀释掉。解决初始噪声不要超过0.3并且在少量预热episode里只用随机策略填充buffer后续再切换成actor策略加小噪声。预热量不用多200~500个episode即可。等到噪声衰减到0.05时才算进入稳定优化阶段。5.5 现象训练重启后无法恢复之前的战绩MADDPG项目源码跑久一定会遇到中断。如果没有保存actor和critic的完整状态重启后只能从头训练这是最痛苦的“后悔药”缺失。问题往往不只是没保存而是保存了actor却漏了target网络和optimizer的state恢复后目标网络和当前网络差距过大训练行为完全改变。解决每个指定episode保存一次checkpoint包含actor、critic、target_actor、target_critic、两个optimizer的state_dict以及当前噪声系数。保存文件名里写上episode或评估胜率方便回滚。我一般同时保留latest.pth和best.pth其中best按每100个episode的评估胜率更新这样即使训练过程走了弯路也有后悔药可吃。6. 从跑通到出结果评估指标、轨迹可视化和权重保存技巧模型训练完真正拿出来说服人的不是一段loss曲线而是对抗场景里的实际表现。建议先跑一次100局评估使用确定性动作统计追击者的胜率和平均步数。def evaluate(env, agents, episodes100): wins 0 total_steps 0 for _ in range(episodes): obs, _ env.reset() for t in range(env.max_steps): actions [ agents[i].actor(torch.tensor(obs[i], dtypetorch.float32)).detach().numpy() for i in range(len(agents)) ] obs, reward, done, info env.step(actions) if done: if reward[0] reward[1]: wins 1 total_steps t 1 break return wins / episodes, total_steps / episodes胜率只能说明“赢没赢”无法解释“怎么赢的”。我习惯在评估时记录每个智能体的位置序列用matplotlib把追击者和逃逸者的轨迹画出来。如果追击者每次都沿着直线逼近说明策略还没学会拦截如果轨迹出现明显切角说明actor真的学到了博弈行为。轨迹可视化还能帮你发现奖励陷阱比如追击者在边界兜圈子而不是主动碰撞。权重保存上我的固定套路是每500个episode保存一次checkpoint并且只保留最近10份。训练结束前用保存的best.pth重新加载actor权重再做一次完整评估。这个习惯救过我很多次因为训练末尾的web—最后几步往往不如中期那个峰值。最后提醒一句MADDPG这类多智能体算法对随机种子很敏感换一个种子结果可能明显不同复现别人的结果或向别人交付项目时一定把随机种子放进config里固定下来。希望帮到你。本文还有配套的精品资源点击获取