MADDPG多智能体强化学习实战:从DDPG升级到CTDE框架
简介基于Python与MADDPG的多智能体博弈对抗算法资源面向强化学习初学者及进阶开发者适用于毕业设计、课程作业或工程实训场景帮助理解多智能体环境下的策略训练与博弈对抗思路。压缩包共14个文件以Python源码为主包含10个.py脚本、1个Markdown说明文档、1个配置文件及辅助文件覆盖智能体网络构建、经验回放、训练主循环与测试评估等模块整体仅19KB结构紧凑、便于阅读。目前已有266人学习浏览适合用来快速搭建多智能体对抗实验。通过该资源可学习MADDPG算法框架、Actor-Critic网络设计、环境交互流程以及多智能体协作与对抗的训练逻辑并可直接运行主程序观察训练效果为后续改进算法或开展对比实验打下扎实基础。同时简洁的代码风格也便于二次开发与算法调优。1. 为什么单智能体强化学习对抗一升级DDPG 就不够用了两架无人机在空中玩追逃四个轮式机器人围堵一个跑得飞快的目标。这类多智能体博弈对抗场景里把 DDPG 直接拿过来用训练常常不收敛奖励曲线大幅抖动训练好的策略换个对手就吃瘪。原因很直接——单智能体算法假设环境是固定的而博弈对抗里每个对手都在学习环境本身就是个移动靶。MADDPGMulti-Agent Deep Deterministic Policy Gradient正是在这个背景下出现的算法它把 DDPG 的 Actor-Critic 扩展到多个智能体用“训练时全知、执行时只看自己”的方式规避非平稳问题。这篇内容适合已经跑过 DDPG、准备做多智能体博弈对抗课题的 Python 开发者也适合代码能跑但调不出收敛结果的人。2. MADDPG 原理拆解CTDE 框架与 Critic 输入拼接为什么能稳住非平稳环境2.1 环境非平稳为什么多智能体博弈让 Q-learning 集体翻车先看一个最容易踩的认知坑把多智能体博弈当成多个单智能体并行训练。表面上看每个智能体都有自己的观测、动作和奖励各自跑一套 DDPG 也没什么问题但一旦把经验回放打开问题就暴露了。对智能体 i 来说环境状态转移不只取决于它自己的动作 a_i还取决于其他智能体的动作 a_{-i}。当所有智能体都在用强化学习更新策略时a_{-i} 的分布每几百步就变一次Q 函数拟合的转移概率 P(s|s, a_i) 也跟着漂移。经验回放里存的历史数据和新数据不再同分布梯度方向来回拉扯最终表现就是奖励曲线像心电图。术语上把这种现象叫环境非平稳。它直接破坏了 Q-learning 和策略梯度方法背后的马尔可夫假设如果对手的策略在变当前状态的“价值”本身就是个移动目标。单智能体 DDPG 完全没处理这个问题因为它的 Critic 输入只有自己的观测和动作无法感知其他智能体的存在。实际跑起来你会发现每个智能体的 reward 都在涨但对局结果没有变好因为所有智能体都在“自我感觉良好”。要解决非平稳思路不是让每个智能体更聪明而是让它们在训练阶段共享足够多的信息。这引出了 MADDPG 的核心框架 CTDE即集中式训练、分布式执行。训练时可以把所有智能体的观测和动作都喂给 Critic执行时每个 Actor 仍然只吃自己的局部观测。这样既拿到了全局信息来稳定 Q 函数又不增加运行时的通信负担。2.2 CTDE 框架训练时开全图执行时只留局部视野用一局“三追一逃”的粒子环境举例三个追捕者各有一个 Actor每个 Actor 只看自己的位置和速度。如果它们各自的 Critic 也只输入自身观测训练时每个追捕者都察觉不到另外两个队友在哪个方向很容易出现三个人往同一个角追的现象。CTDE 的做法是训练时三个追捕者的 Critic 全部拼接上三个追捕者和猎物的观测与动作。Critic 相当于开了上帝视角能够判断“当前这个追捕者的动作放在全局局势里是好是坏”。执行阶段Actor 依然只用自己的局部观测输出动作不依赖通信。这一设计把“合作协调”的压力从 Actor 转移到了 Critic 上。Actor 只负责根据局部观测输出动作Critic 负责理解全局局势并给 Actor 提供梯度。由于 Critic 只在训练阶段存在部署阶段的模型尺寸和推理时延都不会被全局输入拖累这也是 CTDE 在多智能体系统落地时被广泛接受的原因。需要注意这里的“全局信息”并不是严格意义的全局状态而是把所有智能体的观测 o_1 到 o_N 和动作 a_1 到 a_N 拼在一起。如果某个智能体的观测本身有遮挡或者噪声Critic 拿到的是“不完整的全局信息”但相比单智能体方案信息量仍然足够缓解非平稳。真正要避免的是执行阶段也去拼接其他智能体信息那样会让系统对通信链路的依赖变得不可控。2.3 Critic 输入拼接与目标策略平滑MADDPG 相对 DDPG 的三个关键修改MADDPG 在结构上不是全新的算法它保留了 DDPG 的 Actor-Critic 骨架、经验回放、目标网络和软更新改动集中在三个位置。第一个改动是 Critic 的输入拼接。智能体 i 的 Critic 输入从 (o_i, a_i) 变成 (o_1, ..., o_N, a_1, ..., a_N)也就是把所有智能体的观测和动作拼接成一个长向量。Critic 的输出仍然是一个 Q 值表示智能体 i 在当前全局联合动作下的期望回报。Actor 的输入和输出不变仍然是 o_i 映射到 a_i。这个改动让每个智能体的 Q 函数都能感知其他智能体的行为从而在训练时把对方的策略变化纳入考量。第二个改动是目标 Q 值的计算方式。DDPG 的目标是 y r γ Q(o, a)其中 a 由目标 Actor 输出。MADDPG 把 a 扩展为所有智能体的目标动作即 y_i r_i γ Q_i(o_1, ..., o_N, a_1, ..., a_N)。这里每个 a_j 都由智能体 j 自己的目标 Actor 生成。实现时一个常见坑是直接用当前 Actor 生成下一步动作而不是用目标 Actor这会让目标 Q 值跟随策略剧烈波动训练后期尤其明显。第三个改动是目标策略平滑。MADDPG 论文在计算目标 Q 值时会对目标 Actor 的输出加上一个截断的高斯噪声让 Q 函数不会对某个精确动作过拟合。这有点类似 TD3 里的 target policy smoothing实际训练中能明显减少 Q 值的尖峰。一个从 DDPG 平滑迁移到 MADDPG 的团队最容易漏掉的就是这一点。下面的对比表可以帮你快速定位差异。维度DDPGMADDPGCritic 输入单个智能体 (o_i, a_i)全体智能体 (o_1..o_N, a_1..a_N)目标动作来源自己的目标 Actor所有智能体的目标 Actor训练阶段信息共享无经验回放共享Critic 全观测执行阶段输入局部观测局部观测无通信非平稳环境适应性弱强MADDPG 的适用边界也要说清楚。它主要解决连续动作空间下的多智能体问题如果动作是离散的需要额外用 Gumbel-Softmax 之类的技巧把离散动作变成可微形式。另外MADDPG 并不保证能收敛到纳什均衡它只是提供一种比独立训练更稳定的学习框架。实际博弈中策略可能在几个均衡之间震荡这一点后文训练避坑部分会展开。3. 环境选型与最小跑通PettingZoo 的 MPE 场景怎么验收3.1 环境选型先跑通 MPE 的 simple_tag再谈自定义博弈场很多第一次接触多智能体博弈对抗的人上来就想自建环境两个矩阵对撞、红蓝双方夺旗。结果环境代码写了两个小时算法一个 epoch 都没跑过。我的建议是第一步直接使用现成的 MPE 环境即 Multi-Agent Particle Environment现在集成在 PettingZoo 库中。MPE 是二维连续空间中的粒子交互场景智能体用力和方向移动适合验证多智能体强化学习算法也便于可视化 debug。推荐先从 simple_tag 场景开始。这个场景里有 3 个追捕者和 1 个猎物猎物速度更快追捕者需要合作围堵才能抓住它。它正好覆盖了多智能体博弈对抗最核心的要素多个智能体要协调策略对手速度快且行为不可预测奖励稀疏但直观。相比简单的 cooperative 搬运场景simple_tag 的对抗属性更能暴露算法在非平稳环境下的真实水平。安装环境之前建议先建一个独立的 Python 虚拟环境。MADDPG 涉及的依赖主要是 PyTorch、PettingZoo 和 gymnasium这些库的版本更新频繁直接装在全局环境里容易把其他项目的依赖搞坏。下面是一份可行安装命令Python 版本推荐 3.10 或 3.11。# 创建并激活虚拟环境 python -m venv maddpg_env source maddpg_env/bin/activate # 安装 PyTorch CPU 版先跑通逻辑再考虑 CUDA pip install torch --index-url https://download.pytorch.org/whl/cpu # 安装 MPE 环境和轻量依赖 pip install pettingzoo1.24.3 numpy gymnasium这里特意指定了 pettingzoo 1.24.x 版本因为新版本 API 以 parallel 接口为主符合多智能体同时 step 的习惯。PyTorch 先装 CPU 版,不是因为你机器不行而是先把算法逻辑调通再切换成 CUDA 版减少“环境配好了但代码没跑起来”时的排查变量。装完后在命令行执行python -c from pettingzoo.mpe import simple_tag_v3; print(ok)没有报错就说明环境就绪。3.2 最小交互脚本装完环境后怎么确认“人在回路”正常环境装好之后第一步不是写神经网络而是写一个几十行的随机策略脚本把环境接口完整跑一遍。这一步能帮你确认动作空间、观测维度和终止条件是否符合预期避免后面算法跑起来才发现 step 接口调用方式不对。MPE 环境在 PettingZoo 中提供 parallel 接口所有智能体同时执行动作、同时返回奖励。下面的代码演示了最简单的交互循环。from pettingzoo.mpe import simple_tag_v3 import numpy as np env simple_tag_v3.parallel_env(max_cycles500, num_good1, num_adversaries3) observations, infos env.reset(seed42) for step in range(100): actions {} for agent in env.agents: # 每个智能体随机采样一个动作 action_space env.action_space(agent) actions[agent] action_space.sample() obs_next, rewards, terminations, truncations, infos env.step(actions) observations obs_next # 全部终止或全部超时结束当前回合 if all(terminations.values()) or all(truncations.values()): break env.close()这段代码里env.agents返回当前所有智能体的名字列表在 simple_tag 里是三个追捕者和一个猎物。action_space.sample()是从动作空间里随机挑一个合法动作这个动作空间在 PettingZoo 的 MPE 里是 Discret(5)对应不上、下、左、右和静止五个选择。env.step接收的是一个 dictkey 是智能体名value 是动作返回值里terminations表示回合是否因胜负结束truncations表示是否超过max_cycles。观察空间是一个向量包含自身位置、速度以及和其他智能体的相对位置维度在 20 左右具体数值可以在代码里打印observations[agent].shape确认。这个脚本跑通后建议再做两个小验证第一打印一个观测值确认它不是全零或 NaN第二手动固定动作比如让所有追捕者向左移观察猎物是否朝反方向逃跑。这能确认环境物理规则正常。很多人跳过了这一步后面训练时不收敛还要回头怀疑是算法问题还是环境问题浪费大量时间。3.3 连续动作适配把 MPE 的离散动作接进连续控制算法MADDPG 原始论文主要面向连续动作空间但 PettingZoo 封装的 MPE 动作是离散的。如果要让 MADDPG 直接跑 MPE一个常见的做法是把离散动作映射成连续向量或者用 Gumbel-Softmax 让离散动作可微。这里先说一个最简单的映射方式把 Actor 输出的连续值映射回离散动作的索引。def continuous_to_discrete(continuous_action): # continuous_action: shape (act_dim,) 值域约 [-1, 1] # 对于 MPE 的 5 维离散动作取最大分量对应的动作作为最终动作 discrete_idx int(np.argmax(continuous_action)) return discrete_idx这种做法的代价是语义比较粗。MPE 的五个离散动作是互斥的用 argmax 取最大分量相当于把连续空间切成了多个区域Actor 需要额外学习一个分段映射。训练初期动作很容易在边界附近抖动导致智能体频繁切换方向。更平滑的方案是 Gumbel-Softmax它能让离散动作的概率分布可微同时保持梯度可以回传到 Actor。这个适配放在算法跑通之后再优化前期用 argmax 完全够用。如果你更想先用完全连续的对抗场景验证 MADDPG可以考虑自己写一个最简单的二维追逃环境动作就是两个方向上的速度分量。这类环境代码量不大但能让算法训练的重心完全放在 MADDPG 更新逻辑上不必纠缠离散动作适配。我在后面实现章节的主循环里先按连续动作假设写代码再补离散适配的细节。4. 从零实现 MADDPGActor-Critic 网络、经验回放与更新主循环4.1 Actor 与 Critic 网络定义维度怎么拼输出为什么用 tanhMADDPG 的网络结构不复杂但维度的拼接方式是整段代码最容易出错的地方。Actor 是一个从局部观测到动作的映射输入维度是单个智能体的观测维度输出维度是动作维度。输出层用 tanh 激活把动作限制在 [-1, 1] 区间这个设计对连续动作环境很重要。下面定义一个标准的两层 MLP Actor。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden256): super().__init__() self.fc1 nn.Linear(obs_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc_out nn.Linear(hidden, act_dim) def forward(self, obs): x F.relu(self.fc1(obs)) x F.relu(self.fc2(x)) # tanh 让动作落在 [-1, 1] return torch.tanh(self.fc_out(x))Actor 的隐藏层维度设为 256对于粒子环境这种低维输入已经足够。如果你的智能体观测维度超过 100比如带图像输入可以调成 512 或加一层。这里有一个新手特别容易踩的坑fc_out的输出不加 tanh而是靠环境内部 clip 动作。这样做不是不行但会让梯度在边界处不稳定。tanh 的作用是把动作压到有限区间同时它的梯度在原点附近是线性的能让更新更平滑。Critic 的输入是全体智能体的观测和动作拼接所以它的维度计算是最需要小心的。class Critic(nn.Module): def __init__(self, n_agents, obs_dim, act_dim, hidden256): super().__init__() self.n_agents n_agents input_dim n_agents * (obs_dim act_dim) self.fc1 nn.Linear(input_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.fc_out nn.Linear(hidden, 1) def forward(self, obs_all, act_all): # obs_all: (batch, n_agents, obs_dim) # act_all: (batch, n_agents, act_dim) x torch.cat([obs_all, act_all], dim-1) x x.view(x.size(0), -1) # 展平成 (batch, n_agents*(obs_dimact_dim)) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc_out(x)x.view(x.size(0), -1)把 (batch, n_agents, obs_dimact_dim) 展平成二维这样全连接层可以直接处理。这里的顺序很关键拼接时观测和动作要按智能体维度对齐确保 Critic 知道每个观测对应哪个动作。如果你的数据是 (batch, obs_dim, n_agents) 的排列拼出来就是乱的Q 值会完全学不出来。经验是统一用(batch, n_agents, dim)的格式存放所有中间数据减少维度排列带来的隐性 bug。与 DDPG 一样MADDPG 需要为目标网络单独维护一份参数。每个智能体都有一对自己的 Actor 和 Actor_target、Critic 和 Critic_target起始时直接深拷贝一份训练中通过软更新慢慢同步。4.2 经验回放与更新主循环一个 buffer 装下所有智能体的博弈轨迹经验回放在 MADDPG 里和 DDPG 类似但存的内容是整个联合状态和联合动作。一条 transition 需要包含所有智能体的观测、动作、奖励、下一时刻观测和终止标志。这里所有智能体共用一个回放缓冲区训练时随机采样打破样本间的时间相关性。缓冲区代码本身很简单关键是存储格式要统一。from collections import deque import random class ReplayBuffer: def __init__(self, capacity1000000): self.buffer deque(maxlencapacity) def push(self, obs_all, act_all, rew_all, obs_next_all, done): # 全部转成 numpy 数组存储节省内存 self.buffer.append(( obs_all, act_all, rew_all, obs_next_all, done )) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) # zip(*batch) 按字段聚合再转成 tensor obs_all, act_all, rew_all, obs_next_all, done zip(*batch) obs_all torch.tensor(np.array(obs_all), dtypetorch.float32) act_all torch.tensor(np.array(act_all), dtypetorch.float32) rew_all torch.tensor(np.array(rew_all), dtypetorch.float32) obs_next_all torch.tensor(np.array(obs_next_all), dtypetorch.float32) done torch.tensor(np.array(done), dtypetorch.float32) return obs_all, act_all, rew_all, obs_next_all, done def __len__(self): return len(self.buffer)容量默认设 100 万对 simple_tag 这类场景足够了。如果你自定义的环境回合很长可以适当减小到 20 万或 50 万因为过旧的博弈轨迹对于当前策略已经没有参考价值反而增加采样噪声。deque(maxlencapacity)会在容量满时自动丢弃最老的经验不需要手动清理这是 Python 里实现回放缓冲区最省心的方式。更新主循环是 MADDPG 实现里最需要集中注意力的部分。每个智能体都有一个 Critic更新时需要逐个处理。以智能体 i 为例先计算目标 Q 值用所有智能体的目标 Actor 生成下一时刻动作再送入目标 Critic得到 Q_i(o, a_1, ..., a_N)。然后计算 Critic 的 MSE 损失用策略梯度更新 Actor最后软更新目标网络。def update_maddpg(agents, replay_buffer, batch_size, gamma0.95, tau0.01): obs_all, act_all, rew_all, obs_next_all, done replay_buffer.sample(batch_size) n_agents len(agents) for i, agent in enumerate(agents): # 1. 用所有目标 Actor 生成下一时刻动作 next_actions [] for agent_j in agents: next_actions.append(agent_j.actor_target(obs_next_all[:, agent_j.idx])) next_actions torch.stack(next_actions, dim1) # 2. 目标 Q 值 q_next agent.critic_target(obs_next_all, next_actions) target rew_all[:, i:i1] gamma * (1 - done.unsqueeze(-1)) * q_next # 3. 更新 Critic q_pred agent.critic(obs_all, act_all) critic_loss F.mse_loss(q_pred, target.detach()) agent.critic_optim.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(agent.critic.parameters(), 0.5) agent.critic_optim.step() # 4. 更新 Actor用自己的 Actor 替换自己的动作其他动作保持 detach act_all_actor act_all.clone().detach() act_all_actor[:, i] agent.actor(obs_all[:, i]) actor_loss -agent.critic(obs_all, act_all_actor).mean() agent.actor_optim.zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_(agent.actor.parameters(), 0.5) agent.actor_optim.step() # 5. 软更新目标网络 for target_param, param in zip(agent.critic_target.parameters(), agent.critic.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data) for target_param, param in zip(agent.actor_target.parameters(), agent.actor.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data)这段更新逻辑有几个关键细节要解释。next_actions必须由目标 Actor 生成而不是当前 Actor否则目标 Q 值和使用当前策略做出的动作强相关训练会偏置。act_all_actor[:, i] agent.actor(obs_all[:, i])这行是更新 Actor 的核心先把已有动作复制并断开梯度然后把自己的动作替换成当前 Actor 的输出其他智能体的动作保持 detach 状态。这样 Actor 的梯度只影响它自己的动作同时 Critic 能感知其他智能体的动作信息。torch.nn.utils.clip_grad_norm_是训练稳定性的保险丝尤其是多智能体环境下 Critic 的 loss 波动比单智能体大得多。done.unsqueeze(-1)把 done 的形状从 (batch,) 变成 (batch, 1)这样才能和 (batch, 1) 的 reward 对齐。这一行形状不匹配报错是复现 MADDPG 时最常见的报错之一。如果报错说维度对不上优先检查这里。4.3 核心超参数表照着这套起点参数先跑到“看着靠谱”MADDPG 的超参数不像 CNN 调起来那么直观很多组合跑出来都不收敛。我整理了一份适合 simple_tag 场景的起点参数表你不需要完全照抄但可以作为第一次跑的基准。如果结果不理想每次只改一个参数不要同时调三个否则你根本不知道是哪个改动起作用。参数推荐值说明actor 学习率1e-4比 Critic 低一个数量级避免策略更新过快critic 学习率1e-3Critic 直接拟合 Q 值学习率可以稍大gamma0.95追逃回合短折扣率太高会增加 Q 值方差tau0.01软更新系数越大目标网络跟随越紧batch size1024MPE 观测维度低大 batch 能压梯度噪声buffer 容量1e6过大采样慢过小样本多样性不足噪声初始值1.0动作加高斯噪声随训练退火到 0.05更新频率1每 step 采样一次并更新简单直接一个需要特别说明的参数是噪声退火。MADDPG 的 Actor 在训练初期需要探索但探索噪声如果一直保持高位策略永远学不到精细控制。常见做法是让噪声从 1.0 线性退火到 0.05训练到后期几乎不加入噪声。退火速度要配合回合数太慢则前期浪费大量探索太快则策略容易陷入局部最优。我在 simple_tag 上一般用 20000 个回合完成退火具体衰减公式看你的总回合数设定。5. 训练避坑记录五条来自训练日志的免疫经验5.1 现象一Critic loss 在降博弈胜率却一动不动这是多智能体强化学习训练里最迷惑人的情况TensorBoard 上 Critic loss 平滑下降每个智能体的 reward 也稳步上升但如果你把训练好的策略和固定策略的对手打一场发现追捕者还是乱跑。原因是 Critic loss 下降可能只是 Q 函数在拟合一个偏差越来越大的目标。多智能体博弈里所有智能体都在更新reward 上升有可能是对手变笨了而不是你变强了。解决方法是加一个“冻结对手”的评估。每隔 1000 回合固定当前所有智能体的 Actor 参数和一个保持随机策略的对手对战 50 局统计胜率。这样能排除对手同时变弱的干扰。如果你发现训练时 reward 高、和固定策略对战胜率却低基本可以断定是 Critic 的 Q 值高估了当前策略的真实表现这时优先考虑调低 gamma或者检查 Critic 输入拼接的正确性。5.2 现象二奖励曲线前脚还在涨后脚突然崩盘训练到中后期reward 曲线突然跳水式下降这种现象在多智能体博弈里很常见。直接原因通常是探索噪声没有退火到位策略在某个临界点被噪声推离了“好策略区域”。博弈场景下策略对动作精度要求高一旦噪声过大动作偏离最优动作太多暴露给 Critic 的全是次优样本Q 函数开始低估原来的好策略形成恶性循环。另一个常见原因是 Critic 的梯度裁剪缺失。多智能体环境下 Q 值的 TD 误差比单智能体大得多一次回传的梯度范数过大会让网络参数突然跳到另一个区域再回来就很难。我在更新代码里特意加了clip_grad_norm_(..., 0.5)这个值对 MPE 场景比较合适。如果你发现网络参数更新后 loss 突然变成 NaN优先检查梯度裁剪是否生效。5.3 现象三同一个代码不同随机种子结果像抽盲盒MADDPG 的策略梯度方差大这是算法本身的性质不是你的代码写得有问题。第一次跑可能追捕者 80% 胜率换个随机种子直接掉到 30%让人怀疑自己是不是在跑同一个程序。原因来自两方面一是环境初始化随机MPE 中猎物初始位置随机范围大二是经验回放的采样顺序不同导致网络中后期走向不同的局部最优。正确的做法是固定三个随机种子各训练一遍报告平均胜率和波动范围不要只报最好的一次。训练好之后选和固定随机策略对抗胜率最高的那版部署。这里也顺带说一句不要在训练中途手动改随机种子那样只会浪费时间让结果更难看。一个项目里固定 5 个种子跑完看看方差没有收敛趋势再考虑调整超参数。5.4 现象四加了注意力模块效果反而更差不少人在跑通 MADDPG 之后第一件事就是往 Critic 里加 attention 机制想让 Q 函数更聪明地利用其他智能体的信息。结果往往是训练时间翻倍最终效果还不如原始的拼接方案。原因很直接simple_tag 只有三四个智能体观测维度不到 30注意力机制需要学习的权重矩阵在这个规模下属于过度参数化数据量不足以支撑它学出有意义的注意力分布。这个坑背后的教训是在小规模多智能体系统里结构简单就是优势。如果你想在算法层面做改进先把 baseline 跑稳再一个变量一个变量地加。加 attention 之前先问自己原始 Critic 的拼接输入到底哪里不够用如果智能体数量在 10 个以上输入维度爆增attention 才真正有用武之地。否则它的收益大概率抵不过额外的方差。5.5 现象五追求大 buffer 大 batch训练慢到怀疑人生经验回放容量设得越大理论上样本多样性越高但这不意味着训练效果越好。MPE 场景里一个回合最长几百步连续保存几百万条 transition其中大量是早期随机策略产生的低质量样本。采样时抽到这些旧样本的概率居高不下Critic 被迫拟合过时策略的数据反而拖慢收敛。我见过有人把 batch size 调到 4096结果一次采样和更新要半秒钟一个动作 3000 万次训练直接无法继续。建议 MPE 场景里 buffer 容量控制在 20 万到 50 万之间batch size 用 1024。如果你发现训练速度随步数明显下降先看是不是 buffer 满了之后采样变慢。另一个加速小技巧是 buffer 里存 numpy 数组而不是 tensor采样时才转成 tensor能省下大量内存带宽。这里不要过度优化先保证训练速度能让你在半天内看出一组超参数的好坏。6. 验证博弈强度与两大进阶方向冻结策略测试和离散动作扩展先说一个验证技巧。训练完成后不要只看训练曲线把训练好的 Actor 保存下来写一个对战脚本让追捕者用训练好的策略、猎物用固定的随机策略或简单规则策略连打 100 局统计胜率。这样做能直接回答“这个策略能不能用”的问题也方便你对比不同训练阶段保存的模型。如果训练后期模型反而打不过前期的说明策略发生了退化需要回滚到表现最好的那个检查点。import torch def evaluate(env, agents, n_episodes100): wins 0 for _ in range(n_episodes): obs, infos env.reset(seedNone) done False while not done: actions {} for agent in env.agents: idx agent_to_idx[agent] obs_t torch.tensor(obs[agent], dtypetorch.float32).unsqueeze(0) with torch.no_grad(): act agents[idx].actor(obs_t).squeeze(0).numpy() actions[agent] continuous_to_discrete(act) obs, rewards, terminations, truncations, infos env.step(actions) done all(terminations.values()) or all(truncations.values()) if adversary in rewards and rewards[adversary] 0: wins 1 return wins / n_episodes这个评估函数的要点是 Actor 的推理阶段不要加探索噪声动作直接由最大输出值决定。如果加了噪声评估出来的胜率会偏低而且波动大。评估时环境不固定种子多试几组随机初始位置更能反映策略的真实泛化能力。进阶方向上大部分人在跑完 MPE 之后会往两个方向走。第一个是离散动作空间。MADDPG 原生是连续动作算法但很多实际博弈场景动作是离散的比如 MOBA 游戏里的技能选择、棋牌类游戏的出牌策略。这时需要把 Actor 输出变成 Gumbel-Softmax 采样让离散动作的梯度可回传。这个改造的核心是温度系数温度太高梯度均匀温度太低梯度消失一般从 1.0 开始退火。第二个是更接近真实部署的信息受限场景即观测不是全局的而是带遮挡的局部视野。MADDPG 的 CTDE 框架天然支持这种改造只需要让 Critic 在训练时拼接所有观测Actor 保持局部视野即可。在多个博弈项目里反复踩坑后我的习惯是每次训练启动前固定三个随机种子跑通后先做 100 局冻结策略对抗测试再决定要不要继续调参。这个习惯帮我挡掉了不少凭感觉调参的翻车。多智能体博弈对抗的难点不在单个模块而在模块之间的配合如果你能控制住变量一点点验证MADDPG 在中小规模场景下的表现值得投入。希望帮到你。本文还有配套的精品资源点击获取