ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MADDPG实战指南:多智能体强化学习中的博弈对抗算法选型、代码拆解与训练避坑

MADDPG实战指南:多智能体强化学习中的博弈对抗算法选型、代码拆解与训练避坑 简介基于MADDPG的多智能体博弈对抗算法Python实现项目源码面向正在完成课程设计或期末大作业的计算机专业学生以及希望动手实战多智能体强化学习的学习者。项目包含完整可运行的MADDPG实现覆盖DDPG、经验回放、网络结构等核心模块并附有测试脚本与环境配置下载后即可直接运行可帮助理解多智能体博弈对抗的训练流程与调参思路。资源包共13个文件以Python源码为主含DDPG.py、MADDPG.py、network.py、rl_utils.py等核心脚本另有txt说明、gitignore和cfg配置整体仅15KB轻量易上手。已有385人学习下载。源码结构清晰代码经过严格调试适合作为课程报告的数据支撑与算法改造的起点。1. 从“追着跑”到“学会骗”MADDPG 到底解决的是哪类问题跑过单智能体强化学习的人上手 MADDPG 时多半会有种“回到新手村”的感觉明明 reward 设计得更精细了网络结构也更宽了可两个智能体一碰面训练曲线就开始坐过山车——前 100 个 episode 里互相 chase 得好好的第 120 个 episode 突然集体“摆烂”绕着地图边缘转圈。这不是你代码写错了而是多智能体环境里每个人都面对一个移动中的靶子你更新策略时对手的策略也在变环境是非稳态的。MADDPGMulti-Agent Deep Deterministic Policy Gradient就是冲着这个痛点来的。它的核心思路是“集中训练、分散执行”训练时每个智能体额外看全局信息部署时只用局部观测。这套办法在博弈对抗场景里特别好使捕食者-猎物、攻防对抗、战队对战这类任务里它能让智能体不光学会追还学会围剿和预判。本文面向的是手里已经有一份 Python 实现源码、想弄懂原理、改参数、跑通训练并评估效果的从业者——不管你拿到的项目是哪个版本读完这篇你再打开代码看到的就不是一团乱麻而是一条清晰的数据流环境交互 → 经验回放 → critic 更新 → actor 更新 → 目标网络软更新。2. 为什么博弈对抗场景下 MADDPG 比 DDPG、PPO 更合适算法选型的三条硬理由2.1 非稳态环境你改策略对手也在改DDPG 直接“看不懂”梯度单智能体 DDPG 训练时环境转移概率是固定的critic 学习 Q 值时面对的输入分布基本稳定。但多智能体博弈里经验回放缓冲池中的数据来自“不同策略阶段”的交互——同一个状态对手早期可能选择追击中期可能选择绕后其动作分布一直在漂移。这时如果每个智能体独立套 DDPG它算出来的 Q 值梯度实际上混入了“对手策略变化”的噪声critic 的 loss 永远降不到预期水位actor 拿到的梯度方向也是混乱的。MADDPG 的做法是给每个智能体单独配一个 critic这个 critic 的输入是所有智能体的观测和动作拼接。也就是说智能体 i 在评估 Q 值时是站在“上帝视角”上考虑全体行动的联合效果对手策略的变化被显式建模进了 Q 函数。训练时 critic 看到的是全量信息actor 仍然只用自己的局部观测做决策——这就是“集中训练、分散执行”的精髓。你第一次看代码时重点看 actor 和 critic 的输入维度差异就能确认实现是否正确。2.2 连续动作空间里的博弈DQN 的动作离散化会丢失围剿精度捕食者-猎物这类对抗任务动作往往是连续力或速度控制。DQN 及其变体只能离散化动作空间比如固定八个方向这会让围剿策略显得僵硬猎物斜向逃跑时捕食者只能走折线追赶。MADDPG 走的是确定性策略梯度路线actor 直接输出连续动作值配合 exploration noise 做随机探索动作精度远高于离散选项。代码里一般会在 actor 的最后一层用 tanh 激活把输出压缩到 [-1, 1]。如果项目的环境动作范围不是 [-1, 1]比如是 [-2, 2] 或者 [0, 5]你需要在环境 wrapper 或 actor 输出后做一次线性映射否则你会在训练曲线里看到“前期学得还行中后期完全不动”的现象因为动作边界被 tanh 截断了。2.3 与 PPO 的对比on-policy 样本效率在对抗任务里扛不住PPO 在单智能体连续控制里表现很好但它本质是 on-policy 的每轮策略更新后旧样本全部作废。多智能体博弈里每个 episode 的长度动辄几百步样本采集成本高再加上多个智能体共享同一批交互数据on-policy 方法的样本利用率就更低了。MADDPG 属于 off-policy经验回放缓冲池可以反复采样一个小时的交互数据能支撑上万次梯度更新这对个人开发者的单卡环境非常友好。当然off-policy 的代价是训练稳定性更依赖超参数后面第 4 章我会展开讲 buffer 容量、噪音衰减、软更新系数这些参数的调法。这里先给你一个判断依据如果你的对手策略是固定的脚本比如一个规则 AI那么 PPO 也能收敛如果你的对手是另一个学习型智能体二者共同进化MADDPG 的联合 critic 就是更稳妥的选型。3. 跑通的最小环境与目录识别拿到源码后最先确认的六件事3.1 Python 环境配置与依赖安装maddpg 算法包版本兼容是第一道坎拿到一份 MADDPG 源码项目先别急着跑。多智能体强化学习的依赖版本冲突是出了名的多尤其是 gym 接口在 0.26 前后改动很大。我一般按下面这套流程来配置环境# 创建虚拟环境Python 版本锁定 3.8 或 3.9避开 3.10 的 API 变动 conda create -n maddpg_env python3.8 conda activate maddpg_env # 安装核心依赖 pip install torch1.13.1 pip install numpy1.24.4 pip install gym0.21.0 # 如果项目用到 pettingzoo 的 mpe 环境需要额外安装 pip install pettingzoo1.22.4 pip install sacred注意如果你拿到的源码里用的是gym.make()且没有传render_mode参数那说明它是 gym 0.21 时代的写法。不要升级 gym 到 0.26否则env.step()返回的维度会从四元组变成五元组代码直接崩溃。配置完成后用一行命令验证环境是否通python -c import torch, gym, numpy; print(torch.__version__, gym.__version__, numpy.__version__)这里的关键点是 torch 版本。MADDPG 源码里大量使用torch.distributions和nn.utils1.13 和 2.x 之间没有根本性差异但如果你用的是 2.0 搭配 gym 0.21有时会遇到gym内部调用了torch旧接口的兼容告警不影响运行但会刷屏。建议按上述版本组合起步跑通后再考虑升级。3.2 源码目录结构速览主循环、网络定义、经验池分别在哪一份典型的 MADDPG 项目源码不管作者怎么整理核心文件不会超过六个。先用tree命令快速摸清结构tree -L 2 -F你大概率会看到以下文件或等价物maddpg.py算法主体包含 Actor, Critic, MADDPG 三个类buffer.py经验回放缓冲池通常用 deque 或 list 实现env_wrapper.py环境封装统一动作空间和观测空间格式train.py训练主循环包含 episode 循环、梯度更新、日志输出utils.py工具函数比如 one-hot 编码、噪声生成器config.py或hyperparams.py超参数集中配置拿到源码后第一时间打开train.py看它的主循环结构。我见过不少“能跑但完全不能用”的项目问题往往出在经验回放缓冲池是全局共享的但不同智能体的动作维度不同往同一个 buffer 里塞数据时没做 padding。这种 bug 不会立刻报错但会在 300 个 episode 后让训练曲线陷入平台期而且你很难定位问题。3.3 最小可运行命令先把基线成绩打出来再谈调参不管项目里 README 写得多么花哨你第一步要追求的是“无报错跑完 1000 个 episode”。以下是我惯用的启动方式# 训练模型--scenario 指定对抗场景--episodes 控制训练轮数 python train.py --scenario simple_tag --episodes 1000 --save-interval 200 # 测试模型加载已保存的权重并渲染可视化需要 GUI 环境 python test.py --scenario simple_tag --load-dir ./models/simple_tag/ --rendersimple_tag是 MPEMulti-Agent Particle Environment里最经典的捕食者-猎物场景3 个捕食者追 1 个猎物猎物可以“瞬移”逃出包围圈。这个场景被 MADDPG 论文选为 baseline90% 的公开源码项目都默认用它演示。参数说明--scenario指定加载哪个环境脚本在envs/目录下找.py文件--episodes训练总轮数。简单对抗场景下1500 轮基本能看到稳定的围剿行为--save-interval每多少轮保存一次模型权重建议 200方便中途回滚--render打开可视化窗口。如果你在服务器上跑要加上--headless或者设置DISPLAY环境变量如果这个最小命令跑不通先别怀疑模型代码90% 的概率是环境问题# 查看当前环境的 Python 路径确认是在虚拟环境里 which python # 如果环境依赖报错直接根据报错信息逐个安装 pip install missing_package跑通最小示例后你要立刻做一件事把训练日志里的 episode reward 保存下来画一条学习曲线。后续调参、判断收敛、论文配图都靠这条曲线。很多项目源码里已经集成了 plot 函数如果没有自己在训练循环里每 10 个 episode 打印一次 reward 均值不然后期你根本不知道模型是在进步还是在原地打转。4. 核心代码拆解从 Buffer 到 Critic每个数据流环节的值函数与维度设计4.1 训练主循环step 函数返回值解析与经验数据组装MADDPG 的数据流起点是env.step()。在 gym 0.21 中它的返回值是四个量obs_n, reward_n, done_n, info_n。注意变量名的_n后缀它代表这是“每个智能体各一份”的列表。# 每个 episode 开始时重置环境多智能体环境返回的是列表不是单个 ndarray obs_n env.reset() while True: # 根据当前观测选择动作actor 前向 探索噪声 actions_n [] for i, obs in enumerate(obs_n): action agents[i].select_action(obs, noise_scale0.1) actions_n.append(action) # 与环境交互返回每个智能体自己的奖励和观测 next_obs_n, reward_n, done_n, info_n env.step(actions_n) # 存入经验回放缓冲池 for i, agent in enumerate(agents): replay_buffer.add(obs_n[i], actions_n[i], reward_n[i], next_obs_n[i], done_n[i]) # 每 100 步做一次梯度更新 if step_count % 100 0: for agent in agents: agent.update(replay_buffer, agents) obs_n next_obs_n if all(done_n): break逻辑说明这段代码展示了“每个智能体只存自己的观测、动作、奖励”的做法这是最标准的 MADDPG 数据流。关键约束是动作列表actions_n必须是一个numpy.ndarray列表且每个元素 shape 是(action_dim,)不能是(1, action_dim)否则环境会报维度不匹配。参数说明noise_scale0.1是探索噪声的初始幅度。这个值决定了智能体前期的“乱试”程度。0.1 适合小规模动作范围如 [-1, 1]如果动作范围是 [-2, 2]可以按比例放大到 0.2。噪声过大训练曲线前期会一直居高不下看不出学习趋势噪声过小智能体会过早收敛到局部最优策略围剿动作明显僵硬。4.2 Actor-Critic 网络结构隐藏层维度怎么设才能既拟合又不过拟合MADDPG 的网络结构在不同源码项目里略有差异但共同点是Actor 输入是局部观测obs_dim输出是连续动作action_dimCritic 输入是所有智能体的观测拼接 动作拼接输出是单个 Q 值。下面是我惯用的初始化方式class Actor(nn.Module): def __init__(self, obs_dim, action_dim, hidden_dim64): super(Actor, self).__init__() self.fc1 nn.Linear(obs_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, action_dim) def forward(self, obs): x torch.relu(self.fc1(obs)) x torch.relu(self.fc2(x)) # tanh 将动作限制在 [-1, 1]配合环境动作边界做缩放 return torch.tanh(self.fc3(x)) class Critic(nn.Module): def __init__(self, obs_dim_n, action_dim_n, hidden_dim64): super(Critic, self).__init__() # 输入维度 所有智能体的观测和动作拼接后的总维度 input_dim sum(obs_dim_n) sum(action_dim_n) self.fc1 nn.Linear(input_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, obs_n, act_n): # 将观测和动作拼接成一个大向量 x torch.cat([*obs_n, *act_n], dim-1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return self.fc3(x)逻辑说明Actor 的tanh输出层是 MADDPG 实现里最容易出 bug 的地方。如果源项目里的动作空间范围不是 [-1, 1]你一定得在环境 wrapper 里做映射最常见的做法是# 环境动作范围是 [-2, 2] 时的映射逻辑 action_scaled action * 2.0 # 将 [-1, 1] 缩放到 [-2, 2]参数说明hidden_dim64是中等规模配置。simple_tag 这种小场景足够用了如果你的智能体观测维度超过 30建议提到 128 或 256。但注意一点Critic 的输入维度是所有智能体的观测和动作拼接智能体越多Critic 第一层权重矩阵越大对显存和样本量的要求越高——这是 MADDPG 的一个天然扩张瓶颈后面第五节我会给具体的应对方案。4.3 经验回放与目标网络软更新两个隐藏的梯度灾难源头MADDPG 中每个智能体有四个网络在线 actor、目标 actor、在线 critic、目标 critic。目标网络参数的更新方式是“软更新”soft update即每次梯度更新后目标网络参数向在线网络参数滑动一点。这个tau值直接决定了训练稳定性。# 软更新目标网络参数 def soft_update(target, source, tau): for target_param, param in zip(target.parameters(), source.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data) # 调用示例tau 一般取 0.01 soft_update(agent.target_actor, agent.actor, tau0.01)参数说明tau0.01意味着目标网络每次只向在线网络移动 1%这样 critic 的学习目标本身是缓慢变化的Q 值的回归不会震荡。如果 tau 设成 0.1训练曲线看起来像一条锯齿智能体今天学会满分策略、明天又全部遗忘。在博弈对抗场景里想把 tau 取 0.005 到 0.01 之间因为对手策略的漂移本身已经很大目标网络再快速更新梯度就彻底变成白噪声了。经验回放部分注意replay_buffer.add()存入的是每个智能体独立的五元组。但在 MADDPG 里有一个与单智能体 DDPG 的关键不同agent.update()时critic 需要的“全局观察和全局动作”是人为拼接的所以 buffer 里存的数据要有索引关联才能知道“第 k 时刻所有智能体的状态分别是什么”。有些低质量源项目会把 buffer 设计成每个智能体独立一套训练时 critic 无论如何拼不出完整的全局信息表现为“训练一段时间后 loss 发散到 NaN”。判断方法很简单打开agent.update()看它的采样逻辑如果sample()之后只拿到一个 batch 的 dict 列表而不是能按时刻对齐的五元组那这个项目基本是坏的需要你重写 buffer。4.4 损失函数与反向传播critic loss 降到多少才算正常MADDPG 的每个训练循环分为两步。第一步更新 critic让它预测的 Q 值更接近“实际回报 未来回报估计”。第二步更新 actor目标是让 Q 值预测变大。看以下标准实现# 从经验回放中采样一批数据 batch replay_buffer.sample(batch_size256) # 1. 更新 Critic回归目标 即时奖励 gamma * 目标 Q 值 next_q_values critic_target(next_obs_n, next_actions_n) target_q reward_n gamma * next_q_values * (1 - done_n) current_q_values critic(obs_n, actions_n) critic_loss nn.MSELoss()(current_q_values, target_q) # 反向传播更新 critic 参数 optimizer_critic.zero_grad() critic_loss.backward() optimizer_critic.step() # 2. 更新 Actor目标是让 Q 值预测变大即最大化当前策略的期望 Q 值 actor_loss -critic(obs_n, actor(obs_n)).mean() optimizer_actor.zero_grad() actor_loss.backward() optimizer_actor.step() # 3. 软更新目标网络 soft_update(critic_target, critic, tau0.01) soft_update(actor_target, actor, tau0.01)逻辑说明critic 更新的核心是target_q的计算。这里的reward_n不是单个标量而是一个 batch 大小的向量。done_n的维度必须与reward_n一致且数值为 0 或 1否则(1 - done_n)会把终止时刻的 Q 目标错误地放大或缩小。这是 MADDPG 里非常频繁的隐性 bug 源。关于 critic loss 降到多少算正常不同环境的奖励尺度不一样没法给一个绝对标准。以 simple_tag 为例每个捕食者撞到猎物的奖励是 10每步有 0.5 的惩罚所以 critic loss 大约在 100 到 300 之间存在波动是合理的。但你要看的不是 critic loss 本身而是它是否持续下降——如果 500 个 episode 后 loss 还在同一个量级内剧烈震荡说明经验回放中的数据分布太杂或者 critic 的学习率过高。我通常把 actor 学习率设为 1e-4critic 学习率设为 1e-3critic 学得快一些负责准确评估actor 学得慢一些负责稳定提升。两者学习率同量级时训练曲线会变成高频噪声。这个时候train.py里一般会包含一个agent.update()方法集中编排上述步骤。你在源码项目里看到for agent in agents: agent.update(...)时要注意每个智能体的 update 都会用到其他所有智能体的网络这是 MADDPG 和独立 DDPG 的本质区别。如果某个agent.update()传入的智能体列表不完整critic 的输入维度会少一块训练初期不出问题但一旦对手学到了新的行为模式Q 值的预测就会整体偏移。5. 避坑指南多智能体训练翻车的 5 个常见问题与排查方向5.1 问题前 200 个 episode 效果还行后面所有智能体集体“躺平”奖励全面归零现象训练日志里每 episode 的平均奖励从 -40 缓步上升到 -15然后突然掉回 -60之后再也起不来。观看可视化时所有捕食者都在地图角落原地打转。原因这是典型的“策略崩溃”本质是经验回放缓冲区里的样本来自“不同能力的对手”。前期大家水平相当数据分布自然后期某些智能体先学出了拉扯战术旧样本里“猎物还在移动”的经验不再适用actor 的梯度方向被老的优质样本带偏。解决把经验回放缓冲区的大小从 1e6 降到 2e5加大新数据的权重。同时把 batch size 提到 512让每次梯度更新更全面地覆盖新旧数据分布。如果不行就用技巧性的做法每 200 episode 清空一次 buffer让智能体从头学习。这个操作虽然在论文里没有明确提及但在实践中能显著缓解策略崩溃问题。5.2 问题训练 1000 个 episode 后actor 输出的动作始终是边界值比如全是 -1 或全是 1现象训练曲线收敛得很漂亮但可视化里智能体一直朝着地图边界冲行动完全不像是在追猎物。原因Actor 的输出层是 tanh它对输入特别敏感。当 critic 给 actor 的梯度指向“输出更大动作”时tanh 工作在饱和区梯度消失actor 被钉死在动作边界。这被称为“动作饱和”在连续控制里非常经典。解决两种思路。一是给 actor 的 loss 加一个正则项惩罚动作的幅度# 在 actor_loss 上增加动作平滑惩罚lambda 一般取 0.001 到 0.01 actor_loss -critic(obs_n, actor(obs_n)).mean() lambda_reg * (actor(obs_n) ** 2).mean()二是引入更温和的探索方式比如 initial noise 大、衰减快让智能体前期充分探索动作空间避免被早期随机梯度带进饱和区间。5.3 问题一个智能体学得很好另一个完全没进展现象训练日志里智能体 1 的奖励曲线缓慢上升智能体 2 的奖励一直贴着初始值波动。原因Madpopg 里每个智能体的 critic 都用全局信息但 actor 只用局部观测。如果你的环境给两个智能体的观测维度不同比如猎物能看到全图而捕食者只能看局部那么观测信息少的那个智能体天然学习更慢。另一种常见情况是两个智能体的 actor 学习率一样但它们的奖励尺度差异巨大一个每次拿到 10一个只有 -0.5损失量级不同导致梯度更新幅度失衡。解决给每个智能体单独设置学习率或者对奖励做归一化处理。最简单的方式是记录每个智能体的历史奖励均值训练时用reward - mean_reward替代原始 reward。这会切断奖励的绝对大小对梯度的影响但保留相对差异。5.4 问题训练时 loss 变成 NaN但前 500 个 episode 一切正常现象训练到中期critic loss 变成 nan随后 actor loss 也变成 nan整个训练进程报废。原因绝大多数情况下是 reward 或 done 信号出现异常。比如某个状态下 reward 是inf经过几轮迭代后 Q 值爆炸。多智能体环境里最常见的原因是某一帧所有智能体都在同一个位置环境返回了重叠检测的 reward 累加导致数值溢出。解决在训练循环里对 reward 做截断# 在存储进 buffer 前对 reward 做数值截断防止 inf/nan 污染经验池 reward np.clip(reward, -10, 10)5.5 问题测试时可以复现训练时的行为但一旦关掉 noise 就完全失灵现象训练时加 noise 效果好测试时noise_scale0结果智能体像没学过一样撞墙、走直线。原因actor 在训练过程中过度依赖探索噪声带来的“抖动”来维持动作多样性。当你把噪声清零后actor 的输出退化成确定性策略但它本身还没学会在没有噪声的情况下产生好的动作序列。解决训练后期逐步衰减噪声幅度比如每 100 episode 把noise_scale减半让 actor 在探索和利用之间平滑过渡。衰减到 0.02 以下再关掉噪声做测试一般就能连贯表现。6. 评估对抗博弈训练效果的三个硬指标单靠奖励曲线判断收敛是危险的6.1 胜率曲线关闭噪声后让两个训练好的智能体硬碰硬奖励曲线在多智能体博弈里有个严重缺陷它衡量的是绝对回报而非相对胜负。一个捕食者可能因为拿到了 5 的碰撞奖励而显得“进步”但同期猎物也学到了更好的逃跑路线它的 5 其实是被动碰上的。真正值得跟踪的是硬碰硬的胜率曲线。评估方法如下每隔 50 个 episode保存一次模型权重然后“冻结”两个智能体的策略关闭所有探索噪声让它们互相对战 100 局统计捕食者的胜利次数。记录在训练日志里你会发现胜率曲线比奖励曲线滞后约 100 个 episode 才能真正反映策略强度。如果奖励在涨但胜率不涨大概率是“虚假进步”可能只是环境本身的随机性波动或者策略学会了某个脆弱的投机行为。6.2 行为多样性指标策略是否模式单一决定了对抗的鲁棒性博弈对抗里最忌讳的是策略收敛到单一模式。如果捕食者永远只从左边包抄猎物学会向右逃之后你就永远抓不到它。但如果你只盯胜率这个问题完全看不出来——因为训练时的对手还没学会针对你的弱点。一个轻重量的验证方式是把保存下来的不同训练阶段模型两两对战做交叉评估。比如用第 500 episode 的模型打第 1000 episode 的模型如果后者胜率超过 75%说明策略有明显的演化趋势如果胜率一直在 50% 附近徘徊说明模型只是把训练中的对手记住了而不是学到了通用的对抗能力。这种验证不需要重写代码只要在测试脚本里多写一个 for 循环加载两个模型分别做决策。6.3 训练稳定性指标多次随机种子下的中位数与方差最后一个维度的评估不是看单次训练的结果而是看多次训练的稳定性。MADDPG 对初始化非常敏感同一套超参数两个随机种子可能得到截然不同的策略行为。我自己经历的对比是用 5 个不同的 seed 分别训练 1000 个 episode最后对每个 seed 的胜率结果做统计看中位数和四分位距。如果四分位距超过整体胜率值的 20%说明你的超参数还在“碰运气”区间不建议直接信任这个模型的性能。这个习惯在项目验收或论文实验时尤为重要。很多“效果惊艳”的对抗演示视频只展示了一次成功运行真实情况是同一套代码重跑三遍可能只有一遍复现出那个效果。多 seed 验证能帮你区分“算法的能力”和“运气的加持”。6.4 一个实用的落盘习惯每隔 200 episode 保存一次权重全部保留别覆盖训练到后期如果发现当前权重不如 1000 episode 时的权重好用你会无比怀念那个版本的模型。我现在有个固定习惯每个评估节点保存权重时文件名带上当前 episode 数和胜率绝不覆盖。这样如果后续调参把模型调崩了随时能回滚。毕竟 MADDPG 这种靠经验回放和软更新慢慢磨出来的模型每一版权重都对应一段独特的学习历程覆盖掉就没有后悔药可吃了。希望这些从选型、跑通到评估的路径能帮你在多智能体博弈方向少走一段弯路。本文还有配套的精品资源点击获取
返回列表