
简介一份基于MADDPG的多智能体博弈对抗算法Python实现源码包面向计算机专业正在完成课程设计、期末大作业的学生以及需要强化学习实战项目的开发者。项目将多智能体训练拆分为经验回放缓存、DDPG单智能体网络、MADDPG多智能体训练主流程、辅助工具与独立测试脚本等模块能帮助读者理清策略更新、奖励分配、目标网络同步和探索机制等关键环节。压缩包共13个文件以10个Python脚本为核心另含环境配置文件、说明文本和Git版本忽略清单整体仅15KB结构紧凑且无冗余下载后即可运行调试。已有385人学习使用可直接作为课程设计与期末大作业的参考实现并在此基础上快速搭建博弈对抗基线、复现多智能体协作与对抗实验进而调整算法细节用于自己的项目。1. MADDPG 是解决多智能体博弈对抗最稳的起点吗做过多智能体强化学习的人都有同一种体验单智能体的 DDPG 在倒立摆上很听话一旦放进两个互相博弈的智能体里就开始失灵——因为环境是非平稳的对手的策略一直在变你的智能体观测到的状态分布就一直在变。MADDPG 算法正是冲着这个痛点来的它用“集中式训练、分散式执行”的结构把对抗场景里的非平稳性压到最低训练时每个智能体的 Critic 拼进所有人的观测和动作执行时 Actor 只吃自己的局部观测。这套基于 MADDPG 的多智能体博弈对抗算法的 Python 源码项目通常会把网络定义、经验回放、环境封装和训练主循环拆成独立模块解压后补好依赖就能在追逃博弈里看到奖励曲线。它适合做课程设计、课题预研以及想快速验证多智能体策略的从业者。2. 从 DDPG 到 MADDPG对抗场景里的收敛逻辑与源码落点2.1 集中训练分散执行为什么博弈对抗一定要用 CTDEDDPG 处理单智能体连续动作很成熟但多智能体对抗场景里有一个致命问题对某个智能体而言其他智能体的策略变化让环境变成了非平稳的。Q 函数的输入只包含自己的观测和动作时这个 Q 值会被对手策略干扰得剧烈抖动经验回放里的旧样本几乎全失效——你拿一分钟前采到的样本训练当前策略可这一分钟里对手已经换了打法样本对应的“环境”已经不存在了。MADDPG 的核心改动就在 Critic 的输入上。第 i 个智能体的 Critic 输入是全局观测 x(o1,…,oN) 和所有智能体的动作 (a1,…,aN)输出 Q_i。这样一来即使对手策略在更新Critic 也能观察到对手当前动作带来的影响环境非平稳性就被“消化”掉了。Actor 侧保持分散执行每个智能体只依赖自己的观测 o_i 输出动作部署时不需要中心节点实时决策——这在博弈对抗里很关键因为对战阶段往往不可能拿到对手的完整状态。从梯度公式上看MADDPG 对每个智能体独立做策略梯度Critic 的回归目标同时包含了所有智能体的策略这就是为什么它能处理“你不动我不动、你一动我反制”这种博弈动态。和 MAPPO 相比MADDPG 在连续动作、样本量有限的对抗场景里更稳代码也更直观和 QMIX 这类值分解方法相比MADDPG 不需要强加“联合动作价值可分解”的结构假设所以它成了多智能体博弈对抗里最常见的落地起点。2.2 解压后先认准这几个 Python 模块别急着跑一个可运行的 MADDPG 源码项目通常按模块组织拿到的 zip 解压后一般能看到这几个文件。先把每个文件的作用摸清楚再动手改参数。文件职责maddpg.py算法主类包含每个智能体的 actor/critic 初始化、软更新和梯度更新逻辑network.pyActor 和 Critic 的网络结构定义replay_buffer.py经验回放存储所有智能体的 (obs, act, rew, next_obs, done) 元组train.py训练入口负责创建环境、循环采样、调用更新config.py / config.yaml超参数集中配置改动最频繁的文件我一般会建议拿到源码后第一件事就是打开 config 文件看学习率、软更新系数 tau 和环境名。很多时候“跑不出来”不是代码问题是超参和环境不匹配。好的项目里通常还会有一个 eval.py 或 test.py用来加载训练好的权重做对战验证——这个文件直接决定了你能不能量化“博弈对抗”的胜负关系。2.3 环境选型simple_tag 与 simple_adversary 怎么选MPEMulti-Agent Particle Environment是这个领域最常见的验证环境。源码里的环境名决定任务性质常见的三个场景智能体构成任务性质simple_spread3 个合作者3 个地标纯合作无对抗simple_adversary1 个对抗者 2 个合作者混合博弈对抗者要靠近目标合作者要引开它simple_tag3 个追捕者 1 个逃跑者完全对抗带障碍物选 simple_tag 最能体现“博弈对抗”——追捕方要学会围堵逃跑方要学会利用障碍物甩开追兵。如果只想验证算法能否收敛先从 simple_adversary 开始它的解空间更小训练时间短。拿 MPE 而不是自己写环境是因为对抗博弈的难点在验证自己写一个 gym 环境很容易写出“看起来合理但奖励不收敛”的 bug。MPE 的物理引擎非常简单——圆形粒子、线性运动、碰撞反弹它把环境的不可控因素降到最低这样训练不收敛时你能确信问题在算法而不是环境实现。还有个细节容易被忽略MPE 环境接口是 gym 风格的env.reset() 返回一个 list每个元素是每个智能体的局部观测 ndarrayenv.step(action_list) 接收一个 list长度必须等于智能体数量顺序不能乱。这个接口和单智能体 gym 差异很大第一次写训练循环的人容易在这里摔跤。很多源码项目会自己封一个 env_wrapper.py 做类型转换和归一化用之前先确认里面有没有做 obs 归一化没有的话要先加不然 actor 输入的量纲差距过大会导致训练极其缓慢。3. 本地跑通 MADDPG 训练环境配置与最小命令3.1 Python 环境准备与依赖安装这个项目是纯 Python PyTorch 实现不依赖 ROS 或编译型库环境部分不会卡太久。我一般建议用 conda 建独立环境把 Python 版本锁在 3.9这个组合在 PyTorch 2.x 和 gym 0.21 下的兼容性最稳。conda create -n maddpg python3.9 -y conda activate maddpg pip install torch2.1.0如果机器有 NVIDIA GPU去 PyTorch 官网选对应的 CUDA 版本安装没有 GPUCPU 版本也能跑 simple_adversary只是要等。MADDPG 的网络很小CPU 训练 500 个 episode 大约在几分钟到十几分钟的量级不必强求 GPU。装完后确认一下import torch print(torch.__version__) print(torch.cuda.is_available())如果源码里还依赖 numpy 和 gym顺手一起装。版本注意一点MPE 官方仓库适配的是 gym 0.21 左右的接口gym 新版 API 变动较大比如 reset 返回格式装得太新反而会报错。3.2 安装 MPE 环境scenarios 加载方式要写对MPE 通常以源码方式安装常见做法是把 OpenAI 的 multiagent-particle-envs 仓库克隆到本地然后在项目根目录执行git clone https://github.com/openai/multiagent-particle-envs.git cd multiagent-particle-envs pip install -e .安装完验证环境能否正常创建下面这段代码可以直接跑from multiagent.environment import MultiAgentEnv import multiagent.scenarios as scenarios scenario scenarios.load(simple_tag.py) world scenario.make_world() env MultiAgentEnv( worldworld, reset_callbackscenario.reset_world, reward_callbackscenario.reward, observation_callbackscenario.observation, done_callbackscenario.done, ) print(智能体数量:, env.n) print(动作空间:, env.action_space[0])注意 scenarios.load 传入的是文件名带 .py不是场景裸名。动作空间如果是 Box说明是连续动作和 MADDPG 的 tanh 输出匹配如果是 Discrete说明 world 里默认开了离散动作后面训练前要在 make_world 里把 world.discrete_action 改成 False。这一步是很多项目跑起来后表现差的隐藏原因。3.3 最小训练命令与跑通后先看什么依赖装好后直接执行源码根目录下的训练脚本python train.py --env simple_tag --max-episodes 20000 --batch-size 1024参数说明--env 指定 MPE 场景名--max-episodes 控制总训练轮数simple_tag 通常要 15000-30000 个 episode 才能看到明显对抗行为--batch-size 是每次从回放缓冲区采样的样本数MADDPG 比单智能体 DDPG 更吃 batch1024 比 256 在对抗场景里更稳。跑起来后看终端输出的两个指标每个 agent 的平均 episode 奖励和最近 100 个 episode 的平均奖励。源码里如果只打印每个 episode 的即时奖励你会看到曲线剧烈抖动这不是 bug是对抗博弈的正常现象——一方学得比另一方快奖励就会瞬间偏转。建议把滑动平均窗口拉长到 100再判断趋势。如果窗口拉到 100 后奖励还是完全不动才需要怀疑代码或超参问题。3.4 模型保存与加载验证训练脚本一般会在 runs/ 或 checkpoint/ 目录下按时间戳生成文件夹里面是每个智能体的 actor 网络权重文件。MADDPG 必须保存“每个智能体”的权重不能只存一份——在 simple_tag 里 3 个追捕者共享 Actor 结构但参数独立加载时也要按 agent 索引取python eval.py --env simple_tag --checkpoint runs/20240822_143500 --episodes 30 --rendereval.py 会加载每个智能体的 actor 权重用固定随机种子做 30 次完整 episode输出胜率。我建议把“胜率”定义成逃跑者在 1000 步内被追上则追捕方胜。源码里如果没有这个指标自己加一个也不难——用一个计数器在 done 为 True 时统计。注意 MPE 的 done_callback 在 simple_tag 里默认是步数上限你要在 eval 里显式判断“追上”条件否则 done 永远只在步数耗尽时触发胜率计算就失真了。4. 核心模块拆解Actor、Critic、回放缓冲区与训练循环4.1 Actor 网络输出层为什么必须用 tanh无论哪个版本的源码Actor 的网络骨架基本就是 MLP ReLU输出层一个 tanh 把动作压到 [-1, 1]。MPE 连续动作模式下动作空间的边界就是 -1 到 1tanh 的输出和它天然匹配。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden64): super(Actor, self).__init__() self.fc1 nn.Linear(obs_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, act_dim) # DDPG 系算法对输出层初始化极敏感 self.fc3.weight.data.uniform_(-0.003, 0.003) self.fc3.bias.data.uniform_(-0.003, 0.003) def forward(self, obs): x F.relu(self.fc1(obs)) x F.relu(self.fc2(x)) return torch.tanh(self.fc3(x))这个网络做的事很单纯输入当前智能体的局部观测输出下一步的连续动作。obs_dim 需要和 config 里设置的观测维度一致MPE 的 simple_tag 观测一般包含自身位置、速度、相对其他粒子的方向和距离维度在 14~18 之间。改网络宽度时两个 hidden 层保持一致即可不用追求大网络。注释里加粗那句是重点——如果源码里输出层没有做这个小尺度初始化建议自己补上这是“能跑”和“能收敛”的分水岭之一。默认的 PyTorch 初始化会让初始动作偏向边界探索阶段奖励方差巨大直接拖慢收敛。4.2 Critic 网络全局拼接与集中训练的真正含义Critic 的结构比 Actor 多了一个关键步骤把所有智能体的观测和动作拼接成一个大向量。class Critic(nn.Module): def __init__(self, n_agents, obs_dim, act_dim, hidden64): super(Critic, self).__init__() input_dim n_agents * (obs_dim act_dim) self.fc1 nn.Linear(input_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, 1) def forward(self, obs_all, act_all): x torch.cat([obs_all, act_all], dim-1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)obs_all 的形状是 (batch, n_agents, obs_dim)act_all 是 (batch, n_agents, act_dim)在最后一维上拼接后得到 (batch, n_agents * (obs_dim act_dim))。注意这里是把所有智能体的观测和动作拼接成一个大向量不是分别过网络再融合——分别过网络会丢失智能体之间动作的交互信息等于没做集中训练。Critic 的输出是一个标量 Q 值代表当前全局状态和全局动作下的期望累计收益。这里有个实现细节MADDPG 论文里每个智能体都有自己的 Critic即使它们观测范围完全一样也不能共用同一个 Critic 网络。因为不同智能体的奖励函数不同共用网络会导致梯度互相干扰。4.3 回放缓冲区多智能体样本的存储结构回放缓冲区在 MADDPG 里存的是“全局样本”每条样本包含所有智能体的观测、动作、奖励和下一时刻观测这样才能在更新 Critic 时拿到全局拼接输入。from collections import deque import random import numpy as np class ReplayBuffer: def __init__(self, capacity1000000): self.buffer deque(maxlencapacity) def push(self, obs_n, act_n, rew_n, obs_next_n, done_n): # obs_n: list of np.array每个智能体一个 self.buffer.append((obs_n, act_n, rew_n, obs_next_n, done_n)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) obs_all, act_all [], [] rew_all, obs_next_all, done_all [], [], [] for obs_n, act_n, rew_n, obs_next_n, done_n in batch: obs_all.append(obs_n) act_all.append(act_n) rew_all.append(rew_n) obs_next_all.append(obs_next_n) done_all.append(done_n) # 转成 (batch, n_agents, dim) 的 tensor return (np.stack(obs_all), np.stack(act_all), np.stack(rew_all), np.stack(obs_next_all), np.stack(done_all))容量 1e6 在 simple_tag 场景下没问题观测维度小内存占用不高。如果换到高维观测场景1e6 条样本可能会吃掉几个 GB 内存这时候要把容量降到 2e5 到 5e5。采样时用 random.sample 而不是按顺序取是为了打破样本间的时间相关性。push 的入参是 list 还是 ndarray 在源码里可能不一致建议统一收成 list在 sample 里再 stack这样环境返回什么格式都不会崩。4.4 训练主循环更新时序与软更新训练循环是 MADDPG 最容易写错的地方核心在于更新某个 actor 时其他智能体的动作必须保持固定。def update(self, replay_buffer, batch_size, gamma0.95, tau0.01): obs_all, act_all, rew_all, obs_next_all, done_all replay_buffer.sample(batch_size) device next(self.actors[0].parameters()).device for i in range(self.n_agents): # 更新 Critic i with torch.no_grad(): next_act_n [] for j in range(self.n_agents): next_act_n.append(self.target_actors[j](obs_next_all[:, j])) next_act_n torch.cat(next_act_n, dim-1) # 等价于 cat([a1, a2, ..., aN], dim-1) target_q self.target_critics[i](obs_next_all, next_act_n) target_v rew_all[:, i].unsqueeze(1) gamma * (1 - done_all) * target_q current_q self.critics[i](obs_all, act_all) critic_loss nn.MSELoss()(current_q, target_v.detach()) self.critic_optims[i].zero_grad() critic_loss.backward() nn.utils.clip_grad_norm_(self.critics[i].parameters(), 0.5) self.critic_optims[i].step() # 更新 Actor i # 其他智能体动作保持 buffer 里的样本值只有当前 actor 重新采样 new_act_n list(act_all) # (n_agents, batch, act_dim) new_act_n[i] self.actors[i](obs_all[:, i]) new_act_n torch.cat(new_act_n, dim-1) actor_loss -self.critics[i](obs_all, new_act_n).mean() self.actor_optims[i].zero_grad() actor_loss.backward() nn.utils.clip_grad_norm_(self.actors[i].parameters(), 0.5) self.actor_optims[i].step() # 软更新所有 target 网络 for i in range(self.n_agents): for target_param, param in zip(self.target_actors[i].parameters(), self.actors[i].parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data) for target_param, param in zip(self.target_critics[i].parameters(), self.critics[i].parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data)这段代码有三个关键点。第一更新 Critic 时 target 网络的输入是“target actor 在下一观测上输出的动作”不是当前 actor 输出的这保证了 TD 目标的稳定性。第二更新 Actor 时new_act_n 里只有第 i 个智能体的动作是刚算出来的新动作其他人的动作保持 buffer 里的旧值这是 MADDPG 多智能体梯度能够正确解耦的前提——如果所有人都同时换新动作Critic 的输入里多个维度同时变化某个 actor 的梯度会被其他 actor 的更新噪声淹没。梯度裁剪 0.5 是一个很稳妥的默认值实际调参时可以在 0.3 到 1.0 之间试。第三软更新系数 tau 决定了 target 网络的跟踪速度tau 越大跟踪越快但稳定性越差0.01 是论文里的原始设定遇到震荡时往 0.005 调。5. MADDPG 训练避坑清单5 个最常见的收敛失败诱因5.1 现象奖励在 3000 个 episode 时冲到高位随后 2000 轮内全崩这是 DDPG 系算法最经典的“近因遗忘”问题。原因在于 Critic 过估计target Critic 和当前 Critic 同时在更新TD 目标里包含了对未来 Q 值的估计误差会像滚雪球一样累积。多智能体场景里这个效应更严重因为 Critic 输入维度翻了好几倍拟合难度变大。解决把 tau 从 0.01 调到 0.005降低 target 网络跟踪速度同时把“每步都更新”改成“每隔 50 步更新一次”给经验回放里的样本更多筛选空间。我在实际项目里见过最简单有效的止血方式就是降低更新频率代价只是训练时间拉长 30% 左右但稳定性提升非常明显。5.2 现象所有追捕者都朝逃跑者的同一侧挤过去围堵永远构不成原因多智能体奖励独立优化导致行为同质化——每个 agent 的策略都选择最大化自己 Q 的唯一路径忽略了队友占据的空间。这是博弈对抗里最容易出现的“策略坍缩”。解决奖励塑形加入互斥正则。在追捕者 reward 里加一项“与最近队友的距离惩罚”当两个追捕者距离小于某个阈值时每个追捕者扣 0.05~0.1 分。具体改法是在环境 reward_callback 里加一个循环遍历所有追捕者 pair距离小于阈值就扣分。这个改动不会改变最优策略但会大幅加快探索阶段的分工速度。5.3 现象训练到 3000 episode 时出现 NaNloss 直接炸掉原因梯度爆炸或观测方差过大。MPE 的观测里包含相对距离如果智能体位置被障碍物挤出边界距离值会出现极端值加上收益函数里如果带了碰撞惩罚大尺度系数Q 值就容易发散。解决先检查奖励尺度把所有 reward 归一化到 [-1, 1] 区间这是治本。然后检查观测是否做了归一化很多项目只做了简单 concat 没做 obs norm这一步建议在 env_wrapper 里补上。代码里的梯度裁剪 clip_grad_norm_(max_norm0.5) 是最后防线但注意它只能防梯度爆炸不能治奖励尺度过大的病。5.4 现象别人能复现的结果自己跑就是不一样原因随机种子没锁全。PyTorch 的 dropout 等随机源要 seednumpy 的全局 np.random 也要 seed还有 Python 的 random 模块。另一个隐蔽坑是 MPE 环境在创建 world 时会用 np.random 采样障碍物位置如果环境创建发生在 seed 之前等于白 seed。解决把 seed 顺序固定为先设 Python 的 random.seed再设 np.random.seed再设 torch.manual_seed然后创建环境最后进训练循环。顺序不对会前功尽弃。建议在 train.py 的开头把这段写死不要留给 config 控制——我见过不少项目的 seed 参数写在了 config 里但代码根本没调用等于没有。更隐蔽的一个问题MPE 在 import 阶段可能触发 numpy 随机数生成所以 import multiagent 的语句必须放在 seed 之前否则 seed 会被环境导入时的随机调用“污染”。5.5 现象Windows 上报错 ModuleNotFoundError: No module named mpi4py原因源码里用了 OpenAI baselines 原版的 mpi4py 工具来做并行训练。mpi4py 在 Windows 上必须装 MS-MPI 才能 import装起来非常折腾而且和最新版 PyTorch 的兼容性也一般。解决优先检查项目是不是用了 baselines 的 maddpg 原版。如果确实没办法有两条路换 Linux 环境或者把依赖 mpi4py 的代码替换成纯 torch 实现。当前市面上的这一类 Python 项目为了兼容性大多已经弃用 mpi4py如果拿到的 zip 里还有说明是早期代码需要自己把并行部分替换成单机版——实际上单机跑也没问题MADDPG 的样本效率足够在一个进程里完成训练并行只是加速手段。6. 让博弈对抗水平再上一个台阶奖励塑形与策略验证技巧6.1 奖励塑形的两个方向第一个方向是给行为加“中间目标”。simple_tag 里追捕者的原始奖励只在抓住逃跑者时给 10中间过程奖励全空探索效率很低。常见做法是把 reward 改成“距离减少量”每个时间步追捕者离逃跑者的距离比上一步近了多少就加多少正奖励。这个塑形不改变最优策略只改变探索路径能显著加快前 5000 个 episode 的学习速度。第二个方向是给逃跑者加“存活时间奖励”每多活 10 步给一个小正奖励避免逃跑者原地打转。6.2 用固定策略做胜率验证训练完成后不要只看奖励曲线奖励高不代表博弈水平高。我习惯的做法是冻结三个不同训练阶段的权重训练中途的、训练结束的、以及加了噪声干扰的分别去对战一个固定随机策略和一个启发式策略比如逃跑者永远朝远离最近追捕者的方向跑。统计 100 个 episode 的胜率你会发现中途权重可能已经能赢随机策略但打不过启发式策略——这说明策略学到了“对抗”但没有学到“最优”。只有能稳定打赢启发式策略时才说明 MADDPG 学到的东西有实战价值。这个验证脚本建议在 eval.py 里扩展不要只在训练代码里加。6.3 一张可直接套用的超参模板参数推荐值说明actor 学习率1e-4调大容易震荡critic 学习率1e-3比 actor 快一个量级是常规做法tau 软更新系数0.01震荡时降到 0.005gamma 折扣因子0.95对抗场景不用 0.99未来收益太远反而难学batch_size1024比单智能体大全局拼接后样本方差更大buffer 容量1e6高维观测时降到 2e5噪声标准差0.1线性衰减到 0.01不要直接关这套参数我调过很多次simple_tag 和 simple_adversary 都能在 15000 个 episode 内看到明显对抗行为。如果你换了场景发现不收敛先回退到这张表再逐项改不要一次动三个参数。我自己第一次跑这个项目时就因为 obs 没归一化连续调了三天网络结构最后发现是数据预处理的问题——那种感觉就像对着黑匣子猜谜。现在每次拿到新环境我都会先写一个打印 obs 量级的小脚本看一眼再开训练。希望帮到你。本文还有配套的精品资源点击获取