ARTICLE DETAIL

资讯详情

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

MADDPG多智能体强化学习实战:集中训练分布式执行与PyTorch实现

MADDPG多智能体强化学习实战:集中训练分布式执行与PyTorch实现 简介Python实现的MADDPG多智能体博弈对抗算法源码包面向机器学习、人工智能及相关专业的高校学生、科研人员和开发者解决多智能体环境下合作与对抗策略训练问题。该源码包聚焦多智能体强化学习中的典型博弈对抗场景资源共91个文件以MADDPG、DDPG、网络结构定义等76个Python脚本为主另含GIF实验演示、运行笔记、环境说明与配置文件整体压缩包3.26MB目录按源码、环境、示例分层便于直接运行与二次开发。当前已有92人学习下载。除算法核心实现外还附带基于ma-gym的多智能体对抗环境与实验结果可视化GIF可直观观察训练效果配套测试脚本与说明文档能帮助快速上手从环境配置到模型训练均有对应模块支撑有助于理解MADDPG的Actor-Critic架构与经验回放机制。资源适合作为毕业设计、课程设计或项目初期的立项演示也可在此基础上扩展自定义对抗场景满足进阶学习与科研探索需求。下载即可获得完整项目结构含可直接调用的智能体交互示例。1. 多智能体博弈训练不收敛MADDPG 用一条思路把问题扭转过来在多智能体博弈对抗里直接用单智能体算法去训每个个体十次有九次不收敛。几个智能体各自跑一个 DDPG每个 agent 看起来都在最大化自己的收益实际训练时 reward 曲线一直抖动Q 值涨上去策略却没有进步。问题不在梯度而在环境不平稳对手也在更新策略对你正在训的 agent 而言环境动态一直在变经验回放里的旧样本其实已经失真。MADDPG 的解法很直接——训练阶段给每个智能体的 Critic 喂全部智能体的观测和动作让 Q 函数看清全局执行阶段 Actor 只看自己的局部观测。这个“集中训练、分布式执行”的框架也是读懂 MADDPG 源码和调参的主线。这篇文章按一个最小可跑的 Python 实现来讲怎么搭环境、怎么定义 Actor/Critic、主循环要改哪些细节才能收敛以及最后如何用对抗胜率而不是奖励曲线验证实验结果。适合已经跑过 DDPG 或 PPO、想把这个思路扩展到多智能体博弈场景的工程师和研究者。2. 跑通 MADDPG 实验环境MPE 与 PettingZoo 的选择和兼容性处理拿到一份带“源码及实验结果”的 MADDPG 压缩包通常第一眼看到的会是envs/、maddpg/、train.py这种目录结构。我一般会先不看模型代码而是把实验环境跑起来因为环境接口的版本差异比算法本身更容易劝退。多智能体粒子环境MPE是 MADDPG 论文实验用的那套二维场景后来 PettingZoo 把 MPE 纳入了自己的场景集形成了两套 API 并存的局面。选哪套决定了你后面改代码时遇到的是“环境装不上”还是“接口对不上”。2.1 为什么粒子环境成了多智能体对抗实验的默认起点MPE 的场景设计很简单几个小球在连续坐标上移动通过距离、碰撞和边界条件定义奖励。simple_tag 是典型的捕食与逃跑对抗adversary 一方要靠近目标猎物一方要拉开距离simple_adversary 则带上了“目标混淆”其中一个假目标会让智能体误判。这类场景状态量小、单步开销低一个完整的对抗训练实验可以在几分钟到几十分钟内跑完所以 MADDPG 论文之后的绝大多数源码复现都沿用这一套场景。我在做算法对比时会把 MPE 当成“单元测试”来用场景是否收敛、胜率是否分化能很快暴露实现里的维度错误或梯度问题。但它和真实博弈差得很远动作空间是连续速度观测是相对位置和相对速度没有通信和感知遮挡。所以 MPE 适合验证 MADDPG 框架本身不适合直接迁移到真实多智能体系统里做策略部署。2.2 安装到可渲染的最小代码两条路径对比老项目的依赖一般是multi-agent-particle-envs它依赖旧版 gym API和新环境的命名空间冲突比较多。新项目我一般优先用 PettingZoo 的mpe场景接口统一、安装省事。先给安装命令bash老路径多智能体粒子环境论文复现仓库常见pip install multi-agent-particle-envs新路径PettingZoo 里的 mpe 场景接口更规范pip install pettingzoo安装后用最小脚本验证环境能否 reset 和 step。下面这段用的是 PettingZoo 的 AEC 接口注意它和 gym 的env.reset()返回单个观测不同AEC 需要先取env.last()拿到当前 agent 的观测才能决定动作python from pettingzoo.mpe import simple_tag_v3env simple_tag_v3.env(render_modehuman, max_cycles200) env.reset(seed42)for agent in env.agent_iter(): # 取当前轮到哪个智能体以及它的观测、奖励和终止状态 observation, reward, termination, truncation, info env.last() if termination or truncation: action None # 回合结束时不再需要动作 else: action env.action_space(agent).sample() # 随机策略 env.step(action)env.close()这段代码里agent_iter()负责按顺序调度智能体env.last()是 AEC 接口的固定用法termination对应环境内终止条件truncation对应步数上限被截断。两者都为真时当前智能体这一轮不执行动作。简单跑通之后把render_mode去掉再训练能省掉大量渲染开销。2.3 环境交互循环确认观测、动作空间的形状在把 MADDPG 训练代码套上来之前我习惯先跑一个纯随机策略的循环把每个智能体的观测维度和动作空间打出来。这一步看着多余但能避开后面 80% 的维度拼接错误。下面是旧版 MPE API 的交互方式论文源码和很多早期复现都用它python旧版 MPE 环境按场景名加载接口更接近 MADDPG 论文原版import multiagent.scenarios as scenarios from multiagent.environment import MultiAgentEnvscenario scenarios.load(simple_tag.py).Scenario() world scenario.make_world() env MultiAgentEnv(world, scenario.reset_world, scenario.reward, scenario.observation, info_callbackNone)obs_n env.reset() for step in range(5): action_n [env.action_space[i].sample() for i in range(env.n)] obs_n, reward_n, done_n, info_n env.step(action_n) print(step, [o.shape for o in obs_n], reward_n)注意像 simple_tag 这类场景里不同角色的观测维度和动作维度并不一致捕食者和猎物的观测拼接方式不同。MADDPG 的 Actor 是按角色共用还是每个智能体独立一个通常由源码里的一个配置项控制我的建议是先按同构智能体共用网络跑通再逐步拆成异构否则早期脚本会把大量时间耗在维度对齐上。提示如果你的训练脚本来自旧项目先看它 import 的是gym还是multiagent.environment两条 API 不要混用。PettingZoo 环境内部实现也依赖 gymnasium混用会导致 reset 和 step 返回结构不一致。3. MADDPG 核心源码模块实现Actor 看局部Critic 看全局环境跑通之后下一步是把 Actor 和 Critic 两个网络的实现看懂。MADDPG 的源码规模不大核心改动集中在 Critic 的输入拼接和训练时的 Q 目标计算上。这部分实现正确了训练主循环就只剩超参数问题。3.1 集中训练、分布执行MADDPG 相对 DDPG 改在哪DDPG 的 Critic 输入是单个智能体的观测和动作Q 函数只能感知到环境变化带来的间接影响。在多智能体博弈中对手策略一变Q 值的分布就整体漂移目标网络和经验回放都救不回来。MADDPG 把 Critic 的输入改成全局拼接Q_i(x, a_1, ..., a_N)其中x是所有智能体观测的拼接a_1...a_N是所有智能体的动作。这样 Q 函数在训练时能区分“当前收益下降是因为自己动作差还是对手变强了”。Actor 仍然只用自身观测o_i输出动作。执行时不依赖其他智能体的信息所以不会引入通信延迟或中心化依赖。训练时 Actor 的梯度方向由 Critic 提供∇θi J ≈ E[ ∇θi μi(oi) · ∇ai Qi(x, a1...aN) ]这里ai替换成μi(oi)后的输出。直观理解是Critic 告诉 Actor “如果所有关系统保持不变你往哪个方向调整动作能提高全局 Q 值”。一个容易忽略的细节是计算 Q 目标时下一步动作必须来自目标 Actor而不是当前 Actor。MADDPG 里每个智能体都维护一组target_actor和target_critic目标 Q 值由target_critic(x, a_1, ..., a_N)算出其中a_i target_actor_i(o_i)。这一步保证了目标 Q 的计算过程更稳定不会因为当前策略突变而剧烈跳变。3.2 Actor 与 Critic 的 Python 定义我用 PyTorch 写一个可直接套用的最小实现。Actor 输出连续动作用tanh把动作压到[-1, 1]对应 MPE 连续动作空间的边界python import torch import torch.nn as nn import torch.nn.functional as Fclass Actor(nn.Module): definit(self, obs_dim, act_dim, hidden256): super().init() self.fc1 nn.Linear(obs_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, act_dim)def forward(self, obs): x F.relu(self.fc1(obs)) x F.relu(self.fc2(x)) # tanh 输出范围 [-1, 1]与 MPE 的连续动作空间对齐 return torch.tanh(self.fc3(x))Critic 输入是所有智能体的观测和动作拼接。这里我传两个组合好的张量而不是单个 agent 的 obs是为了让网络结构直接对应“全局 Q”的语义python class Critic(nn.Module): definit(self, total_obs_dim, total_act_dim, hidden256): super().init() self.fc1 nn.Linear(total_obs_dim total_act_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc3 nn.Linear(hidden, 1)def forward(self, obs_all, act_all): # obs_all: [batch, sum(obs_dim)]act_all: [batch, sum(act_dim)] 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)代码里total_obs_dim是每个智能体观测维度之和total_act_dim是每个智能体动作维度之和。如果场景有 3 个智能体观测各为 10 维动作各为 2 维那么 Critic 第一层的输入就是3*10 3*2 36。这个拼接只在训练阶段出现评估时不需要构建obs_all直接让各 Actor 独立决策即可。注意如果场景内智能体数量不固定Critic 拼接逻辑就不能写死维度。常见做法是给最大智能体数量预留位置不足的位置补零并在动作掩码里标记哪些位置是有效的。3.3 经验回放与目标网络软更新MADDPG 的经验回放里存的是一个“联合 transition”(obs_n, action_n, reward_n, next_obs_n, done_n)每个字段都是所有智能体的列表。采样时要保证所有智能体的数据来自同一步否则 Critic 的全局拼接就失去了时间一致性。实现如下python import random import numpy as npclass ReplayBuffer: definit(self, capacity): self.capacity capacity self.buffer [] self.pos 0def push(self, transition): # transition 是 (obs_n, action_n, reward_n, next_obs_n, done_n) if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.pos (self.pos 1) % self.capacity def sample(self, batch_size): batch random.sample(self.buffer, batch_size) # zip(*batch) 转置成按字段聚合再转 tensor return tuple( np.array(x, dtypenp.float32) for x in zip(*batch) )随机采样而不是按 episode 连续采样是为了打破时间相关性。但要注意在多智能体对抗里过旧的样本同样危险对手策略已经更新了很多轮旧样本里的“最佳动作”很可能已经不是最佳了。容量设太大反而会拖慢收敛这一点在第四章会展开。目标网络更新用软更新常见做法是每步训练后执行一次python def soft_update(target, source, tau0.01): for tp, sp in zip(target.parameters(), source.parameters()): # 目标参数向当前参数缓慢靠近 tp.data.copy_(tau * sp.data (1.0 - tau) * tp.data)tau默认取 0.01含义是每步只把目标网络向当前网络移动 1%。这个值越大目标网络跟随越快但训练稳定性越差在 MPE 这种小动作空间里0.01 是论文默认值实战中很少需要调大。4. 训练主循环与收敛调参从探索噪声到奖励尺度的坑网络定义完成后训练主循环看似就只是个 DDPG 的多智能体版本实际跑起来后问题会集中在几个位置探索噪声、更新频率、奖励尺度。这里我给出可直接运行的骨架和一套我常用的定位方式。4.1 train.py 的单进程骨架训练循环按 episode 推进每个 episode 内先采样完整轨迹采样结束后再统一做梯度更新。代码如下python伪代码骨架n_agents 个智能体共用同一个 bufferfor episode in range(num_episodes): obs_n env.reset() episode_rew np.zeros(n_agents)for step in range(max_steps): action_n [] for i in range(n_agents): obs_t torch.FloatTensor(obs_n[i]).unsqueeze(0) with torch.no_grad(): # 训练时叠加高斯噪声简单且够用 action actors[i](obs_t).cpu().numpy().flatten() action np.clip(action noise_scale * np.random.randn(*action.shape), -1, 1) action_n.append(action) next_obs_n, reward_n, done_n, _ env.step(action_n) # 存联合 transition保证所有智能体数据来自同一步 buffer.push((obs_n, action_n, reward_n, next_obs_n, done_n)) obs_n next_obs_n episode_rew np.array(reward_n) if done_n: break # episode 结束后集中更新让样本先积累一小批 for _ in range(updates_per_episode): update_once() # 内部完成 actor/critic 的梯度下降和软更新这里有一个实现细节值得说明obs_n存进 buffer 时是多个数组buffer.sample后要分别取出每个智能体的 obs 拼成[batch, obs_dim]张量再传给各自 Critic。很多复现代码在这一步直接用torch.cat(obs_all, dim-1)如果某个智能体的观测维度不同就会报错所以我会先把所有智能体的样本按固定顺序拼接再做训练。noise_scale一般从 0.3 开始随 episode 线性衰减到 0.05。MADDPG 原论文用的是 OU 噪声但粒子环境动作空间小高斯噪声在多数场景下收敛更快我通常先用高斯跑通再视情况换 OU。关键在于噪声要随时间衰减让策略从探索逐渐转入利用。4.2 必调超参数与推荐值下面是我在 simple_tag、simple_adversary 这类场景里常用的初始参数偏差不会太大参数推荐取值关注点num_episodes20006000粒子环境收敛窗口短胜负分化会在 1000 轮前后出现max_steps2550场景自带 horizon设太大会让奖励分布被拖偏buffer_capacity500k1e6需覆盖最近 12 个对手策略更新周期batch_size2561024MPE 观测简单1024 时梯度更稳定actor_lr1e-4低于 critic_lr避免动作搜索抖动critic_lr1e-3高于 actor_lr否则 Q 值收敛慢gamma0.95有限步博弈0.95 足够tau0.01软更新系数优先不动updates_per_episode100每个 episode 后更新次数我一般不低于 100actor_lr一定要低于critic_lr。Critic 负责学习全局价值函数更新快一些能尽早给 Actor 提供稳定梯度Actor 更新太快会直接改变动作分布让 Critic 刚学到的 Q 值很快失效。这个比例在 DDPG 时代就有效在 MADDPG 里更明显。4.3 训练不稳定时怎么定位问题训练曲线抖动时我一般按三个位置排查。第一看 Q loss 是否下降。如果 Critic 的 loss 长期不降多半是 buffer 里老样本占比过高导致 Q 目标一直互相矛盾。这时把 buffer 容量缩小到 200k或提高updates_per_episode效果比调学习率更直接。第二看 Actor 输出是否饱和。打印每个智能体动作的均值如果长时间接近 ±1说明tanh进入了饱和区梯度接近于零。处理办法是降低noise_scale初始值或者检查 reward 绝对值是否过大——reward 都在几十量级时Actor 的梯度步长会被 Q 值放大动作很快被推到边界。第三看奖励尺度。粒子环境默认 reward 通常是整数级别比如追击成功 10、碰撞 -1。MADDPG 的 actor loss 对 Q 值直接取负均值Q 值尺度不统一会导致不同场景下的学习率不可移植。我一般在训练前把单步 reward 除以一个常数做归一化或者等第一个 episode 跑完后统计 reward 的均值方差再缩放。5. 验证实验结果关闭噪声后的胜率测试与自博弈校验训练曲线只能说明累积奖励在涨不能证明策略具备博弈能力。多智能体对抗里两个常见假象是奖励涨是因为学会了逼近猎物而不是学会对抗或者两个智能体都退化成同一种行为奖励互相抵消但曲线很好看。因此我每次训练完都固定做一次关闭噪声的对抗测试。5.1 关闭噪声跑对抗测试评估时的关键改动是去掉动作噪声并把所有 target 网络换成 eval 模式下的当前网络。代码如下python加载训练好的 actors 列表关闭探索噪声def evaluate(actors, env, episodes100): wins 0 for _ in range(episodes): obs_n env.reset() done False while not done: action_n [] for i, actor in enumerate(actors): with torch.no_grad(): obs_t torch.FloatTensor(obs_n[i]).unsqueeze(0) action actor(obs_t).cpu().numpy().flatten() # 评估时不加噪声测的是确定性策略本身 action_n.append(np.clip(action, -1, 1)) obs_n, reward_n, done, _ env.step(action_n) # judge 是场景里定义胜负的判定比如捕食者抓到猎物 if judge(env): wins 1 return wins / episodeswins / episodes得到的就是对抗胜率。相比累积奖励它更直接反映策略在博弈中的强弱。我建议每 200 episode 就在训练过程中插入一次这样的评估不用等全部训练结束。5.2 把训练检查点当作“对手池”做自博弈校验再往深一步我把每 500 episode 保存的模型权重互相对战形成一个互胜率矩阵。如果最终模型对所有旧版本都稳定在 70% 以上说明学到的是可迁移的博弈能力如果最终模型反而打不过 2000 episode 时的自己问题多半出在训练后期对手分布变窄——MADDPG 所有智能体同时更新后面对手的变化幅度变小策略容易过拟合到最近阶段的对手行为。这时把 buffer 里最近样本的采样权重调高或者每 1000 episode 把某个旧检查点重新放入对手池混合采样通常比继续加训练轮数更有效。这个互胜率矩阵我会直接画成热力图放进实验结果目录和训练曲线放在一起作为“策略是否真的变强”的判据。本文还有配套的精品资源点击获取
返回列表