跳转到正文

7.2 Actor-Critic 同步更新

前两节我们认识了优势函数 Critic 的训练方法。现在让我们把所有零件组装起来,看看 Actor 和 Critic 是如何协作的。

本节会用到的前置知识

从 REINFORCE 到 Actor-Critic

回顾第 6 章 REINFORCE 的梯度公式(回顾:策略梯度定理):

是完整轨迹的累积回报——这就是 REINFORCE 方差大的根源。第 6 章的基线分析告诉我们,减掉 可以降方差。上一节我们又发现,不需要等 episode 结束——用TD Error 就能替代 作为优势估计:

这一替换带来的改变是根本性的:

REINFORCEActor-Critic
优势估计(MC,需要完整轨迹)(TD,走一步就更新)
更新时机episode 结束后每走一步
方差
偏差无偏有偏(自举引入偏差)
代价需要训练 Critic

同一个场景下的两种更新

设 CartPole 环境中,智能体在时刻 处于状态 ,选择动作"向右"(),之后连续交互 5 步后 episode 结束。具体轨迹如下:

时刻状态动作奖励
right1.0
right1.0
left1.0
right1.0
right1.0

取折扣因子

REINFORCE 的计算。 REINFORCE 必须等 episode 结束后才能更新。它从时刻 开始计算完整回报

这个 就是策略梯度的权重。设当前策略 下选 right 的概率为 ,则对数概率为

策略梯度更新为

问题在于:换一条轨迹, 可能是 1.0(只走了 1 步就倒了),也可能是 10.0(走了很久)。 的波动直接传导到梯度上,这就是 REINFORCE 方差大的来源。

REINFORCE 公式符号表

符号含义
策略参数 的对数概率梯度,指示参数该往哪个方向调
从时刻 到 episode 结束的完整折扣回报
时刻 获得的即时奖励
折扣因子,控制未来奖励的衰减速度

Actor-Critic 的计算。 Actor-Critic 不等 episode 结束。假设 Critic 对当前状态和下一状态的估计为 。走了一步,拿到即时奖励 ,立刻可以计算 TD Error:

,说明这一步的结果比 Critic 原本的预期要好。这个正的 TD Error 直接作为优势估计:

用同样的 ,梯度的数量级与 REINFORCE 的相近,但权重不再是整条轨迹的累积回报,而是一步 TD Error。 的波动范围远小于 ——因为它只包含一步的真实随机性,而非整条轨迹的随机性叠加。

Actor-Critic 公式符号表

符号含义
策略参数 的对数概率梯度
TD Error,作为优势 的一步估计
这一步拿到的即时奖励
折扣后的下一状态价值估计(Critic 对未来的预测)
Critic 对当前状态的价值估计(用作基线)

两种方法的核心区别可以总结为一张对比表:

计算步骤REINFORCEActor-Critic
更新前提episode 结束,拿到完整轨迹走一步,拿到
优势估计(5 步累积回报)(一步 TD Error)
梯度权重受整条轨迹随机性影响只受一步随机性影响
需要的额外组件Critic 提供
每步的计算量小(无网络前向传播)大(Critic 要多算一次前向)

Actor-Critic 架构

把优势函数和 Critic 训练整合起来,就得到了强化学习中最经典的架构。Actor 负责选择动作,Critic 负责评估动作的好坏,两者通过优势函数 协作:

Actor-Critic 数据流

  状态 s

    ├──→ Actor(策略网络)
    │      π(a|s) → 选动作 a
    │                  │
    │              执行动作 a
    │                  │
    │                  ▼
    │              环境 → 返回 r, s'
    │                  │
    ├──→ Critic(价值网络)  │
    │      V(s)  ──────────┤
    │      V(s') ──────────┤
    │                      │
    │      δ = r + γV(s') - V(s)
    │            │
    │            ▼
    │      Actor 更新:θ ← θ + α·∇log π(a|s)·δ
    │      Critic 更新:V(s) ← V(s) + α·δ

    └──→ 下一步,重复以上过程

两个网络共享同一个输入(状态 ),但各做各的事:

网络角色输入输出学习目标
Actor(演员)选择动作状态 动作概率 最大化累积奖励
Critic(评论家)评估局面状态 价值估计 准确预测未来回报

如果你仔细看 Critic 的更新规则,——这不就是第 3 章的 TD Learning 吗?Critic 本质上就是第 3 章价值函数 的神经网络实现,它独立地学习"每个状态值多少分"。Actor 则是策略 的神经网络实现,它根据 Critic 提供的评估来调整自己的行为。

两个函数逼近器协同工作——Critic 帮 Actor 判断"这个动作比平均好多少",Actor 根据判断调整策略,然后新的策略又产生新的数据让 Critic 学得更好。这就是 Actor-Critic 名字的由来。

一步更新的完整数值推导

下面用一个具体场景走完 Actor-Critic 的一步更新。设 CartPole 中某时刻的状态向量为 。当前模型参数为 ,前向传播后 Actor 和 Critic 分别输出:

组件输出数值
Actor动作概率
Critic状态价值

其中

第 1 步:采样动作。 按概率采样得到 (第 2 个动作)。对应的对数概率:

第 2 步:执行动作,获取转移。 环境返回即时奖励 ,下一状态

第 3 步:Critic 评估下一状态。 输入 Critic(注意此时不计算梯度):

第 4 步:计算 TD 目标与 TD Error。

——这一步实际拿到的好于 Critic 的预期,说明"在 下选 right"是一个比平均更好的选择。

第 5 步:计算 Actor Loss。

注意 被标记为 .detach()——它作为常量参与 Actor Loss,不对 Critic 反传梯度。

Actor Loss 公式符号表

符号含义
Actor 的损失函数,对其求梯度等价于策略梯度
所选动作的对数概率, 的可微函数
TD Error,作为优势估计,不参与对 Actor 的梯度计算
负号让梯度上升变为梯度下降:最小化 等价于最大化

第 6 步:计算 Critic Loss。

这是均方误差形式——让 尽可能接近 TD 目标

Critic Loss 公式符号表

符号含义
Critic 的损失函数,驱动 逼近 TD 目标
TD Error,其中 参与 Critic 的梯度计算
平方确保正负误差都产生正的损失,且大误差的惩罚更重

第 7 步:总损失与反向传播。

反向传播时,梯度流向两路:

  • Actor 路径 作为常量,只调节梯度的幅度和方向—— 时增大 right 的概率, 时减小。
  • Critic 路径 的可微函数,梯度直接调整 Critic 的预测使其更接近 TD 目标。

一步更新的完整计算链如下:

步骤输入计算输出
前向
采样
环境
评估
TD
损失

用 PyTorch 实现 Actor-Critic

Actor-Critic 的代码比 REINFORCE 多了一个 Critic 网络,但结构依然清晰:

python
import torch
import torch.nn as nn
import torch.optim as optim
import gymnasium as gym
import numpy as np

# ==========================================
# 1. Actor-Critic 网络(共享特征提取层)
# ==========================================
class ActorCritic(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        # 共享的特征提取层
        self.shared = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
        )
        # Actor 头:输出动作概率
        self.actor = nn.Sequential(
            nn.Linear(128, action_dim),
            nn.Softmax(dim=-1)
        )
        # Critic 头:输出状态价值
        self.critic = nn.Linear(128, 1)

    def forward(self, x):
        features = self.shared(x)
        action_probs = self.actor(features)
        state_value = self.critic(features)
        return action_probs, state_value

# ==========================================
# 2. 训练循环(每步更新,不需要等 episode 结束)
# ==========================================
env = gym.make("CartPole-v1")
model = ActorCritic(state_dim=4, action_dim=2)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
gamma = 0.99

reward_history = []

for episode in range(500):
    state, _ = env.reset()
    total_reward = 0

    while True:
        state_t = torch.FloatTensor(state)

        # Actor 选动作,Critic 评估状态
        probs, value = model(state_t)
        dist = torch.distributions.Categorical(probs)
        action = dist.sample()
        log_prob = dist.log_prob(action)

        # 执行动作
        next_state, reward, terminated, truncated, _ = env.step(action.item())
        done = terminated or truncated
        total_reward += reward

        # Critic 评估下一个状态
        with torch.no_grad():
            _, next_value = model(torch.FloatTensor(next_state))
            next_value = 0 if done else next_value

        # TD Error = 优势估计(回顾:第 6.1 节 A ≈ δ)
        td_target = reward + gamma * next_value
        td_error = td_target - value

        # Actor 损失:策略梯度 × 优势
        actor_loss = -log_prob * td_error.detach()

        # Critic 损失:让 V(s) 接近 TD Target(回顾:第 6.2 节 L = δ²)
        critic_loss = td_error.pow(2)

        # 总损失
        loss = actor_loss + critic_loss

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        state = next_state
        if done:
            break

    reward_history.append(total_reward)
    if (episode + 1) % 50 == 0:
        avg = np.mean(reward_history[-50:])
        print(f"Episode {episode+1} | Avg Reward: {avg:.1f}")

和第 6 章的 REINFORCE 代码相比,关键区别是:多了一个 Critic 网络(输出 ),用 TD Error(td_target - value)替代了 ,Critic 有自己的损失函数(MSE),而且不需要跑完 episode 才更新。

代码的数值追踪 与 一个完整的训练步

下面假设模型处于某个训练阶段,追踪一次完整的循环。设当前状态 ,折扣因子

前向传播。state_t = torch.FloatTensor([0.1, 0.2, -0.3, 0.4]) 输入模型:

python
probs, value = model(state_t)
# probs = tensor([0.6000, 0.4000])   ← Actor 输出 与 left 概率 0.6, right 概率 0.4
# value = tensor(1.2000)             ← Critic 输出 与 V(s) = 1.2

采样动作与对数概率。

python
dist = torch.distributions.Categorical(probs)
action = dist.sample()           # action = tensor(1),即 right
log_prob = dist.log_prob(action) # log_prob = log(0.4) = tensor(-0.9163)

环境交互。 执行 action.item() = 1(right):

python
next_state, reward, terminated, truncated, _ = env.step(action.item())
# reward = 1.0
# terminated = False, truncated = False

评估下一状态。

python
with torch.no_grad():
    _, next_value = model(torch.FloatTensor(next_state))
    # next_value = tensor(2.0000)    ← V(s') = 2.0
    # done = False, 所以 next_value 不被置零

计算 TD 目标与 TD Error。

python
td_target = reward + gamma * next_value  # = 1.0 + 0.99 * 2.0 = tensor(2.9800)
td_error  = td_target - value            # = 2.98 - 1.2 = tensor(1.7800)

计算两个损失。

Actor Loss(.detach() 切断梯度,作为常数参与计算):

python
actor_loss = -log_prob * td_error.detach()  # = -(-0.9163) * 1.78 = tensor(1.6310)

Critic Loss( 包含 ,梯度通过 反传到 Critic 参数):

python
critic_loss = td_error.pow(2)  # = 1.78^2 = tensor(3.1684)

总损失。

python
loss = actor_loss + critic_loss  # = tensor(4.7994)

反向传播与参数更新。 loss.backward() 计算梯度后,optimizer.step() 按学习率 更新参数。这次更新的效果:

  • Actor 方向,说明选 right 比预期好。梯度上升会增大 ——下次遇到类似状态时更倾向选 right。
  • Critic 方向 低于 TD 目标 的梯度会拉高 ,使其更接近

整个计算链的关键数值汇总:

变量含义
probs[0.6, 0.4]Actor 对两个动作的概率分布
value1.2Critic 对当前状态的估计
log_prob-0.9163所选动作 right 的对数概率
reward1.0环境返回的即时奖励
next_value2.0Critic 对下一状态的估计
td_target2.98
td_error1.78
actor_loss1.6310(.detach 后)
critic_loss3.1684
loss4.7994

CartPole 上的 Actor-Critic 训练曲线

Actor-Critic 在 CartPole 上的训练曲线

 500 ┤
     │                              ━━━━━━━━━━━━━━━
 400 ┤                         ━━━━
     │                    ━━━━
 300 ┤              ━━━━━
     │         ━━━━
 200 ┤    ━━━━
     │ ━━
 100 ┤╱
     └────────────────────────────────────────────
     0    50   100  150  200  250  300  350  400  450  500
                    Episode

 对比 REINFORCE 的典型曲线(更多锯齿、更慢收敛)

Actor-Critic 在 CartPole 上通常在 200-300 个 episode 内就能稳定到 500 分(满分),而 REINFORCE 可能需要 500+ episode 且曲线锯齿明显。这就是"用偏差换方差"的收益——每一步都有更稳定的梯度信号,策略更新不再被运气牵着走。

Actor-Critic 的后续演进

Actor-Critic 不是终点,而是一个骨架。后续章节中你会看到它的各种变体:

章节变体关键改进
第 8 章 PPOPPO-Clip限制策略更新幅度,防止"步子迈太大"
第 8 章 GAE广义优势估计多步 TD Error 的指数加权和,精确控制偏差-方差权衡
第 15 章 DPO隐式 Actor-Critic用偏好数据替代 Critic,去掉 on-policy 的限制
第 16 章 GRPO去掉 Critic用组内均值替代 ,省掉一个网络

所有的变体都共享同一个骨架:一个负责选择的网络 + 一个负责评估的信号。变化的只是"评估信号怎么来"和"选择网络怎么更新"。

思考题:既然 Actor-Critic 比 REINFORCE 好,为什么不用纯 Critic(只用 V)?

因为只有 Critic 没办法直接输出策略。Critic 学的是 ,从中推导策略需要用 (回顾:贪心最优策略)——但在连续动作空间中,这个 不存在解析解(你不可能对无限多个连续值逐一比较)。

Actor 的价值在于:它直接输出动作概率,天然适用于连续动作空间。这就是为什么需要两个网络——Critic 负责"评价",Actor 负责"选择",缺一不可。

思考题:Actor-Critic 的"偏差"从哪来?它有害吗?

偏差来自 Critic 的自举(Bootstrapping)——Critic 用自己的估计 来更新 。如果 本身就不准确,误差会传播回来。这就像你用一把不准的尺子去校准另一把尺子——误差会累积。

但这种偏差不一定是坏事。适度的偏差可以换来更低的方差,整体上可能比无偏但高方差的 REINFORCE 收敛更快。第 8 章的 GAE 就是在精确控制这个"偏差-方差权衡"——用参数 在纯 TD(高偏差低方差)和纯 MC(无偏高方差)之间平滑插值。

现在让我们看看 Actor-Critic 架构在大规模应用中的表现——Actor-Critic 的前沿大规模应用


现代强化学习实战课程