简介:本资源面向多智能体强化学习方向的课程大作业、期末设计与入门研究者,提供基于Python实现的VDN、QMIX、QTRAN、QPLEX四种经典值分解算法源码及配套模型文件,帮助读者理解从个体Q网络到混合网络的完整训练流程。包内共131个文件,以36个py源码、29个npy数据、25个pkl模型、18张png训练曲线图为主,另含xml环境配置、pdf说明文档与loss日志,压缩包约9.05MB,结构清晰便于按算法模块检索。代码围绕MultiAgentController、SMAC环境封装与ReplayBuffer三大组件展开,涵盖个体Q值计算、动作生成、on-policy与off-policy数据存储差异等关键实现细节,可直接对照复现实验并迁移到自定义环境。目前已有347人学习下载,适合需要快速搭建多智能体训练框架、完成课程设计或对比算法性能的读者参考。
1. 多智能体强化学习源码包:从 VDN 到 QPLEX 的选型与复现路径
如果你手头有一个基于 Python 的多智能体强化学习源码包,里面包含 VDN、QMIX、QTRAN、QPLEX 四种算法的实现和对应模型文件,第一反应大概率是「先跑起来看看效果」。但真正动手时你会发现,这四个算法虽然都属于值分解(Value Decomposition)家族,却在网络结构、单调性约束、参数共享策略上存在关键差异,直接套用同一套训练脚本很容易翻车。这个源码包解决的核心问题是:在 StarCraft II 的 SMAC 环境或类似的合作型多智能体任务中,让多个智能体通过局部观测学会协同策略。它适合已经掌握 Python 基础、了解 PyTorch 或 TensorFlow 基本用法、想做多智能体算法对比实验的从业者。接下来我会按「先理解每个算法为什么这样设计,再落到具体代码和参数,最后说清楚哪些地方容易踩坑」的顺序,把这条路径拆开讲透。
2. VDN 与 QMIX:值分解的起点与单调性约束
2.1 VDN 的加性分解为什么是最小可用基线
VDN(Value Decomposition Networks)的核心假设非常朴素:联合 Q 值等于各智能体 Q 值的和。这个假设在代码里体现为一行torch.sum(agent_qs, dim=1),但它的意义在于把信用分配问题简化成了可加性约束。源码包中 VDN 的实现通常包含三个部分:每个智能体的局部 Q 网络(通常是 GRU 或 MLP)、一个求和模块、以及一个共享的经验回放缓冲区。
我一般会先跑 VDN 作为基线,因为它的训练最稳定,超参数最少。关键参数只有三个:hidden_dim(局部网络隐藏层维度,常用 64 或 128)、lr(学习率,1e-3 到 5e-4 之间)、batch_size(32 或 64)。在 SMAC 的 2s3z 地图上,VDN 通常能在 200k 步以内收敛到 80% 以上的胜率。
# VDN 的 Q 值聚合核心逻辑 # agent_qs: [batch_size, n_agents, 1] # 每个智能体的局部 Q 值直接求和得到联合 Q 值 joint_q = torch.sum(agent_qs, dim=1) # 加性分解,无额外参数 # 后续用 joint_q 计算 TD 损失 td_loss = F.mse_loss(joint_q, target_q)这段代码的逻辑是:VDN 不引入任何额外的混合网络参数,联合 Q 值就是局部 Q 值的线性加和。参数说明上,agent_qs的维度必须是[batch, n_agents, 1],如果维度不对,torch.sum会沿着错误的方向聚合,导致训练完全失效。这是新手最容易犯的错误之一。
2.2 QMIX 的单调混合网络与超参数设置
QMIX 在 VDN 基础上引入了一个混合网络(Mixing Network),通过单调性约束让联合 Q 值对每个局部 Q 值的偏导数非负。代码实现上,混合网络的权重通过一个超网络(Hypernetwork)生成,并对权重取绝对值来保证单调性。
class QMixer(nn.Module): def __init__(self, n_agents, state_dim, embed_dim=32): super().__init__() # 超网络:从全局状态生成混合网络的第一层权重 self.hyper_w1 = nn.Linear(state_dim, n_agents * embed_dim) # 第二层权重生成,输出维度为 embed_dim self.hyper_w2 = nn.Linear(state_dim, embed_dim) # 偏置项 self.hyper_b1 = nn.Linear(state_dim, embed_dim) self.hyper_b2 = nn.Sequential( nn.Linear(state_dim, embed_dim), nn.ReLU(), nn.Linear(embed_dim, 1) ) self.embed_dim = embed_dim self.n_agents = n_agents def forward(self, agent_qs, states): bs = agent_qs.size(0) # 生成第一层权重并取绝对值保证单调性 w1 = torch.abs(self.hyper_w1(states)) # [bs, n_agents * embed_dim] w1 = w1.view(bs, self.n_agents, self.embed_dim) b1 = self.hyper_b1(states).view(bs, 1, self.embed_dim) # 第一层混合:局部 Q 值加权求和后过 ELU hidden = F.elu(torch.bmm(agent_qs.view(bs, 1, self.n_agents), w1) + b1) # 第二层权重同样取绝对值 w2 = torch.abs(self.hyper_w2(states)).view(bs, self.embed_dim, 1) b2 = self.hyper_b2(states).view(bs, 1, 1) # 输出联合 Q 值 joint_q = torch.bmm(hidden, w2) + b2 return joint_q.view(bs, 1)这段代码的关键在于torch.abs的使用位置:权重取绝对值保证单调性,但偏置项不取绝对值。embed_dim控制混合网络的容量,常用 32;设得太大会导致过拟合,太小则表达能力不足。state_dim是全局状态的维度,在 SMAC 中通常等于所有智能体观测拼接后的长度。训练时 QMIX 的lr通常比 VDN 略小,建议从 5e-4 开始调。
2.3 从 VDN 迁移到 QMIX 时需要改哪些配置
如果你已经跑通了 VDN,想切换到 QMIX,需要改动的配置项包括:在config中把mixer_type从"vdn"改成"qmix",增加mixer_embed_dim参数(默认 32),并把agent_qs的聚合方式从直接求和替换为调用QMixer的forward。经验回放缓冲区需要额外存储全局状态state,因为混合网络的权重生成依赖它。在 SMAC 中,全局状态通常是所有智能体观测的拼接,但要注意有些地图的state维度会随智能体数量变化,需要在reset时动态获取。
提示:从 VDN 切到 QMIX 后,如果训练曲线出现剧烈震荡,先检查
state是否正确传入混合网络,再检查torch.abs是否只作用在权重上。
3. QTRAN 与 QPLEX:突破单调性约束的两条路线
3.1 QTRAN 的松弛约束与实现代价
QTRAN 试图解决 QMIX 单调性约束过强的问题。它的核心思想是:不要求联合 Q 值对局部 Q 值单调,而是通过一个松弛的线性约束来保证最优策略的一致性。代码实现上,QTRAN 需要额外的网络分支来计算一个修正项,训练时同时优化 TD 损失和约束损失。
# QTRAN 的约束损失计算(简化版) # joint_q: 混合网络输出的联合 Q 值 # agent_qs: 各智能体局部 Q 值 # state: 全局状态 # 计算最优局部动作对应的联合 Q 值 q_tot_optimal = mixer(agent_qs_optimal, state) # 计算当前联合 Q 值与局部 Q 值之和的差 constraint_loss = F.mse_loss( q_tot_optimal, agent_qs_optimal.sum(dim=1) # 线性约束项 ) # 总损失 = TD 损失 + 约束损失 total_loss = td_loss + lambda_constraint * constraint_losslambda_constraint是 QTRAN 最关键的参数,通常设为 1.0 或 0.5。设得太小约束不起作用,设得太大则 TD 损失无法收敛。QTRAN 的训练稳定性明显差于 QMIX,在 SMAC 的复杂地图上经常需要多次随机种子才能跑出好结果。我一般只在需要对比「单调性约束是否必要」时才用 QTRAN,日常实验还是以 QMIX 和 QPLEX 为主。
3.2 QPLEX 的 dueling 结构与优势函数分解
QPLEX 把 dueling 网络的思想引入多智能体值分解,把联合 Q 值分解为状态价值项和优势项,并通过一个变换网络把局部优势映射到联合优势。代码实现上,QPLEX 包含三个核心模块:局部 Q 网络、dueling 混合网络、以及变换矩阵生成网络。
class QPLEXMixer(nn.Module): def __init__(self, n_agents, state_dim, embed_dim=32): super().__init__() # 状态价值项:只依赖全局状态 self.V = nn.Sequential( nn.Linear(state_dim, embed_dim), nn.ReLU(), nn.Linear(embed_dim, 1) ) # 优势项变换网络:生成变换矩阵 self.hyper_w = nn.Linear(state_dim, n_agents * n_agents) # 局部优势的权重 self.hyper_adv = nn.Linear(state_dim, n_agents) self.n_agents = n_agents def forward(self, agent_qs, states): bs = agent_qs.size(0) # 计算状态价值 v = self.V(states) # [bs, 1] # 计算局部优势:局部 Q 值减去局部 Q 值的均值 adv = agent_qs - agent_qs.mean(dim=1, keepdim=True) # 生成变换矩阵并取绝对值 w = torch.abs(self.hyper_w(states)).view(bs, self.n_agents, self.n_agents) # 生成优势权重 adv_weight = self.hyper_adv(states).view(bs, 1, self.n_agents) # 变换后的联合优势 joint_adv = torch.bmm(adv_weight, torch.bmm(adv, w)) # 联合 Q 值 = 状态价值 + 联合优势 joint_q = v + joint_adv.view(bs, 1) return joint_qQPLEX 的embed_dim通常设为 32 或 64,lr建议从 5e-4 开始。相比 QMIX,QPLEX 在智能体数量较多(如 5 个以上)的地图上优势更明显,因为 dueling 结构能更好地处理不同智能体对联合 Q 值贡献不均衡的情况。
3.3 四种算法的参数共享与网络结构对比
| 算法 | 混合网络结构 | 单调性约束 | 额外参数 | 典型 lr | 适用场景 |
|---|---|---|---|---|---|
| VDN | 无,直接求和 | 隐式满足 | 无 | 1e-3 | 小规模、快速基线 |
| QMIX | 超网络 + 绝对值权重 | 显式强制 | 中等 | 5e-4 | 中等规模、稳定训练 |
| QTRAN | 混合网络 + 约束损失 | 松弛约束 | 较多 | 5e-4 | 对比实验、研究单调性 |
| QPLEX | dueling + 变换矩阵 | 无强制约束 | 较多 | 5e-4 | 大规模、非均衡贡献 |
参数共享方面,四种算法通常都采用「所有智能体共享局部 Q 网络参数」的策略,这在同构智能体场景下能大幅减少参数量。但如果智能体类型不同(如不同兵种),需要关闭参数共享,改为每个智能体独立网络。
4. 训练流程与模型文件管理:从配置到保存
4.1 配置文件的关键字段与修改方法
源码包中通常有一个config字典或 YAML 文件,控制算法选择、环境参数、训练超参数。关键字段包括:algorithm(vdn/qmix/qtran/qplex)、n_agents、state_dim、obs_dim、hidden_dim、lr、batch_size、buffer_size、target_update_interval、epsilon_start、epsilon_end、epsilon_decay。
# 典型的多智能体训练配置 algorithm: qmix n_agents: 3 obs_dim: 48 state_dim: 144 hidden_dim: 64 lr: 0.0005 batch_size: 32 buffer_size: 5000 target_update_interval: 200 epsilon_start: 1.0 epsilon_end: 0.05 epsilon_decay: 50000 mixer_embed_dim: 32修改配置时要注意:state_dim必须等于n_agents * obs_dim(在 SMAC 中通常成立),否则混合网络输入维度会出错。buffer_size设得太小会导致样本相关性过高,训练不稳定;设得太大则内存占用高,建议从 5000 开始。
4.2 模型保存与加载的目录结构
模型文件通常按「算法名/地图名/随机种子」的层级保存。每个检查点包含三个文件:agent_net.pth(局部 Q 网络参数)、mixer_net.pth(混合网络参数)、optimizer.pth(优化器状态)。加载时先实例化网络结构,再调用load_state_dict。
# 保存检查点 torch.save({ 'agent_net': agent_net.state_dict(), 'mixer_net': mixer_net.state_dict(), 'optimizer': optimizer.state_dict(), 'train_step': train_step, 'epsilon': epsilon }, f"checkpoints/{algorithm}/{map_name}/seed_{seed}/step_{train_step}.pth") # 加载检查点 checkpoint = torch.load(path, map_location='cpu') agent_net.load_state_dict(checkpoint['agent_net']) mixer_net.load_state_dict(checkpoint['mixer_net']) optimizer.load_state_dict(checkpoint['optimizer'])保存频率建议每 10000 步一次,同时保留最近 5 个检查点,避免磁盘占满。加载时注意map_location参数,如果在 CPU 上加载 GPU 训练的模型,必须指定map_location='cpu'。
4.3 训练日志与评估指标
训练日志至少记录:train_step、td_loss、mean_episode_reward、win_rate、epsilon。评估时每 5000 步跑 10 个测试回合,计算平均胜率和平均回报。如果td_loss持续上升而win_rate不涨,通常是目标网络更新太慢或学习率太大。
5. 避坑与排查:多智能体训练中的五个血泪教训
5.1 现象:训练初期 loss 直接变成 NaN
原因:学习率过大或混合网络中出现了除零操作。QMIX 的绝对值权重在初始化时如果全为零,梯度会爆炸。解决:把lr降到 1e-4 试一次,同时在混合网络初始化时给权重加一个小的偏置(如 0.01),避免全零初始化。
5.2 现象:胜率在 50% 附近震荡,始终不涨
原因:epsilon衰减太快,智能体过早陷入局部最优;或者经验回放缓冲区太小,样本相关性太高。解决:把epsilon_decay从 50000 调到 200000,buffer_size从 5000 调到 20000。如果还不行,检查奖励函数是否过于稀疏,考虑加入中间奖励。
5.3 现象:切换算法后模型加载报维度不匹配
原因:不同算法的混合网络输入维度不同。VDN 不需要state,QMIX 和 QPLEX 需要state,QTRAN 还需要额外的约束项。解决:加载前先打印state_dict的 key 和 shape,确认网络结构一致。如果从 VDN 切到 QMIX,必须重新初始化混合网络,不能复用 VDN 的检查点。
5.4 现象:GPU 显存溢出,batch_size 调小后训练变慢
原因:混合网络中的torch.bmm操作在n_agents较大时显存占用急剧上升。解决:把embed_dim从 64 降到 32,或者使用梯度累积(每 4 个 batch 更新一次参数)。如果还不行,把agent_qs和states转为float16进行混合网络计算。
5.5 现象:评估时胜率远低于训练时的胜率
原因:训练时使用了epsilon-greedy探索,评估时如果没关掉探索,动作会随机;或者评估环境的地图种子和训练时不同。解决:评估时设epsilon=0,并固定地图种子。另外检查agent_net是否处于eval()模式,BatchNorm 和 Dropout 在训练和评估时的行为不同。
6. 进阶技巧:用参数共享与课程学习加速 QPLEX 收敛
如果你已经把四种算法都跑通了,下一步可以尝试两个进阶方向。第一个是参数共享的粒度控制:默认所有智能体共享一套局部 Q 网络参数,但在异构场景下,可以按智能体类型分组共享。比如 SMAC 中如果同时有地面单位和空中单位,可以建两个参数组,每组内部共享。实现上只需把agent_net改成nn.ModuleList,每个元素对应一个参数组。
第二个是课程学习:先在简单地图(如 2s3z)上训练到收敛,再把学到的参数迁移到复杂地图(如 5m_vs_6m)作为初始化。迁移时只加载agent_net的参数,混合网络重新初始化,因为不同地图的state_dim和n_agents可能不同。我一般会把lr在迁移后调小一半,让模型在复杂地图上微调而不是从头学。
# 课程学习:从简单地图迁移到复杂地图 # 加载简单地图的局部网络参数 simple_checkpoint = torch.load("checkpoints/qplex/2s3z/seed_1/step_200000.pth") agent_net.load_state_dict(simple_checkpoint['agent_net']) # 混合网络重新初始化,因为 state_dim 可能变化 mixer_net = QPLEXMixer(n_agents=new_n_agents, state_dim=new_state_dim) # 学习率减半,微调 optimizer = torch.optim.Adam( list(agent_net.parameters()) + list(mixer_net.parameters()), lr=2.5e-4 )验证迁移是否有效的方法:对比「从头训练」和「迁移训练」在相同步数下的胜率曲线。如果迁移训练在 50k 步内就超过了从头训练 100k 步的胜率,说明迁移有效。如果迁移后反而更差,可能是简单地图和复杂地图的观测空间差异太大,需要先做观测归一化。
我自己的习惯是:每次跑新地图前,先花 10 分钟检查state_dim、obs_dim、n_agents三个维度是否匹配,再确认epsilon_decay和buffer_size是否按地图复杂度调整过。这个习惯帮我省下了大量重跑实验的时间。希望帮到你。
本文还有配套的精品资源,点击获取