☰
MADDPG多智能体强化学习源码解析与训练避坑实战
2026/11/2 18:25:54 网站建设 项目流程

简介:这是一份基于MADDPG(多智能体深度确定性策略梯度)的博弈对抗算法Python实现项目,源自个人期末大作业,最终成绩98分,代码完整、经过严格调试,下载后即可直接运行。资源面向计算机相关专业正在完成课程设计、期末大作业,或需要进行项目实战练习的学生,重点演示了如何将DDPG扩展至多智能体博弈对抗场景,实现可复现的完整工程,既能用于巩固多智能体强化学习理论,也可直接作为课程设计演示程序。压缩包共13个文件,其中10个Python脚本构成核心代码,覆盖DDPG、MADDPG、神经网络、经验回放、训练工具、环境测试等功能模块,结构清晰,便于按需深入阅读;同时配套txt说明文档和cfg配置文件,用于查看运行说明与Python环境依赖。整个项目仅15KB,体型紧凑、依赖简单,学习者可在此基础上替换环境、调整奖励函数,快速扩展为自己的课程设计或毕业设计;代码分层合理、可读性强,适合复盘高分项目的工程组织方式。目前已有385人学习、使用该项目,对于希望获得高分项目参考、以最短时间上手MADDPG落地实现的读者,是一份难得的源码资料。

1. 先把这份MADDPG源码读明白:多智能体博弈对抗的完整闭环

拿到一份标注“下载即用”的多智能体博弈对抗算法源码,第一反应别急着跑训练脚本。这份基于MADDPG的Python项目,解决的核心问题是:当多个智能体在同一个环境里互相博弈,每个智能体的策略都在改变其他人的状态转移,单智能体强化学习算法(DDPG、TD3、PPO)会因环境非平稳性而失效。MADDPG通过“集中式训练、分布式执行”让每个智能体在训练时看到全局信息、在执行时只用本地观测,从而学到稳定的对抗策略。项目代码是完整闭环——从经验回放buffer、Actor-Critic网络到训练主循环和测试脚本全部就位,适合做课程设计、期末大作业或刚入门多智能体强化学习想跑通第一个项目的同学。源码结构清晰,能直接看到MADDPG与DDPG的差异落在哪里。

2. MADDPG的原理与代码结构:集中式训练、分布式执行怎么落地

2.1 从DDPG到MADDPG:为什么单智能体方法在多智能体场景会翻车

先回忆DDPG的设定。DDPG是深度确定性策略梯度算法,维护一个Actor网络输出连续动作,一个Critic网络评估当前状态下动作的价值。Actor用确定性策略梯度更新,Critic用TD误差更新,两者配合目标网络稳定训练。这套方法在单智能体连续控制任务上表现稳定,比如机械臂控制、自动驾驶的纵向决策,因为环境的状态转移只由自己的动作决定,本质上是一个标准的马尔可夫决策过程。

但把它直接搬到多智能体博弈里,问题就出现了。设想一个两个智能体互相追逃的场景:追捕者策略在变强,逃跑者为了活命也在调整策略。对追捕者来说,逃跑者的行为就是环境的一部分,而这个“环境”每时每刻都在变。DDPG的Critic在计算Q值时,假设环境转移是固定的,它把对手的动作当成噪声的一部分忽略掉,训练出来的Q值在数学上就不成立。实际表现就是训练曲线乱跳,智能体学不到有意义的对抗策略,跑几百个回合都看不到上升趋势。

MADDPG的解决方案是Centralized Training with Decentralized Execution,也就是常说的CTDE。训练阶段,每个智能体的Critic可以拿到所有智能体的观测和动作,相当于站在上帝视角评估当前的局势。Actor则保持独立,只用自己本地的观测输出动作。这样Critic能感知到其他智能体策略的变化,从而缓解环境非平稳带来的偏差。执行阶段不需要任何通信,每个智能体各自决策,这一点对实际部署非常友好。

多智能体强化学习领域里,CTDE已经成为主流框架,MADDPG是最经典的落地范式之一。项目里的mag.py就是multi-agent game的缩写,负责组织多个智能体的交互流程。rl_utils.py这类工具文件一般放通用函数,比如折扣回报计算、one-hot转换、经验预处理。把这些文件对应到算法流程里,读代码的速度会快很多。

2.2 项目文件角色拆解:main.py、MADDPG.py、network.py各管什么

拿到项目先别急着跑,把文件之间的关系捋清楚,后面调参才知道改哪里。这个压缩包里的核心文件大致分成四层:入口层、算法层、网络层和环境与测试层。

文件职责关键内容
main.py训练入口初始化环境、智能体、缓冲区,跑训练循环并保存模型
MADDPG.py算法主体管理所有智能体的Actor与Critic、目标网络软更新、梯度更新
network.py网络结构定义Actor和Critic的MLP结构,激活函数与观测动作拼接
buffer.py经验回放存储多智能体联合状态、联合动作、奖励与终止信号
DDPG.py单智能体基线DDPG实现,用于对比或作为理解MADDPG的参照
mag.py / test_env.py博弈环境定义对抗场景的观测、动作边界、奖励函数与终止条件
ceshi.py / test.py测试脚本加载已训练模型,观察智能体实际对抗表现

文件命名在不同版本里可能有出入,比如mag.py也可能叫env.py,但职责划分基本一致。我的建议是从main.py开始追踪整个调用链,把“环境怎么建、每个回合的交互怎么写、更新频率设多少”这几个问题搞清楚,其他文件自然就顺了。训练主循环常见写法如下:

# main.py 训练主循环核心片段 for episode in range(episode_num): # 控制总回合数 obs = env.reset() # 返回所有智能体的观测列表 episode_reward = np.zeros(n_agents) # 记录本回合各智能体累计奖励 for step in range(max_steps): # 限制单回合最大步数 actions = [] for i in range(n_agents): action = maddpg.get_action(obs[i], noise_scale=0.1) actions.append(action) next_obs, rewards, dones, _ = env.step(actions) buffer.push(obs, actions, rewards, next_obs, dones) obs = next_obs episode_reward += rewards if buffer.size() > batch_size: for _ in range(update_times): batch = buffer.sample(batch_size) maddpg.update(batch) if episode % 100 == 0: print(f"Episode {episode}, reward: {episode_reward}")

几个关键点要解释清楚。noise_scale控制探索强度,前期要大一些,让智能体尝试不同动作;后期要逐步减小,让策略稳定下来,这是一个从探索到利用的退火过程。update_times表示每次采样后更新几次参数,增加它可以加速学习,但太高会让策略在最新经验上过拟合,反而加剧震荡。env.step返回的rewards是长度为n_agents的数组,说明每个智能体收到的是独立奖励——这是博弈对抗里很关键的设计,智能体不是共享回报,而是各自最大化自己的收益。

2.3 Actor-Critic网络的参数细节:维度拼接与激活函数

network.py是理解整个算法的钥匙。Actor网络输入单个智能体的观测维度,输出对应动作空间的连续值,通常用tanh把动作压缩到[-1, 1]区间,再在外部乘上环境要求的动作缩放系数。Critic网络则完全不同,它的输入是所有智能体的观测拼接再加上所有智能体的动作拼接,输出一个标量Q值。这个拼接动作决定了Critic是否真的“集中式”。下面是常见的Actor定义:

# network.py 中的Actor网络 class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim=64): super(Actor, self).__init__() self.fc1 = nn.Linear(obs_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, act_dim) self.tanh = nn.Tanh() def forward(self, obs): # 两层ReLU隐藏层后接tanh输出,保证动作在[-1, 1]区间 x = torch.relu(self.fc1(obs)) x = torch.relu(self.fc2(x)) return self.tanh(self.fc3(x))

代码逻辑很直接:两层64维全连接加ReLU激活,最后一层输出动作维度后接tanh。注意hidden_dim是可以调的,观测维度高、博弈关系复杂时,加宽到128或256有助于表达更复杂的策略,但也会增加训练难度。Critic的定义稍复杂,因为它要处理拼接输入:

# network.py 中的Critic网络 class Critic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim=64): super(Critic, self).__init__() self.fc1 = nn.Linear(obs_dim + act_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, 1) def forward(self, obs, act): # obs和act都是包含所有智能体信息的拼接张量 x = torch.cat([obs, act], dim=-1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)

这里最容易写错的就是Critic的输入维度。obs是全部智能体观测的拼接,act是全部智能体动作的拼接,合并后过一层全连接。如果只传入自己的观测和动作,Critic就没有全局视野,退化成DDPG,非平稳性问题会原样保留。改造网络结构时,先打印每个智能体的obs_dim和act_dim,用sum()算清楚拼接后的维度,再决定fc1的输入尺寸,这个过程能避免大量返工。

提示:tanh输出范围是[-1, 1],但实际环境里的动作范围往往不是这个区间。如果追逃场景里速度控制是[0, 2],需要在Actor输出后乘缩放系数,并确保缩放逻辑与环境action_space的上界一致,否则智能体永远打不到边界动作。

3. 跑通项目的完整流程:环境配置、训练执行与结果验证

3.1 环境配置与依赖安装:pyvenv.cfg里的版本线索

项目里有一个pyvenv.cfg文件,这是Python虚拟环境的配置文件,记录了虚拟环境对应的系统Python路径和版本。看到这个文件,说明作者是在虚拟环境里开发运行的。打开它能看到Python版本线索,比如3.8还是3.9,这个信息直接决定你怎么建环境。版本不匹配是很多新手翻车的第一步,尤其是torch与Python版本不兼容的报错,错起来毫无头绪。

依赖一般集中在torch、numpy和gym或pygame这类环境库上。如果项目用自定义的mag.py作为环境,可能连gym都不需要,但要确保环境文件的底层依赖都装好。我的习惯是先用命令检查项目根目录有没有requirements.txt,没有就手动追踪import的第三方库。下面是完整的环境搭建流程:

# 用虚拟环境隔离,避免污染系统Python python -m venv maddpg_env source maddpg_env/bin/activate # Windows下用 maddpg_env\Scripts\activate pip install torch numpy gym matplotlib python test_env.py # 先跑环境自检脚本验证维度

如果test_env.py输出正常,说明环境接口没问题,再跑main.py开始训练。如果test_env.py报错,先别往下走,维度错位在训练时会被放大到极其难排查。VSCode里配置Python环境时,记得把解释器指向maddpg_env这个虚拟环境的路径,否则你装的依赖全部白费,训练时依然报ModuleNotFoundError。

3.2 训练主循环:从env.reset()到buffer回放的调用链

跑起来之前,理解main.py、MADDPG.py、buffer.py三者间的调用链,决定了你改代码时改动点落在哪里。整体流程是:环境构造完成后,main.py拿到每个智能体的观测,调用MADDPG的get_action得到动作,把动作交给env.step推进仿真,得到新观测、奖励和终止信号。这组数据被压入经验回放buffer,等积累到一定量,再抽样交给MADDPG.update更新网络参数。

buffer.py在多智能体场景下有一个很容易忽略的细节。它存的不是单条(state, action, reward, next_state),而是一组联合数据:所有智能体的观测列表、动作列表、奖励列表和终止标志。抽样时也是一次抽一整条联合数据,保证Critic计算时能看到全局信息。如果改成按单智能体方式存储和抽样,Critic拿不到其他智能体的动作,集中式训练就名存实亡。

MADDPG.py里的update函数是训练的核心,它要同时处理当前网络和目标网络两套Actor-Critic。目标网络的作用是让Q值更新的目标保持相对稳定,每隔一定步数用软更新方式缓慢拉近与当前网络的距离。软更新逻辑通常长这样:

# MADDPG.py 中的软更新逻辑 def soft_update(self, tau=0.01): for target_param, param in zip(self.actor_target.parameters(), self.actor.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data) for target_param, param in zip(self.critic_target.parameters(), self.critic.parameters()): target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data)

tau是软更新系数,取0.01是常见值,代表目标网络每次只向当前网络移动1%。tau太大会让目标Q值跟随当前网络剧烈变化,训练容易振荡;太小则目标网络更新过慢,前期收敛偏慢。不同训练阶段可以考虑调tau,前期用稍大的值加速学习,后期调小让策略稳定。训练时每隔一定步数调用一次soft_update,具体频率由main.py里的update_times间接决定。

3.3 超参数表与收敛判断:学习率、gamma、buffer容量怎么调

项目里可能已经给了一套能跑通的默认参数,但换场景后必须重新调。下表是我在多智能体博弈项目里常用的参数区间,可以直接对照:

参数常见取值影响
Actor学习率1e-3 ~ 1e-4过大策略震荡,过小收敛慢
Critic学习率1e-3 ~ 1e-4一般与Actor同量级或略大
gamma0.9 ~ 0.99越大越考虑长期收益
buffer容量1e5 ~ 1e6太小遗忘旧经验,太大采样到过时数据
batch_size128 ~ 512过小梯度噪声大,过大训练慢
噪声初始值0.1 ~ 0.3探索强度,后期衰减到0.01左右
软更新tau0.01 ~ 0.05目标网络更新速度

判断训练是否收敛,别只看loss,核心是每个智能体的回合奖励曲线。博弈对抗场景的奖励曲线通常不是单调上升的,因为对手也在变强。合理现象是曲线前期剧烈波动,中期出现上升或保持稳定,后期逐渐平稳。如果两条智能体的奖励曲线长期一高一低且不改变,说明策略已经定型,优势方没有进步空间,这时候考虑调大探索噪声或调整奖励函数。

提示:多智能体对抗的训练曲线比单智能体更“玄学”,几百回合内没上升不代表死局。先确认奖励值的量级,奖励在个位数和千位数的场景,适配的学习率完全不同。

4. 训练避坑指南:不收敛、维度错位与结果不可复现

4.1 现象:Critic loss 不降反升,Q值越估越大

如果发现critic loss一直在涨,Q值的绝对值不断膨胀,先检查奖励的数值量级。MADDPG的critic用TD误差更新,当奖励数值很大时Q值也被放大,MSE loss自然居高不下。解决办法是先对reward做归一化或缩小reward权重,让Q值维持合理范围,再考虑调学习率。

另一种常见原因是目标网络更新没有生效。检查软更新代码是否真的被调用,有没有在update函数里遗漏target_param的赋值。有人定义了target网络,但忘了在训练循环里调soft_update,结果target网络一直是随机初始化的权重,critic学得再好,target Q值也是乱跳的。排查时在迭代里打印一行target网络参数的均值,看它是否在缓慢变化即可确认。

第三种情况是buffer里存了过多终止状态样本。如果terminal后没有重置环境就继续采样,错误的done信号会进入buffer,训练时目标Q值被错误置零,策略会以为提前终止能得到更好结果,行为变得激进且不稳定。处理方式是确保每一步都能拿到正确的done标志,并在回合结束后及时重置环境。

4.2 现象:维度报错频繁,拼接张量反复对不上

多智能体项目的维度错位是我见过最多的翻车点,报错一般是RuntimeError: size mismatch。出现这类错误,第一步打印每个智能体的obs_dim和act_dim,确认观测是numpy数组而非list,list拼接和数组拼接的行为完全不同,经常在这里埋雷。

维度错位的常见原因有两个。第一,Critic输入拼接顺序不一致。训练时按[agent1_obs, agent2_obs]顺序拼接,算target Q值时换成了[agent2_obs, agent1_obs],维度虽然对但语义错乱,训练出的Q值没有意义。第二,batch维度处理出错。从buffer抽样出来是(batch, n_agents, dim)的结构,要转成(batch, n_agents * dim)再喂给网络,少一个reshape就报错。

解决思路是写一个shape断言函数,在网络forward里打印关键张量的shape,跑一个回合确认后再开始长时间训练。我在这里吃过亏:维度问题在单个回合内不暴露,要等buffer积累到一定量抽样时才触发,等触发时已经跑了几百回合,浪费时间。

4.3 现象:训练曲线震荡剧烈,时好时坏

多智能体博弈对抗的reward曲线天然是震荡的,因为每个智能体都在动态调整策略。但如果震荡到完全看不出趋势,先检查探索噪声设置。噪声过大,智能体大部分时间在随机游走,策略只是偶然碰到的好结果;噪声衰减太慢,后期策略也稳不下来。

另一个隐藏因素是buffer容量。容量过大时,早期随机策略的经验占比太高,后期采样到大量旧经验,拖慢甚至干扰当前学习;容量过小则经验多样性不足,智能体容易在局部策略里打转。我的习惯是前期用较大噪声加较小buffer,等策略有一定雏形后,逐步调大buffer保留更成熟的经验。训练过程中把噪声衰减曲线和reward曲线画在一起看,能明显看出相关性。

4.4 现象:换环境后完全不收敛

项目自带的对抗环境能跑通,但换成自己的博弈场景后学不动,先别怀疑算法,优先检查环境接口。MADDPG要求环境返回的obs和action都是连续值,如果场景里有离散动作或稀疏观测,需要先做映射或编码,把离散动作转成one-hot或连续代理值。

再看奖励函数是否提供了有效梯度。如果奖励只在回合结束时给一次,中间步骤全是0,MADDPG的critic很难学出有意义的Q值,因为TD误差只在少数几步非零。常见做法是加入势能函数或中间奖励,比如追逃博弈里根据距离变化给连续奖励,让智能体每一步都获得反馈。这是把算法从玩具环境搬到实际问题时必经的工程改造,也是课程设计拿高分和低分的分水岭。

4.5 现象:固定随机种子但结果不可复现

固定了seed但不同机器上结果差异巨大,在多智能体强化学习里很常见。原因是浮点数运算的微小偏差被策略的不稳定性不断放大。固定seed要同时设三层:random.seed、numpy.random.seed、torch.manual_seed;如果用了CUDA,还要设torch.cuda.manual_seed_all,并关闭cudnn的自动调优。

换机器后性能变化明显时,优先检查batch_size。GPU和CPU上同一个batch_size的行为不同,batch越小梯度噪声越大,对多智能体这种多损失函数叠加的训练影响更明显。其次是线程数,PyTorch的CPU并行线程会影响非确定性,设置torch.set_num_threads(1)能减少一部分波动。做实验对比时固定这些条件,比反复调参更重要,否则分不清策略改善是真实的还是随机波动。

5. 进阶玩法:把这套MADDPG迁移到自己的博弈场景

5.1 自定义环境接口:观测列表、动作列表与独立奖励

把项目自带的博弈环境换成自己的场景,核心是让环境接口符合MADDPG的约定:reset返回n_agents个观测组成的列表,step接收n_agents个动作组成的列表,返回的奖励也必须是列表。很多人在这里翻车:环境只返回全局奖励,所有智能体共享同一个reward,这会破坏博弈对抗的前提——每个智能体必须有自己的利益。

以最简单的追逃场景为例,一个追一个逃,观测是相对位置和速度,动作是水平和垂直方向的速度控制:

# 自定义追逃环境接口骨架 class PursuitEvasion: def __init__(self): self.obs_dim = [4, 4] self.act_dim = [2, 2] def reset(self): self.pos_p = np.random.uniform(-1, 1, 2) self.pos_e = np.random.uniform(-1, 1, 2) return [np.concatenate([self.pos_p, self.pos_e]), np.concatenate([self.pos_e, self.pos_p])] def step(self, actions): done = np.linalg.norm(self.pos_p - self.pos_e) < 0.1 reward_p = 1.0 if done else 0.0 reward_e = -1.0 if done else 0.0 return obs, [reward_p, reward_e], [done, done], {}

奖励塑形是迁移时最值得花时间的部分。追捕者抓住目标拿正奖励,逃跑者被抓住拿负奖励,对抗才成立。如果两个奖励都设成正数,两个智能体可能演化出“一起站着不动”的投机行为,这就是reward hacking的一种。给每个智能体设计独立奖励、适当加连续中间奖励,是提高训练质量和稳定性的关键做法。

5.2 评估与模型保存:固定种子测试和关键文件

训练完成后,评估分两个层面。数值层面固定测试回合数,统计平均回合奖励和胜率;可视化层面把test.py里的决策过程打印出来,看动作是否合理。测评估算要固定随机种子和初始位置,保证每次测试的初始条件一致,否则几次评估结果差异会误导你对模型质量的判断。

模型保存时注意,保存的应该是当前网络和target网络两个部分,否则下次加载还要重新软更新很久才能恢复。我一般在每个智能体奖励达到新高的回合保存一次actor参数,文件名带上回合数和奖励值,方便后续筛选最优版本。

从那以后,我每拿到一个MADDPG项目,第一件事不是跑训练,而是先打印main.py、MADDPG.py、network.py、buffer.py的调用链,标注每个张量的shape,确认Critic的输入确实包含全部智能体的观测和动作。这个习惯帮我避开了一堆维度错位和时间浪费。项目里的默认超参数是作者在自己的环境上调出来的,换场景后务必按前面的避坑清单重新对齐。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询