多智能体强化学习算法源码包:VDN、QMIX、QTRAN、QPLEX一次跑通

发布时间:2026/11/2 14:42:40
多智能体强化学习算法源码包:VDN、QMIX、QTRAN、QPLEX一次跑通

简介:一套基于 Python 的多智能体强化学习算法实现合集,覆盖 VDN、QMIX、QTRAN、QPLEX 四种经典价值分解方法,并配有对应模型文件与详细代码注释。适合毕业设计、期末大作业或课程设计参考,也适合希望从代码层面入门多智能体强化学习的读者。包内共 131 个文件,以 36 个 py 源代码、29 个 npy 与 25 个 pkl 模型数据为主,另含训练过程图片、事件日志及说明文档,可辅助理解算法收敛趋势与网络结构。压缩包约 9.05MB,轻量易部署,下载后稍作环境配置即可运行。已有 482 人学习使用。作者自述为个人手打高分项目并获导师认可,代码注释清晰,新手也能跟随阅读;目录按算法与训练结果组织,除可直接运行的算法实现外,还包含可视化图表与训练日志,便于对照分析不同值分解方式在合作环境中的表现。

1. 多智能体强化学习算法源码包:VDN、QMIX、QTRAN、QPLEX 一次跑通

多智能体强化学习入门和高分毕设之间,往往就差一份能同时跑通 VDN、QMIX、QTRAN、QPLEX 的 Python 源码。这套基于 Python 实现的多智能体强化学习算法源码包,把四个经典值分解算法放进同一个工程,代码带注释,模型文件和 TensorBoard 事件日志一并附上,解压后装好依赖就能直接训练和对比,不用再对着论文公式从零实现,也不必担心算法写错了自己还看不出来。

对毕业设计、期末大作业来说,最大的成本不是看懂公式,而是把环境、网络结构和训练循环打磨到能稳定产出曲线。这套资源把整条链路备齐,新手能顺着注释理解每个文件,熟手能直接拿它当四算法对比的基准框架,改环境、加算法都顺手。

我拆这个包最直观的感受是,它是真正训练过多轮的完整工程,不是半成品。后面就按“原理 → 跑通 → 验证”的顺序讲,每一步都落到具体文件和参数上。

2. 值分解算法原理:VDN、QMIX、QTRAN、QPLEX 的边界在哪里

2.1 从联合动作空间爆炸到 IGM 原理

多智能体强化学习第一个绕不开的问题是动作空间爆炸。假设有 N 个智能体,每个智能体有 m 个离散动作,联合动作空间就是 m 的 N 次方。像 SMAC 这种 5v5 战斗场景,每个智能体几十个动作,联合起来就是天文数字。直接对联合动作求 Q 值,训练量和个人电脑的显存都扛不住。

于是有了一个折中:不显式建立联合动作表,而是保留每个智能体自己的 Q 网络,再想办法把个体 Q 值组合成联合 Q 值。这个组合过程有讲究,不是随便加一加就行。为了保证“最优联合动作 = 最优个体动作的集合”,需要满足 IGM 条件(Individual-Global-Max):每个智能体各自取 argmax 得到的动作组合,恰好是整体取 argmax 的最优联合动作。

VDN、QMIX、QTRAN、QPLEX 四个算法的本质区别,就在于用不同方式逼近甚至部分放松这个条件。VDN 和 QMIX 严格保证 IGM,代价是对联合动作值的表达能力有限制;QTRAN 和 QPLEX 则在理论和实现上把约束重构,换来更强的拟合能力。

这部分概念对应到代码里,就是每个算法文件里总能看到的那行注释:joint Q = f(q1, q2, ..., qN)。理解了这个约束,再看训练循环里的 loss 公式,就会清楚为什么有的算法损失函数多一项、有的多两项。

2.2 VDN 与 QMIX:加性分解和单调网络约束

VDN(Value Decomposition Networks)的思想最简单:联合 Q 值等于所有个体 Q 值直接相加,Q_tot = Σ Q_i。个体网络各自输出自己的 Q 值,最后过一个求和层。结构上只是一个加和操作,实现就是十几行代码的事。

QMIX 的改进在于不再用线性求和,而是用一个混合网络(Mixing Network)去拟合联合 Q 值和个体 Q 值的关系。关键在于限制混合网络的权重全部非负,这样联合 Q 值对任意一个个体 Q 值都是单调递增的,从而满足 IGM 条件。混合网络还能把全局状态 s 作为额外输入,让系统感知到个体观测不到的信息。

对比维度VDNQMIX
分解方式线性求和非线性混合网络
权重约束无非负权重约束
表达能力强弱弱中等,受单调性限制
实现难度很低低到中
典型适用场景简单协作任务大多数典型 MARL 环境

代码实现上,两者的区别主要集中在混合网络那一层。VDN 就是一个求和操作,QMIX 需要单独定义一个 Mixer 网络,里面用取绝对值的方式约束权重符号,或者在初始化后固定权重符号不变。我帮人调代码时发现,QMIX 最常见的问题是忘了把混合网络权重约束成非负,导致训练中后期曲线忽高忽低。这个坑很隐蔽,因为前期训练曲线看着正常,到后期单调性约束一失效,协作行为就开始崩。

如果你只是想让毕业设计快速出结果,VDN 是最省心的选择;但答辩时容易被导师问一句“为什么不用更 advanced 的方法”。所以大多数合理的选择是直接上 QMIX,它既有理论可讲,又在大多数地图上表现稳定。

2.3 QTRAN 与 QPLEX:约束放松与优势函数显式建模

QTRAN 的出现是为了打破 QMIX 的单调性限制。QMIX 的单调约束意味着智能体间的协作只能同向加强,但真实场景里“一个智能体更激进、另一个智能体就得更保守”这种负相关协作,QMIX 拟合不了。QTRAN 引入了辅助联合值函数的可加性约束,通过额外的约束项来逼近任意联合值函数,理论上放松了 IGM 限制。

QTRAN 的问题在于理论完善但训练敏感。它需要同时优化多个约束项,于是有三个损失函数叠加。我在实际使用中看到的现象是:收敛速度比 QMIX 慢,超参数稍微动一下就出现奖励回退。尤其那个正则系数,设大了训练被约束项带着走,设小了约束就形同虚设。

QPLEX 换了一个思路,把联合 Q 值拆成联合值函数 V 和联合优势函数 A,优势函数再按个体优势分解。这种方式既能表达复杂的负相关协作,又有比 QTRAN 更强的理论保证。实现上会出现一个明显的 Advantage Module,把联合优势映射成个体优势的加权组合。

四个算法的实现复杂度排序,按我拆代码的经验是 VDN < QMIX < QTRAN < QPLEX,参数量和训练耗时也基本是这个递增顺序。选型建议我给得很直接:环境交互简单、希望快速看效果就 QMIX;任务存在明显负相关协作且你有时间调 QTRAN 的正则系数,可以试 QTRAN;如果想让毕设显得有深度,QPLEX 是四个算法里最值得展开分析的一个,代码结构里可讲的内容也最多。

3. 把源码跑起来:MARL.iml、事件文件和训练入口逐个拆开

3.1 项目文件解构:MARL.iml、.gitignore 和 tfevents 各自的作用

刚拿到压缩包,根目录里除了算法文件夹外,有几个东西要特别注意。MARL.iml 是 IntelliJ IDEA 的模块描述文件,记录着这个工程在 IDE 里的模块名称和源码路径。用 PyCharm 打开时,IDE 识别到 iml 文件会按既定模块结构加载,省去手动标记源码目录的步骤。.gitignore 定义版本管理时忽略的目录,比如模型输出目录、运行缓存这些不该提交的文件。

events.out.tfevents.* 这串文件是训练产出的 TensorBoard 事件日志,文件名里那串数字是 Unix 时间戳。这个工程里的事件文件时间戳集中在 1659774917 到 1659853150,换算过来大约是一天内多次训练留下的。同一批事件文件放在根目录,说明这个工程在不同时间点重新跑过多轮。正因为有这些事件文件,你下载后打开 TensorBoard 就能看到已训练好的奖励曲线,这对判断“项目到底通没通”非常关键。

需要说明的是,事件文件不分框架,无论是 TensorBoard 官方写的,还是 PyTorch 的 torch.utils.tensorboard 写的,格式一致,读取方式完全一样。所以拿到项目后第一步别急着看代码,先用 TensorBoard 把已有曲线打开,能读出来就说明环境问题解决了一半。

3.2 环境准备:Python 版本与依赖安装

这类多智能体项目的依赖组合常见有三种:纯 TensorFlow、PyTorch 全家桶、或者 PyTorch 写算法加 TensorBoard 写日志。从事件文件格式看你无法直接判断底层框架,所以安装依赖前先打开源码里的 requirements.txt 或环境说明看一眼。我在多个 MARL 项目里最稳的一套组合是 python 3.9 + torch 1.13.1 + tensorboard,配合 CUDA 11.7:

conda create -n marl python=3.9 -y conda activate marl pip install torch==1.13.1 --index-url https://download.pytorch.org/whl/cu117 pip install numpy==1.23.5 tensorboard==2.12.0

逻辑说明:第一步创建独立 conda 虚拟环境,避免和系统 Python 混用导致依赖冲突;第二步按 CUDA 版本装对应 PyTorch 轮子;第三步装经过验证的 numpy 和 tensorboard。参数说明里值得注意的只有 cu117,它对应 CUDA 11.7。如果你的显卡驱动只支持更老版本,把 cu117 改成 cu116 或 cu113 即可。tensorboard 2.12 能正常读取 1.x 时代写出来的 tfevents 文件,兼容性没问题。

装完验证一下是否可用:

python -c "import torch, tensorboard; print(torch.__version__, tensorboard.__version__)"

能正常输出版本号,环境基本就绪。接下来要确认事件文件能被读取,这一步放到第五章专门讲。

注意:如果项目里的事件文件在根目录而不是 runs 子目录,启动 TensorBoard 时要把 --logdir 指到事件文件所在层,直接指到项目根目录会解析失败。

3.3 训练脚本组织:以 QMIX 为例的核心循环与参数清单

工程里四个算法的入口一般对应四个可执行脚本,比如 train_qmix.py、train_qplex.py。以 QMIX 为例,训练脚本的核心循环是下面这种组织方式。这并非这份资源的原脚本,而是我在大多数 MARL 源码里都会见到的通用结构,实际文件内容以你下载的源码为准:

# train_qmix.py 核心训练循环(结构示例) import torch from torch.utils.tensorboard import SummaryWriter from algo.qmix import QMIX def train_qmix(config): env = create_env(config['map_name']) # 环境工厂函数 agent = QMIX( obs_dim=env.obs_dim, # 单智能体观测维度 n_actions=env.n_actions, # 离散动作数 n_agents=env.n_agents, # 智能体数量 state_dim=env.state_dim, # 全局状态维度 hidden_dim=config['hidden_dim'], lr=config['lr'], gamma=config['gamma'] ) buffer = ReplayBuffer(capacity=config['buffer_capacity']) writer = SummaryWriter(log_dir=config['log_dir']) for episode in range(config['max_episodes']): obs = env.reset() ep_reward = 0.0 while not env.done: actions = agent.choose_actions(obs, epsilon=config['epsilon']) obs_, rewards, done, _ = env.step(actions) buffer.push(obs, actions, rewards, obs_, done) obs = obs_ ep_reward += sum(rewards) if len(buffer) >= config['batch_size']: loss = agent.learn(buffer.sample(config['batch_size'])) writer.add_scalar('train_loss', loss, agent.learn_step) if episode % 50 == 0: eval_reward = evaluate(agent, env, episodes=3) writer.add_scalar('eval_reward', eval_reward, episode)

逻辑说明:第 17 到 24 行是单 episode 的执行循环,多个智能体同时出动作,环境返回下一观测和一组奖励,每一步的 s、a、r、s' 都会被推进 replay buffer。第 25 到 28 行只有 buffer 里样本足够多时才开始学习,这是绝大多数强化学习项目避免“样本不足导致 Q 值震荡”的常规做法。第 30 行每 50 个 episode 评估一次,评估用的是确定性策略,不受 epsilon 干扰。

参数说明几个关键值。epsilon 通常从 1.0 线性衰减到 0.1,决定探索和利用的平衡;gamma 一般取 0.99,如果任务本身很短,0.95 也能接受;hidden_dim 在 64 到 128 之间选,SMAC 这类中等复杂环境用 64 就够,换成 attention 结构的个体网络再考虑 128。下面是一份可用的参考配置:

参数名推荐值说明
lr1e-4 ~ 5e-4超过 1e-3 极易震荡
batch_size3264 需要更大的 buffer
buffer_capacity50000再大会明显吃内存
hidden_dim64中等地图足够
max_episodes5000太少看不出收敛趋势
epsilon 衰减终点0.1训练后期保留最低探索

最容易翻车的一步是直接把 batch_size 拉到 128,然后发现 GPU 显存撑不住。后面第四章专门讲这些坑。

4. 多智能体训练避坑:事件文件混乱、不收敛与模型加载失败

4.1 一堆 tfevents 分不清哪次训练:先改 log_dir 结构

现象:项目根目录下躺着十几个 events.out.tfevents 文件,打开 TensorBoard 后所有曲线重叠在一起,根本分不清哪条是 VDN、哪条是 QPLEX。

原因:训练脚本里的 log_dir 是固定值,比如直接写死成 "logs"。每次重新训练都在同一个目录里新写事件文件,算法名和训练时间没有体现在目录结构里。这批事件文件之所以这么乱,就是因为连续多轮训练用了同一个输出目录。

解决:从一开始就把 log_dir 设计成按“算法名 + 时间戳”分层。我一般会这样写:

import time log_dir = f"runs/{config['algo']}_{time.strftime('%Y%m%d_%H%M%S')}"

逻辑说明:算法名区分不同算法,时间戳区分同算法的不同轮训练。参数说明里,%Y%m%d_%H%M%S 生成的是形如 20220806_153000 的字符串,精确到秒,同一秒内启动两次训练的概率可以忽略。启动 TensorBoard 时用tensorboard --logdir=runs指向上层目录,左侧就能按子目录折叠浏览。对于下载下来的这个资源,你也可以在本地把已有事件文件按算法名分到子目录下再启动 TensorBoard,不必重跑训练。

4.2 奖励曲线一直不涨:按这四个方向排查

现象:训练跑了 2000 个 episode,TensorBoard 上的平均奖励始终在初始值附近,loss 降得极慢甚至不降。

原因:不是所有“不收敛”都是算法写错了。常见四种:epsilon 没衰减导致一直随机探索;reward 数值范围过大导致 loss 震荡;episode 长度过长导致单条轨迹里有效信号占比太低;环境本身奖励稀疏,几百个 episode 根本积累不出有价值的样本。

解决:把排查顺序固定下来。先看 epsilon 衰减计划,确认训练后期确实掉到 0.1 以下;再打印 reward 的均值和方差,方差太大就做奖励缩放,最简单的做法是reward = reward / 20.0;然后把 episode 长度从 200 减到 50,观察曲线是否有明显反应;最后拉长总训练步数到 3000 个 episode 再下结论。实际操作中,前两步做完就能解决七成问题。

提示:奖励缩放属于“后悔药”式修复,能让 loss 波动快速变小,但别指望它在奖励定义本身有 bug 时兜底。

这里头真正称得上玄学的是 epsilon 衰减曲线。衰减太快,智能体还没探索够就进入利用阶段;衰减太慢,训练后期还在大量随机瞎逛,曲线平台期提前出现。线性衰减在中型地图上够用,但如果你换了一张奖励更稀疏的地图,建议改成指数衰减。

4.3 checkpoint 加载错位:先分清 online 和 target

现象:模型文件目录里按 episode 保存了多份权重,加载最新那份后评估分数反而不如加载早一点的版本。

原因:保存策略是“每 N 个 episode 保存一次”,但文件名里的编号和实际训练进度对不上,或者存的是 target network 的权重而不是 online network。target network 是缓慢更新的旧网络,直接用它做推理,效果自然比 online 网络差。

解决:加载 checkpoint 时先打印一下内部字段,确认存的是 online 还是 target:

# 加载检查脚本片段 import torch ckpt = torch.load('results/model_5000.pth', map_location='cpu') print(list(ckpt.keys())) # 看含 'online' 还是 'target' print("saved step:", ckpt.get('step'))

逻辑说明:ckpt.keys() 能列出字典里所有字段,一般会有 q_net、target_q_net、mixer、optimizer 等。参数说明:saved step 是训练到第多少步存的权重,可以用它判断这份 checkpoint 靠前还是靠后。如果确实只有 target 网络权重,也不必慌,大多数实现里 target 每隔固定步数会从 online 拷贝,加载后多跑几十步就能追上来。关键是别拿错的权重去评估并写进毕设结论。

4.4 显存内存爆掉:batch size 与序列截断的取舍

现象:训练一两个小时后,系统内存从 4GB 一路涨到 20GB;或者在 GPU 上直接报 CUDA out of memory。

原因:replay buffer 容量没设上限,一直在塞数据;或者 batch size 设成 128,网络输入又包含循环层展开的时间步,显存峰值直接翻倍。多智能体项目的 obs 通常是多个智能体的观测堆叠,一个样本的维度比单智能体大 N 倍,同样的 batch size 显存占用完全不是一个量级。

解决:先看 replay buffer 实现,确认是固定容量还是无边界 list。固定容量压在 50000 以下是稳妥的。如果 batch size 已经降到 32 还爆,大概率是 episode 长度太长,把单条轨迹切成固定长度片段再入库:

# 截断示例:把单条轨迹切成固定长度片段再入库 truncate_len = config['truncate_len'] # 常见取 10~30 for t in range(0, len(trajectory) - truncate_len + 1, truncate_len): buffer.push(trajectory[t:t+truncate_len])

逻辑说明:把长度为 L 的轨迹切成若干长度为 truncate_len 的片段,每段独立入 buffer,后续训练采样以片段为单位。参数说明:truncate_len 取 10 到 30 之间。这个操作会损失长程依赖信息,但多智能体协作通常在几步之内就能看出动作好坏,截断到 20 步左右对效果的影响比预期小得多。如果项目用的是循环神经网络,截断后的序列依然保留时间顺序,训练时按时间步展开即可。

5. 横向验证四个算法:TensorBoard 事件文件读取与对比实验设计

5.1 读 TensorBoard 事件文件的三种姿势

拿到这份资源后,第一个值得做的验证就是把事件文件读出来,确认训练确实发生过,而且四个算法的曲线能分开看。三种方式里我最常用第二种,它可以直接跟实验代码衔接。

第一种,启动 TensorBoard Web 界面:

tensorboard --logdir=runs --port=6006

浏览器打开 http://localhost:6006,进 scalar 面板,就能看到 loss、reward 曲线。下拉列表里所有 tag 都可以展开,包括 train_loss、eval_reward 这类指标。

第二种,用 event_accumulator 在 Python 里读:

from tensorboard.backend.event_processing.event_accumulator import EventAccumulator ea = EventAccumulator('runs/qmix/events.out.tfevents.1659774917') ea.Reload() print(ea.Tags()) # 列出所有可用的 tag train_reward = ea.Scalars('eval_reward') steps = [x.step for x in train_reward] values = [x.value for x in train_reward]

逻辑说明:EventAccumulator 是 TensorBoard 的底层读盘模块,加载目录或单个事件文件后,Tags() 返回所有被记录的指标名,Scalars() 返回的每个 x 对象拥有 step、value、wall_time 三个属性。参数说明:路径要写到事件文件那一层,如果写到 runs 目录下面一层可能解析不出来。这个方式的优点是拿到数据后可以直接用 matplotlib 重画曲线,也可以计算收敛步数和均值方差,不用从浏览器截图。

第三种,直接从事件文件名的数字读时间戳,判断哪次训练在后。文件名里的数字是 Unix 秒,比如 1659774917 和 1659853150,差值约 7.8 万秒,也就是相差大约 22 小时,说明这两批训练隔了将近一天。这个方法适合快速确认训练历史,不适合做精细分析。

5.2 对比实验必须锁死的变量

用这份源码做四算法对比,最忌讳的是不控制变量。如果让 QMIX 跑 1000 个 episode、QPLEX 跑 3000 个,最后说 QPLEX 更强,这个结论站不住脚。我通常的控制矩阵是这样的:

控制维度设定值说明
随机种子seed ∈ {1, 2, 3}每个算法跑 3 个 seed,结果取均值加减方差
训练总步数统一按 5000 个 episode 或 2e5 环境步
网络隐藏层统一hidden_dim 固定 64
环境配置统一同一张地图、同一智能体数量
评估方式统一训练结束后各跑 10 个 episode 取平均奖励

每个算法的每个 seed 单独开一个 log_dir,例如 runs/seed1/qmix、runs/seed1/qplex。这样 TensorBoard 上能用正则过滤出同一 seed 下的四条曲线,生成的对比图也干净。

血泪经验是:训练中途不要手动改任何超参数,包括学习率。一旦改了,就相当于一个新的实验,得重新起一个 log_dir。所有对比实验跑完后统一汇总均值,别边跑边改边看,这样你在论文里写的“QPLEX 比 QMIX 快了多少”才有可复现性。

5.3 曲线形状怎么读:四种典型结果与结论边界

曲线读出来后,常见四种形态。VDN 在简单环境里率先冲到不错的奖励,但后期平台期明显,因为线性分解撑不住复杂协作。QMIX 前期爬升比 VDN 慢,但中后期能超过 VDN,在单调性任务里最稳定。

如果环境中存在非单调协作,QTRAN 的曲线可能出现“先高后低”的情况:约束正则项在训练初期帮它找到不错策略,后期约束项和 Q 值更新的平衡没稳住,奖励开始回退。QPLEX 通常是四条曲线里后期最稳的,前期预热慢,但中后期保持上升趋势。

另一种值得警惕的情况是四条曲线几乎完全重合。这通常意味着环境任务太简单,四个算法的表达力差异没有被激活。这时候别急着换算法,先把环境难度调大,比如增加智能体数量或加大动作空间,让差异显现出来。这套源码里不同算法在同一环境下的模型文件差异,正是用来做这种环境灵敏度检验的。

对比实验的结论边界也要讲清楚:在一个地图上 QPLEX 优于 QMIX,只能说明在这个地图上成立。多智能体算法的表现高度依赖环境特性,你在论文里至少要跑两张以上难度不同的地图,并且报告标准差。导师真正看的不是哪个算法第一,而是你清不清楚算法适用的边界。

6. 加载预训练模型做迁移验证:自定义任务中的低成本启动技巧

拿到模型文件后,常见想法是直接在自己定义的任务环境里评估算法。但这会撞上一个现实问题:自定义环境的观测维度、动作空间定义和项目原环境大概率不一致,强行加载会报 shape mismatch,或者静默加载了一堆无用的权重。

我的做法是先在源码自带的环境里把模型文件跑出一个基线分数,再改环境,每次只改一个维度,比如只改奖励函数,不动观测和动作维度。这样做的理由是,模型在单一维度变化下的表现差异,能隔离出那个维度对算法的影响。如果同时改观测和奖励,出了问题根本分不清是环境配置错了还是算法不适合。

具体加载方式参考下面的脚本:

# 在加载模型前先验证 shape 是否匹配 import torch def load_pretrained(agent, model_path): ckpt = torch.load(model_path, map_location='cpu') # 只加载能匹配的键,避免因维度不同直接崩掉 incompatible = agent.load_state_dict(ckpt['model'], strict=False) print("missing keys:", incompatible.missing_keys) print("unexpected keys:", incompatible.unexpected_keys)

逻辑说明:strict=False 会把网络结构差异打印出来,而不是直接抛异常终止脚本。missing_keys 是原模型没有的层,unexpected_keys 是原模型有但新网络没有的层。这两个输出能准确告诉你改动范围。如果项目是 TensorFlow 版本,对应操作是 tf.train.latest_checkpoint 加 tf.train.load_checkpoint,思路一样。

检查完差异之后,如果只是新增了网络层,可以考虑冻结预训练参数,只训练新增层。在迁移场景里,我把预训练权重当作初始化手段,而不是当作已经收敛的解。每次换新环境,我都会先跑一个 50 个 episode 的短训练,确认奖励曲线的起点比随机策略高,才开始完整训练。起点高说明预训练权重确实提供了有效信息,起点和随机差不多说明环境改动太大,权重的迁移价值有限。

从那以后,我每次拿到一份多智能体源码,都会强制走一遍“环境自检 → 短训练 → 看曲线 → 对比随机策略”四步流程,确认真的有学习信号之后再考虑往里面加自己的模块。这个流程帮我挡住了至少三次因为环境配置错误导致的无效实验。希望帮到你。

本文还有配套的精品资源,点击获取