4、Actor-Critic方法:A2C算法原理、A2C代码实现、GAE(广义优势估计)、A3C异步架构

好,咱们今天聊聊 Actor-Critic。说实话,这个系列的方法在强化学习里地位很特殊。它既不像纯 Policy Gradient 那样慢吞吞,也不像 DQN 那样只盯着价值。它把两者揉在一起,取长补短。我个人觉得,这是入门深度强化学习必须跨过的一道坎。

4.1 从 Policy Gradient 到 Actor-Critic

先回忆一下 Policy Gradient。它的核心公式很简单:∇J(θ) = E[∇log π(a|s) * Q(s,a)]。说白了,就是让好动作出现的概率变大,坏动作变小。

但这里有个问题。Q(s,a) 怎么算?在 REINFORCE 算法里,我们用一整条轨迹的累计回报 G_t 来近似。你想想看,这方差得多大?我刚开始跑实验的时候,那个 loss 曲线抖得跟心电图似的,根本没法收敛。

后来有人想了个办法:我们能不能用另一个网络来估计 Q(s,a)?这样方差不就小了吗?嗯,这就是 Actor-Critic 的雏形。

核心思想:Actor 负责选动作(策略网络),Critic 负责打分(价值网络)。两者互相促进,共同进步。

4.2 A2C 算法原理

A2C 全称是 Advantage Actor-Critic。它比原始的 Actor-Critic 多了一个东西——优势函数。

为什么要用优势?因为直接用 Q(s,a) 作为更新信号,会有一个基线偏移的问题。举个例子,某个状态下所有动作的 Q 值都很大,但相对大小才是关键。优势函数 A(s,a) = Q(s,a) - V(s) 正好解决了这个问题。

我当年在做一个机器人抓取项目时,就踩过这个坑。直接用 Q 值训练,Actor 死活学不会精准抓取。换成优势函数后,效果立竿见影。为什么呢?因为优势函数告诉 Actor:「这个动作比平均水平好多少」,而不是「这个动作值有多大」。

A2C 的更新流程大致如下:

  1. 用当前策略与环境交互,收集 n 步经验
  2. 计算每一步的优势估计 A(s,a)
  3. 用优势更新 Actor:∇J(θ) = E[∇log π(a|s) * A(s,a)]
  4. 用 TD 误差更新 Critic:L(φ) = E[(R + γV(s') - V(s))²]

我的习惯:每次更新前,先把 Critic 的梯度做一次裁剪。不然偶尔遇到一个异常大的 TD 误差,整个网络就崩了。

4.3 A2C 代码实现

下面给一个精简版的 A2C 实现。注意,这里只展示核心逻辑,完整代码需要配合环境一起跑。

import torch
import torch.nn as nn
import torch.optim as optim

class ActorCritic(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.fc = nn.Linear(state_dim, 128)
        self.actor = nn.Linear(128, action_dim)
        self.critic = nn.Linear(128, 1)

    def forward(self, x):
        x = torch.relu(self.fc(x))
        action_probs = torch.softmax(self.actor(x), dim=-1)
        value = self.critic(x)
        return action_probs, value

def train_a2c(env, model, optimizer, gamma=0.99, n_steps=5):
    for episode in range(1000):
        state = env.reset()
        log_probs = []
        values = []
        rewards = []

        for step in range(n_steps):
            state_tensor = torch.FloatTensor(state)
            probs, value = model(state_tensor)
            dist = torch.distributions.Categorical(probs)
            action = dist.sample()

            next_state, reward, done, _ = env.step(action.item())
            log_probs.append(dist.log_prob(action))
            values.append(value)
            rewards.append(reward)

            state = next_state
            if done:
                break

        # 计算优势
        _, next_value = model(torch.FloatTensor(next_state))
        returns = []
        R = next_value.item() if not done else 0
        for r in reversed(rewards):
            R = r + gamma * R
            returns.insert(0, R)

        returns = torch.tensor(returns)
        values = torch.cat(values).squeeze()
        advantages = returns - values.detach()

        # 更新
        actor_loss = -(torch.cat(log_probs) * advantages).mean()
        critic_loss = (returns - values).pow(2).mean()
        loss = actor_loss + 0.5 * critic_loss

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

我曾经踩过的坑:Critic 的 loss 权重设太大,导致 Actor 学不动。建议 Critic loss 系数设在 0.5 左右,别超过 1.0。

4.4 GAE(广义优势估计)

GAE 是什么?说白了,它是对优势函数的一种平滑处理。你想想看,n 步回报的 n 到底取多少?取小了偏差大,取大了方差大。GAE 用了一个指数加权平均,把不同步数的优势估计融合起来。

公式长这样:A_GAE = Σ(λγ)^k * δ_{t+k},其中 δ 是 TD 误差。

这里 λ 是个超参数。λ=0 时退化成一步 TD,λ=1 时退化成蒙特卡洛。我一般取 0.95,效果比较稳。

λ 取值 特点 适用场景
0 低方差,高偏差 任务简单,步数少
0.95 平衡 大多数任务
1 高方差,低偏差 任务复杂,需要长期记忆

我记得有一次调参,λ 设成 0.99,结果训练曲线抖得飞起。后来改成 0.95,立马稳了。所以说,GAE 的 λ 是个很敏感的参数,别乱调。

4.5 A3C 异步架构

A3C 是 A2C 的异步版本。它开了多个 worker,每个 worker 独立与环境交互,然后把梯度异步传回全局网络。

为什么要异步?因为这样可以打破数据相关性,让训练更稳定。我当年在 8 核机器上跑 A3C,速度确实比单线程快不少。但要注意,A3C 的收敛性对 worker 数量很敏感。我试过 16 个 worker,反而比 8 个 worker 效果差。

后来大家发现,A2C 用同步方式也能达到类似效果,而且实现更简单。所以现在 A2C 反而更流行。但 A3C 的思想——多线程并行采样——对后来的 IMPALA 等算法影响很大。

一句话总结:A2C 是同步版,A3C 是异步版。实际项目中,我建议先从 A2C 开始,跑通了再考虑 A3C。

4.6 本章知识体系

下面这张图展示了 Actor-Critic 方法的核心脉络。从 Policy Gradient 出发,经过 A2C 的改进,再到 GAE 的平滑处理,最后到 A3C 的异步架构。每一步都是为了解决前一步的问题。

Policy Gradient Actor-Critic A2C (优势函数) GAE (广义优势) A3C (异步架构) 引入 Critic 加入优势函数 平滑优势估计 多线程并行

嗯,这一章的内容就这些。Actor-Critic 是个大家族,A2C 和 A3C 只是入门。后面还有 PPO、SAC 这些更厉害的算法。但基础打牢了,后面学起来就快了。

我的建议:动手写代码的时候,先别管 GAE 和 A3C。把最基础的 A2C 跑通,然后慢慢往上加东西。一口吃不成胖子,强化学习更是如此。

可可精品资料库