ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MADDPG多智能体博弈对抗实战:从DDPG到中心化训练去中心化执行

MADDPG多智能体博弈对抗实战:从DDPG到中心化训练去中心化执行 简介面向希望系统学习多智能体强化学习的高校学生与开发者这份基于Python与MADDPG的多智能体博弈对抗算法资源完整覆盖算法搭建、训练与测试流程可直接用于毕业设计、课程设计、工程实训或初期项目立项。压缩包共含14个文件其中10个Python脚本构成核心代码分别实现网络结构定义、DDPG与MADDPG智能体构建、经验回放缓冲区、训练主循环、测试环境交互等模块同时附带README说明文档、配置文件及虚拟环境配置信息方便快速还原运行环境。整个压缩包仅19KB代码量不大但结构完整便于逐行阅读与二次开发。目前已有266人学习适合希望从零理解多智能体博弈机制的入门者或中高级学习者。读者可通过这些脚本搭建自己的对抗实验观察智能体在竞争与合作环境中的策略演化并在此基础上修改奖励函数或环境逻辑扩展出更强的自定义博弈算法。1. 多智能体博弈对抗为什么简单复制单智能体策略会全线翻车设一个场景三架无人机围堵一架入侵机。你把单智能体的 DDPG 复制成三个独立进程去跑结果经常是——三架无人机先乱撞一气然后集体躺平在原地转圈。原因不复杂多智能体强化学习里每个智能体面对的“环境”都包含其他智能体的策略而别人的策略一直在变单智能体那套“环境稳定”假设被直接打破。MADDPG 算法就是冲这个痛点来的。它保留 DDPG 在连续控制上的优势用“中心化训练、去中心化执行”的思路让训练阶段每个智能体能看到他人的状态和动作部署时却只依赖自身局部观测。这篇文章会从算法原理、环境搭建、PyTorch 落地到训练踩坑把一条可复现的博弈对抗训练路线完整走一遍。适合刚开始做多智能体强化学习的同学也适合想把现有单智能体代码升级改造的工程师。2. 读懂 MADDPG 的核心思想中心化训练、去中心化执行改的是什么2.1 先理解 DDPG再看 MADDPG 改了什么DDPG 是单智能体连续控制的经典方案actor 网络根据状态输出动作critic 网络评估“状态 动作”的价值。训练时 critic 给 actor 提供梯度方向actor 负责实际决策。它在机械臂、自动驾驶等单智能体连续控制任务上表现稳定到了多智能体对抗场景却会出问题。根本原因是每个智能体的 reward 不只取决于自己的动作还取决于对手和队友的当前策略当队友的策略随训练不断更新“环境”对任意单个智能体都是非平稳的。非平稳性对经验回放的伤害尤其明显。DDPG 依赖经验回放假设过去采样的经验仍然能代表当前环境分布但多智能体对抗中对手策略一变旧经验的奖励含义就过时了直接拿去更新会让价值估计左右摇摆。MADDPG 的解法很直接每个智能体训练时把环境中所有智能体的观测和动作都喂给自己的 critic让价值函数在“看得见全局”的前提下做评估从而把其他智能体策略变化带来的非平稳性部分转化成了可观测的输入变量。这就是它和 DDPG 最本质的区别。2.2 critic 看全局、actor 看局部为什么这是对抗场景下的正确折衷先明确一点MADDPG 并不是一个全局策略算法而是一组“每个智能体一套 actor-critic”的结构。训练阶段第 i 个智能体的 critic 输入是环境中所有智能体的观测拼接、所有智能体的动作拼接而它的 actor 输入只有自己的观测。两者结构差异可以这样对比结构项单智能体 DDPGMADDPGactor 输入自身观测 o自身观测 o_icritic 输入自身观测 自身动作全部观测 [o_1,...,o_N] 全部动作 [a_1,...,a_N]目标网络数量actor、critic 各 1 个每智能体 actor、critic 各 1 个目标网络部署时actor 可用只部署 actorcritic 留在训练端critic 看全局能带来两个实际好处。第一在对抗中对手动作被显式作为输入Q 值拟合时不用再去“猜测”对手下一步会干什么对手策略更新造成的影响被纳入了输入维度经验回放的失效问题大幅缓解。第二在协作对抗混合场景里队友动作也在 critic 输入里价值评估能正确反映“配合”带来的增益而不是把队友造成的奖励变化误判成环境噪声。actor 保持局部观测则是一个工程折衷。在真实对抗系统里部署时往往没有全局通信条件链路可能丢包、带宽有限、某些观测天然不可共享。如果策略依赖全局状态部署时就会因为信息缺失而直接失灵。MADDPG 把“全局信息”只留在训练端部署时每个智能体靠自己的局部观测独立决策这符合绝大多数实际系统的约束。很多初学着看到 CTDE 就以为算法能集中决策这是最常见的误解——它只是集中地“学”并不要集中地“做”。2.3 目标网络与软更新参数怎么设才不激进MADDPG 的更新逻辑沿袭 DDPG每个智能体都有主网络和目标网络两组参数。计算 TD 目标时下一时刻的 Q 值用目标网络算避免自举式更新导致的价值发散。目标值的表达式是y r γ × Q_target(全部下一观测, 全部目标动作)这里的“目标动作”不是主网络输出的动作而是每个智能体各自目标 actor 网络输出的动作。我见过不少实现图省事直接用当前 actor 输出算下一时刻动作这样会让目标值里同时包含两份在更新的参数梯度被持续注入偏置收敛会慢很多。正确做法是目标 actor 输出后接 detach切断梯度回传。软更新参数 tau 是第二个容易踩的坑。常见做法是每训练一步就把目标网络参数向主网络滑动一点tau 取 0.01 起步。在多智能体对抗里我一般取 tau0.01同时把更新频率从“每步”改成“每 100 步同步一次”相当于兼顾平滑和稳定。如果 Q 值曲线像锯齿一样剧烈抖动把 tau 降到 0.005并把同步间隔拉大到 200 步。注意目标网络参数不是复制越勤越好。多智能体场景里每个 critic 都耦合了所有智能体的策略某个智能体策略突变会被同步放大到其他人的目标值里表现为集体振荡。软更新参数宁可保守。另外要强调的是MADDPG 并没有修改 DDPG 的另一个基础组件——探索。训练时 actor 输出的动作要叠加噪声常见做法是高斯噪声或 Ornstein-Uhlenbeck 噪声。对抗环境下 OU 噪声的时序相关性有助于维持探索的连贯性但参数不好调我实际用下来简单的高斯噪声配合噪声方差衰减在大多数仿真环境里已经够用也更少出现调参玄学。3. 环境与博弈配置用 MPE 搭一个可训练的对抗环境3.1 选哪个环境simple_tag 里天然带对抗与追逐博弈算法学习不能只在 gym 的连续控制环境里跑那里压根没有第二个对手。做 MADDPG 博弈对抗入门最常用的是 OpenAI 早期开源的 multiagent-particle-envs简称 MPE。它是一组 2D 粒子世界环境虽然物理精度不高但动作连续、观测可控、训练速度快非常适合先验证多智能体算法逻辑再迁移到重仿真场景。MPE 里有几个环境选型时要分清楚simple_spread 是纯协作多个智能体分散覆盖地标里面没有对手不适合验证对抗simple_adversary 是“1 个追踪者 vs 2 个被追者”的不对称博弈适合练手simple_tag 则是最经典的追逐对抗追捕方若干、猎物若干还有一块障碍区域每个 agent 都能施加连续加速度。做 2v2 或 3v3 对称对抗我通常从 simple_tag 改起。选这个环境的理由很实际。一是它足够小一次完整训练几分钟到十几分钟就能看出策略有没有分化迭代速度快。二是它的观测是“相对位置 相对速度 自身信息”这种低维向量便于排查 Q 值、奖励曲线等训练指标比一上来就上 AirSim、Isaac 这类重型仿真器好定位问题。等算法在 MPE 上真的学出追逐和逃跑策略再迁移到高保真环境是效率最高的技术路线。3.2 多智能体如何配置最小安装与依赖版本MPE 的安装不算复杂但版本兼容问题容易卡住新手。这个环境包依赖较早版本的 gym 接口直接用最新 gym 运行常会报 API 不匹配。常见的做法是 clone 源码后在虚拟环境里以可编辑模式安装之后如果跑出 gym 相关报错就说明依赖版本偏新需要把 gym 降到老版本。我第一次跑的时候问题就出在 gym 新老接口的差异上折腾了一个多小时才定位到所以这步值得提前注意。一个更省事的配置做法是先把环境目录放到项目文件夹里直接在代码里 import避免全局安装带来依赖污染。下面的命令是最小配置路径# 创建虚拟环境避免污染系统 Python python3 -m venv maddpg_env source maddpg_env/bin/activate # 安装 torch 和基本依赖 pip install torch numpy # 从源码安装 MPE在项目目录下执行 git clone https://github.com/openai/multiagent-particle-envs.git cd multiagent-particle-envs pip install -e .参数说明虚拟环境是这里最重要的一步多智能体项目后续会频繁改依赖版本没有虚拟环境隔离系统级 Python 会被搞得一团糟。-e表示可编辑安装环境包源码在 project 目录中改动后立即生效方便临时修改环境逻辑比如改奖励函数。安装完成后用一段脚本确认环境能正常加载# test_env.py from multiagent.environment import MultiAgentEnv import multiagent.scenarios as scenarios # 加载 simple_tag 场景 scenario scenarios.load(simple_tag.py).Scenario() world scenario.make_world() env MultiAgentEnv( world, scenario.reset_world, scenario.reward, scenario.observation, info_callbackscenario.info, ) obs_n env.reset() print(智能体数量:, env.n) print(观测维度:, [o.shape[0] for o in obs_n]) print(动作空间:, env.action_space[0])这段代码只做一件事确认环境对象能不能正常创建。环境数量env.n打印出来应该是场景中的智能体总数观测维度每个智能体可能不同这取决于场景各自的观测函数定义。动作空间打印出的类型决定了后续网络输出层怎么设计。如果这段脚本直接报错先检查 gym 版本再看 Python 版本是否过新而不是去怀疑算法代码。3.3 观测空间与动作空间写 reward 前先把坐标算明白MPE 的动作空间默认是离散的 5 个动作上下左右不动。但 MADDPG 的 actor 输出层通常接 Tanh输出连续值在 [-1, 1]。两个对不上时常见的做法是写一个动作映射层把连续动作映射到离散动作区间。例如 [-1, -0.5) 映射为向左、[-0.5, 0.5) 映射为不动、(0.5, 1] 映射为向右。更顺手的做法是直接改场景把动作空间改成连续力控制这样 MADDPG 原生的连续动作输出就能直接用。我建议走连续动作这条路原因有两个。一是省掉映射层的精度损失离散映射会让细小的策略差异被抹平二是后续迁移到真实机器人或无人机仿真时控制量本来就是连续的提前用连续动作训练策略迁移性更好。改法不复杂在场景的make_world里把 mover agent 的action_space从离散改成连续即可。观测空间则要看场景的observation函数。simple_tag 里每个智能体的观测大致包括自己的相对位置、相对速度、标记以及与其他实体的相对坐标。训练前一定要打印一份实际观测向量逐段搞明白每个维度是什么。我踩过最典型的坑是reward 里用了“距离差”做目标但观测里给的是相对坐标网络要自己学习“距离 坐标差的范数”这个映射。这个映射不是学不会而是会拖慢收敛。不如直接在 reward 和观测里都用距离让网络把精力放在策略上而不是解算术。一个稳定的 reward 设计方案是分层给追逐方设置“接近猎物给正奖励 持续贴近保持不动给正奖励 出界惩罚”给猎物设置“远离追捕者给正奖励 被抓住给大惩罚”。注意奖励幅度不要跨数量级距离奖励量级在 0.1 左右、捕获惩罚在 -5 左右这样一个量级内的奖励设计Q 值不会因为方差过大而爆炸。4. PyTorch 落地MADDPG 训练主循环的四个关键代码步骤4.1 网络结构两层 MLP 起步先别上复杂结构很多初学者一上来就给 actor 加注意力机制、给 critic 加图神经网络结果收敛更差。做 MADDPG 博弈对抗我的经验是最稳的结构就是两层 MLPactor 中间层 128critic 中间层 128。先把这条基线跑通再根据任务复杂度逐步加深。下面是最小可用的网络定义import torch import torch.nn as nn class Actor(nn.Module): 每个智能体一套输入只看自己的观测 def __init__(self, obs_dim, act_dim, hidden128): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, act_dim), nn.Tanh(), # 输出限制在 [-1, 1]匹配连续动作范围 ) def forward(self, obs): return self.net(obs) class Critic(nn.Module): 训练阶段用输入所有智能体的观测和动作 def __init__(self, n_agents, obs_dim, act_dim, hidden128): super().__init__() input_dim (obs_dim act_dim) * n_agents self.net nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1), ) def forward(self, obs_all, act_all): x torch.cat([obs_all, act_all], dim-1) return self.net(x)逻辑说明actor 输出层用 Tanh 把动作限制在 [-1, 1]这正好匹配 MPE 连续动作空间的边界。如果动作空间范围不是 [-1, 1]可以在环境侧做 scale而不是改网络的激活函数。critic 把“所有观测”和“所有动作”拼成一个大向量这个拼接发生在特征维度上所以每个智能体的 obs 和 act 都要先展平再拼接。参数说明hidden128是起步值对绝大多数 MPE 级别的任务足够如果任务状态维度特别高可以先翻倍到 256而不是直接加网络层数。激活函数用 ReLU 即可不需要 Gelu 这类花活多智能体训练稳定性瓶颈通常不在激活函数。4.2 经验回放多智能体数据的结构比容量更重要多智能体的经验回放和单智能体有一个关键区别一条经验必须同时保存所有智能体的观测、动作、奖励否则更新时 critic 拿不齐全局输入。很多直接把单智能体经验回放搬过来的实现只存了当前智能体的数据训练时其他智能体的历史数据全是乱的导致 critic 输入格式对不上。import numpy as np from collections import deque class ReplayBuffer: def __init__(self, capacity100000): self.buffer deque(maxlencapacity) def push(self, obs_n, act_n, rew_n, next_obs_n, done_n): # 一次性存入所有智能体的数据 self.buffer.append((obs_n, act_n, rew_n, next_obs_n, done_n)) def sample(self, batch_size): batch np.random.choice(len(self.buffer), batch_size, replaceFalse) # 按智能体维度拆开每个智能体拿到自己的一批数据 obs_n [[] for _ in range(len(self.buffer[0][0]))] act_n [[] for _ in range(len(self.buffer[0][1]))] rew_n [[] for _ in range(len(self.buffer[0][2]))] next_obs_n [[] for _ in range(len(self.buffer[0][3]))] done_n [[] for _ in range(len(self.buffer[0][4]))] for i in batch: o, a, r, no, d self.buffer[i] for j in range(len(o)): obs_n[j].append(o[j]) act_n[j].append(a[j]) rew_n[j].append(r[j]) next_obs_n[j].append(no[j]) done_n[j].append(d[j]) return ([np.array(x) for x in obs_n], [np.array(x) for x in act_n], [np.array(x) for x in rew_n], [np.array(x) for x in next_obs_n], [np.array(x) for x in done_n])逻辑说明push这次把整条多智能体经验存进去而不是只存某个智能体的sample返回的是按智能体索引拆分后的列表每个元素形状为(batch_size, obs_dim)。更新时访问obs_n[i]就拿到第 i 个智能体的一批观测。参数说明capacity100000是中小型对抗任务的起步值太小会让经验分布频繁被新策略覆盖失去回放的意义太大则老样本占比过高策略更新后旧经验价值失真。跑一次训练如果发现前中期 Q 值摆动剧烈优先检查 buffer 容量是不是设太小。batch_size我建议取 512 或 1024比单智能体常用的 64 大一个量级因为 critic 的输入维度更高小 batch 的梯度方差更大。4.3 更新公式的代码化actor 的梯度到底从哪来这一节是整个实现里最容易写错的部分。MADDPG 更新分两步先更新 critic再更新 actor。critic 的 loss 是 TD 误差actor 的 loss 则不是常见的策略梯度形式而是“最大化当前状态下被 critic 打分的 Q 值”。换句话说actor 的梯度来自 critic 对自己输出动作的评分def update(agent_id, replay_buffer, batch_size, actors, critics, target_actors, target_critics, optimizers_actor, optimizers_critic, gamma0.95, tau0.01): obs_n, act_n, rew_n, next_obs_n, done_n replay_buffer.sample(batch_size) # 当前智能体自己的数据 obs torch.FloatTensor(obs_n[agent_id]) act torch.FloatTensor(act_n[agent_id]) rew torch.FloatTensor(rew_n[agent_id]).unsqueeze(-1) next_obs torch.FloatTensor(next_obs_n[agent_id]) done torch.FloatTensor(done_n[agent_id]).unsqueeze(-1) # 用目标 actor 生成所有智能体的下一个动作 next_act_n [] for j in range(len(actors)): next_act_j target_actors[j](torch.FloatTensor(next_obs_n[j])).detach() next_act_n.append(next_act_j) obs_all [torch.FloatTensor(o) for o in obs_n] act_all [torch.FloatTensor(a) for a in act_n] next_obs_all [torch.FloatTensor(o) for o in next_obs_n] # 更新 critic target_q target_critics[agent_id](next_obs_all, next_act_n).detach() y rew gamma * (1 - done) * target_q q_current critics[agent_id](obs_all, act_all) critic_loss ((q_current - y) ** 2).mean() optimizers_critic[agent_id].zero_grad() critic_loss.backward() optimizers_critic[agent_id].step() # 更新 actor # 用当前 actor 替换掉自己原来的动作 act_all[agent_id] actors[agent_id](obs) q_actor critics[agent_id](obs_all, act_all) actor_loss -q_actor.mean() optimizers_actor[agent_id].zero_grad() actor_loss.backward() optimizers_actor[agent_id].step() return critic_loss.item(), actor_loss.item()逻辑说明更新 critic 时y里的 target Q 值完全用目标网络计算并 detach防止梯度穿回目标网络。更新 actor 时act_all[agent_id]被替换成当前 actor 的输出然后整体送给 critic 打分对actor_loss反向传播时梯度会流经 critic 网络再回到 actor这就是“critic 指导 actor”的含义。其他智能体的动作在这次更新里保持采样值不变这体现的是 MADDPG“站在别人真实动作上评估自己动作价值”的思想。参数说明gamma0.95适合步数较短、回合结束快的对抗博弈如果任务是长程围捕可以提到 0.99。但不要在高 gamma 下配合稀疏奖励直接训前期的 Q 值会因为远期回报方差过大而飘。我一般把 gamma 和回合长度挂钩回合长度越长gamma 越接近 1。4.4 目标网络软更新一条命令的事别省略目标网络更新有两种写法硬复制每 N 步直接赋值和软更新每步滑动混合。多智能体对抗里我坚持用软更新因为多个智能体的目标网络同时硬复制会造成目标值跳跃式变化这种跳跃在多智能体相互耦合的情况下会被放大成集体振荡def soft_update(target_net, net, tau): for target_param, param in zip(target_net.parameters(), net.parameters()): target_param.data.copy_( tau * param.data (1.0 - tau) * target_param.data ) # 每个智能体更新后调用一次 for agent_id in range(n_agents): soft_update(target_actors[agent_id], actors[agent_id], tau0.01) soft_update(target_critics[agent_id], critics[agent_id], tau0.01)参数说明tau0.01意味着每步只把主网络参数向目标网络移动 1%目标网络的变化足够平滑。实际项目中我常用另一个版本每 100 步把上面这段soft_update循环执行一次每次 tau 取 0.05。这样既控制了更新频率又保持了平滑度比每步都软更新省计算资源效果反而更稳。训练结束时还有一个容易被忽略的细节actor 网络保存时要把训练时叠加的噪声去掉后再保存一轮“干净版本”。如果直接保存正在带噪声探索的 actor评估时会发现策略表现明显偏差因为噪声已经把动作污染了。保存前先eval()模式跑一遍推理再存权重文件。torch.save(actors[0].state_dict(), actor_0.pt) torch.save(actors[1].state_dict(), actor_1.pt) # 加载时同样先建网络再 load_state_dict加载模型时注意和保存时保持相同的网络结构隐藏层数、激活函数都对齐否则state_dict会直接报 key 不匹配。多智能体项目里每个智能体一套权重文件是最清晰的做法比混合在一个文件里省去不少定位时间。5. 训练避坑收敛慢、策略崩溃、Q 值爆炸的六个典型问题5.1 现象所有智能体 reward 一起归零策略集体“摆烂”训练到中期曲线突然断崖式下跌所有智能体的回合奖励都在零附近无论调学习率还是 batch size 都拉不回来。原因是 reward 设计给对抗双方设了绝对值相反的导向而某一方策略先崩了另一方跟着失去训练信号。比如追逐方的奖励是“接近猎物 1”猎物是“被接近 -1”追逐方一旦找到“站住不动也能拿 0 分”的局部最优猎物就再没有逃跑压力双方策略同时固化。解决的做法是在 reward 里加一个“过程导向”的持续激励而不是只给相对量。追逐方除了“接近”加分还要在持续追出距离时给额外奖励让“行动”而不是“不动作”成为收益来源。同时给双方训练曲线分开记录不要只看平均 reward——对抗场景下平均奖励被两个相反方向的曲线抵消看起来像归零实际是双方在对抗。5.2 现象Q 值起步正常几千步后直接上千万训练日志里 critic loss 从个位数跳到百万级Q 值输出数值爆炸。原因是 reward 量级跨度过大或者 target Q 的更新里用了没 detach 的目标网络输出。MADDPG 的 critic 输入是所有智能体拼接向量维度高、数值量级差异大比如速度在 0.1 量级、距离在 10 量级未经归一化直接进全连接层梯度会不稳定。解决的顺序先确认更新代码里next_act_n和target_q都加了.detach()再检查 reward 量级是否控制在 0.1 到 5 之间最后给 critic 输入做标准化。我常写一个简单的观测归一化层把 reward 和观测都缩放到 [-1, 1] 区间Q 值爆炸基本会在前一万步内暴露并解决。5.3 现象训练曲线反复横跳甚至出现间歇性疯狂尖峰同一份代码同一份超参数训练曲线每次跑出来长得很不一样有几条 run 的曲线会突然出现尖峰后回弹。原因是多智能体环境初始状态是随机的智能体出生位置分布差异大某些初始配置下回合特别长或奖励特别高被存进经验回放后干扰了价值估计。这在博弈对抗里是常态因为对抗双方的初始位形直接决定了一整局的难度。解决的关键是固定初始分布的随机种子并做“混合采 样”。我在环境 reset 时固定随机种子让每局初始条件覆盖到主要位形但不必完全确定。更有效的办法是经验回放里按回合分段抽样让每局的经验被均匀抽到避免某一局极端经验独占 batch。实现的代价不大但对曲线稳定性的改善非常明显。5.4 现象训练完成后策略变成“高频抖动机”actor 输出的动作序列在很小的时间尺度上剧烈跳动仿真里表现为智能体原地高频抖动几乎没有有效位移。原因是追求平滑的策略梯度下actor 被训练得对输入中的噪声高度敏感而输入观测本身就有 1e-3 量级的数值噪声。策略在噪声方向放大了微小差异反映出动作的高频分量。解决的思路是在 replay buffer 保存时对观测做轻量低通滤波或者更直接的做法是训练时给 actor 输出加动作平滑惩罚将相邻两步动作差值的平方加入 loss。我一般会同时做两件事训练开始时把噪声方差的衰减调慢一些让探索噪声在最早期保证动作充分多样性训练后期则对动作加平滑项让策略只保留低频控制分量。5.5 现象target 网络每步都在更新前期正常后期震荡加剧目标网络跟随主网络太紧训练后期主网络参数在局部区域轻微来回移动目标网络也跟着在每个时间步追最新值导致 target Q 持续小幅跳变critic 永远追不上会动的标靶震荡越到后期越明显。原因是 tau 设成较大固定值加上目标网络更新频率和主网络训练频率完全一致。在多智能体场景这种“跟得太紧”的问题尤其突出因为每个智能体的目标网络耦合了所有人的策略变化。解决的要点是把软更新变成“低频大幅”模式每 100 到 200 步做一次完整软更新tau 取 0.05比每步 0.01 的版本更稳。运行完第一万步后如果震荡仍明显把同步间隔继续加大到 300 步。这个操作不对实验曲线造成显著副作用却能把后期策略崩溃的概率降一个量级。5.6 现象reward 曲线一路向上但实际对抗还是一碰就碎训练日志里 reward 稳步上升你觉得快成功了拿训练好的 actor 去做实际对抗测试却发现策略莽撞、经常被简单战术克制。原因是 reward 曲线只能衡量“在当前对手策略下的收益”不能衡量策略的真实强度。对抗训练中对手是自己所以 reward 上升可能只是双方同时变笨后的“和平相处”而不是真的更强。repo 里训练日志展示的 reward 上升往往没法和实际对抗能力画等号。解决的唯一可靠手段是定期评估对抗胜率。训练过程每 N 局保存一组候选模型训练结束后把不同 checkpoints 两两对战绘制固定种子的胜率热力图。只有胜率矩阵能告诉你哪个 checkpoint 真正更强而不是看谁 reward 曲线更高。这个习惯应该从第一天就建立不要等技术方案全跑完了才去补评估脚本。6. 从“能跑”到“能打”自对弈、优先经验回放与胜率评估多智能体博弈对抗训练里最难的不是让 reward 上升而是让策略“真的变强”。要验证和固化成 果我常用的三招是自对弈蓄水、优先经验回放、胜率矩阵选模型。自对弈的思路是维护一个“历史策略池”每隔一段训练轮数就把当前模型复制一份放进去然后让在线智能体和历史版本随机对战。这样做的目的很朴素如果只和“现在的自己”练双方会同时进化但可能一起走向某个局部最优和历史版本打相当于逼策略去适应更多变体而不是只记住当前对手的弱点。在 MPE 这类轻环境里策略池每个阶段就存一份池子容量 10 个左右每次对战随机抽一个对手效果比单纯和当前版本对练好很多。优先经验回放是另一个收益明显的改动。博弈对抗中大部分经验是平淡的双方距离远、没接触、奖励接近零这些样本对网络价值不大。反而是那些发生捕获、发生剧烈机动、奖励差异大的稀有经验携带了更多策略信息。给每条经验按 TD 误差大小记一个优先级采样时按优先级加权能让网络更频繁地“复习”关键时刻。实现时注意两点优先级指数 alpha 取 0.6 左右不要设过高否则采样分布太偏刚存进去的经验给一个中等偏上的初始优先级避免完全不被采到。胜率评估是最终裁决手段。训练过程中每几个训练轮次固定保存一组 actor全部训练结束后把所有 checkpoint 两两对阵用相同随机种子跑足够多局统计胜率。为什么必须这样因为对抗训练里 reward 的绝对数值没有意义只有相对某个固定对手的胜率才反映真实强度。我看过太多 reward 曲线很好看、实际对战一碰就碎的项目都是因为跳过了这一步。评估用的对手必须是“冻结策略”不能在评估时打开探索噪声否则胜率数据会混入噪声变量无法反映策略本身水平。这三步做完一个 MADDPG 博弈对抗项目才算真正闭环自对弈保证训练动力优先回放提高样本效率胜率矩阵给出可信的模型选择依据。做项目时我养成的习惯是评估脚本和训练脚本同步开发第一次启动训练前评估代码就已经能跑通了而不是训练跑了几天之后才补评估。这个习惯省下过无数次返工希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表