
简介这是一套基于Python实现的多智能体强化学习MARL算法复现源码覆盖QMIX、VDN、QTRAN、MAVEN等经典算法面向需要完成毕业设计、课程设计或项目开发的学生与开发者适合有一定Python和深度学习基础、希望直接获得可运行基线代码的读者。压缩包共10个文件其中6个py脚本构成核心代码按功能分为智能体、策略网络、神经网络、配置、主流程等模块2个Markdown文档提供使用说明与实现细节另附1张结果可视化图片整体仅47KB结构清晰、便于快速上手。已有277人学习下载源码经过严格测试可放心运行并在此基础上扩展。建议结合说明文档梳理各模块关系重点关注agent、policy、main等文件可帮助深入理解多智能体强化学习的训练流程与参数配置也便于后续开展算法对比、消融实验或二次开发。1. 多智能体强化学习复现从值分解入手为什么最稳妥当毕业设计或课程设计的题目落在“基于Python的多智能体强化学习MARL算法复现”上大多数人的第一反应是QMIX、VDN、QTRAN、MAVEN这四个算法能不能在一个工程里全部跑起来答案是能而且难度没有想象中高。值分解这一脉算法有个先天优势——网络结构简单、损失函数直观、训练范式统一只要环境接口选对一套源码就能支撑四个算法的对比实验。这篇笔记会沿着“原理→代码→训练→踩坑→答辩”的顺序把这条落地路径完整铺开。它适合两类人一类是赶毕设、需要尽快出曲线和对比表的同学另一类是刚接触MARL、想搞懂值分解到底在做什么的工程师。前提是你已经会写基本的PyTorch下面所有代码都按这个基础来。2. 从VDN到QMIX联合Q值的两种拆解方式2.1 分解的动机联合动作空间为什么大到算不动MARL最棘手的问题不是“多个智能体分别学习”而是“联合动作空间爆炸”。N个智能体、每个有M个动作联合动作就有M的N次方种。如果直接学一个Q(s, a₁, a₂, …, aₙ)输出的维度随智能体数量指数膨胀训练样本根本喂不满这个空间。值分解Value Decomposition的思路是反其道而行既然联合Q值难学那就把它拆成“每个智能体自己算自己的Q值”再用某种方式合并起来。这个“合并方式”就是各算法的分水岭。中心训练、分布式执行CTDE是这套方法的框架前提。训练时有一个中心控制器能看到全局状态能用联合奖励去优化所有智能体执行时每个智能体只依赖自己的局部观测独立决策。VDN和QMIX都属于CTDE框架下的值分解算法它们的核心区别只有一句话VDN用加法合并QMIX用单调性约束的神经网络合并。2.2 VDN加法分解的最小实现VDNValue-Decomposition Networks做的是最朴素的一件事联合Q值等于所有智能体Q值之和即 Q_tot(τ, a) Σᵢ Qᵢ(τᵢ, aᵢ)。这里的τᵢ是智能体i的历史观测序列通常用GRU编码。因为求和是可交换的VDN天然满足“全局最优动作等于各智能体最优动作的组合”这是它最大的优点——协作时不需要额外协调机制。每个智能体的网络结构一样一个GRU 一个全连接输出层。共享参数还是独立参数复现时我一般让所有智能体共享底层编码器输出层各自独立这样既减少参数又能保留个体差异。动作选择则用ε-greedy或softmax采样。具体实现如下class VDNAgent(nn.Module): def __init__(self, obs_dim, action_dim, hidden_dim64): super().__init__() self.rnn nn.GRUCell(obs_dim, hidden_dim) self.fc nn.Linear(hidden_dim, action_dim) def forward(self, obs, hidden): # obs: (batch, obs_dim)每个时间步传入当前观测 # hidden: (batch, hidden_dim)上一步的GRU隐状态 h self.rnn(obs, hidden) q self.fc(h) # (batch, action_dim) return q, h class VDNMixer(nn.Module): def forward(self, q_i): # q_i: (batch, n_agents, action_dim)已经按动作分好 # 对每个智能体取当前动作的Q值再求和 return q_i.sum(dim1) # (batch, action_dim)代码核心就两段智能体网络负责把观测映射成Q值混合器负责求和。训练目标和单智能体DQN几乎一样只不过把单智能体的Q换成了Q_tot。误差是TD误差目标值用target网络计算公式为loss MSE(q_tot, r γ * max_next_q_tot)。要注意GRU的输入观测只包含局部信息不能把全局state传进去——这是CTDE的硬约束。VDN的实现太简单了简单到很多人在复现时会怀疑“就这”。确实就这。它的表达能力也因此受限加法分解假设智能体之间对联合价值的贡献是线性可加的一旦任务里存在“负协作”或“超可加性”VDN就会失准。这也是为什么QMIX在后面做了更精细的合并。2.3 QMIX单调性约束与超网络QMIX的改进点非常明确联合Q值和个体Q值的关系不再是固定的加法而是一个由全局状态调制权重的神经网络同时要求这个网络对每个个体Q值是单调递增的。单调性约束写作 ∂Q_tot / ∂Qᵢ ≥ 0它能保证一个数学性质——Q_tot取最大值的那个联合动作恰好等于每个智能体argmax自己Q值得到的动作组合。实现单调性最省事的办法混合网络的权重一律取正。QMIX于是引入一个超网络Hypernetwork用全局状态state作为输入生成混合网络每一层的权重矩阵再对权重施加绝对值或softplus约束。这样state就能影响“每个智能体的话事权重”但不会破坏单调性。class QMixer(nn.Module): def __init__(self, state_dim, n_agents, mixing_embed_dim32, hypernet_embed_dim64): super().__init__() self.n_agents n_agents self.mixing_embed_dim mixing_embed_dim # 超网络从state生成混合网络第一层权重 self.hyper_w1 nn.Sequential( nn.Linear(state_dim, hypernet_embed_dim), nn.ReLU(), nn.Linear(hypernet_embed_dim, n_agents * mixing_embed_dim)) # 超网络生成第二层权重 self.hyper_w2 nn.Sequential( nn.Linear(state_dim, hypernet_embed_dim), nn.ReLU(), nn.Linear(hypernet_embed_dim, mixing_embed_dim)) # 第二层偏置 self.hyper_b1 nn.Linear(state_dim, mixing_embed_dim) def forward(self, q_i, states): # q_i: (batch, n_agents)每个智能体当前动作的Q值 # states: (batch, state_dim) batch q_i.size(0) w1 torch.abs(self.hyper_w1(states)).view(batch, self.n_agents, self.mixing_embed_dim) w2 torch.abs(self.hyper_w2(states)).view(batch, self.mixing_embed_dim, 1) b1 self.hyper_b1(states).view(batch, 1, self.mixing_embed_dim) # 先做第一层线性变换过ReLU再做第二层 hidden torch.relu(torch.bmm(q_i.unsqueeze(1), w1) b1) q_tot torch.bmm(hidden, w2).squeeze(-1) return q_tot代码里最关键的就是torch.abs那两行。它保证权重非负单调性条件被硬编码进结构里。超网络的输出维度要精确对齐第一层权重形状是 (n_agents, mixing_embed_dim)第二层是 (mixing_embed_dim, 1)。很多复现翻车都是因为这里view之后维度对不上或者忘记对w2也做非负处理。训练时QMIX需要两个优化器吗不需要一个Adam就够了只是反向传播会同时更新智能体网络和混合网络。但target网络的拷贝要整体一起拷贝不能只拷智能体部分。这里有一个很隐蔽的细节——混合网络的输入state在SMAC这类环境里维度会非常大地图上所有单位的完整信息超网络的输入层参数随之膨胀训练变慢。我一般会把state先过一个小的线性编码层降维再接超网络效果基本不受影响。2.4 值分解家族复现的一点个人体会VDN和QMIX的复现难度都不高但不要让“能跑”等同于“有效果”。值分解算法有一个共同毛病当智能体数量多、协作结构分层时单一单调关系撑不住复杂交互。这是后续QTRAN和MAVEN要解决的问题。复现时先把VDN跑通确认环境、回放缓冲区、训练循环没有问题再往QMIX上扩展能少踩很多环境层面的坑。3. QTRAN与MAVEN放弃单调性之后的两种方向3.1 QTRAN用额外修正项逼近真实联合Q值VDN和QMIX的问题在于分解方式是受限的要么线性加和要么单调网络。QTRAN直接把这个限制拿掉思路变为我仍然算每个智能体的Qᵢ但我额外维护一个联合Q网络和一个状态值网络用它们之间的差值来修正分解误差。整件事可以理解为先用分解Q值算一个粗略的联合价值再通过全局state和联合动作的修正项把它拉回真实值。QTRAN的损失由三项构成第一项是常规TD误差作用于联合Q网络第二项是最优性损失要求对最优动作 Q_tot - ΣQᵢ 等于一个只与state有关的修正项V第三项是一致性损失要求非最优动作时的差值不小于修正项V。复现时最常见的方式是用QTRAN-alt变体它用一个baseline网络显式计算V训练稳定不少。# QTRAN-alt 的核心损失构造 def qtran_loss(q_joint, q_individual_sum, v_state, q_target, r, done, gamma, lambda_opt0.1): # q_joint: 联合Q网络输出针对当前联合动作 # q_individual_sum: 所有智能体Q值之和 # v_state: 修正项V(s)由state输入的网络输出 # 最优性损失对最优动作差应等于V(s) td_target r gamma * q_target * (1 - done) td_loss nn.MSELoss()(q_joint, td_target.detach()) # 最优性损失q_joint - (q_individual_sum - v_state) 应与TD对齐 diff q_joint - (q_individual_sum - v_state.detach()) opt_loss nn.MSELoss()(diff, td_target.detach()) # 一致性损失非最优动作的差应不小于V(s) cons_loss torch.relu(v_state - (q_joint.detach() - q_individual_sum)).mean() total_loss td_loss opt_loss lambda_opt * cons_loss return total_lossQTRAN的训练需要三个网络同时进行联合Q网络、个体Q网络、修正项V网络。它们的更新频率不一样个体Q网络更新得慢一些更稳。我在复现时用的比例大致是联合Q网络每步都更新个体Q每两步更新一次V网络每四步一次。这个比例不是出自论文是调出来的经验值你在自己的任务上可以再调。一个血泪教训QTRAN的联合Q网络非常容易过拟合到近期经验上TD loss掉得很快但胜率曲线整体跑不上去。原因往往是修正项V和联合Q网络耦合太深两个网络一起震荡。解法是在V网络的输入侧加一个低学习率或者干脆把V的梯度裁剪得比主网络更狠一些。3.2 MAVEN潜变量让智能体有多样化行为MAVEN的出发点不是“分解准不准”而是“探索够不够”。QMIX的单调性约束带来一个副作用联合Q值对个体Q的梯度方向永远一致整个策略空间被限制在单模态里。也就是说所有智能体可能只学会一种协作模式遇到需要多策略切换的任务就卡死。MAVEN的做法是引入一个潜变量z它定义了智能体的“意图”——同一个观测下z不同每个智能体的Q值就不同行为模式也随之不同。MAVEN的架构分成四块个体Q网络输入增加z、混合网络沿用QMIX的monotonic mixer、潜变量生成模块、互信息估计网络。训练时除了TD损失还多一项互信息最大化目的是保证z确实编码了行为模式差异而不是被网络忽略。# MAVEN潜变量与互信息损失的核心逻辑 def maven_loss(q_tot, q_target, r, done, gamma, z, trajectory_features, mi_net, lambda_mi0.1): td_loss nn.MSELoss()(q_tot, (r gamma * q_target * (1 - done)).detach()) # 互信息估计用轨迹特征预测z最大化log似然 # trajectory_features由GRU编码器生成代表当前历史观测 log_q_z_pred mi_net(trajectory_features, z) # 预测分布 mi_loss -log_q_z_pred.mean() # 最大化互信息 最小化负对数似然 total_loss td_loss lambda_mi * mi_loss return total_loss潜变量z的采样在训练和执行时都要进行而且同一个episode内保持不变。也就是说每个episode开始时先从标准正态分布采样一个z整个episode里所有智能体的决策都带上了这个z的信息。实验时z的维度设4到8比较合适太小表达不出多种模式太大会让个体Q网络的输入维度膨胀、训练变慢。MAVEN的坑主要在互信息估计上。如果mi_net结构太弱z就会被网络当成噪声忽略掉互信息损失趋近于零表现退化成QMIX。如果结构太强z变成了episode的分类标签而非行为特征探索能力同样起不来。复现时观察指标是同一state下不同z对应的Q值分布差异是否明显。如果差异不明显说明互信息没有真正约束住。3.3 四个算法的选型对比算法分解方式表达能力复现难度适用场景VDN线性求和最弱低同质智能体、弱协作任务QMIX单调网络中等低大多数SMAC地图的基线QTRAN联合Q修正项强高非单调协作、复杂联合行动MAVEN单调网络潜变量较强中高需要多模态策略的任务实际复现时不要只看论文效果。QTRAN论文里刷分很漂亮但复现过程中对初始化、学习率、损失权重都极其敏感新手翻车概率很高。MAVEN相对稳一些但训练时间更长因为它要同时学好个体Q、混合网络和互信息网络。我的建议是毕设必须交全部四个算法时把VDN和QMIX做成高质量的对比基线QTRAN和MAVEN作为“探索性实验”展示重点讲清楚它们的原理差异而不是硬要让后两个刷出最高分。3.4 一套清晰的源码目录结构四个算法共用同一套环境接口、回放缓冲区和训练循环差异只在模型和损失函数上。源码目录我一般这样组织agents/放单智能体网络mixers/放四个混合器learners/放损失计算逻辑runner/放经验采集config/放yaml超参。这样一个新算法只需要新增两个文件老代码一行不用动。4. 把训练跑起来环境、回放与基础超参4.1 环境选型SMAC与PettingZoo怎么选复现MARL算法绕不开环境。最主流的是SMACStarCraft Multi-Agent Challenge它基于星际争霸2提供多种微操对战场景QMIX、VDN论文的对比曲线都是在它上面跑出来的。但SMAC有一个现实问题安装链路长需要下载星际争霸2本体、安装地图包、再配置Python接口Windows上偶尔还会遇到路径中文导致的报错。如果你只是想把算法跑通、验证代码正确性我建议先用PettingZoo里的MPE环境比如Simple Spread做快速冒烟测试数据维度低、一个episode几秒就跑完等确认训练循环没有问题再切到SMAC出正式实验曲线。PyTorch版本建议选用Python 3.8到3.10之间的环境torch装2.x版本即可不要用太老的1.x。numpy版本和torch的匹配是个隐形的坑高版本numpy1.24以上会把np.float移除某些老代码会直接报错。项目依赖里最好锁死numpy版本不要用最新的。总之环境选型的原则是先用轻量环境调通代码再用重量环境出结果而不是一上来就和SMAC死磕。4.2 训练主循环与经验回放值分解算法训练时经验回放以episode为单位最容易实现。一个完整的episode包含每个智能体每一步的观测、动作、奖励和终止标志整段存入缓冲区。采样时随机抽若干个完整episode沿时间维度做截断反传。GRU的隐状态要在episode边界清零批量训练时序列长度一致的episode才能组成一个张量。class ReplayBuffer: def __init__(self, capacity5000): self.buffer deque(maxlencapacity) def push(self, episode_data): # episode_data: dict包含每个时间步的obs, actions, rewards, dones self.buffer.append(episode_data) def sample(self, batch_size, episode_len): episodes random.sample(self.buffer, batch_size) # 批量整理为张量shape: (batch, episode_len, n_agents, obs_dim) obs_batch torch.stack([torch.stack([torch.tensor(t[obs]) for t in ep]) for ep in episodes]) return obs_batch def train_one_batch(learner, batch, agent_net, mixer_net, target_mixer_net): q_i, _ agent_net(batch[obs], batch[hidden]) # 当前个体Q q_tot mixer_net(q_i, batch[state]) with torch.no_grad(): next_q_i, _ target_agent_net(batch[next_obs], batch[next_hidden]) next_q_tot target_mixer_net(next_q_i, batch[next_state]) target batch[rewards] gamma * next_q_tot.max(dim-1, keepdimTrue).values * (1 - batch[dones]) loss nn.MSELoss()(q_tot, target) optimizer.zero_grad() loss.backward() optimizer.step()这段代码有几个关键参数要留意。capacity控制回放缓冲区大小SMAC场景下5000个episode够用太大占用内存暴涨太小样本多样性不足。gamma设0.99是标配。episode_len必须取所有采样episode中的最小长度短episode补零并计算mask防止padding影响梯度。最容易被忽略的是batch[hidden]——训练时GRU的初始隐状态在完整episode上必须为零向量不能复用上一步的残留值否则梯度会串到别的任务上。4.3 一组能复现出曲线的基础超参数超参数推荐值调整方向学习率5e-4震荡时降到3e-4收敛慢时提到1e-3批大小32显存紧张时降到16回放容量5000 episodes样本多样性差时增大目标网络更新间隔200 episodes不稳定时缩短到100ε-greedy衰减1.0→0.05探索不足时放慢到0.1GRU隐层维度64任务复杂时加到128梯度裁剪10NaN时降到5混合网络嵌入维度32表达力不足时加到64这些超参不是玄学它们背后有明确的约束关系。学习率和批大小共同决定了每一步更新的方差目标网络更新间隔太短会让训练变成“在移动靶上练枪”ε-greedy衰减窗口要和episode的平均长度匹配——如果环境平均每一个episode有200步那ε从1.0衰减到0.05需要至少5000个episode才合理。判断训练是否正常不要只看loss曲线要同时看两个信号TD loss在稳定下降但不是直线下坠以及每N个episode后跑一次固定seed的评估胜率在震荡中抬升。5. 复现过程中必须记录的五类坑5.1 TD loss下降但胜率纹丝不动现象训练日志里loss掉得很漂亮像模像样地收敛了但eval胜率一直趴在0附近。原因最常见的是行为策略和目标策略脱钩。训练时目标值用ε-greedy采样的动作计算而评估用argmax动作两者在探索率还很高的时候偏差巨大。另外如果SMAC地图里某些动作具有长期后果比如过早交技能单步TD误差小但策略整体是错的。解决先确认评估用的是target网络还是当前网络必须是当前网络且关闭探索。再把loss计算中的动作选择改成double DQN方式——当前网络选动作、target网络给值。5.2 混合网络梯度爆炸或出现NaN现象训练到中途loss突然变为NaN或者Q_tot输出值到几百上千。原因QMIX超网络的输入state没有做标准化个别维度数值极大导致hyper_w1的输出权重爆炸softplus或relu的激活对极端输入也会产生数值溢出。解决对state做z-score标准化再进超网络同时把梯度裁剪设到10以下。如果NaN出现在第一次优化step优先检查混合网络输出层的初始化和state的scale和模型结构无关。5.3 同一份代码换机器后结果差异大现象在自己电脑上跑通的效果提交到服务器或换一台电脑后胜率掉了20个点。原因浮点数运算在不同GPU/CPU架构上结果不完全一致更重要的是随机种子没有完整固定——torch、numpy、随机库各写各的seed。解决初始化时同时设torch.manual_seed(seed)、np.random.seed(seed)、random.seed(seed)环境自身的seed也要重置。如果用了cudnn把torch.backends.cudnn.deterministic设为True。所有实验记录里必须包含seed和硬件信息否则两个结果没法比较。5.4 GRU隐状态跨episode串用现象训练曲线持续震荡换了好几个超参数都不解决问题。原因批量训练时不同episode长度不一padding的隐状态没有重置为零导致梯度从padding部分反传污染了真实样本的更新。这是复现MARL时特别容易翻车的地方。解决在缓冲区中存储每个episode的实际长度训练时对超过实际长度的部分用mask抹掉梯度或者直接只取真实长度为batch对齐标准短episode不补零而是单独放入另一个batch。5.5 SMAC版本与地图难度导致结果无法对标现象论文里QMIX在某个地图上胜率90%你复现只能到70%怎么调都上不去。原因SMAC的多个版本smacv1、smacv2地图设置不同星际争霸版本更新后单位属性也会变。同一张地图名在不同版本中难度差异极大而论文大多不标注精确版本。解决在项目requirements里锁死环境版本比如明确指定用哪个版本的SMAC。评估时用官方推荐的difficulty7再和基线对比。如果只想验证算法实现正确性不要跟论文数字对齐跟自己跑在相同环境下的基线算法对齐。5.6 目标网络和评估时机设置不当现象胜率曲线上串下跳每100个episode的评估结果差异很大。原因评估时用了非固定的随机seed或者评估回合数太少——32回合和5回合的方差完全不是一个量级。解决评估固定用3个seeds每个seed跑20局记录平均胜率和标准差。目标网络更新间隔同样影响曲线平滑度200个episode更新一次会让曲线呈阶梯状你看到的“跳变”其实不是策略进步而是目标网络刚被替换。把间隔设成训练步数而非episode数更符合训练分布。6. 从能跑到能答辩评估指标与可视化代码跑到能出曲线只完成了60%。剩下40%在于你的结果可解释、可对比、可展示。最直接的评估指标是三件套胜率win rate、平均回报average return和episode长度。SMAC任务中胜率是主指标平均回报要同时看趋势episode长度能够反映策略执行效率——同样是赢100步赢和150步赢的含金量不同。可视化从两个维度展开。第一是训练曲线本身用TensorBoard记录TD loss、Q值分布、胜率曲线和ε值衰减四张图放在一张画布上答辩时能直接解释“训练是否健康”。第二是策略行为可视化比如在MPE的Spread任务里把每个智能体的位置轨迹画出来你会直观看到协作策略有没有形成。对QMIX而言还有一个很有说服力的可视化在固定state下画出不同个体Q值组合对应的Q_tot等高线图展示单调性约束的真实形态。答辩时最容易扣分的问题是“你怎么证明你的复现是正确的”。我的建议是做一个baseline对比表同一个环境、同一个seed集合把VDN、QMIX、QTRAN、MAVEN四条曲线和一个随机策略拉在一起再加上一个你实现的改进点。改进点不需要很大比如给QMIX加入Double DQN、给MAVEN调整互信息loss权重都能成为你自己贡献的切入点。最后说一个我自己的习惯每次改完一个模块我会先在固定seed下跑50个episode快速冒烟测试确认loss没有发散再继续调下一处。这个过程看起来很慢但避免了很多次“训练了一晚上结果发现环境接口写错”的悲剧。复现算法不是炼丹每一个参数、每一个网络层都应该在你的掌控之内。希望帮到你。本文还有配套的精品资源点击获取