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如何协同工作:
4.6 实际项目中的避坑指南
做了几个项目后,我总结了几条实用经验:
- Critic不要学得太快。Critic的更新频率应该比Actor慢,或者用更小的学习率。我习惯把Critic的学习率设为Actor的0.5倍。
- 优势估计的归一化。把计算出的优势做标准化(减均值除标准差),能显著提升稳定性。特别是多个环境并行采样时,不同环境的优势尺度可能差很多。
- 共享网络参数要小心。如果Actor和Critic共享底层特征提取层,梯度冲突会拖慢训练。我建议在力矩控制这类高维连续任务中,用两个独立的网络,或者至少用两个独立的输出头。
- GAE的λ别死磕0.95。我做过一个精密装配任务,环境延迟很大,λ调到0.98才有效果。多试试,别迷信默认值。
一句话总结:Actor-Critic的核心就是用Critic估计的优势函数来降低策略梯度的方差。优势函数怎么算——TD误差、N步回报、GAE——决定了偏差-方差的平衡点。选对了,训练稳如老狗;选错了,曲线原地蹦迪。