4. Actor-Critic架构:从策略梯度到值函数,优势函数(Advantage)的计算

策略梯度方法我们聊完了。说白了,它像个莽夫——只知道往哪个方向走能涨奖励,但每一步该迈多大,它心里没数。值函数方法呢,又像个精算师——算得特别准,但真要它做决策,它又犹豫不决。

那能不能把两者结合起来?让莽夫带上精算师,一个负责方向,一个负责步幅。这就是Actor-Critic架构的核心思想。

4.1 为什么需要Actor-Critic?

先回顾一下策略梯度的问题。标准的REINFORCE算法,梯度估计长这样:

∇J(θ) = E[ ∇log π(a|s) * G_t ]

G_t是整个轨迹的累计回报。问题在哪?方差太大。你想想看,同一个动作,这次运气好得了高分,下次运气差得了低分,策略就被来回拉扯。我在做机械臂抓取项目时遇到过这问题——明明动作一模一样,就因为传感器噪声,回报忽高忽低,策略死活不收敛。

那怎么降方差?一个直观的想法:减去一个基线。

核心洞察:策略梯度中的G_t可以替换为"这个动作比平均好多少",而不是"这个动作得了多少分"。这个"比平均好多少"就是优势函数。

数学上,我们引入基线b(s):

∇J(θ) = E[ ∇log π(a|s) * (G_t - b(s)) ]

b(s)通常选值函数V(s)。为什么?因为V(s)恰好是"在状态s下能拿到的平均回报"。G_t - V(s)就是"这次的实际回报比平均高多少"。

嗯,这里要注意:基线不能有偏。只要b(s)与动作a无关,期望就不变。V(s)刚好满足这个条件。

4.2 Actor-Critic的两大组件

名字已经说得很明白了:

  • Actor(演员):负责做决策,输出动作。就是策略网络π(a|s)。
  • Critic(评论家):负责评价决策好不好。就是值函数网络V(s)或Q(s,a)。

两者互相促进:Actor根据Critic的反馈改进策略,Critic根据Actor的实际表现更新评价标准。有点像带徒弟——师傅(Critic)指出哪里做得不好,徒弟(Actor)照着改,改完师傅再重新评估。

组件 输入 输出 更新目标
Actor 状态s 动作a(或动作分布) 最大化优势函数
Critic 状态s(或状态-动作对) 值函数V(s)或Q(s,a) 最小化TD误差

4.3 优势函数的三种计算方式

优势函数A(s,a)的定义很简单:

A(s,a) = Q(s,a) - V(s)

意思是"这个动作在状态s下,比平均水平好多少"。但实际计算时,我们很少同时维护Q和V两个网络。太贵了。常用的方法有三种:

4.3.1 TD误差法(最简单)

用单步TD误差近似优势:

A(s,a) ≈ r + γV(s') - V(s)

这就是TD误差δ。好处是计算快,方差小。坏处是偏差大——单步估计太短视了。

我的经验:在力矩控制任务中,如果环境延迟不大,TD误差法够用。我之前做的一个倒立摆项目,用TD误差法做优势估计,收敛速度比完整蒙特卡洛快了三倍。

4.3.2 N步回报法(折中方案)

走N步再算优势:

A(s,a) ≈ (r_t + γr_{t+1} + ... + γ^{N-1}r_{t+N-1} + γ^N V(s_{t+N})) - V(s_t)

N取多少?这是个超参数。N=1退化成TD误差,N=∞退化成蒙特卡洛。我一般从N=5开始调,效果不好再加减。

4.3.3 GAE法(最推荐)

GAE(Generalized Advantage Estimation)是2015年Schulman等人提出的方法。它用指数加权平均,把不同步数的优势估计融合起来:

A^{GAE(γ,λ)} = Σ (γλ)^k * δ_{t+k}

其中δ_t = r_t + γV(s_{t+1}) - V(s_t)。λ是衰减因子,控制偏差-方差权衡:

  • λ=0:退化成TD误差(高偏差,低方差)
  • λ=1:退化成蒙特卡洛(低偏差,高方差)
  • λ=0.95:常用值,平衡得比较好

我曾经踩过的坑:GAE的λ和折扣因子γ不要搞混。γ是任务层面的折扣,λ是优势估计的平滑参数。我见过有人把λ设成0.99,γ设成0.9,结果优势估计几乎不衰减,方差爆炸。建议γ按任务设(0.99或0.995),λ从0.95开始调。

4.4 完整的Actor-Critic训练流程

来个实际代码框架。我用PyTorch风格写,但去掉了具体API,保留核心逻辑:

# 伪代码:Actor-Critic单步更新
def update(actor, critic, replay_buffer):
    # 从buffer采样一个transition
    s, a, r, s_next, done = replay_buffer.sample()

    # 1. Critic更新:最小化TD误差
    V_current = critic(s)
    V_next = critic(s_next) * (1 - done)
    TD_target = r + gamma * V_next
    critic_loss = MSE(TD_target, V_current)
    critic.optimize(critic_loss)

    # 2. 计算优势
    TD_error = TD_target - V_current
    advantage = TD_error.detach()  # 阻止梯度流入critic

    # 3. Actor更新:最大化优势
    log_prob = actor.get_log_prob(s, a)
    actor_loss = -log_prob * advantage
    actor.optimize(actor_loss)

注意第2步的.detach()。如果不做这个操作,Actor的梯度会通过advantage传到Critic,导致两个网络互相干扰。我刚开始写的时候忘了加,训练曲线像心电图一样上下乱跳。

4.5 架构图:Actor-Critic的信息流

下面这张图展示了Actor和Critic如何协同工作:

Actor-Critic 架构信息流 环境 Environment 状态 s Actor(策略网络 π) 输出动作分布 动作 a 执行动作 环境反馈 奖励 r 下一状态 s' Critic(值函数 V) 估计状态价值 Critic(值函数 V) 估计下一状态价值 优势计算 A = r + γV(s') - V(s) 或使用 GAE 多步估计 优势信号(梯度更新) Actor 用优势 更新策略参数

4.6 实际项目中的避坑指南

做了几个项目后,我总结了几条实用经验:

  1. Critic不要学得太快。Critic的更新频率应该比Actor慢,或者用更小的学习率。我习惯把Critic的学习率设为Actor的0.5倍。
  2. 优势估计的归一化。把计算出的优势做标准化(减均值除标准差),能显著提升稳定性。特别是多个环境并行采样时,不同环境的优势尺度可能差很多。
  3. 共享网络参数要小心。如果Actor和Critic共享底层特征提取层,梯度冲突会拖慢训练。我建议在力矩控制这类高维连续任务中,用两个独立的网络,或者至少用两个独立的输出头。
  4. GAE的λ别死磕0.95。我做过一个精密装配任务,环境延迟很大,λ调到0.98才有效果。多试试,别迷信默认值。

一句话总结:Actor-Critic的核心就是用Critic估计的优势函数来降低策略梯度的方差。优势函数怎么算——TD误差、N步回报、GAE——决定了偏差-方差的平衡点。选对了,训练稳如老狗;选错了,曲线原地蹦迪。