2026/10/26 17:26:07

MADDPG多智能体博弈算法Python源码解析与实战训练指南

MADDPG多智能体博弈算法Python源码解析与实战训练指南 简介基于Python的MADDPG多智能体博弈对抗算法实现属于机器学习与强化学习领域完整涵盖DDPG与MADDPG核心训练逻辑、神经网络结构构建、经验回放机制、探索策略及独立测试脚本并集成ma-gym多智能体环境库与可视化演示模块面向人工智能、自动化、通信工程、电子信息等专业的高校学生、科研工作者和行业从业者适用于毕业设计、课程设计、项目初期演示及算法对比验证。压缩包内共91个文件主体为76个Python源码另有ipynb交互式笔记本、GIF实验录屏、配置文件与说明文档整体仅3.26MB轻量且目录清晰。目前已有92人学习或浏览具备一定参考价值。资源除完整可运行的算法代码外还提供多智能体对抗场景案例、环境封装接口及训练结果动画可直观呈现智能体学习与策略演化过程同时附带基础配置说明和调试指引帮助使用者快速搭建实验环境也便于二次开发与算法改进。1. MADDPG 多智能体博弈对抗算法这份 Python 源码能跑出什么MADDPGMulti-Agent Deep Deterministic Policy Gradient是目前做多智能体博弈对抗绕不开的一版基线算法。很多第一次跑多智能体强化学习的人都会遇到一个反直觉的现象同一套 DDPG 代码在单智能体环境里收敛正常改成双方对战之后训练几千回合也不涨分。原因不是代码写错了而是对手策略在变环境对单个智能体来说始终是“非平稳”的。这份资源就是针对这个问题的 Python 实现它把 MADDPG 算法源码和 ma-gym 环境库打包在一起包含 combat、predator_prey、switch、checkers 等多个对抗场景还带 main.py、test.py 和实验结果数据。适合正在做强化学习课设、多智能体博弈方向毕设的计算机相关专业学生也适合想快速验证 MADDPG 基线效果的工程师和研究者。2. 从 DDPG 到 MADDPG为什么训练时要开全局视角2.1 多智能体博弈为什么让单智能体算法失灵单智能体 DDPG 的训练逻辑其实很简洁actor 根据观测输出动作critic 给这个动作打分经验回放池把历史数据打散然后随机采样更新。这套逻辑在单智能体环境里没问题因为环境状态转移只取决于自己的动作数据分布相对稳定。但到了多智能体博弈里状态转移概率还取决于对手的动作对手策略一变你过去积累的旧经验就变成“过期数据”用它们去更新 Q 值会把估计带偏训练自然就卡住。MADDPG 的解法是在训练时引入全局视角每个智能体的 critic 输入所有智能体的观测和动作相当于让评论员站在“上帝视角”做价值判断而 actor 仍然只输入自己的局部观测执行时不需要依赖别人信息。这就是常说的 CTDE集中训练、分布执行。这个设计改动很小但正好解决了环境非平稳带来的 Q 值高估问题因此在博弈对抗场景里比独立训练 DDPG 稳定得多。2.2 源码组织从 buffer.py 到 network.py一条数据怎么流转这份资源解压后的结构大致是这样MADDPG/ ├── MADDPG.py # 主算法MADDPG 训练与更新逻辑 ├── DDPG.py # 单智能体 DDPG用于对照实验 ├── network.py # Actor / Critic 网络定义 ├── buffer.py # 多智能体经验回放池 ├── rl_utils.py # 噪声、奖励处理等工具函数 ├── main.py # 训练主入口 ├── test.py # 加载模型跑验证 ├── test_env.py # 环境冒烟测试 └── ma-gym-master/ # 多智能体环境库 └── ma_gym/envs/ ├── combat/ ├── predator_prey/ ├── switch/ ├── checkers/ ├── traffic_junction/ ├── lumberjacks/ └── pong_duel/先看 buffer.py。与单智能体最大的区别是一条经验记录里存的是所有智能体的联合数据而不是单智能体各自的独立数据# buffer.py 里一条经验记录的核心结构 experience { obs: obs_all, # shape: [n_agents, obs_dim] act: act_all, # shape: [n_agents, act_dim] rew: rew_all, # shape: [n_agents, 1] next_obs: next_obs_all, # shape: [n_agents, obs_dim] done: done # 全局终止标志 }这里必须用联合数据因为 MADDPG 的 critic 更新时要拿到全体观测和全体动作去计算 target Q。如果把经验拆成单智能体维度训练时就拼不齐联合信息了。我在自己复现时最先看这个文件因为只要这里存错维度后面网络一跑就报 shape mismatch。再看 network.py。Actor 和 Critic 的结构通常是这样的class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden64): super().__init__() self.fc1 nn.Linear(obs_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.out nn.Linear(hidden, act_dim) def forward(self, x): x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return torch.tanh(self.out(x)) class Critic(nn.Module): def __init__(self, obs_dim, act_dim, n_agents, hidden64): super().__init__() # 输入维度 所有智能体的观测 所有智能体的动作 self.fc1 nn.Linear(n_agents * obs_dim n_agents * act_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.out nn.Linear(hidden, 1) def forward(self, obs_all, act_all): x torch.cat([obs_all, act_all], dim-1) x torch.relu(self.fc1(x)) return self.out(self.fc2(x))注意 Actor 的输出层用了 tanh 激活把动作压缩到 [-1, 1] 区间这是 DDPG 系列处理连续动作的常见做法。但 ma-gym 里的环境大多是离散动作所以外部还需要做一次动作映射这个映射如果做错训练会直接不收敛后面避坑章节会专门讲。2.3 三行核心更新公式critic 和 actor 分别在学什么MADDPG.py 里最核心的是更新代码拆开看逻辑并不复杂# 1. 先算 target Q使用 target 网络输入所有智能体的下一步动作 with torch.no_grad(): next_actions [] for i in range(n_agents): next_actions.append(target_actors[i](next_obs_all[i])) next_actions torch.cat(next_actions, dim-1) target_q target_critics[i](next_obs_all, next_actions) y reward_i gamma * (1 - done) * target_q # 2. 更新 Critic让当前 Q 值逼近 target Q critic_loss F.mse_loss(critics[i](obs_all, torch.cat(actions, dim-1)), y) # 3. 更新 Actor最大化当前 Critic 对“自己当前策略”的打分 actor_loss -critics[i](obs_all, torch.cat(actions_with_current_actor, dim-1)).mean()第一段代码里 target Q 的计算关键点在于 next_actions 用的是 target actor 的输出不是行为策略的动作。第二段更新 critic 时输入是采样时的真实联合动作。第三段更新第 i 个智能体的 actor 时联合动作里除了第 i 个智能体的动作来自当前 actor其余智能体的动作保持采样时的动作不变。这个细节很隐蔽如果图省事把全部动作都换成当前 actor 重新算actor 的梯度方向就错了。参数方面我一般会按下面这套默认值起步参数常见默认值说明gamma0.95折扣因子稀疏奖励环境下可适当调高tau0.01target 网络软更新系数actor_lr1e-4actor 学习率不宜过大critic_lr1e-3critic 学习率通常比 actor 大一档batch_size128每次采样的经验条数buffer_size1e6经验池容量够用即可这套参数在 ma-gym 的大多数环境里都能起步等确认 baseline 跑通后再按场景调。3. 跑通训练主流程main.py 与 combat 场景的参数解读3.1 运行前的环境准备先用 test_env.py 做冒烟测试我拿到这份资源后第一件事不是急着跑 main.py而是先确认 Python 环境和环境库本身可用。项目里带了 pyvenv.cfg 文件说明作者是在虚拟环境里开发的但这个虚拟环境在你机器上不一定能直接激活所以最常见的做法是重建一个 venvcd MADDPG python -m venv venv source venv/bin/activate pip install numpy torch如果你本机已经装好了 PyTorch 和 numpy这一步可以跳过。然后先跑 test_env.pypython test_env.pytest_env.py 做的是环境冒烟测试创建环境实例、重置、用随机动作步进几步确认 reset / step / render 接口都正常工作。如果这里报错大概率是 gym 版本和 ma-gym 不兼容或者环境里的依赖比如 pygame没装齐。这一步通过之前不要碰 main.py否则容易把环境问题误判成算法问题。3.2 main.py 训练主循环每步在做什么main.py 的训练主循环结构上大致是这样for episode in range(n_episodes): obs_all env.reset() episode_rewards [] for step in range(max_steps): actions [] for i in range(n_agents): # 1. actor 输出连续动作 action_continuous actors[i](obs_all[i]) # 2. 加探索噪声 action_noisy action_continuous noise[i]() # 3. 离散环境要做动作映射 action map_to_discrete(action_noisy) actions.append(action) # 4. 环境步进得到联合奖励和全局终止标志 next_obs_all, rew_all, done, _ env.step(actions) # 5. 存入多智能体经验回放池 buffer.push(obs_all, actions, rew_all, next_obs_all, done) obs_all next_obs_all episode_rewards.append(rew_all) # 6. 经验攒够后每个 step 更新一次所有智能体 if buffer.size() batch_size: update_all_agents(buffer, actors, critics, target_actors, target_critics) if episode % 100 0: avg_reward np.mean(episode_rewards, axis0) print(fepisode {episode}, avg reward: {avg_reward})这段代码里我特别想强调两个地方。第一个是噪声的加入方式MADDPG 原论文和多数实现都是用 Ornstein-Uhlenbeck 噪声做时序相关探索rl_utils.py 里应该就有实现但 OU 噪声的方差如果不随时间退火训练后期策略会一直在最优动作附近抖reward 曲线就始终带着毛刺。第二个是 update_all_agents 的调用时机常见做法是每步都更新一次也有每 100 步更新一次的写法这份代码默认是攒够 batch_size 后每步更新理论上没毛病但训练开销会明显偏大。训练回合数方面ma-gym 的 combat 场景我一般先跑 3000 到 5000 个 episode 看趋势。500 个 episode 内曲线不涨是正常的因为这个环境里击杀事件本身是稀疏的前期全靠随机探索碰运气。3.3 训练结果怎么看test.py 验证要关掉噪声训练结束后test.py 用来做模型验证python test.py --model-path ./results/model.pttest.py 和 main.py 最大的区别是验证时所有 actor 都不加噪声纯确定性策略跑完若干 episode统计平均奖励、击杀数等指标。这一步很容易踩坑的地方是忘记区分“训练模式”和“验证模式”——如果用带噪声的策略去验证reward 会被拉低你会误以为模型没训好。我一般会直接看每个 episode 里双方阵营的击杀差而不是只看 reward因为击败对手的稀疏事件最能反映策略是否真的在博弈。4. 切换博弈对抗环境ma-gym 里的七种场景怎么选4.1 环境全景每种环境适合验证什么ma-gym 环境库整合在ma-gym-master/ma_gym/envs/目录下每一种环境都是一个独立的博弈对抗或协作场景。它们的共同点是接口风格对标 OpenAIGym都有 reset / step / render但博弈结构和奖励设计差别很大环境博弈特征适合验证什么动作空间combat两队士兵互相移动、开火团队对抗、稀疏奖励离散predator_prey猎手围捕猎物协作追逐、分工围堵离散switch智能体协作开关门时序决策、逃出房间离散checkers简化跳棋对弈完全对抗、回合制博弈离散traffic_junction车辆通过路口避碰、密集交通协作离散lumberjacks收集木头的拼图任务协作规划离散pong_duel双人乒乓球对打反应式对抗离散如果你做的是“双方对战”这种对抗场景combat 和 checkers 最合适如果你想看协作predator_prey 是经典选择。多数时候我建议先用 combat 起步因为它的胜负判定直观击杀事件明显训练过程中肉眼能看出策略变化。4.2 环境切换操作主要改两个地方切换环境不需要改算法代码主要改 main.py 里的环境构建部分# 方式一直接用 ma-gym 环境类建议方式 from ma_gym.envs.combat.combat import Combat env Combat(grid_size15, n_agents5, n_opponents5) # 换成 predator_prey 只需要换环境类和参数 # from ma_gym.envs.predator_prey import PredatorPrey # env PredatorPrey(grid_size15, n_agents2, n_preys1)切换时有两个容易翻车的点。第一个是构造函数参数名ma-gym 不同环境接收的初始化参数不一样比如 combat 是n_opponentspredator_prey 是n_preys写错会直接 TypeError。第二个是观测维度会变combat 里每个智能体看到的是周围一定范围内的敌我单位信息predator_prey 里看到的可能是相对猎物的位置obs_dim 不同时 network.py 里的输入维度必须同步改否则 actor 网络直接报 shape 错误。另外ma-gym-master 里自带scripts/generate_env_markdown_table.py可以生成一份环境清单文档跑一下就能确认本机环境名和对应参数省得翻源码。4.3 离散动作映射MADDPG 输出连续环境要离散这是切环境时最隐蔽的坑。原版 MADDPG 是为连续动作设计的actor 输出的是 [-1, 1] 之间的连续向量但 ma-gym 大多数环境的动作空间是 Discrete比如 combat 里一个智能体的动作可能是“上下左右移动 开火”组成的离散索引。处理方式有两种# 方式一直接取 argmax把连续输出映射成离散索引 action torch.argmax(action_continuous, dim-1).item() # 方式二把 actor 输出当作离散动作的 logits按概率采样 action torch.distributions.Categorical(logitsaction_continuous).sample().item()我建议先用方式一简单稳定训练初期探索靠噪声等需要更细腻的策略再换成采样方式。换成离散动作后每回合的回合制博弈比如 checkers还需要注意动作输入格式可能是坐标式的不能简单套 argmax这类环境最好先看它自带的 random_agent.py 是怎么调 step 的。5. MADDPG 训练常见问题五个必须记录的踩坑点5.1 训练十几轮后 loss 直接变 NaN现象critic loss 前面还正常跑了十几个 episode 突然输出 nan之后 reward 曲线也全部变 nan。原因最常见是两个一是观测或奖励数值没做归一化量纲太大导致梯度爆炸二是 critic 学习率设得过高MADDPG 的 critic 输入是所有智能体拼接后的高维向量比 DDPG 更容易梯度爆炸。解决先把 reward 做 clip限制在 [-1, 1] 左右再把 critic_lr 从 1e-3 降到 1e-4 试一轮如果还炸检查观测里有没有 inf 或极大值通常 combat 环境里坐标归一化到 [0, 1] 之后就正常了。5.2 报错 action shape mismatch维度对不上现象环境 step 时报ValueError: action shape ... does not match action space ...或者 actor 网络输出维度正常但 env.step 不接受。原因actor 输出是连续向量但 ma-gym 的环境期望离散索引或者期望每个智能体单独传一个整数而代码里传了一个浮点数组。解决在 step 之前做动作映射确认映射后的 action 类型是 int。判断方法很简单打印env.action_space看一下是Discrete(n)还是MultiDiscrete如果是 Discrete每个智能体的动作就是一个整数不是向量。5.3 训练几百轮 reward 几乎不涨现象episode 平均 reward 一直在初始值附近抖动偶尔高一次立刻掉回来长时间没有上升趋势。原因探索噪声没有退火是头号嫌疑OU 噪声方差恒定导致策略一直在最优动作附近转圈第二个原因是 reward 太稀疏combat 里击杀事件是小概率事件随机探索很难碰到正样本。解决先给噪声加一个随时间衰减的系数比如每 1000 episode 乘以 0.9同时把奖励设计改成“击中减血给小正奖励、被击中给小负奖励”让智能体在稀疏的大奖励之外能学到梯度信号。这个方法比调网络结构有效得多。5.4 test.py 加载模型时报 state_dict 大小不匹配现象加载.pt文件时 torch 报size mismatch for fc1.weight或者干脆提示键名对不上。原因多数是因为训练时保存的是所有智能体的网络列表而加载时只 load 了一个 actor或者环境切换后 obs_dim / act_dim 变了网络结构对不上。还有一种情况是保存了整个 model 对象但用 load_state_dict 去读。解决加载前先打印torch.load(path).keys()看清楚里面是单网络还是网络列表如果只是线性层的维度不匹配说明是环境参数和训练时不一致把 grid_size、n_agents 改回训练时的配置即可。5.5 训练结束后策略只会原地打转现象test 时智能体站在原地不动或者反复在同一格来回移动双方几乎不发生接触。原因最典型的是“消极策略”被 reward 鼓励了——如果环境里不动不会受惩罚而主动出击容易被反杀策略网络就会学习到“不动最安全”另一个原因是训练中期策略崩溃actor 坍缩到一个固定输出失去了探索能力。解决在 reward 里加一个小的每步惩罚比如 -0.01打破原地站立的收益优势同时把噪声退火系数调慢一点让策略后期仍有探索能力。这个问题的判定方法很直观把 test 过程录成 gif看一眼就知道智能体到底是在博弈还是在挂机。6. 把实验结果变成验证闭环记录曲线与录制对抗过程6.1 把 episode 平均 reward 落地成 CSVmain.py 里打印的 reward 只在控制台一闪而过不落地后面没法分析。我一般会在训练循环里加一小段import csv csv_file open(rewards.csv, a, newline) writer csv.writer(csv_file) # 每个 episode 结束后追加一行回合数, 本轮平均奖励, 本轮胜方 writer.writerow([episode, avg_reward, winner]) csv_file.flush()用 pandas 读出来画个 rolling mean 曲线比肉眼看控制台可靠得多。画图时记得做平滑处理原始曲线毛刺太重rolling window 取 50 左右能看清趋势。6.2 用 record_environment.py 录制 gif验证策略是否在真博弈ma-gym-master 里带了scripts/record_environment.py可以把环境渲染过程录成 gif。常见的运行方式类似python scripts/record_environment.py -e Combat -n 5 --episodes 3 -o combat_demo.gif具体参数名先跑一遍python scripts/record_environment.py --help确认不同版本脚本的参数定义略有差异。录 gif 的价值不在于好看而在于验证策略是不是在“真博弈”两个队伍是真正在接近对手、开火、闪避还是各自在原地随机抖动。reward 曲线可以骗人策略可视化骗不了人这是多智能体强化学习里最重要的验证手段。我从那以后每次拿到一个新环境做 MADDPG都会先固定随机种子跑一遍 random_agent.py记下随机策略的平均奖励作为最低基线然后跑 MADDPG把训练曲线和录制的对抗 gif 放在一起看确认提升不是来自环境本身的偶然波动。这个流程帮我挡掉了不少“看起来收敛了、其实是环境太简单”的虚假成功希望帮到你。本文还有配套的精品资源点击获取