ARTICLE DETAIL

资讯详情

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

MADDPG多智能体强化学习实战:Python实现与对抗场景训练指南

MADDPG多智能体强化学习实战:Python实现与对抗场景训练指南 简介一份基于Python的MADDPG多智能体博弈对抗算法实现面向人工智能、自动化、电子信息等专业的学生、科研人员及开发者用于学习多智能体强化学习、博弈对抗策略设计或作为课程设计、毕业设计起点。压缩包共91个文件以76个Python源码文件为主体涵盖算法核心模块、环境定义、训练与测试脚本另含10个演示GIF、1个Jupyter Notebook及说明文档可直观查看对抗过程与结果。资源包大小3.26MB轻量易部署。内容上不仅提供DDPG、MADDPG等算法实现还集成多智能体对抗环境如追逃、攻防等与实验记录便于对比分析不同策略效果。代码结构清晰配套README与测试说明方便二次开发。目前已有92人学习下载适合具备一定Python基础、希望进阶多智能体强化学习的读者参考实践。借助完整源码与实验数据可快速复现实验、验证算法性能并在此基础上扩展新的博弈场景。1. MADDPG 到底在解决什么问题Python 多智能体博弈对抗的核心算法做多智能体博弈对抗绕不开的第一个算法基本就是 MADDPG。它是 Python 生态里最容易上手、引用率最高的多智能体强化学习方案每个智能体只有一个 Actor 管自己的观测和动作训练时却有一个能看全局的 Critic 同时掌握所有智能体的状态与动作专门对付你在动、对手也在动的非平稳环境。拿到一份带源码和实验结果的 MADDPG 压缩包目标无非是快速跑通、复现对抗效果、再改到自己的场景里。这篇笔记适合正在入门强化学习、要做课程设计或博弈对抗策略实验的读者按下面的步骤走就能把训练跑起来。2. 中心化训练与去中心化执行MADDPG 的原理和网络骨架MADDPG 出自 2017 年的论文 Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments解决的核心问题是多个智能体同时学习时环境不平稳。它的答案概括成一句话训练时 Critic 看全景执行时 Actor 只看自己。2.1 为什么把 DDPG 直接搬到多智能体场景会翻车先看单智能体 DDPG 能成立的前提环境转移概率 P(s|s,a) 只取决于自己的动作经验回放里存下来的 (s,a,r,s) 分布是固定的旧经验可以反复学。多智能体里这个前提不成立。假设两个捕食者在追一个猎物猎物的策略从直跑变成绕圈捕食者 1 在同一个状态下采取的同一个动作导致的结果会完全不同——因为转移还取决于捕食者 2 和猎物的动作而这两个策略都在随训练改变。从每个智能体自己的视角看这就是一个分布一直在漂移的环境。独立 DDPG 的做法是每个智能体一套网络、各存各的经验本质上是把非平稳问题硬当成平稳问题去解。回放池里存的是上一版本环境的经验用旧经验更新新 Q 函数就像拿今天的模型学昨天的规律Q 值永远在追一个移动靶。实际表现一般是奖励曲线小幅上升后剧烈震荡最后发散。很多新手把 DDPG 改一改就搬过来翻车就翻在这。2.2 CTDE全局 Critic 与本地 Actor 的分工MADDPG 把看和动拆开。训练时每个智能体的 Critic 输入是所有智能体的观测和动作拼接输出对当前智能体动作价值的估计 Q(s_1..s_n, a_1..a_n)。Actor 则只输入自己的观测输出自己的动作。到了推理阶段只用 Actor执行时不需要任何通信这就是中心化训练与去中心化执行CTDE。还有一个容易被忽略的细节计算目标 Q 值时MADDPG 不去拿其他智能体的真实动作而是用他们的 target actor 网络预测动作。这样有两个实际好处。第一训练时不需要知道对手的真实策略对手是黑匣子也能训第二目标 Q 的方差更小不会因为对手策略突然变化导致目标值跟着跳。这两个好处在博弈对抗场景里尤其重要因为对抗双方本来就不该互相暴露策略。2.3 Actor 与 Critic 的网络骨架每一行对应什么参数用 PyTorch 落地一份最小实现通常只有两个网络类# networks.py —— Actor 和 Critic 的最小实现 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().__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)) return torch.tanh(self.fc3(x)) # tanh 压缩到 [-1, 1] class Critic(nn.Module): def __init__(self, global_obs_dim, global_act_dim, hidden64): super().__init__() self.fc1 nn.Linear(global_obs_dim global_act_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) # 输出单个 Q 值逻辑说明Actor 用三层全连接最后一层 tanh 激活因为粒子环境multi-agent-particle-envs的动作空间是 [-1, 1]如果你换成别的动作范围要在外层乘上动作上限。Critic 的关键在 forward 里的 torch.cat把全局观测和全局动作拼成一个向量Q 值才能表达其他智能体怎么动会影响我的价值。参数说明obs_dim 是单个智能体的观测维度act_dim 是单个智能体的动作维度global_obs_dim 在代码里通常传 obs_dim × 智能体数。hidden 是隐藏层宽度64 是起步值观测维度高或者智能体数量多时可以调到 128~256调大 hidden 会增加单步计算量但对小规模博弈场景影响不大。装好依赖后先做一次前向验证确认维度拼接没有问题# 快速验证前向 n_agents, obs_dim, act_dim 3, 4, 2 actor Actor(obs_dim, act_dim) critic Critic(obs_dim * n_agents, act_dim * n_agents) obs torch.randn(n_agents, obs_dim) act torch.cat([actor(obs[i]) for i in range(n_agents)], dim-1) # (6,) q critic(obs.view(1, -1), act.view(1, -1)) print(q.shape) # torch.Size([1, 1])逻辑说明view(1, -1) 把三个智能体的观测横向拼成 (1, 12)动作也拼成 (1, 6)Critic 输出一个标量 Q。这个验证能跑通说明网络定义和维度拼接约定是一致的后面训练循环里的数据布局照这个来就不会错。3. 把源码跑起来Python 环境、工程结构与最小训练命令拿到压缩包先建独立环境别直接装进 base 环境否则后面 numpy 和 gym 的版本冲突会找上门。这一章把从环境准备到第一次训练跑通的完整路径走一遍。3.1 Python 环境准备版本锁死是第一步我一般用 conda 建环境顺手把关键依赖版本锁住conda create -n maddpg python3.8 conda activate maddpg pip install torch1.13.1 numpy1.23.5 gym0.21参数说明python 3.8 兼容性最好网上大部分 MADDPG 示例是按 3.7~3.9 写的torch 1.13.1 对 CPU 和 CUDA 11 都友好装的时候根据机器选 CPU 或 CUDA 版本即可numpy 锁 1.23.5 是为了避开 1.24 之后移除 np.bool8 导致的报错gym 锁 0.21 是因为 0.26 起 reset 和 step 的返回格式变了老代码直接崩。如果你手里的源码自带 requirements.txt以它的版本为准但 numpy 和 gym 这两个坑几乎每个包都会踩到。然后装多智能体粒子环境。这份源码如果自带环境目录就在根目录执行cd multi-agent-particle-envs pip install -e .提示可编辑模式-e意味着你修改环境源码后改动即时生效。调试自定义场景时这个模式能省掉反复重装的时间强烈建议保持。3.2 源码包结构一份标准 MADDPG 工程的文件地图解压后先别急着跑按文件结构对照一遍。常见做法是下面这几个文件分工文件职责关键点main.py训练入口解析 --scenario、--episodes 等参数maddpg.py算法主体每个智能体持有一组 Actor/Criticnetworks.py网络定义Actor 和 Critic 类buffer.py经验回放存全体观测、动作、奖励config.py超参数集中管理学习率、噪声、gamma 等envs/环境代码粒子环境或自定义环境注意 buffer.py 和单智能体版本的区别MADDPG 的回放池存的是全体智能体的观测和动作快照而不是单条经验。这个设计是由中心化 Critic 决定的——训练时它需要拿到所有人的信息buffer 里少存了一个维度后面的更新函数就拼不出完整输入。3.3 最小训练命令与训练循环解读python main.py --scenario simple_spread --episodes 20000 --save-interval 1000参数说明--scenario 指定环境simple_spread 是 3 个智能体协作散布到 3 个地标用来验证代码能否正常收敛--episodes 是总回合数--save-interval 是每 1000 回合存一次 checkpoint。粒子环境默认一局 25 步。训练主循环剥掉命令行参数后核心逻辑是这样# main.py 训练主循环节选 for episode in range(args.episodes): obs_all env.reset() total_reward 0 # 噪声随训练进度线性衰减前期探索后期利用 noise_scale max(args.noise_min, args.noise_init * (1 - episode / args.episodes)) for step in range(args.max_episode_length): actions [] for i, agent in enumerate(maddpg.agents): obs_t torch.FloatTensor(obs_all[i]).unsqueeze(0) act agent.actor(obs_t).detach().numpy().flatten() act np.random.normal(0, noise_scale, sizeact.shape) # 探索噪声 actions.append(np.clip(act, -1.0, 1.0)) obs_next_all, rewards, done, _ env.step(actions) # 注意整条经验以全体为单位存入回放池 replay_buffer.push(obs_all, actions, rewards, obs_next_all, done) if replay_buffer.size() args.batch_size * 10: batch replay_buffer.sample(args.batch_size) for agent in maddpg.agents: agent.update(batch) obs_all obs_next_all total_reward sum(rewards) if episode % 1000 0: print(fep {episode}, reward {total_reward:.2f}, noise {noise_scale:.3f})代码逻辑说明每个智能体先用当前策略算动作加高斯噪声做探索clip 到 [-1, 1] 后统一交给 env.step。replay_buffer.push 里传的是 obs_all 和 actions 的完整列表这是 MADDPG 和独立 DDPG 在数据层面的关键区别。update 触发条件设了 buffer 超过 batch_size 十倍的门槛避免前几百条经验反复学。参数说明noise_scale 从初始值线性衰减到 noise_min常见组合是 0.3 衰减到 0.05衰减太快会导致前期探索不足太慢则后期策略不稳定。如果脚本里没有这个门槛建议自己加上能明显减少早期崩坏。训练循环里调用的 agent.update也就是 MADDPG 的核心更新逻辑长这样# maddpg.py —— Agent 类的 updateself 是当前智能体 # self.all_agents 是所有智能体对象列表 def update(self, batch): obs_all, act_all, rew, obs_next_all, done batch B act_all.shape[0] agent_id self.agent_id # 1) 目标 Q用所有智能体的 target_actor 生成下一时刻动作 with torch.no_grad(): next_act torch.cat( [agent.target_actor(obs_next_all[:, i]) for i, agent in enumerate(self.all_agents)], dim-1) q_next self.target_critic(obs_next_all.reshape(B, -1), next_act) target rew[:, agent_id].unsqueeze(-1) \ self.gamma * (1 - done) * q_next # 2) 更新 Critic q_now self.critic(obs_all.reshape(B, -1), act_all.reshape(B, -1)) critic_loss F.mse_loss(q_now, target) self.critic_optim.zero_grad() critic_loss.backward() self.critic_optim.step() # 3) 更新 Actor只替换本智能体的动作片段其他智能体保持采样值 act_modified act_all.clone() act_modified[:, agent_id] self.actor(obs_all[:, agent_id]) actor_loss -self.critic(obs_all.reshape(B, -1), act_modified.reshape(B, -1)).mean() self.actor_optim.zero_grad() actor_loss.backward() self.actor_optim.step() # 4) 软更新目标网络 for tp, p in zip(self.target_actor.parameters(), self.actor.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data) for tp, p in zip(self.target_critic.parameters(), self.critic.parameters()): tp.data.copy_(self.tau * p.data (1 - self.tau) * tp.data)逻辑说明第 1 步用目标网络算 Q 目标注意用的是 target_actor 预测的虚拟动作不是对手的真实动作。第 3 步是 MADDPG 和 DDPG 最核心的差别——Actor 的梯度来自 Critic 对自己的动作被替换后的 Q 值其他智能体的动作块在计算图里保持原样这样梯度只回传到当前 Actor。参数说明tau 是软更新系数0.01 表示目标网络每次只往当前网络方向挪 1%太小收敛慢太大会让目标 Q 抖动变大。这里 shape 都按 (batch, n_agents, dim) 布局写的如果你的 buffer 存的是扁平拼接格式按对应方式 reshape 即可。4. 调参和读实验结果让多智能体真的学会对抗跑通只是第一步实验效果好不好取决于超参数怎么配、结果怎么看。这一章给出可抄的起点参数以及对抗场景下读曲线的方法。4.1 关键超参数表先按这组起点跑再动两三个参数常见起点作用与调整方向lr_actor1e-4Actor 学习率偏大策略震荡lr_critic1e-3Critic 学习率偏大 Q 高估gamma0.95折扣因子短局场景 0.95~0.99tau0.01目标网络软更新系数batch_size256采样批量多智能体可降到 128buffer_size1e6回放池容量够大即可noise_init0.3初始探索噪声标准差noise_min0.05噪声衰减下限hidden64网络宽度复杂场景用 128max_episode_length25粒子环境默认步数调整口诀是先固定环境侧参数只动 lr 和噪声。gamma、tau、buffer_size 这类参数在不同场景间的敏感性不高不需要反复试。lr_critic 比 lr_actor 大一档是常见做法因为 Critic 要拟合的目标比策略复杂学快一点是合理的但超过 3e-3 就要警惕 Q 高估。tau 不要超过 0.05否则目标网络跟着当前网络抖训练会变成自己追赶自己。hidden 和 batch_size 之间的平衡也要注意batch 越大梯度越稳但多智能体的观测拼接会让单条样本很长显存吃紧时先降 batch_size。4.2 实验结果是看什么的别被单条奖励曲线骗了压缩包里如果有实验结果目录里面通常是奖励曲线 CSV、评估日志和训练好的 checkpoint。看结果时最忌讳只盯着所有智能体奖励之和。在捕食者-猎物这类对抗场景里捕食者奖励上升、猎物奖励下降恰恰说明系统在变好但两者相加可能几乎是一条平线。正确做法是同时看三样东西总奖励曲线、每个智能体各自的奖励曲线、对抗胜率。以 simple_adversary 为例两个捕食者共享抓到猎物的正奖励猎物单独获得到达目标地标的正奖励。训练初期大家乱跑捕食者奖励为负中期捕食者学会包抄捕食者奖励转正猎物奖励下降后期两者都收敛捕食者胜率稳定在一个值。单看总奖励曲线你只会觉得没在学其实是学到了。另外注意训练曲线天然带噪声别因为某一小段往下走就断定失败。多智能体场景里策略互相影响曲线抖动幅度比单智能体大很多看趋势而不是看单点至少用滑动平均平滑后再判断。4.3 复现实验结果的三个验证点第一同一个随机种子跑三次奖励曲线的主干要一致允许尾部抖动这排除运气成分。第二训练结束后把噪声置零重新跑 100 局统计胜率这个数字才是实验报告里该写的训练曲线上的奖励不是。第三和 baseline 对比比如每个智能体独立跑 DDPGMADDPG 的收敛速度和最终胜率通常明显占优。以我跑过的简单对抗场景经验看默认参数下 3000~5000 个 episode 通常能看到胜率拐点。如果 10000 个 episode 还是一条平线先回第 5 章排查不要盲目加 episodes——多数情况是参数或环境适配的问题不是训练量不够。5. 避坑排查训练崩溃、策略趋同与版本冲突的四个典型问题这一章是血泪经验汇总。多智能体训练的坑和单智能体不太一样很多问题表面看是玄学实际都能找到明确原因。5.1 奖励曲线先涨后崩然后一直回不来现象前几百回合奖励上升很快突然在某个区间暴跌之后无论怎么跑都涨不回去。原因最常见的是 lr_critic 偏大导致 Q 值高估。Critic 学得太激进把 Q 值估得虚高Actor 就朝着假的高 Q方向优化等 Critic 修正过来策略已经被带到沟里。另外一个常见诱因是初始噪声过大前期的随机动作污染了经验池。解决把 lr_critic 降到 1e-3 以下同时在 update 里加梯度裁剪# update 中的梯度裁剪放在 loss.backward() 与 optim.step() 之间 critic_loss.backward() torch.nn.utils.clip_grad_norm_(self.critic.parameters(), 0.5) self.critic_optim.step() actor_loss.backward() torch.nn.utils.clip_grad_norm_(self.actor.parameters(), 0.5) self.actor_optim.step()参数说明clip 到 0.5 表示梯度向量的范数超过 0.5 就按比例缩放限制单步更新幅度专治一崩到底。如果裁剪后还在崩把 noise_init 从 0.3 降到 0.2同时确认噪声衰减到 0.05 的时间点是否太靠后。5.2 三个智能体学成了同一个智能体现象多个智能体输出几乎相同的动作协作场景里互相抢任务对抗场景里围堵阵型完全消失。原因Actor 网络初始化相同或者 Critic 的对称结构让梯度方向趋于一致经验池里早期样本又几乎一样策略就被拉齐了。这在博弈对抗里是致命的因为围攻、包抄这类策略天然要求智能体之间行为有差异。解决给每个智能体不同的随机种子做网络初始化这是最少的工作量。另一个实用手段是在 Critic 输入里拼一个 agent_id 的 one-hot 向量破坏对称性——我用这个方法后协作场景的奖励方差明显变大智能体开始出现分工。还可以给每个智能体设独立的噪声偏移比如乘一个各自的小系数早期就让行为拉开距离。5.3 GPU 利用率接近 0训练慢到没法忍现象代码里写了 .cuda()nvidia-smi 也看得到显存占用但利用率一直在 10% 以下两万回合的实验要跑一整天。原因粒子环境在 CPU 上跑仿真每个 step 都要等 env.step() 返回网络本身又小GPU 只在 update 那一瞬间被调度。数据搬移的时间远大于计算时间GPU 大部分时间在空等。解决两个方向。第一降低更新频率从每个 step 更新改成每 100 步更新把 batch_size 加到 512 或 1024攒够一批再更新第二更直接的做法是——这种小网络场景直接用 CPU 训练常常更快省掉了 tensor 在 CPU/GPU 间反复搬运的开销。我自己的准则是隐藏层小于 256 且智能体少于 5 个直接 CPU。5.4 numpy 与 gym 版本冲突训练一开始就报错现象启动就报 AttributeError: module numpy has no attribute bool8或者 env.reset() 返回值数量对不上解包直接 TypeError。原因numpy 1.24 移除了 np.bool8老代码里大量 np.bool8(done) 全部失效gym 0.26 起 reset 返回 (obs, info) 二元组、step 返回五元组和旧代码的约定不一致。解决这个问题 90% 靠锁版本解决回到第 3 章的环境准备命令numpy1.23.5、gym0.21。如果你必须用新版 gym就需要写一个 env_wrapper 把新 API 包装回旧接口把 reset 的 info 丢弃、把 step 的 truncated 合并进 done工作量不大但是纯体力活。排查时先看报错栈里第一个第三方库调用来自哪里通常答案就在环境适配层。6. 进阶自定义博弈场景与对抗胜率验证把 MADDPG 换成自己的场景最省力的做法是复制粒子环境改三处观测归一化、奖励塑形、动作边界。观测里的位置、速度全部除以场景尺度避免量纲差异主导网络权重奖励别用纯稀疏型25 步的短局里稀疏奖励基本学不动建议加距离塑形比如越靠近目标奖励越高动作保持 [-1, 1] 输出在外层乘你的速度上限。6.1 加载 checkpoint 做冻结评估训练结束后我习惯单独跑一段评估脚本关噪声、冻结参数统计 100 局的胜率# evaluate.py —— 加载最后的 checkpoint 做对抗评估 maddpg.load(checkpoints/ep_20000.pt) success 0 for _ in range(100): obs_all env.reset() for t in range(max_episode_length): actions [] for agent, obs in zip(maddpg.agents, obs_all): act agent.actor(torch.FloatTensor(obs).unsqueeze(0)) actions.append(act.detach().numpy().flatten()) obs_all, _, done, _ env.step(actions) if env.is_success(): success 1 print(fwin rate: {success / 100:.2f})逻辑说明和训练循环唯一的区别是删掉了加噪声那行网络参数也是冻结的测的才是当前策略的真实强度。胜率的判定标准要在环境里定义清楚比如捕食者-猎物场景就是25 步内至少一个捕食者触达猎物。参数说明评估局数 100 或 200 都行太少方差大太多浪费时间obs 用 unsqueeze(0) 补一个 batch 维Actor 的输入约定是 (batch, obs_dim)别漏掉。6.2 两个评估习惯第一训练奖励和评估胜率分开记录只有胜率值得写进实验报告训练曲线只用来判断收敛状态。第二同一个 checkpoint 在不同随机种子下多测几次防止单局偶然性误导判断。我自己的习惯是把评估脚本和训练脚本分开每次训练完自动跑一遍评估并追加到实验日志。见过太多看起来学会了最后被评估打回原形的翻车都是只看训练曲线、没做冻结评估。希望帮到你。本文还有配套的精品资源点击获取
返回列表