ARTICLE DETAIL

资讯详情

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

多智能体强化学习价值分解算法演进:从VDN到QPLEX实战指南

多智能体强化学习价值分解算法演进:从VDN到QPLEX实战指南 简介多智能体强化学习MARL是处理群体协作决策的重要技术方向其核心挑战在于联合动作空间随智能体数量指数膨胀。价值分解方法通过将全局联合动作价值拆解为个体价值之和在Scalability与策略表达能力之间取得平衡。从最朴素的VDN求和分解到QMIX引入单调混合网络保证IGM条件再到QTRAN用校正项突破单调限制最终QPLEX借助优势函数与注意力机制实现更灵活的协作建模这一演进线展示了MARL求解思路的逐步深化。该类技术在游戏对战、机器人编队、交通调度等场景中均有落地价值。对Python工程师而言基于PyTorch复现VDN、QMIX、QTRAN、QPLEX四套算法并跑通完整训练循环是理解多智能体系统与部署工程化方案的高效路径同时还需关注经验回放结构、目标网络更新及checkpoint保存恢复等关键工程细节。1. 多智能体强化学习入门为什么先跑通VDN到QPLEX这四条线多智能体强化学习MARL这几年在游戏对战、机器人编队、交通调度里被反复验证而VDN、QMIX、QTRAN、QPLEX这四个算法基本上构成了价值分解方法的完整演进线。很多Python工程师拿到这套源码的第一反应是想知道四个算法各自在什么条件下有效为什么有人说QMIX收敛顺利、换QTRAN就翻车模型文件保存下来之后又该怎么验证。这篇笔记就顺着这一条线把网络结构、训练循环、超参设置和常见坑逐一拆开。适合刚接触MARL的读者照着复现也适合已经在调参的熟手快速对照排查。2. 从VDN到QMIX求和分解与单调混合网络怎么选2.1 VDN算法详解为什么多智能体的Q值可以直接相加VDN的全称是Value Decomposition Networks核心假设是联合动作价值Q_tot(s,a)可以写成每个智能体价值Q_i(s,a_i)的加和。这个假设看起来很朴素但在同构单元组成的战队场景里往往够用。比如五个相同兵种打五个相同兵种智能体之间主要通过远处队友的动作间接影响加和分解已经能够表达大部分协作信号。基于python实现时VDN的代码量在四个算法里最小适合作为第一条跑通的基线。实现上一般分两块个体Q网络和求和操作。个体Q网络对不同智能体可以用同一套参数再用one-hot的智能体ID拼到观测里作区分也可以每个智能体独立一套参数。前者参数少、收敛快后者在小规模任务上更稳。我常用的是共享参数加ID因为后续换QMIX、QTRAN时网络结构不用大改。import torch import torch.nn as nn import torch.nn.functional as F class RNNAgent(nn.Module): def __init__(self, input_dim, action_dim, hidden_dim64): super().__init__() self.fc1 nn.Linear(input_dim, hidden_dim) self.gru nn.GRUCell(hidden_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, action_dim) def forward(self, obs, hidden_state): # obs: (batch, input_dim)hidden_state: (batch, hidden_dim) x F.relu(self.fc1(obs)) h self.gru(x, hidden_state) q self.fc2(h) return q, h这个RNNAgent用GRU记录历史观测因为多智能体场景里单步观测往往不足以判断全局状态比如能否看到敌方、队友在执行什么动作都需要时间维度的信息。forward返回两个值当前每个动作的Q值q以及更新后的隐藏状态h。训练时把上一个时刻的隐藏状态传进来形成沿时间步展开的循环图解。实际实现里要用一个reshape把(batch, n_agents, obs_dim)合并成(batch * n_agents, obs_dim)过一次GRU再view回来这里省略了这层样板代码。class VDNMixer(nn.Module): def forward(self, q_selected): # q_selected: (batch, n_agents)每个智能体已选动作对应的Q值 return q_selected.sum(dim1) # (batch, 1)VDNMixer严格说不是一个网络只是一个按智能体维度求和的算子。q_selected的获取方式是先拿到每个智能体的完整Q向量再用实际执行的动作索引取出对应值。联合Q值就是这个和训练目标用标准的DQN TD误差公式如下L E[(r γ * Q_tot_target(s, argmax Q_tot(s, ·)) - Q_tot(s, a))²]这里有个容易踩的细节VDN的target Q_tot不直接取target网络求和再取argmax而要用online网络选动作、target网络给值也就是double DQN的套路。原因和单智能体DQN一致argmax套在同一个网络里会高估Q值四个算法共用这一套逻辑后面不再重复解释。2.2 QMIX的混合网络用hypernetwork生成非负权重QMIX对VDN的批评很直接求和是等权的不同智能体对团队价值的贡献被固定成1:1无法表达非线性交互。它的做法是保留个体Q网络另外训练一个mixing network把个体Q值映射成联合Q_tot同时要求该映射是单调的也就是Q_tot对任意Q_i的偏导大于等于0。这个条件被称为IGMIndividual-Global-Max它保证联合动作的argmax等于各智能体各自argmax的组合这样训练时才能从全局最优反推出每个智能体该执行什么动作。mixing network的输入不是观测而是全局状态state个体Q值只是被混合的对象。结构上权重由hypernetwork从state生成为了让权重非负生成后强制取绝对值或用softplus。QMIX的代码量比VDN多一个文件主要就是这部分。class QMIXMixer(nn.Module): def __init__(self, n_agents, state_dim, hidden_dim32): super().__init__() self.n_agents n_agents self.hidden_dim hidden_dim # hypernetwork从全局状态生成混合网络第一层权重 self.hyper_w1 nn.Linear(state_dim, n_agents * hidden_dim) # 生成第一层偏置 self.hyper_b1 nn.Linear(state_dim, hidden_dim) # 生成第二层权重 self.hyper_w2 nn.Linear(state_dim, hidden_dim) def forward(self, q_vals, states): # q_vals: (batch, n_agents)states: (batch, state_dim) batch q_vals.shape[0] # 权重强制非负保证单调性 w1 torch.abs(self.hyper_w1(states)).view(batch, self.n_agents, self.hidden_dim) b1 self.hyper_b1(states).view(batch, 1, self.hidden_dim) hidden F.relu(q_vals.unsqueeze(1).bmm(w1) b1) # (batch, 1, hidden_dim) w2 torch.abs(self.hyper_w2(states)).view(batch, self.hidden_dim, 1) q_tot hidden.bmm(w2).view(batch, 1) return q_tot这段代码的要点有三个。第一view把权重reshape成矩阵乘需要的形状(batch, n_agents, hidden_dim)对应每个agent到每个隐藏单元的连接第二abs保证了单调性这是QMIX成立的生命线第三第二层偏置直接被省略因为第一层的b1已经能表达非线性偏移再加第二层偏置会让表达冗余。部分实现会在第二层也加hyper_b2实测对结果影响不大。bmm是批量矩阵乘法batch里每个样本独立做一次矩阵乘效率比循环高很多。VDN和QMIX的选型我的习惯是任务里智能体数量少、协作简单VDN就够训练快、超参省心任务有明显分工或者需要根据战场态势动态调整贡献权重直接上QMIX。在SMAC这类基准上QMIX通常比VDN高几个百分点的胜率代价是mixing network多吃了不少显存和训练时间。如果追求更快的迭代调试先用VDN跑通整个数据流再替换成QMIX是成本最低的路径。Python环境上我习惯用3.8或3.9配PyTorchnumpy和gym装好就能跑这套流程不需要追新版本。3. QTRAN和QPLEX去掉单调约束之后的价值分解路线3.1 QTRAN把联合Q值拆成个体Q值加校正项三个loss怎么配QMIX的单调性约束保证了IGM但也限制了表达能力。QTRAN想解决这个矛盾不要求Q_tot对Q_i单调而是用一个额外的变换关系来维持IGM。它的核心是引入两个辅助函数联合动作值Q_joint(s,a)和联合状态值V_joint(s)并要求Q_tot(s,a) Σ Q_i(s,a_i) V_joint(s) - Σ V_i(s) 加上一个校正项。这样不再需要网络结构上强制单调理论上能表达更多非单调的协作模式。代价是训练目标从一条TD loss变成三条joint loss约束online的Q_joint与Q_tot一致agent loss约束个体Q_i与Q_agt一致optimality loss约束在最优联合动作处Q_tot与Q_joint一致。这三条loss的量级和梯度尺度完全不同QTRAN在复现里翻车基本都翻在这里。实现上需要为每个episode保存完整的动作选择概率或Q值因为agent loss要对所有动作求Q_i不是只对被选中动作。class QTRANTransformer(nn.Module): def __init__(self, obs_dim, action_dim, n_agents, state_dim, hidden_dim64): super().__init__() self.n_agents n_agents self.action_dim action_dim # 个体Q网络结构与VDN一致 self.agent_q RNNAgent(obs_dim, action_dim, hidden_dim) # 联合Q网络输入全局状态所有智能体动作的one-hot拼接 self.joint_q nn.Sequential( nn.Linear(state_dim n_agents * action_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) # 联合V网络只输入全局状态 self.joint_v nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, obs, states, actions_onehot, hidden): q_i, hidden self.agent_q(obs, hidden) # (batch * n_agents, action_dim) q_joint_input torch.cat([states, actions_onehot.flatten(1)], dim1) q_joint self.joint_q(q_joint_input) # (batch, 1) v_joint self.joint_v(states) # (batch, 1) return q_i, q_joint, v_joint, hidden注意joint_q的输入维度是state_dim加上n_agents * action_dim也就是说要一次性把全局state和所有智能体的动作都灌进去这比QMIX多一个数量级的输入节点也是QTRAN训练慢的直接原因。在实现里这个类并不直接算Q_tot真正的Q_tot是后面在loss层把个体Q、联合Q、联合V组合出来的这样三个loss才能各自独立回传。actions_onehot要在环境交互时预先生成并随episode一起存进buffer否则训练时临时构造既慢又容易出错。QTRAN的两个变体QTRAN-base和QTRAN-alt区别在于校正项的形式。alt版本又加了一个用来逼近Σ Q_i的辅助网络效果通常更好但参数也更多。我的建议是除非你要在非单调的对抗任务里做对比否则先不要拿QTRAN当主力它的数学漂亮工程上却非常难稳定收敛。3.2 QPLEXduplex dueling如何用优势函数保证IGMQPLEX站在QTRAN的肩膀上换了一条路用dueling结构把Q拆成V和优势函数A然后对优势函数做变换。直觉是Q_tot的argmax只和优势函数A_tot有关因为V_tot不随动作变化。于是QPLEX构造每个智能体的优势A_i(s,a_i)再通过一个变换网络把它们组合成联合优势A_tot组合权重由attention机制根据隐藏状态动态生成并限定为非负从而在结构上保证IGM同时不需要QTRAN那种三条loss的软约束。这个设计比QTRAN聪明的地方在于IGM是硬约束而不是loss优化稳定性大幅提升。QPLEX在SMAC的多个地图上通常超过QMIX尤其那些需要差异化分工的地图优势函数分解比单调Q值混合更容易学到“谁该在什么时刻承担关键动作”。实现上个体网络除了输出Q_i还要输出V_i两者相减得到A_i。变换网络通过attention的query/key把各智能体的优势加权聚合权重经过softmax约束后再乘上A_i。下面是最核心的mixer部分class QPLEXMixer(nn.Module): def __init__(self, n_agents, hidden_dim64): super().__init__() self.hidden_dim hidden_dim self.query nn.Linear(hidden_dim, hidden_dim) self.key nn.Linear(hidden_dim, hidden_dim) self.weight_net nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), ) def forward(self, q_i, v_i, hidden_state): # q_i, v_i: (batch, n_agents)hidden_state: (batch, n_agents, hidden_dim) adv_i q_i - v_i # 个体优势 (batch, n_agents) # attention权重由每个agent的隐藏状态生成 queries self.query(hidden_state) # (batch, n_agents, hidden_dim) keys self.key(hidden_state) scores torch.bmm(queries, keys.transpose(1, 2)) # (batch, n_agents, n_agents) weights torch.softmax(scores, dim-1) # 组合个体优势再经一层网络得到联合优势 adv_combined torch.sum(weights * adv_i.unsqueeze(1), dim-1) # (batch, n_agents) adv_tot self.weight_net(adv_combined.unsqueeze(-1)).squeeze(-1).sum(dim1) # (batch, 1) v_tot v_i.sum(dim1, keepdimTrue) q_tot v_tot adv_tot return q_tot这段代码里权重网络weight_net对组合后的优势做逐智能体放缩最后求和得到联合优势。v_tot直接取所有智能体V_i的和这是QPLEX相对简单的部分真正的表达能力都在优势变换上。注意hidden_state携带的是GRU的隐藏层不是观测本身它刻画了每个智能体对当前态势的理解attention用它来配对“哪些智能体之间需要协作”。选型上如果已经在QMIX上跑通并且表现不错不值得为了炫技换QPLEX如果任务里明显存在非单调协作比如两个智能体同时开火才能击杀敌人、谁先动手都没意义那QPLEX比QMIX更值得投入。QTRAN更多是学术对比里的参照物实战里它的稳定性短板让调参成本居高不下。4. 把四个算法放进同一套Python训练循环回放缓冲、目标网络与超参速查4.1 经验回放按episode存多智能体RNN训练的前提四个算法都用RNNAgent训练时必须按时间步展开不能像DQN那样随意抽一条transition。因为GRU的隐藏状态依赖前序观测拆开单条transition等于丢掉时序上下文。经验回放的设计差异就在这里buffer里存的是完整episode而不是transition。一个episode在Python里可以表示成列表每个元素是一个字典记录该时间步的全局观测obs、各智能体动作actions、奖励rewards、下一观测next_obs、终止标志done。obs的形状是(n_agents, obs_dim)不是单个智能体的方便batch处理。Python3.8以上环境跑这套代码没有兼容性问题numpy、torch装好就行。import random class EpisodeBuffer: def __init__(self, capacity): self.capacity capacity self.episodes [] def push(self, episode): # episode: list of dict长度等于episode步数 self.episodes.append(episode) if len(self.episodes) self.capacity: self.episodes.pop(0) def sample(self, batch_size): # 随机抽batch_size个完整episode return random.sample(self.episodes, batch_size)capacity一般按episode数量设常见是1000到5000。环境每回合结束调用一次push训练时用sample抽出一批完整episode沿时间维度做循环展开。这种设计在代码结构上统一了四个算法无论是VDN的求和、QMIX的mixing network还是QTRAN的三条loss输入都是同一个batch结构只是前向和loss计算不同。这也是多智能体代码比单智能体难调试的地方一个episode长度200、batch_size 16一次训练迭代要处理3200个时间步显存和CPU的占用都比DQN高不少。4.2 统一训练主循环四套网络共用一套调度代码训练主循环做的事是固定的采样、存buffer、按batchsize抽episode、沿时间步前向、算loss、回传、更新目标网络。差异只在前向和loss函数。所以代码组织上最好把“计算Q_tot和loss”抽出来单独写让四种算法各自实现而环境交互、buffer管理、logger、checkpoint完全共用。def train_step(batch, agent, mixer, target_agent, target_mixer, optimizer, gamma, device): total_loss 0.0 hidden None target_hidden None for t in range(len(batch)): obs_t batch[t][obs].to(device) # (batch, n_agents, obs_dim) acts_t batch[t][actions].to(device) # (batch, n_agents) rew_t batch[t][rewards].to(device) # (batch, 1) m batch[t][mask].to(device) # (batch, 1)1表示未结束 next_obs_t batch[t][next_obs].to(device) # online网络 q_i, hidden agent(obs_t, hidden) q_sel q_i.gather(2, acts_t.unsqueeze(-1)).squeeze(-1) # (batch, n_agents) q_tot mixer(q_sel, batch[t][states].to(device)) # 目标网络online选动作target给值 with torch.no_grad(): q_i_next, target_hidden target_agent(next_obs_t, target_hidden) q_next agent(next_obs_t, hidden) # online网络选动作 online_acts q_next.argmax(dim2) q_next_sel q_i_next.gather(2, online_acts.unsqueeze(-1)).squeeze(-1) q_tot_next target_mixer(q_next_sel, batch[t][next_states].to(device)) target rew_t gamma * m * q_tot_next loss (q_tot - target).pow(2).mean() total_loss loss optimizer.zero_grad() total_loss.backward() torch.nn.utils.clip_grad_norm_( list(agent.parameters()) list(mixer.parameters()), 10.0) optimizer.step()这段代码有几个关键点需要注意。第一hidden和target_hidden沿着整个episode传递不能每一时间步都清零第二target网络的值计算用了online网络的argmax也就是double DQN第三mask或done用来在终止步把future项清零避免把回合外的虚构奖励算进去第四clip_grad_norm防止RNN展开到200步时梯度爆炸clip值在5到10之间问题不大。如果你在四个算法之间切换有一个常见做法是把agent和mixer按策略类注册到同一个训练器里。VDN的mixer是求和QMIX是mixing networkQTRAN和QPLEX各自实现forward训练函数对mixer只要求输入输出接口一致。这样能省掉大量重复代码也是这类源码里最常见的组织方式。如下是四套超参的速查基准实际以你的地图规模微调参数常见值调参说明learning_rate5e-4 ~ 1e-3过大会震荡过小GRU展开收敛太慢batch_sizeepisode数16 ~ 32取决于显存episode越长batch越小buffer容量1000 ~ 5000过小样本相关性高过大训练滞后gamma0.99稀疏任务可到0.995epsilon1.0 - 0.05衰减按episode数走总步数要足够长target更新间隔200 episode或hard resetsoft update的tau取0.005梯度裁剪5 ~ 10RNN展开必配hidden_dim64 ~ 128简单地图64复杂地图128提示target网络更新间隔、epsilon衰减总步数、buffer容量三者要一起设计否则会出现训练中期buffer被新数据刷爆、探索率已经归零的情况。4.3 模型文件保存与恢复checkpoint里该装什么标题里强调“对应模型文件”所以保存这块单独拿出来说。很多第一次跑MARL的人只save model.state_dict结果恢复训练后效果波动很大其实是因为优化器的动量信息丢了。一个完整的checkpoint至少应该包含个体网络和混合网络的state_dict、优化器state_dict、当前episode数、epsilon当前值、完整超参配置。checkpoint { agent: agent.state_dict(), mixer: mixer.state_dict(), optimizer: optimizer.state_dict(), episode: episode_idx, epsilon: epsilon, config: vars(args), } torch.save(checkpoint, fcheckpoint/{algo}_seed{seed}.pt)恢复时把路径映射到相应结构ckpt torch.load(path, map_locationdevice) agent.load_state_dict(ckpt[agent]) mixer.load_state_dict(ckpt[mixer]) optimizer.load_state_dict(ckpt[optimizer]) epsilon ckpt[epsilon]务必保持保存和加载时网络结构、参数命名完全一致哪怕改了一个Linear的hidden_dim加载也会直接报错或出现静默的随机初始化。我习惯把algo名、seed、地图名写进文件名因为对比实验一多seed信息很容易弄混。评估阶段建议用torch.no_grad()包住前向并打开model.eval()去除dropout的影响。模型文件只是一个快照它有没有价值取决于你保存时是否把环境配置、seed、超参一起固化下来。5. 复现这四种算法时的踩坑记录与排查手册5.1 QMIX混合网络权重出现负值单调性悄悄失效现象训练曲线下降正常但评估胜率卡在随机水平偏上一点或者把Q_tot和Q_i分别打出来看发现某个智能体Q_i变大时Q_tot反而变小。 原因mixing network的权重来自hypernetwork的输出如果代码里没有对权重做非负约束或者使用了tanh、linear直接输出训练中权重会飘到负区间单调性被破坏IGM不再成立。 解决对w1和w2强制abs()或softplus在forward里加一行调试代码assert (w1 0).all() and (w2 0).all()。跑几十个episode如果断言触发就能在训练早期抓到而不是等一轮训练完才发现评估不对。另一个可视化技巧是把w1的分布打印成直方图正常情况应该集中在正值区间。5.2 QTRAN三个loss系数不匹配训练直接爆炸现象总loss在几十和几千之间跳变梯度更新到一半权重变成NaN或者agent loss正常但joint loss始终降不下去。 原因QTRAN的三条loss尺度差异太大。joint loss是Q值的MSEagent loss是Q_i和Q_agt的MSEoptimality loss是argmax动作附近的约束前者数值可能比后两者大一个量级。如果系数都设成1梯度会被joint loss主导另外两条loss等于摆设。 解决把三条loss分别打印均值按量级归一化常见的系数比例是joint:agent:optimality 1:1:0.1也可以试1:0.5:0.1optimality改用smooth L1而不是MSE对异常值更稳。调QTRAN的loss系数一度被我归为玄学后来发现本质是量级对齐问题先打印每个term的均值再按比例缩放比盲调系数靠谱得多。5.3 epsilon衰减太快联合动作空间根本没铺满现象VDN和QPLEX在简单地图表现好在复杂地图策略单一训练后期epsilon已经接近0但胜率明显卡在平台期。 原因多智能体的联合动作空间是各智能体动作空间的笛卡尔积。n个智能体、每个a个动作联合空间是a的n次方5个智能体6种动作就是7776种联合动作。epsilon按episode线性从1衰减到0.05总episode只有1000时后面几百个episode的探索率极低很多联合动作从未被采样。 解决把epsilon衰减曲线拉长常见做法是用指数衰减epsilon max(0.05, 1.0 * 0.995^episode)或者在前2000个episode保持线性下降之后最低到0.05。如果任务需要精准协作探索阶段让epsilon从1衰减到0.2并在后半段保持比一路降到0好。评估时强制epsilon0训练时用单独的探索噪声不要把两者混在一起。5.4 reward尺度不一致导致四种算法横向对比失真现象QMIX在某张图上loss最低但评估不如VDN或者同一算法不同seed结果差一倍。 原因QMIX的mixing network直接回归Q值对reward的绝对值很敏感。Q值被RNN和混合网络层层放大后如果reward没有归一化梯度尺度会随episode长度变化。一个episode长度200、reward每步0.1的任务累积奖励是20而长度50的任务累积只有5同一个learning_rate对两者表现完全不同。 解决统一对reward做缩放最简单是除以单步reward的绝对值上限或者用随机策略预先统计累积奖励的均值和方差再标准化。横向对比时四个算法必须用同一个环境包装器reward的scale处理在包装器里完成不要散落在各算法代码中。把reward scale写进config换环境时不用改训练代码这是一个能省很多事的习惯。5.5 加载模型文件后评估结果与训练记录不一致现象训练日志里胜率85%加载checkpoint后评估只有60%多跑几次又变成65%忽高忽低。 原因训练时记录的胜率是在训练过程中动态评估的可能带着探索噪声或没固定环境种子评估阶段如果忘记关闭梯度、忘记model.eval()或者开了dropout结果也会有偏差。另外环境的随机性很大时单次或十次episode的均值方差极高。 解决写一个独立的evaluate函数固定环境seed、model.eval()、torch.no_grad()、epsilon0至少跑50个episode统计中位数和四分位不要只报均值。训练日志里的胜率只当作趋势看最终结论以离线评估为准。这和单智能体评估的准则一致但MARL里联合动作空间大评估方差更大所以更需要注意。6. 用保存的模型文件做离线评估验证协作策略比训练指标更可靠6.1 固定种子、关exploration用中位数和分位数下结论评估的代码套路并不复杂难的是排除干扰变量。我会在evaluate函数里固定环境seed、固定最大episode长度并把模型切到eval模式。动作选择直接用online网络输出的argmax不套epsilon。奖励统计用中位数作为主指标因为少数极端episode会把均值拉偏中位数更稳定。代码骨架如下torch.no_grad() def evaluate(algo, ckpt_path, env_fn, n_episodes50, max_steps200): agent, mixer, config load_checkpoint(algo, ckpt_path) agent.eval() env env_fn(seed42) returns [] for _ in range(n_episodes): obs env.reset() hidden None episode_return 0.0 for step in range(max_steps): q_i, hidden agent(obs, hidden) actions q_i.argmax(dim1).cpu().numpy() obs, reward, done, _ env.step(actions) episode_return reward if done: break returns.append(episode_return) returns.sort() median returns[len(returns) // 2] return median, returns评估时把max_steps设为和训练一致避免无限循环拖垮脚本。如果环境支持渲染可以在一个固定seed下保存视频或gif用肉眼确认协作行为是不是真的存在而不仅仅是数字好看。我一般会把多个算法的评估结果放在同一个表里算法与算法之间只允许seed不同其余环境、步数、reward缩放全部一致否则对比没有说服力。6.2 从Q值分布反推分解质量检查每个智能体有没有学到东西除了看奖励还要看每个智能体的Q值分布。训练完把一批episode的q_i按智能体分组打印每个智能体Q值的均值和方差。如果某个智能体的Q值恒为一个常数或者变化极小说明它根本没学到有效策略只是把共享网络的梯度平均吃掉了。对QMIX还要检查Q_tot和ΣQ_i的关系是否单调在同一条episode里把某个智能体的Q_i人为抬升一点看Q_tot是否也上升这是验证IGM是否成立的最直接黑盒测试。做法是拿一段真实轨迹对同一个state分别计算原始Q_i和扰动后的Q_i过一遍mixer看输出。这些检查听起来繁琐但都是血泪经验换来的。我吃过最大的亏是拿training loss当模型好坏的唯一标准结果某次在评估阶段用同一组checkpoint连续跑三个seed发现QMIX最高最低差了两倍才意识到seed固定和评估episode数量不够是复现里最容易忽视的坑。从那以后我把config和seed一起写进checkpoint文件名评估脚本固定seed并至少跑50个episode再下结论。模型文件只是一个序列化快照真正值钱的是一套可重复的评估流程希望帮到你。本文还有配套的精品资源点击获取
返回列表