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 的更新流程大致如下:
- 用当前策略与环境交互,收集 n 步经验
- 计算每一步的优势估计 A(s,a)
- 用优势更新 Actor:
∇J(θ) = E[∇log π(a|s) * A(s,a)] - 用 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 的异步架构。每一步都是为了解决前一步的问题。
嗯,这一章的内容就这些。Actor-Critic 是个大家族,A2C 和 A3C 只是入门。后面还有 PPO、SAC 这些更厉害的算法。但基础打牢了,后面学起来就快了。
我的建议:动手写代码的时候,先别管 GAE 和 A3C。把最基础的 A2C 跑通,然后慢慢往上加东西。一口吃不成胖子,强化学习更是如此。
可可精品资料库