从DDPG到MADDPG:图解多智能体强化学习的进化之路

当我们谈论让机器学会协作或竞争时,多智能体强化学习(MARL)无疑是当前最激动人心的前沿之一。想象一下,一群无人机自主编队飞行,或者多个交易算法在复杂市场中动态博弈,这些场景的核心挑战在于,每个智能体的学习环境因为其他智能体的存在而变得非平稳且极其复杂。传统的单智能体算法在这里常常“水土不服”,而MADDPG(Multi-Agent Deep Deterministic Policy Gradient)的出现,为这片混沌之地带来了一束清晰的光。这篇文章不是一篇枯燥的论文复述,而是想带你像拆解一台精密仪器一样,从DDPG的基石开始,一步步看清MADDPG是如何巧妙地重构学习框架,以应对多智能体世界的独特挑战。我们会用直观的图解对比核心差异,并深入到代码层面,看看这些理论是如何落地为可运行的策略的。无论你是希望将MARL应用于机器人集群、游戏AI,还是复杂系统建模,理解这条从DDPG到MADDPG的进化路径,都将为你打开一扇新的大门。

1. 基石:为什么连续动作空间需要DDPG?

在深入多智能体之前,我们必须先理解其单智能体基础——DDPG。很多初学者会困惑,已经有处理离散动作的DQN和处理连续动作的经典策略梯度(Policy Gradient),为什么还需要DDPG?关键在于效率和稳定性。

离散动作空间的算法,比如DQN,它需要计算每个可能动作的Q值,然后选取最大值。在连续动作空间(例如机器人的关节扭矩、自动驾驶的方向盘转角),动作是无限多的,你不可能枚举所有动作去求argmax。策略梯度方法虽然能直接输出连续动作,但传统的随机策略梯度(Stochastic Policy Gradient)存在两个痛点:一是需要同时在状态和动作空间采样,数据效率较低;二是在离策略(Off-Policy)学习中,为了修正行为策略和目标策略的差异,不得不引入重要性采样(Importance Sampling),这会带来估计方差大、训练不稳定的问题。

DDPG的核心思想,是采用确定性策略(Deterministic Policy)。这意味着,给定一个状态,策略网络会直接输出一个确定的动作值,而不是一个动作的概率分布。这个转变带来了一个关键优势:策略梯度可以简化为价值函数Q对动作的梯度,再通过链式法则传递到策略参数。公式上,策略参数的更新方向变成了期望状态分布下,Q值对动作的梯度与策略对参数的梯度的乘积:

∇θ J ≈ Es~ρ [∇θ μ(s) * ∇a Q(s, a) | a=μ(s)]

这里,μ(s)是确定性策略网络,θ是其参数。你会发现,更新不再依赖于动作的采样分布,从而绕过了重要性采样。这就像从“盲猜哪个方向更好”变成了“沿着价值上升最快的方向直接迈步”,不仅计算更直接,而且在连续控制任务中表现出了卓越的稳定性和效率。

注意:确定性策略带来了探索不足的风险。DDPG的解决方案是在动作输出上添加噪声(如OU噪声),让智能体在训练中进行探索,这与探索性行为策略(如ε-greedy)的思想一脉相承,但实现方式不同。

为了稳定训练,DDPG从DQN那里借鉴了经验回放(Experience Replay) 和目标网络(Target Network) 两大法宝。它一共维护四个神经网络:

  • Actor(策略网络):输入状态,输出确定性动作。
  • Critic(价值网络):输入状态和动作,输出Q值估计。
  • Actor_target 和 Critic_target:分别是Actor和Critic的慢更新目标网络,用于计算稳定的目标Q值。

它们之间的关系和更新流程,可以通过下面这个简化的代码框架来理解:

# 伪代码示意DDPG更新核心
def update(self, batch):
    states, actions, rewards, next_states, dones = batch

    # 计算目标Q值
    next_actions = self.actor_target(next_states)
    target_q = self.critic_target(next_states, next_actions)
    target_q = rewards + self.gamma * (1 - dones) * target_q.detach()

    # 更新Critic:最小化当前Q值与目标Q值的差距
    current_q = self.critic(states, actions)
    critic_loss = F.mse_loss(current_q, target_q)
    self.critic_optimizer.zero_grad()
    critic_loss.backward()
    self.critic_optimizer.step()

    # 更新Actor:最大化Critic给出的Q值
    actor_loss = -self.critic(states, self.actor(states)).mean()
    self.actor_optimizer.zero_grad()
    actor_loss.backward()
    self.actor_optimizer.step()

    # 软更新目标网络
    soft_update(self.actor_target, self.actor, self.tau)
    soft_update(self.critic_target, self.critic, self.tau)

Actor的损失函数是负的Q值均值,这直接体现了“让Critic对Actor输出动作的评价尽可能高”这一目标。DDPG的成功,为处理连续动作空间问题提供了一个强大而优雅的框架,也为后续的多智能体扩展铺平了道路。

2. 多智能体的困境:当环境不再静止

将DDPG直接应用到多智能体场景中,我们会立刻撞上一堵墙:环境非平稳性(Non-stationarity)。在单智能体设定中,环境动态(状态转移概率和奖励函数)是固定的,或者变化缓慢。但在多智能体中,对于任何一个智能体而言,其他智能体都是其环境的一部分。当其他智能体也在学习并不断改变其策略时,从单个智能体的视角看,整个世界规则就在不停地剧烈变动。

这导致了两个直接后果:

  1. 经验回放失效:经验回放池中存储的旧经验(s, a, r, s')是在过去其他智能体策略下产生的。当其他智能体策略更新后,这些旧经验所描述的环境动态已经过时,用它们来训练当前的策略会产生误导,就像用去年的地图导航今年新修的道路。
  2. 训练极其不稳定:每个智能体都在一个移动的目标上学习。策略梯度或Q-learning的更新依赖于对环境动态的估计,而这个估计本身因为其他智能体的改变而变得不可靠,导致训练震荡,难以收敛。

我们可以用一个简单的矩阵博弈例子来直观感受。假设两个智能体玩一个协调游戏,同时选择行动A或B。如果都选A,各得+1;都选B,各得+2;否则得0。如果每个智能体独立学习,它们可能会在(A,A)和(B,B)两个均衡点之间反复振荡,因为一个智能体刚学会“选B能得高分”(假设对方也选B),对方可能又学会了别的策略,使得“选B”反而得分变低。

传统单智能体算法在这里的失败,根源在于它们将其他智能体视为环境的一部分,而忽略了其策略性(Strategic) 本质。MADDPG的突破性思路在于:如果我们能在训练时获取更多信息,是否就能“看穿”环境的非平稳性?

3. MADDPG的核心架构:集中训练,分散执行

MADDPG的答案是一个巧妙的范式:集中式训练,分布式执行(Centralized Training with Decentralized Execution, CTDE)。这个范式是整个算法灵魂所在,也是它解决环境非平稳性的关键。

  • 分布式执行:在执行(测试或部署) 阶段,每个智能体只根据自己的局部观测o_i,通过其自身的Actor网络μ_i产生动作a_i。这与传统分散式算法无异,保证了算法的可扩展性和实时性。
  • 集中式训练:在训练阶段,每个智能体拥有一个特殊的Critic网络。这个Critic的输入不再是该智能体自己的观测和动作,而是全局信息——所有智能体的观测x = (o_1, ..., o_N)和所有智能体的动作a = (a_1, ..., a_N)。

这个设计带来了革命性的好处:在训练时,由于Critic掌握了全局状态和联合动作信息,它能够准确地评估在给定全局形势下,某个智能体动作的长期价值。此时,从单个Critic的视角看,环境又变得“平稳”了,因为所有其他智能体的动作都作为已知输入提供了。因此,经验回放可以安全使用,Q-learning的更新也变得稳定。

然而,在执行时,我们并不需要这个全局Critic,每个智能体依然只依赖自己的局部观测行动。这就像一个足球队在平时训练时,教练(集中式Critic)拥有上帝视角,可以分析每个队员在整体阵型中的跑位是否合理,并给出指导。但到了正式比赛,教练不能上场指挥,队员(分散式Actor)必须依靠自己看到的局部情况(球、队友、对手位置)来做出决策,而他们决策的能力已在训练中被“教练”培养出来。

MADDPG中智能体i的Critic网络和Actor网络的更新目标如下:

Critic更新(最小化时序差分误差):

L(θ_i) = E_{(x, a, r, x')} [ (Q_i^μ(x, a_1, ..., a_N) - y)^2 ]
其中,y = r_i + γ * Q_i^{μ'}(x', a_1', ..., a_N') | a_j' = μ'_j(o_j')

这里,x是所有智能体的观测,a是所有智能体的动作,μ'是目标策略网络。

Actor更新(策略梯度):

∇ J(θ_i) ≈ E_{x, a~D} [ ∇θ_i μ_i(o_i) * ∇a_i Q_i^μ(x, a_1, ..., a_N) | a_i = μ_i(o_i) ]

Actor的梯度方向,是让Critic(基于全局信息)对自己输出动作的评价Q_i^μ尽可能高。

下面这个表格清晰地对比了DDPG与MADDPG在关键设计上的差异:

特性维度DDPG (单智能体)MADDPG (多智能体)
训练范式完全分散集中式训练,分布式执行 (CTDE)
Critic输入自身状态s,自身动作a全局观测x,联合动作a
Actor输入自身状态s自身局部观测o_i
经验回放可用且稳定可用且稳定(因Critic掌握全局信息)
应对非平稳性不涉及通过全局信息输入,将非平稳环境转化为平稳学习问题
策略类型确定性策略确定性策略(可为每个智能体添加独立探索噪声)
通信需求无仅训练时需要中心化信息,执行时无需通信

从代码层面看,MADDPG的Critic网络结构变化最为显著:

import torch.nn as nn
import torch.nn.functional as F

class MADDPGCritic(nn.Module):
    def __init__(self, n_agents, obs_dim_per_agent, act_dim_per_agent):
        super().__init__()
        # 将全局信息拼接起来
        total_obs_dim = obs_dim_per_agent * n_agents
        total_act_dim = act_dim_per_agent * n_agents
        
        self.fc1 = nn.Linear(total_obs_dim, 256)
        self.fc2 = nn.Linear(256 + total_act_dim, 256) # 将联合动作拼接进来
        self.fc3 = nn.Linear(256, 128)
        self.q_out = nn.Linear(128, 1)
        
    def forward(self, global_obs, global_acts):
        # global_obs: [batch_size, n_agents * obs_dim]
        # global_acts: [batch_size, n_agents * act_dim]
        x = F.relu(self.fc1(global_obs))
        x = torch.cat([x, global_acts], dim=-1)
        x = F.relu(self.fc2(x))
        x = F.relu(self.fc3(x))
        return self.q_out(x)

Actor网络则与DDPG中的几乎一致,只处理自己的观测。这种架构上的清晰分离,使得MADDPG既能享受集中训练带来的稳定性和高效性,又能获得分布式执行的实用优势。

4. 超越MADDPG:实践中的挑战与进阶技巧

理解了MADDPG的基本原理后,真正想把它用起来,你还会遇到几个现实的“坑”。知道这些坑在哪里,以及如何绕过它们,往往比理解理论本身更重要。

挑战一:对他人策略的强依赖 MADDPG在训练时,需要知道其他智能体的策略μ_j(或其动作a_j)来计算目标Q值。这在合作环境中,如果智能体共享参数或可以通信,是可行的。但在竞争或更一般的环境中,我们通常无法直接获取对手的策略。原论文提出了一种实用的解决方案:策略推断(Policy Inference)。即为每个其他智能体维护一个近似策略网络(例如一个循环神经网络),通过观察其历史观测和动作来在线学习并预测其策略。这样,Critic在计算目标Q值时,可以使用预测的动作来代替真实的未知动作。

挑战二:策略过拟合与鲁棒性 即使知道了其他智能体的策略,训练出的策略也可能过度适应训练时遇到的特定对手策略,一旦对手策略改变(例如从合作变为竞争),性能会急剧下降。提升鲁棒性的一种有效方法是集成策略(Ensemble Policy)。即为每个智能体训练多个策略子网络,在每一个训练回合中随机选择一个子策略来执行。这相当于让智能体见识了更多样化的“对手行为”,从而学到一个更通用、更稳健的策略。在实践中,即使只集成2-3个策略,也能带来显著的鲁棒性提升。

挑战三:信用分配(Credit Assignment) 在合作任务中,当团队获得一个全局奖励时,如何将功劳合理地分配给每个智能体的个体动作,是一个经典难题。MADDPG的全局Critic天然地部分解决了这个问题,因为它评估的是联合动作下的个体Q值。我们可以进一步设计反事实基线(Counterfactual Baseline) 来优化信用分配。思路是:计算智能体i采取实际动作a_i时的Q值,与假设其采取一个默认基线动作时的Q值之差。这个差值更能纯粹地反映智能体i动作的贡献。在MADDPG框架下,这可以通过修改Critic的结构或Actor的梯度计算来实现。

挑战四:可扩展性与计算成本 随着智能体数量N增加,Critic网络的输入维度(N*(obs_dim+act_dim))线性增长,可能导致训练困难。对于大规模智能体系统,可以考虑以下技巧:

  • 函数分解:设计一个结构化的Critic,例如先对智能体两两之间的交互建模,再聚合。
  • 注意力机制:让Critic使用注意力机制来聚焦于当前最重要的其他智能体信息,而不是简单拼接所有信息。
  • 参数共享:在合作环境中,所有智能体可以共享同一个Actor网络和Critic网络(但输入仍是各自的观测和全局信息),这能极大减少参数量并促进学习。

在实际编码中,一个健壮的MADDPG训练循环需要仔细处理经验收集、批量采样、多网络更新顺序等问题。下面是一个简化但关键的核心训练步骤示意:

def train_maddpg(agents, replay_buffer, episode):
    # 假设已用agents与环境交互,将数据存入replay_buffer
    if len(replay_buffer) < batch_size:
        return
    
    # 采样批量经验
    batch = replay_buffer.sample(batch_size)
    # batch中包含:obs_n(所有智能体观测),act_n,rew_n, next_obs_n, done_n
    
    # 为每个智能体准备全局信息
    global_obs = torch.cat(batch.obs_n, dim=1)      # [batch, total_obs_dim]
    global_acts = torch.cat(batch.act_n, dim=1)     # [batch, total_act_dim]
    global_next_obs = torch.cat(batch.next_obs_n, dim=1)
    
    # 更新每个智能体
    for i, agent in enumerate(agents):
        # 1. 更新Critic
        # 计算目标动作(使用目标Actor网络)
        next_acts = []
        for j in range(n_agents):
            next_acts.append(agents[j].actor_target(batch.next_obs_n[j]))
        target_global_acts = torch.cat(next_acts, dim=1)
        
        # 计算目标Q值
        target_q = agent.critic_target(global_next_obs, target_global_acts)
        target_q = batch.rew_n[i] + gamma * (1 - batch.done_n) * target_q.detach()
        
        # 计算当前Q值并更新Critic
        current_q = agent.critic(global_obs, global_acts)
        critic_loss = F.mse_loss(current_q, target_q)
        agent.critic_optimizer.zero_grad()
        critic_loss.backward()
        # 可能需要对梯度进行裁剪,防止爆炸
        torch.nn.utils.clip_grad_norm_(agent.critic.parameters(), max_grad_norm)
        agent.critic_optimizer.step()
        
        # 2. 更新Actor
        # 重新计算当前全局动作,但用智能体i的Actor输出替换其动作
        new_acts = []
        for j in range(n_agents):
            if j == i:
                # 智能体i使用其Actor网络计算新动作
                new_act = agent.actor(batch.obs_n[i])
            else:
                # 其他智能体动作从缓冲区取(或使用推断的策略)
                new_act = batch.act_n[j].detach() # 注意detach
            new_acts.append(new_act)
        new_global_acts = torch.cat(new_acts, dim=1)
        
        # 计算Actor损失:最大化Critic对当前新联合动作的Q值评价
        actor_loss = -agent.critic(global_obs, new_global_acts).mean()
        agent.actor_optimizer.zero_grad()
        actor_loss.backward()
        torch.nn.utils.clip_grad_norm_(agent.actor.parameters(), max_grad_norm)
        agent.actor_optimizer.step()
        
        # 3. 软更新目标网络
        soft_update(agent.actor_target, agent.actor, tau)
        soft_update(agent.critic_target, agent.critic, tau)

调试MADDPG时,我习惯先在一个简单的、可验证的环境(如simple_spread)中运行,确保基础逻辑正确。然后,密切监控每个智能体的奖励曲线和Critic损失。如果Critic损失一直居高不下或剧烈波动,往往是学习率过高、网络结构不合理或探索噪声设置不当的信号。多智能体训练就像指挥一个乐队,需要耐心地调整每个“乐手”(智能体)的参数,才能最终奏出和谐的乐章。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐