简介:面向高校多智能体强化学习课程设计与期末大作业,这份压缩包提供了一套完整的Python算法实现合集,涵盖VDN、QMIX、QTRAN、QPLEX四种主流价值分解方法,并附带训练好的模型权重文件。项目以SMAC星际争霸环境为默认场景,代码模块划分清晰:MultiAgentController用于构建智能体网络、生成动作并计算个体Q值,ReplayBuffer同时支持transition与episode两种存储方式,训练时可直观对比on-policy和off-policy的差异,也能替换自定义环境进行扩展实验。压缩包共含131个文件,整体大小约9.05MB,包括36个Python源码、29个npy中间数据、25个pkl模型文件、18张训练曲线图和PDF说明文档,另有TensorBoard事件文件与损失记录,便于查看训练细节和继续调参。已有347人浏览学习,适合作为期末项目或课程设计的多智能体强化学习参考实现,能帮助读者对照原论文理解算法设计思路,并在SMAC场景下直接复现、横向比较不同算法的收敛表现。
1. 多智能体强化学习课程设计:一套 python 源码跑通 VDN、QMIX、QTRAN、QPLEX
做过多智能体强化学习课程设计的人大概都有过这种经历:论文里的 VDN、QMIX、QTRAN、QPLEX 递进关系能讲清楚,但真到交期末大作业时,被环境安装、依赖冲突和代码实现卡得死死的,最后只能拿一份理论报告凑数。这份基于 python 实现的多智能体强化学习源码包,把四个价值分解算法和对应模型文件一次配齐,核心模块划分得很清楚:MultiAgentController 管智能体网络和动作生成,SMAC 管环境交互,ReplayBuffer 管训练数据存取。装上依赖就能跑,解决的是「课程设计必须拿出对比实验曲线」这件事。适合正在赶期末大作业的学生,也适合想快速复现 QMIX 做基线对比的从业者。
2. 从价值分解到四大算法:这套源码包里各改了什么
多智能体强化学习和单智能体最大的差别不在网络结构,而在「怎么给每个智能体分功劳」。在 SMAC 这类场景下,你看到的是 8 个单位在打架,每个单位只能看到自己视野内的局部信息,但训练时你又希望它们能完成协同配合。联合动作空间是每个智能体动作数的乘积,直接学一个全局 Q(s,a) 在智能体数量稍微上去之后就彻底不可行。这套源码包的四个算法走的是同一条路线:中心化训练、分布式执行(CTDE),训练时用全局状态做指导,执行时每个智能体只用自己的局部观测决定动作。
2.1 价值分解的动机:单智能体 DQN 为什么不能直接搬到多智能体
直接搬 DQN 会遇到两个绕不开的问题。首先是环境非稳态:其他智能体的策略也在更新,导致当前智能体看到的转移概率时刻在变,Q 网络要去拟合一个不停晃动的目标,非常容易震荡。其次是信度分配:一场团战打赢了,到底是哪个单位的关键操作?如果不做分解,所有智能体共享同一个团队奖励,梯度回传时「一锅端」,每个 agent 都拿不到真正属于自己的那份信号。
价值分解的思路就是学一个联合动作价值 Q_tot,把它拆成每个智能体个体价值 Q_i 的函数:每个智能体有自己的 Q 网络,一个混合结构把个体 Q 合成全局 Q,训练时只优化 Q_tot,执行时每个智能体直接用 Q_i 选动作。VDN、QMIX、QTRAN、QPLEX 这四个算法,本质上是「Q_tot 怎么从 Q_i 合成」的四种方案,合成方式的表达能力,决定了这个算法能覆盖什么样的协作模式。
2.2 四条技术路线的递进:VDN 求和、QMIX 单调、QTRAN 松弛、QPLEX 优势分解
四个算法的差别用一张表能看得很清楚:
| 算法 | 核心约束 | Q_tot 合成方式 | 表达能力 |
|---|---|---|---|
| VDN | 无条件 | Q_tot = sum Q_i | 只能表达线性可加的简单协作 |
| QMIX | 单调性约束 | 非负权重混合网络 | 单调函数类,适合大多数 SMAC 同构场景 |
| QTRAN | 松弛约束 | 引入全局状态值函数逼近 Q_tot | 理论上可表示任意联合价值函数,训练偏难 |
| QPLEX | advantage 分解 | dueling 结构 + 优势函数逐项分解 | 理论完备,难图上更稳,但网络更重 |
VDN 是 2017 年的工作,做法最直接:把联合 Q 写成所有个体 Q 的求和。这里隐含一个强假设——智能体之间的贡献是线性可加的,不存在交互项。好处是实现简单、训练稳定,坏处是很多需要协同的场景它学不出来。
QMIX 在 VDN 基础上引入 mixing network,用一组非负权重把个体 Q 混合成联合 Q。非负权重的约束保证了「个体 Q 变大,联合 Q 不会变小」,这就是单调性。在 SMAC 的 8m、3m 这类同构地图上,QMIX 的收敛速度和最终胜率都非常稳,是这套源码包里最适合当默认基线的算法。
QTRAN 是对 QMIX 表达能力的批判性改进:QMIX 只能表示单调函数,但真实问题里经常出现非单调的协作模式——两个智能体都去执行某动作才是最优,只有一个去反而更差,这种「拐点」是单调函数表达不了的。QTRAN 引入一个全局状态相关的值函数做松弛约束,理论上覆盖任意 Q_tot。代价是多了一个要同时优化的目标,实际训练里超参稍微不合适就掉点,表现容易波动。
QPLEX 走的是另一条路:把联合 Q 写成优势函数分解的形式,同时保留 dueling 网络结构,让每个智能体分别分解出自己的优势项。理论上它能表示任意联合价值函数,在 SMAC 的难图上往往比 QMIX 更稳。代价是网络结构最复杂,显存占用和调参成本都高。
在动手跑之前,建议先把这四个算法的关系在脑子里排一遍:VDN 是最朴素的求和,QMIX 用单调混合扩大表达,QTRAN 想突破单调限制但引入更多优化目标,QPLEX 把优势分解直接做进网络结构。后面调参翻车的时候回头看这一步能省很多时间,很多「训练不收敛」本质上是算法表达能力不适合当前地图,而不是代码写错了。
2.3 三个核心模块的代码分工:MultiAgentController、SMAC、ReplayBuffer
这套源码的结构属于典型的「课程设计友好型」:三个核心类各管一段。MultiAgentController() 包含多智能体的网络和所需操作,value-based 算法下它生成 q net,如果是 AC 类算法则生成 actor net,它不包含 mixing net 或 critic net。对外暴露的操作是计算 individual Q 值、按 epsilon 选动作。
class MultiAgentController: """生成 agent 模型并计算 individual q 值。 value-based 算法用 q net,AC 算法用 actor net。 """ def __init__(self, model_config): self.q_net = QNet(model_config) self.target_net = QNet(model_config) def compute_q(self, obs_batch, agents_mask): # obs_batch: 一批观测张量 # agents_mask: 屏蔽已经阵亡的智能体 return self.q_net(obs_batch) * agents_mask def choose_action(self, q_values, epsilon): # epsilon-greedy 策略,训练初期探索,后期趋于利用 if random.random() < epsilon: return random_action(q_values) return q_values.argmax(dim=-1)这里我一般会特别提醒一个点:agents_mask 不能省。SMAC 里智能体可能阵亡,死亡单位的 obs 还在 batch 里,不屏蔽的话梯度会把死单位的 Q 也算进去,训练会莫名震荡。这是复现 QMIX 的人最常见的隐藏 bug。
ReplayBuffer 的接口同样直接,它的核心设计是两种存储模式:
class ReplayBuffer: """支持 transition 和 episode 两种存储模式的回放缓冲区。 mode="transition" 存单步样本,适合 off-policy 单步更新; mode="episode" 存完整轨迹,适合需要整条序列的算法。 """ def __init__(self, mode="episode", capacity=10000): self.mode = mode self.buffer = deque(maxlen=capacity) def sample(self, batch_size): # 按 mode 决定返回单步还是整条 episode,内部做形状处理 ...模式选择直接对应 on-policy/off-policy 的数据需求,这个点后面第 4 章会专门展开。
3. 把源码跑起来:环境安装、地图摆放与训练启动的逐行拆解
拿到源码包之后,第一步不是读代码,而是先让一个能跑的实验跑起来。课程设计时间紧,你得先确认环境通,再回头去看每个模块怎么实现。下面这套流程我在多个版本的 SMAC 工程上都用过,照着走能省掉大半天的排错时间。
3.1 python 环境与依赖:别在系统环境里硬装
我建议先建一个独立的 conda 环境,python 版本选 3.8 或 3.9。这套源码的核心依赖是 torch、numpy、tensorboard,SMAC 环境本体也是一个 pip 包。卡住最多的地方是 torch 与 python 版本的搭配,千万别在系统 python 里硬装,装坏了连日常脚本都用不了。
conda create -n marl python=3.8 conda activate marl pip install torch numpy tensorboard pip install smac参数说明:conda create -n marl 创建独立环境,避免污染系统 python;python=3.8 是这套源码兼容面最广的版本选择;tensorboard 用来读源码包里那批 events.out.tfevents.* 事件日志;smac 是星际争霸多智能体环境的 python 封装,但装完 smac 不等于环境就绪,它还需要游戏本体和地图文件。
如果机器没有 GPU,torch 的 CPU 版本也能把 8m 这种小地图跑出结果,只是速度慢。QMIX 的 mixing network 和 QPLEX 的 dueling 结构都偏重,有 GPU 能省很多等待时间。我一般会先看任务期限再决定要不要上 GPU,如果只是交课程设计,CPU 跑 2000 个 episode 完全在可接受范围内。
3.2 SMAC 环境与星际争霸地图的落地姿势
SMAC 依赖星际争霸 II 的游戏本体。常见做法是下载 SC2 后把目录位置配到环境变量 SC2PATH,再把 SMAC 官方仓库提供的 maps 解压到游戏目录的 Maps 下。地图文件体积不小,官方没有放进 pip 包,这也是环境配置里最容易卡住的一步。
配好之后,我会先写一段极小的验证脚本,确认环境是真的通了:
from smac.env import StarCraft2Env env = StarCraft2Env(map_name="8m") env_info = env.get_env_info() print("agents:", env_info["n_agents"], "actions:", env_info["n_actions"]) obs = env.reset() # 触发一次 reset,验证环境闭环能走通这段代码的作用是检查环境创建和 reset 两个最基本的环节。env_info["n_agents"] 是该地图的智能体数量,env_info["n_actions"] 是每个智能体的离散动作数,如果你看到类似 (8, 14) 的维度组合,说明 SC2 本体和地图文件都就位了。map_name 值得特别注意:改地图名就能切换场景难度,后面做对照实验时它是最重要的自变量。
提示:如果 StarCraft2Env 创建时报找不到地图,优先检查 SC2PATH 环境变量是否指对了包含 StarCraft II 可执行文件的目录。
3.3 训练启动命令与关键参数解读
环境通了之后就可以启动训练。这份源码的入口是一个 main.py,通过命令行参数切换算法和地图,这是课程设计最舒服的形态——不用改代码就能换算法:
python main.py --algo qmix --map 8m --n_episodes 5000 --buffer_mode episode几个关键参数的含义:--algo 指定算法,取值 vdn、qmix、qtran、qplex,对应第 2 章说的四条路线;--map 指定 SMAC 地图名,8m 是最常用的入门地图,8 个 marine 对阵 8 个 marine,智能体同构,最适合做算法对比;--n_episodes 控制训练总 episode 数,课程设计拿 2000 到 5000 都能出可看的收敛曲线;--buffer_mode 是这份源码特有的参数,episode 模式存整条轨迹,transition 模式存单步,on-policy 的算法必须用 episode 模式并且在训练阶段结束后清空缓冲区。
我一般会先跑 100 个 episode 验证流程不报错,确认模型输出形状、reward 计算、模型保存这些环节都正常,再放开跑完整实验。这一步能救回很多无效训练时间,代价只是几分钟。
3.4 模型文件与事件日志的复盘用法
训练结束之后,日志目录下会多出两类东西:模型权重文件和 events.out.tfevents.* 事件日志。后者就是 TensorBoard 的事件源,源码包里自带的那些 events.out.tfevents.1659* 文件,就是之前跑训练留下的历史日志,可以直接用来恢复当时的训练曲线。
tensorboard --logdir ./logs --port 6006浏览器打开 http://localhost:6006,可以看到 reward、win rate 等标量随训练步数的变化曲线。模型文件则有两个用途:一是加载权重继续训练,相当于断点续跑;二是加载训练好的模型做纯评估,关闭探索噪声(epsilon=0)看稳定胜率。
复盘时有个经验:只看 reward 曲线会被骗。SMAC 里 reward 的尺度会随战斗进程出现很大方差,win rate 才是最终标准。注意观察 win rate 曲线的拐点位置和方差,拐点出现得越早,说明算法在这个场景上学得越快。
4. 做课程对比实验:四算法的统一接口、超参与公平对照组设计
课程设计的核心不是跑通一个算法,而是拿出一个让老师信服的对比。这套源码包最大的价值恰好在这里:四个算法共用同一套训练接口,实验设计得好,答辩时一张图就能把工作量讲清楚。
4.1 一个参数切换算法:统一接口的直接收益
源码里算法注册的逻辑一般是这样的:
ALGO_MAP = { "vdn": VDN, "qmix": QMIX, "qtran": QTRAN, "qplex": QPLEX, } def build_algorithm(algo_name, config): algo_cls = ALGO_MAP[algo_name] return algo_cls(config)ALGO_MAP 把字符串映射到算法类,build_algorithm 只负责按名字实例化。MultiAgentController 不关心上层是哪个算法,它只提供 q net 和动作选择,混合网络和特殊值函数由各算法子类自己实现。这意味着你切换算法时只需要改一个参数,底层的数据流、训练循环、日志记录全部复用。
但要注意:四个算法的超参并不完全一致。QMIX 的 mixing network 有独立的 learning rate,QPLEX 的 advantage 模块还需要额外的分解系数。直接用 VDN 的默认超参跑 QPLEX,效果差是正常的,不代表代码有问题。切换算法之后,至少要把学习率和 batch size 重新看一遍。
4.2 公平对照实验的三要素
答辩时老师最爱问的一句话是「你这几个算法的超参是不是一样的」。如果完全一样,对某些算法不公平,如果完全不一样,又失去了对比意义。我的习惯是固定三样东西:随机种子、地图、训练总 episode 数,超参则每个算法各自调到可接受范围,最后把实际使用的超参列在报告附录里。
一个常见的课程设计结果表会长这样:
| 算法 | 地图 | seed | 最终 win rate | 达到 80% win rate 的 episode |
|---|---|---|---|---|
| VDN | 8m | 0 | 约 78% | 未达到 |
| QMIX | 8m | 0 | 约 95% | 约 1800 |
| QTRAN | 8m | 0 | 约 85% | 约 2500 |
| QPLEX | 8m | 0 | 约 96% | 约 1600 |
具体数字由超参决定,但规律是稳定的:同构简单地图上 QMIX 和 QPLEX 明显领先,VDN 受限于线性加和假设容易到瓶颈,QTRAN 的曲线方差偏大。
还有一个最容易翻车的细节:训练代码里必须显式固定随机种子,并让 cuDNN 走确定性算法。
# 固定随机种子,并关闭 cuDNN 的自动调优,保证可复现 seed_everything(42) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False这三行代码我每次跑对照实验前都会加上。少了它们,同一个 seed 在 GPU 上跑两次结果都可能不一样,答辩时如果被要求现场重跑,结果对不上报告会非常尴尬。
提示:seed_everything 要同时设置 random、numpy、torch 三套随机数生成器,缺一个都不算真正固定。
4.3 on-policy 与 off-policy 的 buffer 清空逻辑
资源正文里反复提过 ReplayBuffer 的两种存储方式,根子就在 on-policy 和 off-policy 的数据哲学差异上。on-policy 要求训练数据必须来自当前正在被改进的策略,所以策略更新之后,旧策略采集的数据就不能再用了,必须清空 buffer 重新采样。off-policy(比如 QMIX 这类 value-based 算法)可以用行为策略采集的数据反复训练,buffer 不清空,但某些带重要性采样的变体会在采样时额外计算权重。
对应到代码里就是这个逻辑:
# on-policy:每个训练阶段结束后清空,防止旧轨迹污染新策略 if config["on_policy"]: replay_buffer.clear() # off-policy:旧数据可复用,不清空,只按优先级或均匀采样两类的差异可以总结成一张表:
| 项目 | on-policy | off-policy |
|---|---|---|
| 数据来源 | 当前策略实时采集 | 行为策略历史采集 |
| buffer 处理 | 阶段结束后清空 | 长期复用,不清空 |
| 更新频率 | 每个 episode 结束后整体更新 | 每个 batch 都可以更新 |
| 典型代表 | 策略梯度类变体 | VDN、QMIX |
在这份源码里,VDN 和 QMIX 按 off-policy 跑即可,QTRAN、QPLEX 要看具体变体的实现方式。你只需要记住一个排查方向:训练曲线在某一步突然断崖式下跌,先检查是不是 buffer 清空逻辑没生效。
5. 常见问题与避坑:跑这四个算法最容易翻车的 5 个场景
前面把流程说通了,下面这 5 个坑是按课程设计作业里出现频率排的,每一条都值得在最终提交前对一遍。前两个是环境问题,中间两个是训练问题,最后一个是答辩思路问题。
5.1 SMAC 环境创建失败:SC2PATH 没指对地图就绪位置
现象:StarCraft2Env(map_name="8m") 直接抛异常,报错信息类似找不到地图初始化参数、找不到 SC2 可执行文件。
原因:pip install smac 只安装了环境的 python 封装,游戏本体的地图数据需要单独放置。SMAC 默认从环境变量 SC2PATH 找游戏目录,这个变量很多教程不会显式设置。
解决:先确认 SC2PATH 指向包含 StarCraftII.exe(或 .app)的目录,再把 maps 解压到游戏目录下的 Maps 文件夹,最后重新执行 3.2 节的验证脚本。如果是 Mac 用户,注意路径里的版本号目录不能嵌套错位。
5.2 训练 loss 不降、win rate 长期为 0
现象:reward 一直徘徊在 0 附近,epsilon 都衰减到底了,win rate 还是 0。
原因:最常见的有两种。一是 on-policy 算法 buffer 没清空,旧轨迹持续拖累新策略的梯度更新;二是地图难度超出算法能力,比如拿最朴素的 VDN 直接跑异构地图,它根本学不会协同。
解决:先在训练循环里加一行打印,确认 buffer 长度在每个训练阶段结束后都被重置。然后换回 8m 这种同构简单地图验证算法本身。如果简单地图能收敛,说明代码没问题,是场景选难了;如果简单地图也不收敛,再回头查 buffer 和 reward 归一化。
5.3 显存溢出或训练慢到怀疑人生
现象:跑一段时间后进程 OOM,或者 QPLEX 一个 episode 要好几秒,实验周期完全不可接受。
原因:SMAC 的 episode 长度往往很长,QMIX 和 QPLEX 需要同时保存完整轨迹并做序列前向,显存和时间的消耗集中在 batch_size 与轨迹长度的乘积上。QPLEX 的 dueling 结构还额外多了一组优势函数分支。
解决:把 batch_size 从 32 调到 16 或 8,配合梯度累积来补偿训练稳定性。如果还吃紧,就把 buffer_mode 从 episode 换成 transition,用单步更新替代整条轨迹更新,牺牲部分样本效率换取可训练性。
5.4 同一个 seed 两次结果不一致:复现翻车
现象:答辩前重跑实验,曲线和提交报告里的差出一截,明明代码一行没改。
原因:GPU 上的非确定性操作,cuDNN 的 benchmark 模式会在不同运行环境下选择不同的卷积算法;另外多进程采样时子进程的随机种子没有正确传递,也会导致采样序列漂移。
解决:设置 torch.backends.cudnn.deterministic = True 和 benchmark = False,在创建 DataLoader 或采样 worker 时用 worker_init_fn 给每个进程单独固定种子。这个坑在跑 QPLEX 时尤其明显,因为它的优势分支对网络初始化和采样顺序更敏感。
5.5 QTRAN 跑得比 VDN 还差:别急着改代码,先看地图特性
现象:QTRAN 在 8m 上 win rate 连 VDN 都打不过,看起来像「算法越高级越不行」。
原因:QTRAN 的松弛约束引入了额外的全局状态值函数,多了个要同时优化的目标。在简单同构地图上,VDN 的线性加和假设已经够用,QTRAN 的优势发挥不出来,反而容易被超参拖累。这不是代码 bug,是算法特性与场景不匹配。
解决:换 3s_vs_5z、5m_vs_6m 这类异构或需要精细配合的地图再跑,QTRAN 理论上的表达优势才体现得出来。答辩时主动说清楚这一点,比藏着不说更加分——这正是课程设计要的训练:理解算法的适用边界。
6. 把这套源码改成自己的:自定义环境、buffer 切换与对比图导出
课程设计交完不是终点,绝大多数人会把这份源码拿来扩展成自己的项目。这一章只讲三个最值钱的改造点,不碰架构重构。
6.1 自定义环境:照着 SMAC 的 API 封一层
写自己的环境时,最省事的做法不是改 MultiAgentController,而是把自己的环境封装成 SMAC 同款接口。reset 返回观测、get_env_info 返回环境元信息、step 返回 (obs, reward, done, info),三种方法对齐之后,整份源码的训练循环可以原封不动地跑你的环境。血泪经验是:封装环境永远比重构训练循环便宜。
6.2 不同算法选不同 buffer 模式
VDN 和 QMIX 用 transition 模式跑单步更新,速度快、显存省;QTRAN 和 QPLEX 用 episode 模式保整条轨迹,训练更稳定。这个选择直接写在 config 里,改一个字段就能切。我自己跑实验时习惯先验证算法本身,再在最终对比实验里统一用 episode 模式,保证数据格式一致,减少答辩时的解释成本。
6.3 导出一张四个算法叠一起的对比图
import matplotlib.pyplot as plt for algo_name in ["vdn", "qmix", "qtran", "qplex"]: data = load_results(f"logs/{algo_name}_8m.csv") plt.plot(data["episode"], data["win_rate"], label=algo_name) plt.xlabel("episode") plt.ylabel("win rate") plt.legend() plt.savefig("comparison.png", dpi=150)这段脚本的逻辑很简单:读每个算法在同一地图上的训练日志,把 win rate 曲线叠加到同一个坐标系。关键是横轴必须用 episode 数,不能用时间戳。四个算法单步耗时不同,时间戳对齐的曲线起点天然错位,答辩时老师一眼就能看出来。
我自己做这个系列实验时吃过一次亏:当时画图用了时间戳当横轴,QMIX 和 QPLEX 因为训练速度快,曲线整体偏右,答辩老师直接问「你横轴是什么」。从那以后我每次做算法对比都会强制走一遍同样的流程——固定种子、统一 episode 数、确认 buffer 模式匹配算法性质、最后用 episode 对齐曲线。这个源码包让我少走了很多弯路,也希望帮到你。
本文还有配套的精品资源,点击获取