简介:基于Python实现的多智能体强化学习算法源码包,完整覆盖VDN、QMIX、QTRAN、QPLEX四种价值分解类算法,适用于合作环境下的多智能体决策任务,面向需要完成毕业设计、期末大作业或系统学习多智能体强化学习的开发者与学生。资源共131个文件,压缩包大小约9.05MB。其中36个py文件为算法核心源码,代码附有逐行注释,能够帮助新手理解多智能体协作训练与值函数分解的完整流程;29个npy和25个pkl文件分别存储模型权重与经验回放数据,可直接加载进行推理或继续训练;18个png图像展示训练曲线和网络结构,4个pdf文档包含实现说明,另有8个TensorBoard日志文件供可视化分析。项目由个人手打完成,作者自评98分并获得导师认可,部署门槛低,模型文件齐全,省去自行训练的耗时,适合作为高分项目参考。目前已有482人浏览学习,适合需要快速复用代码或深入研究算法细节的读者。
1. 多智能体强化学习卡在协作上:VDN、QMIX、QTRAN、QPLEX 四种价值分解怎么选,模型文件怎么用
让两个智能体学合作搬运,给每个 agent 单独接一个 DQN,训练不到半天就能见识到什么叫互相拉扯:赢一局输一局,奖励曲线像心电图。这是我带过多智能体强化学习项目后最常见的起手式,也是必翻的坑。单智能体策略在队友也在变的环境里根本站不住,真正能解决问题的方向,绕不开 VDN、QMIX、QTRAN、QPLEX 这条价值分解线。这四个算法核心思路都是把联合动作的 Q 值拆成每个智能体的效用,中心化训练、去中心化执行。配套的 Python 源码和对应模型文件,让你不必从论文公式重新推网络,而是把时间花在跑实验、调参数、验证 checkpoint 上。这篇笔记适合已经会基础 RL、正被稀疏奖励和信用分配折磨的工程师和学生。
2. 用 IGM 原则看懂价值分解:VDN 求和、QMIX 单调、QTRAN 放松、QPLEX 优势分解的差异与选型
价值分解这一族算法解决的核心问题,用一句大白话说就是:训练时我知道全局的输赢,但执行时每个智能体只能看到自己那点观测,怎么让它在局部信息下做出对全局最有利的动作。VDN、QMIX、QTRAN、QPLEX 四个算法都是在回答这个问题,只是回答的严格程度和网络结构不一样。
2.1 为什么四个算法要放在一起看:中心化训练与去中心化执行
多智能体强化学习和单智能体最大的区别在于环境对每个 agent 来说都是非平稳的。你训练时队友的策略在变,对手的策略也在变,单独优化自己的 Q 函数等于在追一个移动靶。价值分解的做法是:训练阶段用一个中心化的网络去拟合联合 Q 值,这个联合 Q 值可以访问全局状态;执行阶段再把联合 Q 分解到每个智能体,让每个 agent 只用自己的局部观测就能选出动作。
这里面有一条主线叫 IGM,个体全局最大值原则。它说的是:如果每个智能体都按自己的局部 Q 取 argmax,组合起来得到的联合动作,恰好等于全局 Q 取 argmax 得到的联合动作,那这个分解就是一致的。VDN 做到了这一条,QMIX 在限定条件下做到了,QTRAN 和 QPLEX 则试图在更宽的条件里做到。拿到一套源码后,我不建议立刻跑训练,第一件事是把四种模型的局部 Q 和全局 Q 打印出来,确认你手上这份代码把分解逻辑写在哪一层。下面这个最小脚本就是干这个的:
import torch from algo.qmix import QMIX # 假设源码的 algo 目录下有四个算法文件 model = QMIX(model_cfg) model.eval() # batch=2, agent=4, obs_dim=8,这是对齐环境配置后的占位输入 obs = torch.randn(2, 4, 8) local_q, q_tot = model(obs) print("local_q:", local_q.shape, "q_tot:", q_tot.shape) # 如果算法实现正确,局部 argmax 组合后应该和全局 argmax 一致 argmax_global = q_tot.argmax(dim=-1) argmax_local = local_q.argmax(dim=-1)这段代码的意义是验证模型的前向通路没搭错。local_q的形状一般是[batch, n_agents, n_actions],q_tot是[batch, n_actions]。如果运行时报维度不匹配,多半是 config 里的n_agents、obs_dim、n_actions和模型初始化参数没对上。四套算法里只有 VDN 的q_tot可以直接用sum算出来,其他三个都需要走各自的混合网络,所以这个打印动作能帮你快速定位分解逻辑的位置。
2.2 四种算法的分解形式与实现重点
VDN 是最朴素的做法,联合 Q 直接等于每个智能体局部 Q 的和。它不需要混合网络,梯度可以非常干净地回流到每个 agent,缺点是完全没有考虑智能体之间的非线性交互,适合作为 baseline 先跑通。
QMIX 在 VDN 基础上加了单调性约束,使用一个混合网络把局部 Q 组合成联合 Q,网络权重由超网络根据全局状态生成。实现上最关键的是保证混合网络的权重非负,否则单调性会被破坏。常见的做法有两种,一种是对超网络输出取绝对值,另一种是用 ReLU 激活。我一般会用softplus或者绝对值,ReLU 会把负值直接截断成 0,某些初始化下容易让梯度真死掉。QMIX 对纯协作任务非常稳,是目前工程里用得最多的。
QTRAN 走的是另一条路,不强制单调性,而是引入全局联合 Q、局部 Q 和分解 Q 三组目标,通过因子损失让分解尽可能接近真实的联合 Q。它理论上比 QMIX 表达能力强,但训练时多出来的 loss 项对 reward scale 非常敏感,同样的超参换一个环境就要重新调。
QPLEX 是把联合 Q 拆成所有智能体共享的“共同 Q”加上每个智能体的优势函数,优势那一项用非负权重组合,既保留了表达力,又满足 IGM。它的网络结构更复杂,训练稳定性比 QTRAN 好,但比 QMIX 敏感,适合最后拿来冲效果。
| 算法 | 联合 Q 建模方式 | 核心约束 | 实现重点 |
|---|---|---|---|
| VDN | 局部 Q 求和 | 严格满足 IGM | 无需混合网络 |
| QMIX | 混合网络加权 | 非负权重下的单调约束 | 超网络与权重非负处理 |
| QTRAN | 三类 Q 联合优化 | 放松单调性 | 因子损失与超参平衡 |
| QPLEX | 共同 Q + 优势函数 | 优势权重非负 | 优势头与 attention 结构 |
2.3 选型三问:手里的任务到底该跑哪个算法
选算法之前先问三个问题。第一,任务是纯协作还是混合动机?纯协作直接考虑 QMIX 或 QPLEX,混合动机环境下 QTRAN 往往比 QMIX 更稳。第二,智能体是否同构?异构智能体如果用 VDN,求和分解会掩盖个体差异,训练容易偏向某几个 agent。第三,全局状态的信息量够不够?QMIX 的超网络要吃全局状态,如果环境只给局部观测,超网络能利用的信息有限,优势反而变成劣势。
我习惯的起手顺序是:先用 VDN 跑通训练链路和评估脚本,拿到一个最低指标,再切 QMIX。QMIX 跑出稳定收益后,如果发现收益上不去,怀疑是单调性限制太强,再上 QTRAN 或 QPLEX。不要一上来就追最复杂的算法,把 VDN 都跑不出合理指标的实验,换到 QPLEX 只会更难排查。这几个算法之间的切换成本其实很低,后续第 3 章会看到,大多数实现里只改 config 里的algo字段就能换算法。
3. 跑通 Python 源码的最小路径:环境、命令与核心代码走读
源码包拿到手,第一步不是读代码,是让项目先跑起来。很多多智能体强化学习项目跑不起来不是因为算法难,而是环境依赖、Python 版本和配置项三方打架。下面这条最小路径是我在多个项目里反复用的,照着走能省掉大半天的环境折腾。
3.1 环境准备:Python 版本、依赖与目录结构
多智能体强化学习的代码大多基于 PyTorch,Python 版本建议卡在 3.8 到 3.10 之间,太新的 Python 版本反而容易碰到某些老依赖没跟上。用 conda 建独立环境是最省心的做法,避免和系统 Python 混在一起:
conda create -n marl python=3.9 -y conda activate marl pip install torch==2.0.1 numpy==1.24.3 gym==0.21.0 pyyaml tensorboard参数说明:torch版本不必追求最新,很多源码里的 checkpoint 是旧版 torch 存的,版本跨太大加载时会报权重兼容问题;gym用 0.21 而不是 1.x,是因为 SMAC 这类多智能体环境的 wrapper 大多按 0.21 的 API 写,换了新版 gym 环境接口直接崩。这里参考了 python 安装教程里最常见的一条忠告:虚拟环境隔离永远比硬装进系统干净。
装完依赖后先确认目录结构,典型源码包会长这样:
marl_value_decomp/ ├── configs/ # 各算法的 yaml 超参文件 ├── algo/ # vdn.py, qmix.py, qtran.py, qplex.py ├── envs/ # 环境封装与 wrapper ├── runner.py # 训练主循环 ├── train.py # 入口脚本 └── checkpoints/ # 模型文件目录重点是先找到checkpoints目录在哪里。如果源码包里没有这个目录,说明训练脚本会自己创建,但评估脚本大概率需要你手动指定路径,这是一个很容易踩的空指针。
3.2 用一条命令跑通训练:以 QMIX 为例
环境就绪后,先跑一个最短训练命令。以 QMIX 在 SMAC 的 3m 地图上为例,典型命令是:
python train.py --algo qmix --config configs/qmix_smac.yaml --seed 42 --device cuda:0--algo决定加载哪个算法文件,--config决定网络结构和训练超参,--seed让实验可复现,--device指定显卡。第一次跑建议加--device cpu,先用小规模环境确认逻辑通,再切 GPU。如果命令直接报 module not found,先检查是否在虚拟环境里执行,而不是看代码。
对应的配置文件长这样:
algo: qmix gamma: 0.99 lr: 5e-4 buffer_size: 5000 batch_size: 32 update_interval: 100 target_update_interval: 200 double_q: true non_negative_weights: softplus epsilon_start: 1.0 epsilon_end: 0.05 epsilon_decay_steps: 50000gamma是折扣因子,多智能体任务里不要设太低,低于 0.9 会让远期协作收益根本传不回来。non_negative_weights选softplus而不是relu,是为了避免混合网络权重被截断成 0。update_interval和target_update_interval控制的是学习频率,前者是每多少步采样一个 batch 更新,后者是每多少步把 online 网络参数同步到 target 网络。这两个值直接影响训练的稳定性和速度,QMIX 对 target 更新频率比 DQN 敏感,一般取 update 间隔的 2 到 4 倍。
3.3 训练循环与价值分解核心组件的代码走读
训练主循环本身不复杂,采样、存 buffer、更新网络三步循环。价值分解的差异集中在计算q_tot那几行代码上。以 QMIX 为例,核心简化代码是这样的:
# algo/qmix.py 中计算联合 Q 的核心逻辑 def forward(self, states, hidden_states): # 每个 agent 用自己的 RNN 编码局部观测,输出局部 Q local_q = self.agent_net(hidden_states) # [batch, agents, actions] # 超网络根据全局状态生成混合网络权重 w1 = torch.abs(self.hyper_w1(states)) # 非负权重约束 b1 = self.hyper_b1(states) q_tot = torch.einsum('bta,btia->bti', local_q, w1) + b1 return local_q, q_tot逻辑说明:agent_net是每个智能体共享的 RNN 网络,先产出局部 Q;hyper_w1是全连接网络,输入全局状态,输出混合网络的权重矩阵。权重加abs是 QMIX 单调性的实现关键,少这行代码 QMIX 就退化成普通的非线性混合,IGM 不再被保证。einsum的作用是把每个 agent 的局部 Q 按权重组合成联合 Q,这一步替代了 VDN 里的sum。
VDN 的核心代码更简单,就是一个q_tot = local_q.sum(dim=-2)。QTRAN 和 QPLEX 的文件里会多出额外的网络模块,比如 QTRAN 的联合 Q 网络、QPLEX 的优势分解头,但采样和 buffer 的逻辑完全复用。所以从 QMIX 切到 QPLEX,通常只需要改 config 里的algo字段和对应的 yaml 文件,不用动 runner。
4. 模型文件的正确用法:checkpoint 结构、加载验证与算法间迁移
源码包配套的模型文件是很多人会忽略但最容易出问题的地方。训练好的模型文件不是右键加载就能用的,它里面存了什么、对应哪个环境、和哪份 config 绑定,都决定你能不能复现出作者说的效果。
4.1 模型文件里不止权重:一个完整的 checkpoint 包含什么
见过太多人拿到模型文件直接torch.load然后load_state_dict,报 key mismatch 后一脸茫然。多智能体强化学习的 checkpoint 通常需要同时保存算法标识、环境参数、网络参数和训练状态。一份规范的模型文件夹里应该有这几样东西:
checkpoints/qmix_3m_20240101/ ├── q_network.pt # 网络权重 ├── config.json # 训练时的完整配置 ├── env_info.json # 观测维度、智能体数量、动作维度 └── eval_log.json # 评估记录config.json里至少包含这些字段:
{ "algo": "qmix", "env": "smac.3m", "obs_dim": 24, "n_agents": 3, "n_actions": 10, "non_negative_weights": "softplus", "double_q": true, "seed": 42, "trained_steps": 200000 }模型文件下载后如果解压出来只有一个.pt文件,没有 config 和 env_info,这个模型基本就是个黑匣子,只能跑演示不能信任。检验模型文件是否完整,用一条命令就能看到权重内部 key 结构:
python -c "import torch; sd = torch.load('q_network.pt', map_location='cpu'); print(list(sd.keys())[:20])"输出里如果既有agent_net开头的 key,又有hyper_w1开头的 key,说明这是 QMIX 或 QPLEX 的完整模型。如果只有agent_net,那就是只存了局部 Q 网络,评估脚本需要自己重建混合网络。
4.2 加载模型做评估与可视化回放
加载模型做评估的代码不难,但有两个细节必须注意:一是model.eval()必须调用,二是 map_location 要显式指定。代码如下:
import json, torch with open(model_dir / "config.json") as f: cfg = json.load(f) agent = build_model(cfg["algo"], cfg) state_dict = torch.load(model_dir / "q_network.pt", map_location="cpu") agent.load_state_dict(state_dict) agent.eval() # 评估时关闭探索,epsilon 必须置 0 agent.set_epsilon(0.0) # 固定 seed 跑 20 个 episode,记录平均回报 for seed in [42, 43, 44]: set_seed(seed) returns = run_episodes(agent, num_episodes=20) print(f"seed {seed}, mean return: {np.mean(returns):.2f}")逻辑说明:map_location="cpu"是为了避免 GPU 和 CPU 环境不一致时加载报错;agent.eval()会关闭 RNN 里可能存在的 dropout 层,多智能体代码里虽然有 dropout 用得少,但漏了这一行,评估结果会被随机性污染。评估时epsilon必须显式设 0,否则训练时设置的探索率会继续生效,把评估指标拉低。固定多个 seed 各跑 20 个 episode 取均值,是避免单次结果波动误导判断的底线做法。
4.3 算法之间的模型文件能不能互相迁移
严格来说,VDN 的权重不能直接加载到 QMIX 上,因为 QMIX 多了混合网络,state_dict 里的 key 对不上。但有一种工程上很实用的迁移初始化:把 VDN 训练好的agent_net参数迁移到 QMIX 的agent_net,混合网络保持随机初始化。这样相当于让 QMIX 站在 VDN 的肩上开始训练,能省掉前期探索的时间:
vdn_state = torch.load("vdn.pt", map_location="cpu") qmix_state = torch.load("qmix.pt", map_location="cpu") # 只迁移共享的 agent 网络层,跳过混合网络 shared = { k: v for k, v in vdn_state.items() if k in qmix_state and "hyper" not in k } qmix_state.update(shared) agent.load_state_dict(qmix_state, strict=False)strict=False允许缺失部分 key,只加载重合的部分。但用完这个参数必须打印missing_keys确认漏掉的是预期中的混合网络,而不是 agent 网络本身。迁移初始化只适合同质智能体任务,异构智能体之间共享参数本身就说不通。如果模型文件在加载时报错,优先检查 config 里的algo字段和文件是否匹配,常见的comfyui 下载模型文件失败类问题,往往也是校验信息缺失导致的,不是网络问题。
5. 训练与调参避坑:五个最容易复现的翻车现场
价值分解算法的代码跑起来容易,稳定复现难。下面这五个问题是我在多个项目里反复见过的,每个都按现象、原因、解决的顺序写,方便你在训练曲线出问题时直接对照排查。
5.1 训练曲线一条直线:稀疏奖励下的 Q 值死区
现象:训练跑了十几万步,episode return 始终在初始值附近,奖励曲线像一条直线。检查 replay buffer 里的数据会发现,绝大多数 transition 的 reward 都是 0。
原因:多智能体协作任务普遍稀疏奖励,正样本在 buffer 里占比可能低于 1%。Q 学习对这种分布极度敏感,正样本被海量零奖励样本稀释,Q 值根本学不起来。
解决:先统计 buffer 里正样本比例,低于 1% 就要做 reward shaping 或优先回放。统计代码很简单:
rewards = np.array([t["reward"] for t in replay_buffer]) print("正样本占比:", np.mean(rewards > 0))如果占比过低,优先考虑给环境加辅助奖励,而不是调大学习率。学习率调大只会让 Q 值震荡更厉害,解决不了稀疏信号的问题。少数实现里会直接加大正样本的采样权重,效果也明显,但要注意别破坏原来的分布。
5.2 Q 值突然崩坏:RNN 时序与 buffer 采样错位
现象:训练前几千步曲线正常,某次更新后 loss 突然 spike,Q 值乱跳,之后很难恢复。重跑一遍发现崩坏的位置还随机。
原因:价值分解算法里 agent 网络几乎都用 RNN 处理局部观测历史,RNN 对时序顺序极度敏感。如果 replay buffer 按 step 存、按 step 采样,采出来的一条样本里 hidden state 对应的历史和新输入的观测不是同一局游戏,RNN 的输出就会变成噪声。
解决:必须按 episode 采样,并且每个 episode 的 RNN 初始 hidden 要从零开始。伪代码对比:
# 错误做法:随机采样单步 batch = random.sample(buffer, 32) # 正确做法:按整局采样,重新跑一遍 RNN episodes = buffer.sample_episodes(32) hidden = torch.zeros(32, n_agents, hidden_dim) for t in range(episode_len): local_q, hidden = agent(episodes.obs[:, t], hidden)调整采样逻辑后,loss spike 的问题通常会直接消失。如果代码库原本就是按 episode 存的,注意不要在采样时做了截断,截断了同样会破坏 RNN 的时序依赖。
5.3 加载模型文件报 missing key:算法和配置对不上
现象:load_state_dict报 missing keys,提示缺了hyper_w1、hyper_b1这类混合网络权重。或者反过来,多出来一堆用不到的 key。
原因:最常见的是 config 里algo字段和模型文件实际算法不一致,比如 yaml 里写的是qmix,但拿到的模型文件是 QTRAN 训练出来的。另一个常见原因是源码保存的是整个 learner 对象而不是 agent 网络,从 learner 里取agent字段才能拿到真正的权重。
解决:先用第 4.1 节的方法打印 state_dict 的 key 列表,人工确认算法类型。再用strict=False加载并打印缺失项:
result = agent.load_state_dict(state_dict, strict=False) print("missing:", result.missing_keys) print("unexpected:", result.unexpected_keys)missing_keys里有hyper开头的是正常的,说明你加载的是 VDN 或部分迁移的权重;如果agent_net也缺失,说明模型文件本身不完整,重新下载校验文件再跑。
5.4 QMIX 的非负权重把梯度“杀”了
现象:loss 在正常下降,但 episode return 纹丝不动,打印q_tot发现它长时间不变或变化幅度极小。很多实现里把超网络输出接 ReLU,结果大量神经元输出为 0,梯度经过 0 之后彻底消失。
原因:QMIX 的单调性依赖非负权重,但非负不是只有 ReLU 一种实现方式。ReLU 在负区间梯度为 0,混合网络一旦初始化到负区间,权重就永远卡死在 0。这个现象不会报错,训练流程看起来完全正常,最迷惑人。
解决:把non_negative_weights从relu改成softplus,或者对超网络输出取绝对值。改完配置后重新训练,同时打印混合网络每层权重的均值和零值占比,如果零值占比超过 30%,继续检查是否有初始化问题。
5.5 同质智能体顺序打乱导致评估反复横跳
现象:同样的配置、同样的 seed,评估两次结果差一大截。表面看是训练不稳定,实际上可能只是评估时智能体的执行顺序变了。
原因:很多实现里智能体共享参数,但动作选择时按 agent index 从 Q 矩阵取值。如果评估脚本在 reset 环境时把 agent 顺序打乱,或者用了 Python 的 set 遍历智能体,同一个模型会得到不同结果。同质智能体理论上顺序无关,但代码实现里如果依赖 index,顺序一变结果就变。
解决:评估前固定 agent 顺序,所有 episode 都用同一个 index 排列。评估时关闭探索、固定 seed、跑 20 个 episode 取中位数而不是均值,防止个别极端 episode 拉高指标。我一般在评估脚本开头加一行assert sorted(agent_ids) == agent_ids,从源头杜绝乱序。
6. 用固定 seed 和基准场景验证四个算法:进阶评估与检查清单
代码能跑、模型能存、坑也踩得差不多之后,最后一步是把四个算法放在同一套基准下做横向验证,判断你手上这套源码的实现质量到底如何。
6.1 用 toy case 与 SMAC 3m 做四算法快速对比
建议先用一个能秒跑的 toy case 验证四个算法都能学到东西,再上 SMAC 这种重型环境。用同一份 config 基准,只切--algo参数,固定 seed 跑完四个算法:
python train.py --algo vdn --config configs/toy.yaml --seed 42 python train.py --algo qmix --config configs/toy.yaml --seed 42 python train.py --algo qtran --config configs/toy.yaml --seed 42 python train.py --algo qplex --config configs/toy.yaml --seed 42--seed 42必须严格一致,这样四个算法的差异才完全来自算法本身,而不是初始化运气。这一步很像 python 量化交易策略回测里的规矩:同种子、同数据、同评价函数,结论才可信。如果 toy case 上 VDN 都跑不出明显收益,先怀疑环境 wrapper 而不是算法实现。
6.2 用效用可视化发现“假收敛”
很多训练曲线看着收敛了,实际是假收敛。我习惯在评估时回放一个 episode,把每个 agent 的局部 Q 和联合 Q 一起打印出来。如果某个 agent 的局部 Q 长期为负,但联合 Q 仍然很高,说明信用分配没有真正学好,个体效用被混合网络强行拉高。这种情况在 QMIX 的高维状态场景里尤其常见。结合 5.4 检查混合网络权重,如果某些 agent 对应的权重持续接近 0,说明这个智能体在决策中基本被忽略了,需要检查是否动作空间或者观测封装出了问题。
6.3 我固定在工程项目里的三条检查习惯
第一,模型文件夹里必须同时放config.yaml、seed.txt和eval_log.json,否则三个月后回来看模型,你自己都不知道它是谁。第二,只要改过算法、环境或观测维度三者中的任何一个,模型文件名必须重新命名,格式固定为{algo}_{env}_{date}_{seed},我吃过不命名直接覆盖旧模型的亏,训练一天的结果被第二天的实验冲掉,连后悔药都没得吃。第三,上线前用固定 seed 跑 20 个 episode 验证,确认评估结果稳定不是因为某几局探索噪声碰巧赢了。这些习惯都是踩坑换来的,希望帮到你。
本文还有配套的精品资源,点击获取