多智能体强化学习价值分解算法复现实战:从VDN到QMIX
发布时间:2026/9/28 1:26:06来源:尧图网络
简介多智能体强化学习MARL算法复现源码包基于Python实现涵盖QMIX、VDN、QTRAN、MAVEN四大经典算法聚焦多智能体协作与信用分配问题适合毕业设计、课程设计及项目开发场景也适合深度学习初学者进阶理解MARL原理。压缩包共10个文件以Python脚本为主6个py文件包括算法实现、智能体策略、网络结构与通用工具函数等另有2个Markdown说明文档、1张训练结果样例图和1个配置文件整体仅47KB轻量易部署、便于阅读。已有277人学习下载。源码已经过测试目录结构清晰可直接运行主程序并配合说明文档复现各算法结果通过对比QMIX与VDN的值分解思路、分析QTRAN与MAVEN在信用分配上的改进可深入掌握多智能体强化学习的核心难点并方便在此基础上改造网络或加入新场景进行二次开发。1. 为什么毕设里的 MARL 算法复现常卡在“跑通却复现不了”多智能体强化学习MARL这几年几乎是毕业设计和课程设计的“硬通货”QMIX、VDN、QTRAN、MAVEN 这几个名字在论文里高频出现。第一次接触的人往往兴奋地把源码 clone 下来训练一个晚上最后发现自己还是拿不出一张能和论文对齐的收敛曲线。这个标题里的核心不是“跑通代码”而是“复现”同一个环境、同一套超参换一个 seed你的 QMIX 能不能稳定收敛到预期胜率。这直接决定了答辩时是“我实现了算法”还是“我调通了别人的代码”。这篇文章会从价值分解的底层逻辑讲起落到一套用 Python PyTorch 搭起来的统一源码骨架再把我实际调参踩过的坑一一列出来。适合正在选毕设题目、写课程设计报告或者准备用 MARL 做项目开发的从业者。2. 价值分解的四个形状为什么 VDN、QMIX、QTRAN、MAVEN 能放进同一个框架2.1 VDN把联合价值当成智能体价值的“加法表”VDNValue Decomposition Networks是所有价值分解算法里最朴素的一条路线。它假设联合动作价值 Q_tot 可以写成各个智能体局部 Q_i 的线性加和Q_tot Σ Q_i。训练时每个智能体用自己的局部观测学自己的 Q_i再用 Q_tot 去算 TD 误差梯度会自然流回每个 Q_i。源码写起来最简单我一般把它当作正确性基线如果 QMIX 跑出来的结果连 VDN 都比不过那大概率是代码或者超参出了问题而不是方法本身的问题。但加和分解有一个隐含假设智能体之间的贡献近似独立且可互换。两个机器人分头搬箱子这种任务用加和没问题一旦任务里出现冗余、抵消、依赖队友状态才能决策的情况VDN 学到的联合价值会反复叠加每个独立 Q 的乐观误差导致严重的高估。用论文里的术语说可加性只是 IGMIndividual-Global-Max原则的一个特例VDN 天然满足“全局最优等于局部最优组合”但它把这种满足的门槛放得太低了。2.2 QMIX用“单调性”给加法表加上非线性QMIX 明确回应了 VDN 的局限。它不再要求 Q_tot 等于 Q_i 的简单加和而是用一个小型神经网络——mixer 网络——把局部 Q_i 混合成 Q_tot。关键是mixer 的权重由一个 hypernetwork 根据全局状态 s 实时生成并且被约束为非负数。这个非负约束保证了 Q_tot 对每一个 Q_i 单调递增从而仍然满足 IGM全局最优动作仍然等于各自局部最优动作的组合。单调性是一个绝妙的折中。它比“线性加和”的表达式能力强得多能建模一些非线性协作关系同时又不像完全自由的 mixing 函数那样容易破坏 IGM。源码量不算大常见做法就是三件套RNN agent 负责时序建模、单调 mixer 负责价值聚合、episode replay buffer 负责数据供给。我习惯把 QMIX 的 mixer 单独抽成一个模块这样换 VDN 或者换 MAVEN 的 latent 分支时agent 网络可以完全不动。2.3 QTRAN 与 MAVEN两个“升级款”分别补什么QTRAN 解决的是 QMIX 的“天花板”问题。QMIX 的单调性是一种充分条件但它不是必要条件有些任务里某个智能体的最优动作只有在队友做出特定动作后才成立单调性会直接锁死这种表达。QTRAN 同时学一个独立的联合动作价值 Q_joint再用两个约束项把 Q_tot 和 Q_joint 的关系拉回来一个约束对应最优联合动作处两者相等另一个约束对应所有动作处的不等式关系。理论上是更通用的 IGM 实现实际复现难度也高一个等级那两个约束项需要权重平衡稍不注意就互相打架导致训练不稳定。MAVEN 补的则是探索。QMIX 的 agent 是独立探索的早期很容易出现“各探索各的协作信号传不回来”的问题。MAVEN 引入一个共享的潜在变量 z让智能体在训练早期被约束在同一个探索方向上并用互信息 loss 保证 z 和联合动作之间的关联。通俗点说MAVEN 让一队智能体先约定“今天一起试左边”而不是各自随机乱试。源码里会多出一个 latent network 和一条额外的 loss 分支整体结构和 QMIX 的间隔并不大但多出来的两个超参互信息权重、z 的维度足够让人调上一整天。2.4 选型判断毕设、课设、项目开发分别该盯哪一个算法分解约束代码复杂度典型验证场景复现难度VDN线性可加低简单协作任务、矩阵博弈低QMIX单调性中SMAC 的 2m、3m、2s3z 地图中QTRAN通用 IGM 约束高非单调协作任务高MAVEN单调性 潜在变量探索中高SMAC 中探索困难的地图中高我的建议一直很直接课程设计选 VDN最多加一个 QMIX 做对比足够把“价值分解”这条主线讲清楚毕业设计选 QMIX 为主线用 VDN 当 baseline有余力再加 MAVEN项目开发如果目标是产出可演示的 demo优先把 QMIX 做到稳定QTRAN 作为理论分析章节出现即可。四个算法全做进去的形式大于内容最后往往变成“四个都跑了但每一个都解释不清楚为什么收敛”。3. 用 Python 搭一套可复现的 MARL 训练骨架环境、经验池与主循环3.1 环境选型SMAC、PettingZoo 和自己写的对抗任务环境是整个复现项目里最容易忽视的黑匣子。QMIX 论文标准验证场是 SMACStarcraft Multi-Agent Challenge它基于星际争霸 2需要额外安装游戏本体和地图包安装成本不低但胜在和论文对齐程度最高2m 图两个 Marine是区分 VDN 和 QMIX 的经典试金石3m 图则是检验 baseline 是否正常的最小场景。如果你要复现论文曲线SMAC 几乎绕不开。如果只是课程设计或者想快速验证源码逻辑PettingZoo 是更轻的选择。它的接口风格继承自 Gym安装只用 pip但要注意版本差异PettingZoo 老版本和 Gym 的 API 混用经常导致 step 返回值对不上。第三种选择是自己写矩阵博弈环境比如一个 2 智能体 2 动作的 payoff matrix观测和奖励完全可控用来验证算法“是否真的满足 IGM”比任何复杂环境都直接。我一般会先写一个 2x2 矩阵环境把四种算法的行为摸清楚再进 SMAC。提示SMAC 对 StarCraft 版本有要求很多网上教程用的地图参数和论文不完全一致。复现论文曲线前先确认地图版本和 reward 设置否则后面所有对比都会失真。3.2 最小可运行的训练主循环从环境步进到联合回报下面这段是我习惯的最小训练主循环骨架不是某个上游仓库的完整代码而是把“集中训练、分布执行”这个流程落到 Python 里的最小形态# marl_train_loop.py # 所有 MARL 算法共用的 CTDE 训练主循环 import torch import numpy as np from collections import deque def train_marl(env, agents, mixer, replay_buffer, args): # agents: list[AgentRNN]每个智能体一个网络实例 # mixer: VDNMixer / QMixer把局部 Q_i 混合成 Q_tot # replay_buffer: 以 episode 为单位的回放缓冲区 optimizer torch.optim.RMSprop( list(agents.parameters()) list(mixer.parameters()), lrargs.lr, alpha0.99, eps1e-5 ) for episode in range(args.max_episodes): obs env.reset() episode_data [] # 先收集一整条 episode hidden_states [agent.init_hidden() for agent in agents] done False while not done: actions, next_hidden_states [], [] for i, agent in enumerate(agents): # 每个 agent 只用自己的局部观测和隐藏状态不能看全局 state action, h agent.act(obs[i], hidden_states[i], epsilonargs.epsilon) actions.append(action) next_hidden_states.append(h) hidden_states next_hidden_states # 所有智能体联合 step环境返回每个智能体的奖励和全局 done next_obs, rewards, done, _ env.step(actions) episode_data.append((obs, actions, rewards, next_obs, done)) obs next_obs replay_buffer.push(episode_data) if len(replay_buffer) args.batch_size: batch replay_buffer.sample(args.batch_size) # TD loss 在离线阶段统一计算训练时不碰环境 loss, extra compute_td_loss(agents, mixer, batch, args.gamma) optimizer.zero_grad() loss.backward() # RNN 梯度裁剪非常关键防止一条长 episode 把梯度撑爆 torch.nn.utils.clip_grad_norm_( list(agents.parameters()) list(mixer.parameters()), args.clip_grad ) optimizer.step()这段代码里有几个关键设计。第一episode_data 里存的是整条 transition不是每个智能体单独一条经验MARL 的联合动作必须在同一时间步上对齐拆开存会让后续采样时状态和动作错位。第二mixer 只在离线阶段参与计算智能体在环境中执行时完全是分布式的这正是 CTDE 的核心。第三优化器用 RMSprop这是很多 MARL 论文的默认选项Adam 在某些任务上反而容易震荡。参数方面我的起点是 gamma0.99batch_size32clip_grad10lr5e-4epsilon 从 1.0 线性退火到 0.05。注意训练频率不是每个 step 都更新而是攒够 batch_size 个 episode 才更新我一般每 4 个 episode 更新一次。RNN 的 hidden 维度 64 足够跑 SMAC 的小地图上到大图再考虑 128。3.3 经验回放的 episode 切分RNN 数据格式的隐藏陷阱MARL 的经验回放和单智能体 DQN 有一个本质区别基本单位是整条 episode而不是单个 transition。因为 agent 是 RNN时间维度的信息不能丢。但整条 episode 直接丢给 RNN 做 BPTT梯度会随着时间步数累积一条 100 步的 episode 就足以让梯度爆炸。常见做法是从每个 episode 里随机切一段固定长度的子序列比如 10 步。# buffer.py import random from collections import deque class EpisodeReplayBuffer: def __init__(self, capacity, seq_len10): self.capacity capacity self.seq_len seq_len self.buffer deque(maxlencapacity) def push(self, episode_data): self.buffer.append(episode_data) def sample(self, batch_size): episodes random.sample(self.buffer, batch_size) # 从每个 episode 随机抽起点切固定长度子序列 seqs [] for ep in episodes: start random.randint(0, max(0, len(ep) - self.seq_len)) seqs.append(ep[start:start self.seq_len]) # 再按 transition 的各个字段重新组合成 batch obs_b torch.stack([torch.tensor([t[0] for t in s]) for s in seqs]) act_b torch.stack([torch.tensor([t[1] for t in s]) for s in seqs]) rew_b torch.stack([torch.tensor([t[2] for t in s]) for s in seqs]) next_obs_b torch.stack([torch.tensor([t[3] for t in s]) for s in seqs]) done_b torch.stack([torch.tensor([t[4] for t in s]) for s in seqs]) return obs_b, act_b, rew_b, next_obs_b, done_bsample 里有两个细节容易踩坑。一是随机切起点时如果某个 episode 本身比 seq_len 短会切出长度为 0 的空序列需要在 push 时或者采样时做过滤。二是 padding如果最后一小段不足 seq_len通常补零并在计算 loss 时用一个 mask 把 padding 位的梯度清零否则模型会学着预测一堆无意义的推移步。4. 把四套算法写进同一个源码骨架从 IQL 基线到 QTRAN 的约束项4.1 先写独立 Q 网络验证环境、验证反向传播、验证 PyTorch 版本不管最终目标是哪个算法我都会先写一个最朴素的独立 Q 学习IQLagent。它不要求任何价值分解每个智能体把自己的观测放进一个小 MLP输出每个动作的 Q 值单独训练。这东西在大多数协作任务里表现很差但它的价值在于校验环境是否正常、reward 是否传对、网络前向反向是否没问题。如果 IQL 在一个任务里连基本的学习信号都没有那问题大概率不在算法而在环境接口。# agents/iql_agent.py import torch import torch.nn as nn import numpy as np class IQLAgent(nn.Module): def __init__(self, obs_dim, n_actions, hidden64): super().__init__() self.n_actions n_actions self.q_net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, n_actions) ) def act(self, obs, epsilon0.0): if np.random.rand() epsilon: return np.random.randint(self.n_actions) with torch.no_grad(): q self.q_net(torch.as_tensor(obs, dtypetorch.float32)) return int(q.argmax()) def compute_loss(self, obs, actions, rewards, next_obs, dones, gamma): q self.q_net(obs).gather(1, actions.unsqueeze(1)).squeeze(1) with torch.no_grad(): q_next self.q_net(next_obs).max(dim1).values target rewards gamma * q_next * (1 - dones) return ((q - target) ** 2).mean()这个网络看起来简单但四个算法都会保留类似的结构。区别只在 compute_loss 这一层IQL 直接比较自己的 Q 值和 targetVDN 把多个 agent 的 Q 相加后再比较QMIX 则先经过 mixer。后续接任何混合器agent 部分的代码几乎不用动。把 IQL 作为模块化边界后面换算法只是换 mixer 和 loss 的组合方式而已。4.2 VDN 和 QMIX 的 mixer 实现非负权重是 QMIX 的全部秘密VDN 的 mixer 一句话就能说完Q_tot 等于所有 Q_i 的求和。QMIX 则在同一个位置替换成了带 hypernetwork 的混合器。下面这段 QMixer 是我常用的实现它只做一件事根据全局状态生成非负的混合权重然后再用这些权重把局部的 Q_i 合成 Q_tot。# mixers/qmix_mixer.py import torch import torch.nn as nn class QMixer(nn.Module): def __init__(self, n_agents, state_dim, hidden32): super().__init__() self.n_agents n_agents self.hidden hidden # hypernetwork 从 state 生成第一层权重矩阵 self.hyper_w1 nn.Linear(state_dim, n_agents * hidden) self.hyper_b1 nn.Linear(state_dim, hidden) # 生成第二层权重 self.hyper_w2 nn.Linear(state_dim, hidden) self.hyper_b2 nn.Linear(state_dim, state_dim) def forward(self, agent_q, states): # agent_q: [batch, n_agents]每个智能体的局部 Q batch_size agent_q.size(0) w1 torch.abs(self.hyper_w1(states)) w1 w1.view(batch_size, self.n_agents, self.hidden) b1 self.hyper_b1(states).view(batch_size, 1, self.hidden) # [batch, 1, n_agents] x [batch, n_agents, hidden] hidden_out torch.bmm(agent_q.unsqueeze(1), w1) b1 hidden_out torch.relu(hidden_out) # 中间层激活不能省 w2 torch.abs(self.hyper_w2(states)) # 同样必须非负 w2 w2.view(batch_size, self.hidden, 1) b2 self.hyper_b2(states).view(batch_size, 1, 1) q_tot torch.bmm(hidden_out, w2) b2 return q_tot.squeeze(1)这段代码里有两个必须强调的细节。第一hyper_w1 和 hyper_w2 的输出都用 torch.abs 包了一层这是 QMIX 单调性的工程保证用 softplus 也行但 abs 更干脆而且梯度更稳。第二b2 对应的偏置层没有做非负约束因为偏置项不影响单调性。训练时 Q_tot 的 TD target 来自全局奖励loss 是 (Q_tot - target) 的 MSE直接从 Q_tot 往 mixer 和 agent 双向回传。VDN 的版本把整个 mixer 换成一行q_tot agent_q.sum(dim-1, keepdimTrue)就行。也正因如此VDN 的训练速度会比 QMIX 快不少因为少了 hypernetwork 的所有参数量。4.3 QTRAN 与 MAVEN 的额外 loss约束项和互信息项往哪里加QTRAN 在结构上是“VDN/QMIX 的 mixer 一个独立的联合价值网络 q_joint”。q_joint 接受所有智能体的观测和动作拼接输出一个联合价值估计。训练 loss 由三部分组成原始的 TD loss、最优动作约束 loss、所有动作约束 loss。# trainers/qtran_trainer.py # 仅展示 QTRAN 相对 QMIX 多出来的两个约束项 def qtran_extra_loss(q_tot, q_joint, q_opt_joint, actions): # 约束 1: 最优联合动作处q_tot 应该等于 q_joint # 约束 2: 对所有动作q_tot 不能超过 q_joint 的下界 loss_opt ((q_tot.detach() - q_opt_joint) ** 2).mean() loss_nop (torch.clamp(q_tot.detach() - q_joint, min0.0) ** 2).mean() return loss_opt loss_nop这段代码里用的都是q_tot.detach()意思是两个约束项只反馈给 q_joint 网络和 mixer 的“形状学习”不再通过 Q_tot 干扰 agent 网络的 TD 学习。这是我实际复现中发现的最关键细节如果去掉 detach两个 loss 会同时拉扯 agent 网络训练曲线会出现典型的锯齿形震荡。QTRAN 的超参敏感主要就集中在两个约束项的权重上我通常从 0.5 起步观察 loss 组成再调整。MAVEN 的额外结构是潜在变量 z。实现上一般会维护一个 latent network输入全局状态输出 z 的分布agent 网络在接收局部观测的同时也接收 z 向量。互信息 loss 用联合动作去预测 z迫使 z 对探索方向做显式约束。这段在每个仓库里的写法差异很大我习惯把它当成一个“带条件的 QMIX”agent 的输入维度多了 z 的长度mixer 部分保持不变额外加一条互信息 loss。提示QTRAN 和 MAVEN 都不是“改两行就能跑”的算法。它们各自引入的 loss 项和网络会让调参规模翻倍。如果时间只够把一个算法做彻底永远优先 QMIX。4.4 统一源码骨架一个能支撑四套算法的目录划分做四个算法复现最怕的是每个算法一个孤立仓库代码互相复制粘贴最后改一个 bug 要改四遍。我一般用下面的目录结构让所有算法共享 agent、buffer、runner只替换 mixer 和 lossmarl_project/ ├── train.py # 训练入口解析 --algo 参数 ├── configs/ # 每个算法一张 yaml 超参表 │ ├── vdn.yaml │ ├── qmix.yaml │ ├── qtran.yaml │ └── maven.yaml ├── envs/ │ ├── matrix_game.py # 自建矩阵博弈环境 │ └── wrapper.py # SMAC/PettingZoo 的统一接口包装 ├── agents/ │ ├── rnn_agent.py # 所有算法共享的 RNN agent │ └── iql_agent.py # 独立 Q 基线 ├── mixers/ │ ├── vdn_mixer.py │ ├── qmix_mixer.py │ └── qtran_mixer.py ├── runners/ │ └── episode_runner.py # 采集 episode 的通用循环 ├── buffers/ │ └── episode_replay.py └── trainers/ ├── td_loss.py # QMIX/VDN 共用 TD loss ├── qtran_trainer.py └── maven_trainer.py这个骨架的关键原则agent 和 buffer 全局共享mixer 和 trainer 按算法替换。换算法时环境、采样、经验回放这些最容易出 bug 的部分完全不动只替换价值聚合层。这样四个算法的对比实验才公平也方便在答辩时讲清楚“我抽离了公共组件只保留了每个算法的核心差异”。5. MARL 复现最常见的 5 个翻车点排查清单与避坑记录5.1 现象一loss 一直下降但评估胜率纹丝不动这个现象在 QMIX 复现里出现频率极高。loss 下降说明 TD error 在被最小化但胜率不动意味着策略没有变好。最常见原因是评估时 epsilon 没有关闭动作选择仍然带着探索噪声哪怕 Q 值已经很明确评估结果始终卡在随机水平。另一个隐蔽原因是 hidden state 初始化错误评估一个完整 episode 时RNN 的 hidden state 必须从零开始如果你沿用了训练时上一个 batch 的 hidden结果会完全失真。解决路径也很直接评估代码单独写一个 evaluate() 函数强制 epsilon0对每个 episode 重置 hidden state并让目标网络也参与评估或者明确不参与。先在一个 2m 地图上跑 200 个 episode 看曲线而不是直接上大图。如果 loss 和胜率都平再去查 reward 是否被错误缩放。5.2 现象二QMIX 在非单调任务上比 VDN 还差这不是代码 bug而是算法边界。QMIX 的单调性假设决定了它无法处理“智能体 A 的最优动作取决于智能体 B 的动作”这种非单调情境。如果选题任务里刻意设计了一个这样的陷阱QMIX 学出来的策略会整体坍缩。这个位置最容易翻车的地方在于任务看起来简单实际价值结构是非单调的你却在 QMIX 上调了一周超参。解决办法是先画任务的价值结构。在自建矩阵博弈环境里穷举列出所有联合动作的回报如果存在“A 贡献的正负取决于 B 的动作”的情况就明确知道单调性假设不成立。此时要么换 QTRAN要么把任务改成分层结构让上层先做决策、下层再基于上层决策做动作。血泪经验不要试图用更小的学习率救活一个表达力不够的网络。5.3 现象三训练曲线有周期性尖峰loss 突然暴涨展开看通常是 RNN 的 BPTT 出了问题。episode 长度方差太大时采样到的子序列可能包含大量 padding如果 mask 没有正确作用到 loss 上padding 位置的“预测”也会产生梯度。另一个常见原因是经验回放里混入了不同 episode 的 transition导致时间步错位。排查时先打印 batch 里每个序列的 mask 和有效长度确认 padding 比例不超过 20%。然后检查 loss 计算时是否对每个时间步乘了 mask。最后检查 replay buffer 的采样逻辑是否按 episode 为单位而不是按 transition 为单位。我见过有人把单智能体 DQN 的 replay 逻辑直接搬过来按 transition 采样训练出来的 RNN 完全学不到任何时序信息。5.4 现象四同一个仓库换台机器结果完全不一样这属于经典的“随机种子黑匣子”。PyTorch 的 CUDA 卷积和某些算子默认是非确定性的numpy 的随机数生成器、Python 的 random 模块又各自独立。光 set 一个 torch.manual_seed 远远不够。排查顺序是先固定 Python、numpy、torch 三个随机种子再设置 CUDA 确定性模式最后如果还不行检查环境初始化里是否用了系统时间。实践上我不建议在训练阶段开启 PyTorch 的完整确定性模式因为它会让运行速度慢一倍以上。我的习惯是训练时只固定三个随机源保证在大多数机器上可复现真正要出论文级别的结果时开确定性模式跑一次作为最终数据。5.5 现象五跑通源码但曲线永远比论文低一截第一个要检查的是环境版本。SMAC 的地图参数、奖励设置、episode 长度限制在不同版本里有差异论文的曲线是基于特定仓库和特定版本的。第二个要检查的是评估口径论文画的往往是不止一个 seed 的中位数曲线你拿单次 seed 的最好结果去对比自然会觉得“复现不出来”。正确做法是至少跑 3 个 seed每个 seed 训练完用同一个评估函数跑 50 个 episode 取平均胜率再画 median 和四分位区间。最后一个容易忽略的是训练预算。QMIX 在 SMAC 的 2s3z 地图上需要接近两千个 episode 才会出现明显拐点很多人跑到五百个 episode 看曲线平的就停了。建议先用 2m 这种小地图验证整套 pipeline2m 收敛很快几百个 episode 就能看到上升趋势确认没问题后再扩大地图规模。6. 验收一个 MARL 复现项目的三把尺子维度、种子与基线对比6.1 用“单智能体等价测试”验证价值分解的正确性价值分解代码对不对有一个非常便宜的测试把 agent 数量设成 1。此时 Q_tot 必须严格等于唯一的 Q_1任何 mixer 都不该改变这个值。在代码里写死一个断言观察 IQL、VDN、QMIX 三种算法在 n_agents1 时是否收敛到相同曲线。# sanity_check.py # n_agents1 时QMIX 的 Q_tot 应该退化为 Q_1 q_i torch.randn(4, 1) # batch4, agent1 state torch.randn(4, state_dim) q_tot mixer(q_i, state) assert torch.allclose(q_tot.squeeze(-1), q_i.squeeze(-1), atol1e-5)这个测试帮我抓过好几处 bug有一次 QMixer 的 w1 维度没对齐单智能体下输出恒为 0如果不做这个断言我会在 2m 图上白调两天超参。6.2 跨种子取中位数与置信区间而不是最好成绩MARL 的训练曲线是随机性很强的随机过程。哪怕同一个算法、同一个环境、同一个超参换一个 seed最终胜率可能相差 15 个百分点。验收项目时至少要跑 5 个 seed每个 seed 单独画曲线最终呈现的图是 5 条曲线的 median 与四分位区间。答辩时这张图比任何一张“我跑出了 95% 胜率”的单独曲线都有说服力。单次 seed 的最好成绩只能代表运气不能代表复现成功。6.3 提交源码的结构环境、超参、评估脚本分离最后落到交付物。源码仓库里训练入口 train.py、评估入口 evaluate.py、超参配置 configs/ 三项必须分离。训练时用的超参表全部落到 yaml 文件里不要硬编码在代码里评估脚本独立于训练脚本确保不会因为训练状态残留而产生虚假的高分。README 里至少写清楚环境版本、Python 版本、训练和评估两条命令、以及三个 seed 的复现结果。我自己的习惯是每次改动 mixer 或 loss 后先跑一遍单智能体等价测试再跑 5 个 seed 看中位数。现在回头想这个项目里最值钱的已经不是 QMIX 或 MAVEN 本身而是那套能稳定复现出算法差异的评估脚本。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网