☰
MADDPG多智能体强化学习:PARL框架实战与调参避坑指南
2026/10/15 5:33:45 网站建设 项目流程

1. 从单智能体到多智能体:为什么MADDPG值得花时间啃

如果你已经跑通过DQN、PPO这类单智能体强化学习算法,第一次接触多智能体场景时大概率会有一个疑问:把多个单智能体算法拼在一起不就行了?我当初也是这么想的,直到在一个模拟协作任务里被现实狠狠教育了一顿——两个智能体各自学得挺好,凑一块儿就互相拆台,训练曲线像心电图一样上下乱跳,收敛遥遥无期。

这就是多智能体强化学习(MARL)真正棘手的地方。它和单智能体最大的区别在于:环境不再是静态的。你的对手或队友也在学习、也在改变策略,从每个智能体的视角看,环境始终处于非平稳(non-stationary)状态。单智能体算法那套"环境固定、我慢慢逼近最优策略"的假设直接失效了。

MADDPG(Multi-Agent Deep Deterministic Policy Gradient)就是为解决这类问题提出的经典算法。它基于Actor-Critic框架,把DDPG扩展到多智能体场景,核心思路是集中式训练、分布式执行(Centralized Training with Decentralized Execution,CTDE)。训练时每个Critic能看到所有智能体的观测和动作,执行时每个Actor只依赖自己的局部观测。这个设计巧妙地缓解了非平稳性问题,同时保证了部署时的可扩展性。

这篇内容我会基于PaddlePaddle的强化学习框架PARL,把MADDPG从原理到落地完整走一遍。适合两类人:一是已经懂DDPG、想进阶多智能体的同学;二是手上有PARL环境、想找个能跑通的MARL案例的开发者。我会重点讲清楚为什么这么设计、代码里每个关键点对应什么原理、以及实测中那些文档不会告诉你的坑。PARL这个框架在国内生态里算比较友好的,文档中文、示例齐全,配合飞桨的底层算子,跑多智能体实验的体验比想象中顺。

2. MADDPG的核心机制拆解:集中式Critic到底解决了什么

2.1 非平稳性问题的本质:每个智能体都在追一个移动靶

先把这个核心痛点讲透。假设有两个智能体A和B在协作。A在学策略的时候,它观察到的"环境"其实包含了B的行为。而B也在学习,B的策略每更新一次,A眼中的环境就变了一次。A基于旧环境学到的经验,放到新环境里可能完全错误。这就是非平稳性。

用个生活化的类比:你在学打乒乓球,但你的陪练每次都在改变打法。你刚适应了他的上旋球,他突然改打削球,你之前练的接上旋的手感就废了一半。单智能体场景里"陪练"是固定的(环境动力学不变),多智能体场景里陪练一直在变。

传统做法是让每个智能体把其他智能体也当成环境的一部分,用单智能体算法硬学。理论上如果学习率足够小、更新足够慢,也能收敛,但实际中训练极不稳定,样本效率低得让人抓狂。

2.2 集中式Critic:给每个智能体配一个"上帝视角"的裁判

MADDPG的解法很直接:既然问题出在每个智能体看不到全局,那就在训练时给Critic开个上帝视角。具体来说,每个智能体i都有一个对应的Critic网络Q_i,它的输入不是单个智能体的观测和动作,而是所有智能体的联合观测(o_1, o_2, ..., o_N) 和联合动作(a_1, a_2, ..., a_N)。

这样Critic评估"智能体i在當前局面下采取动作a_i有多好"时,是站在全局信息上判断的。因为联合观测和联合动作包含了其他智能体的策略信息,Critic就能感知到"如果B改了策略,A的动作价值会怎么变",从而给出更准确的梯度信号。

关键在于:这个上帝视角只在训练时用。执行阶段,每个Actor只拿自己的局部观测o_i输出动作,不需要其他智能体的信息。这就保证了部署时的去中心化——每个智能体可以独立运行,不需要实时通信。

注意:集中式Critic的输入维度会随智能体数量线性增长。N个智能体、每个观测维度为d,联合观测就是N×d维。智能体一多,Critic网络会变得很大,训练成本飙升。这是MADDPG扩展到大规模场景的主要瓶颈。

2.3 Actor-Critic的梯度传递:策略梯度怎么算出来的

MADDPG的Actor更新用的是确定性策略梯度。对智能体i,其策略网络μ_i的参数θ_i的梯度是:

∇θ_i J = E[ ∇θ_i μ_i(o_i) · ∇a_i Q_i(x, a_1, ..., a_N) | a_i=μ_i(o_i) ]

其中x是联合观测。翻译成人话:Critic告诉Actor"你在这个局面下动作往哪个方向调能拿更高分",Actor就顺着这个方向更新。因为Critic看到了全局,这个方向比单智能体场景下准得多。

Critic的更新则是标准的TD学习,用目标网络计算TD目标,最小化预测Q值和目标Q值的均方误差。目标网络的存在是为了稳定训练,避免自举(bootstrapping)带来的发散。

2.4 和DDPG的对照:改了什么,为什么这么改

对比项DDPG(单智能体)MADDPG(多智能体)
Critic输入单个观测+动作联合观测+联合动作
Actor输入单个观测单个观测(不变)
执行方式中心化去中心化
非平稳性处理无通过全局Critic缓解
网络数量1套Actor+CriticN套Actor+Critic
经验回放单条transition联合transition

从表里能看出,MADDPG对DDPG的改动集中在Critic的输入上,Actor和执行逻辑几乎没变。这种"最小改动解决核心问题"的设计思路很值得学习——不是推倒重来,而是精准打击痛点。

3. PARL框架下的代码骨架:从环境封装到训练循环

3.1 为什么选PARL:它的抽象层次刚好卡在舒服的位置

PARL是飞桨生态里的强化学习框架,它的设计哲学是"把算法逻辑和工程细节分开"。核心抽象是Model、Algorithm、Agent三层:

  • Model:定义网络结构,只管前向计算。
  • Algorithm:定义损失函数和优化逻辑,只管怎么学。
  • Agent:定义和环境交互的流程,只管怎么跑。

这个分层的好处是,你换算法时只需要改Algorithm,网络和交互流程能复用。对MADDPG这种"网络结构不复杂但训练逻辑绕"的算法来说,分层能让你把精力集中在最烧脑的梯度计算上。

另一个实际原因是PARL的中文文档和示例比较全,遇到问题搜中文资料能快速定位。飞桨的底层算子在国内网络环境下安装也顺畅,不用折腾半天环境。

3.2 环境封装:多智能体环境的接口约定

PARL对多智能体环境有一套约定。以常见的粒子环境(MultiAgentParticleEnv)为例,环境需要提供:

class MultiAgentEnv(object): def step(self, actions): # actions: list of N actions # 返回: obs_n, reward_n, done_n, info pass def reset(self): # 返回: obs_n (list of N observations) pass @property def n(self): # 智能体数量 return self.num_agents

关键点是所有返回值都是列表,第i个元素对应第i个智能体。这个约定贯穿整个训练流程,写代码时脑子里要始终清楚"我现在处理的是哪个智能体的数据"。

实测中容易踩的坑:有些环境返回的obs是numpy数组的列表,有些是单个大数组。PARL的示例通常假设是列表,如果你自己写环境,务必对齐这个格式,否则后面拼接联合观测时会报维度错误。

3.3 网络定义:Actor和Critic的输入输出设计

Actor网络很简单,输入是单个智能体的观测,输出是动作。注意输出层要用tanh把动作限制在[-1, 1],这是DDPG系列的标准做法,因为连续动作空间通常有界。

class ActorModel(parl.Model): def __init__(self, obs_dim, act_dim, hidden_dim=64): self.fc1 = nn.Linear(obs_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, act_dim) def forward(self, obs): x = F.relu(self.fc1(obs)) x = F.relu(self.fc2(x)) return paddle.tanh(self.fc3(x))

Critic网络是重点。它的输入是联合观测和联合动作,输出是一个标量Q值。实现上有两种拼法:一种是先把联合观测和联合动作各自过一层,再拼接;另一种是直接拼接后过全连接。PARL示例里常用后者,简单直接。

class CriticModel(parl.Model): def __init__(self, obs_dim, act_dim, n_agents, hidden_dim=64): total_obs = obs_dim * n_agents total_act = act_dim * n_agents self.fc1 = nn.Linear(total_obs + total_act, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, 1) def forward(self, obs_all, act_all): x = paddle.concat([obs_all, act_all], axis=-1) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) return self.fc3(x)

提示:Critic的输入维度是obs_dim * n_agents + act_dim * n_agents。智能体数量增加时这个维度增长很快,隐藏层维度要相应调整,否则容易欠拟合。

3.4 经验回放:联合transition的存储与采样

MADDPG的经验回放池存的是联合数据:(obs_n, act_n, reward_n, next_obs_n, done_n)。每个元素都是长度为N的列表。采样时一次取一个batch的联合transition,然后拆开给各个智能体用。

这里有个细节:每个智能体的Critic用的是同一批联合数据,但只取自己对应的reward。也就是说,智能体i的Critic更新时,TD目标里的reward是r_i,但输入是全局的obs和act。这个设计让每个智能体有独立的奖励信号,同时共享全局信息。

PARL里可以用ReplayBuffer,但要注意它默认存的是单智能体格式。多智能体场景下需要自己封装一层,或者直接用列表存。我实测下来,自己写一个简单的环形缓冲区反而更灵活,代码量也不大。

4. 训练流程逐行拆解:那些文档不会写的细节

4.1 联合观测的拼接顺序:一个顺序错误能让训练彻底失败

这是我最想强调的坑。Critic的输入是联合观测,拼接顺序必须严格一致:训练时怎么拼的,目标网络计算时就要怎么拼,采样时也要怎么拼。一旦顺序错乱,Critic学到的就是错误的价值函数,Actor的梯度全废。

具体来说,如果智能体0的观测拼在前面,那么所有地方都要保持"智能体0在前"。我见过有人为了图方便,用字典存观测,结果Python字典的顺序在不同版本行为不一致,训练时好时坏,排查了两天才发现是顺序问题。

建议的做法:始终用列表,按智能体索引顺序拼接。写个辅助函数统一处理:

def concat_obs(obs_n): # obs_n: list of [batch, obs_dim] return paddle.concat(obs_n, axis=-1)

所有需要联合观测的地方都调这个函数,杜绝手写拼接。

4.2 目标网络的软更新:tau取值背后的权衡

MADDPG用软更新(soft update)来更新目标网络:

θ_target = τ * θ + (1 - τ) * θ_target

τ通常取0.01或0.001。这个值怎么选?τ越大,目标网络跟得越紧,但训练越不稳定;τ越小,目标网络越稳,但学习信号滞后越严重。

我的经验是:智能体数量越多、任务越复杂,τ要取越小。两个智能体的简单协作任务,τ=0.01够用;四个以上智能体的竞争任务,建议降到0.001甚至更低。原因是多智能体场景下价值估计的方差本来就大,目标网络更新太快会放大这种方差。

4.3 探索噪声的添加:OU噪声还是高斯噪声

DDPG系列靠给动作加噪声来探索。常见的有两种:高斯噪声和OU(Ornstein-Uhlenbeck)噪声。OU噪声有时间相关性,适合惯性系统;高斯噪声独立同分布,实现简单。

PARL示例里默认用高斯噪声,通过GaussianNoise类实现。实测下来,在粒子环境这类任务里,高斯噪声完全够用,而且调参更直观——就一个标准差参数。OU噪声多了几个参数(均值回归速度、波动率),调起来反而麻烦。

噪声标准差的衰减策略也重要。训练初期需要大噪声充分探索,后期需要小噪声精细调整。我通常设一个线性衰减:从0.3降到0.05,跑个几万步。如果任务特别难,可以衰减得更慢。

4.4 训练循环的完整骨架

把上面的点串起来,训练循环大致是这样:

for episode in range(max_episodes): obs_n = env.reset() for step in range(max_steps): # 每个智能体用自己的Actor选动作,加噪声 act_n = [] for i in range(n_agents): act = agent[i].predict(obs_n[i]) act = act + noise.sample() act = np.clip(act, -1, 1) act_n.append(act) # 环境交互 next_obs_n, reward_n, done_n, _ = env.step(act_n) # 存联合transition buffer.add(obs_n, act_n, reward_n, next_obs_n, done_n) # 采样训练 if len(buffer) > batch_size: batch = buffer.sample(batch_size) for i in range(n_agents): agent[i].learn(batch, i) # 注意传入智能体索引 obs_n = next_obs_n if all(done_n): break

注意agent[i].learn(batch, i)这个设计:每个智能体的学习逻辑一样,但要从batch里取自己对应的reward和动作。传索引进去是最清晰的做法。

5. 实测中的坑与调参经验:从跑通到跑好

5.1 奖励不上升?先检查这三个地方

训练跑起来但奖励不涨,是最常见的问题。按我的排查顺序:

第一,检查联合观测的维度对不对。打印一下Critic实际收到的输入维度,和理论值对比。维度错了通常不会报错(因为全连接层能接受任意维度),但学出来的东西完全没意义。

第二,检查reward的分配。多智能体环境里,reward_n是每个智能体各自的奖励。如果你的任务里所有智能体共享一个全局奖励,那每个智能体的Critic学到的Q值会高度相关,Actor的梯度方向也会趋同,失去个体差异。这时候要考虑是否需要给不同智能体设计差异化奖励。

第三,检查噪声是否过大。噪声太大时,智能体的动作基本是随机的,Critic学不到有效信号。可以先把噪声标准差调到很小(比如0.01),看奖励能不能涨。如果能涨,说明是探索策略的问题,再慢慢加大噪声。

5.2 训练不稳定的三种表现和对策

多智能体训练不稳定是常态,具体表现和对策:

表现可能原因对策
奖励剧烈震荡学习率过大降低Actor/Critic学习率,Critic通常比Actor小一个量级
后期突然崩溃目标网络更新过快减小τ,或增大目标网络更新间隔
智能体行为趋同探索不足增大噪声,或给不同智能体设不同随机种子
收敛到次优经验回放相关性太强增大buffer容量,确保采样随机性

我个人的调参起点:Actor学习率1e-4,Critic学习率1e-3,τ=0.01,buffer容量1e6,batch size 1024。这个配置在多数中小规模任务里能跑出合理结果。

5.3 智能体数量增加时的性能悬崖

MADDPG有个明显的性能悬崖:智能体数量从2增加到4,训练难度不是线性增长,而是指数级。原因是联合观测和联合动作的维度随N线性增长,Critic的输入空间维度爆炸,需要的数据量和训练时间都大幅增加。

实测中,2个智能体的任务通常几万步能收敛,4个智能体可能要几十万步,6个以上就非常吃力了。如果任务必须用很多智能体,考虑这些方向:

  • 参数共享:如果智能体是同质的,让它们共享Actor和Critic网络,只是输入不同的局部观测。这样参数量不随N增长。
  • 局部Critic:Critic只看邻近智能体的信息,而不是全部。这牺牲了一部分全局性,但大幅降低维度。
  • 注意力机制:用注意力网络动态选择关注哪些智能体,这是MADDPG之后很多改进算法的思路。

5.4 一个容易被忽略的细节:done信号的处理

多智能体环境里,每个智能体可能有自己的done信号。比如某个智能体"死亡"了,但其他智能体还在继续。这时候怎么处理?

标准做法是:只要有一个智能体done,整个episode就结束,或者用每个智能体自己的done来mask它的TD目标。前者简单但浪费数据,后者复杂但样本效率高。

PARL示例里通常用前者。如果你追求样本效率,可以在Critic的TD目标计算里,对done的智能体把bootstrap项置零:

target_q = reward + gamma * (1 - done) * next_q

注意这里的done是每个智能体各自的,不是全局的。这个细节处理好了,训练稳定性会有明显提升。

6. 从MADDPG出发:还能往哪些方向延伸

MADDPG是2017年的工作,之后MARL领域出了不少改进。如果你已经跑通了MADDPG,这几个方向值得继续挖:

MADDPG + 参数共享:同质智能体共享网络,参数量从N套降到1套,训练速度和稳定性都提升。实现上就是把所有智能体的数据合并成一个batch训练。

MADDPG + 注意力Critic:用注意力机制让Critic动态决定关注哪些智能体的信息,而不是无差别拼接。这在智能体数量多、但交互稀疏的场景下特别有效。

MADDPG + 通信学习:让智能体之间学习交换什么信息。执行时虽然还是去中心化,但允许有限通信。这类方法在需要协调的任务里表现更好。

从MADDPG到MAPPO:MAPPO把PPO扩展到多智能体,用集中式Critic+分布式Actor的框架。相比MADDPG,PPO的on-policy特性让训练更稳定,但样本效率低一些。如果你的任务允许大量采样,MAPPO是很好的替代。

PARL框架对这些改进算法的支持在持续更新,社区里也有不少复现。我的建议是先把MADDPG吃透,理解清楚集中式训练、分布式执行这套范式的精髓,再看其他算法会顺畅很多。

最后分享一个我踩过的坑:不要一上来就调算法,先把环境跑通、把数据流打通。我见过太多人(包括我自己)在环境还没跑对的情况下就开始改网络结构、调超参数,结果浪费大量时间。正确的顺序是:环境能跑、数据格式对、单步训练不报错、然后才谈调参。这个顺序能帮你省下至少一半的调试时间。

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

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

立即咨询