第三章:从Actor-Critic到A2C/A3C:为PPO铺路
在上一章中,我们实现了一个纯粹的策略梯度算法REINFORCE。我们亲眼见证了它的“喜怒无常”——如同一个醉汉跌跌撞撞地下山,虽然大方向正确,但过程极其不稳定,训练效果方差巨大。这种不稳定性是纯策略梯度方法的“原罪”。
那么,我们能否给这位“醉汉”请一位“向导”呢?一个能在他每次迈步前,告诉他这一步是好是坏,好多少,坏多少的“聪明人”?
答案是肯定的。这,就是Actor-Critic(演员-评论家) 方法的核心思想。本章,我们将一起探索这个强大的框架,它不仅是REINFORCE的完美升级,更是通往我们最终目标PPO的必经之路。
3.1 引入“裁判”:Actor(演员)和Critic(评论家)的分工与合作
想象一个正在排练话剧的剧团。
- 演员(Actor): 他的任务是表演,即根据剧本(环境状态)做出具体的动作和表情(动作)。他的目标是学习到一套最佳的表演策略(Policy)。
- 评论家(Critic): 他的任务是观看演员的表演,并给出评价。他不会告诉演员具体该怎么做,而是评价演员在“当前场景下(State)”的“这句台词(Action)”表现得“好不好(Value)”。
在强化学习中,这个架构被完美地借鉴了过来:
- Actor(演员): 就是我们之前熟悉的策略网络(Policy Network)。它负责对外输出动作,与环境进行交互。它的输入是状态sss,输出是动作的概率分布π(a∣s)\pi(a|s)π(a∣s)。
- Critic(评论家): 是一个全新的网络,我们称之为价值网络(Value Network)。它不产生动作,只负责“打分”。它的输入是状态sss,输出一个标量值V(s)V(s)V(s),用于评估当前状态的价值高低。
它们如何分工与合作?
整个流程就像这样:
- Actor表演:在状态 sts_tst 时,Actor根据自己当前的策略π\piπ选择一个动作ata_tat。
- 环境反馈:环境执行动作ata_tat后,给出奖励rtr_trt和新的状态st+1s_{t+1}st+1。
- Critic评价:现在轮到Critic出场了。它会对Actor的表现进行评判。但怎么评判才最有效呢?这正是Actor-Critic方法的精髓所在,我们下一节会详细讲解。
- Actor学习:Actor根据Critic的“评价”来更新自己的策略。如果Critic说“刚才那个动作很好”,Actor就会增大在那个状态下选择该动作的概率;反之,则减小概率。
为什么要引入Critic?
回顾一下REINFORCE算法的更新公式:∇J(θ)≈Gt∇logπ(at∣st,θ)\nabla J(\theta) \approx G_t \nabla \log \pi(a_t|s_t, \theta)∇J(θ)≈Gt∇logπ(at∣st,θ)。
我们用整个回合的总回报GtG_tGt来指导更新。这带来一个巨大的问题:高方差。即便GtG_tGt是一个正数(比如10),但可能在当前状态sts_tst下,采取任何动作的平均回报都能达到20。那么这个Gt=10G_t=10Gt=10其实是个相对“差”的结果,但算法却依然会用一个正数去“奖励”这个动作。
Critic的出现,就是为了提供一个更稳定、更可靠的基准(Baseline)。它学习到的状态价值函数V(st)V(s_t)V(st),代表了在状态sts_tst的“平均表现”。我们不再关心动作ata_tat的绝对好坏(GtG_tGt),而是关心它的相对好坏——即它比“平均表现”好多少?
这种“相对好坏”的度量,就是我们下一节的主角——优势函数。
3.2 优势函数(Advantage Function)A(s,a)A(s, a)A(s,a):衡量动作“有多好”的更精确标尺
优势函数A(s,a)A(s, a)A(s,a)的定义非常直观:
A(s,a)=Q(s,a)−V(s)A(s, a) = Q(s, a) - V(s)A(s,a)=Q(s,a)−V(s)
- Q(s,a)Q(s, a)Q(s,a):在状态sss下,执行动作aaa后,期望能获得的总回报(动作价值)。
- V(s)V(s)V(s):在状态sss下,遵循当前策略,期望能获得的总回报(状态价值)。
所以,优势函数的意义就是:在状态 sss 下,执行动作 aaa 相比于平均水平,到底有多大的“优势”。
- 如果A(s,a)>0A(s, a) > 0A(s,a)>0,说明动作aaa比平均表现要好,值得鼓励。
- 如果A(s,a)<0A(s, a) < 0A(s,a)<0,说明动作aaa比平均表现要差,需要抑制。
现在,我们用优势函数A(st,at)A(s_t, a_t)A(st,at)来替换REINFORCE中的总回报GtG_tGt,作为指导Actor更新的信号。策略梯度的更新就变成了:
∇J(θ)≈A(st,at)∇logπ(at∣st,θ)\nabla J(\theta) \approx A(s_t, a_t) \nabla \log \pi(a_t|s_t, \theta)∇J(θ)≈A(st,at)∇logπ(at∣st,θ)
这在直觉上就合理多了!我们终于有了一个精确的标尺来衡量每个动作的“真正”好坏。
但问题来了:为了计算优势函数A(s,a)A(s, a)A(s,a),我们似乎需要知道Q(s,a)Q(s, a)Q(s,a)和V(s)V(s)V(s)。难道我们要为此训练两个网络吗?一个Q网络,一个V网络?这太低效了。
幸运的是,我们可以利用贝尔曼方程中的关系:
Q(st,at)=E[rt+γV(st+1)]Q(s_t, a_t) = \mathbb{E}[r_t + \gamma V(s_{t+1})]Q(st,at)=E[rt+γV(st+1)]
将它代入优势函数的定义中,我们得到:
A(st,at)=(rt+γV(st+1))−V(st)A(s_t, a_t) = (r_t + \gamma V(s_{t+1})) - V(s_t)A(st,at)=(rt+γV(st+1))−V(st)
这个 rt+γV(st+1)−V(st)r_t + \gamma V(s_{t+1}) - V(s_t)rt+γV(st+1)−V(st) 被称为时序差分误差(TD Error)。它完美地诠释了优势:我们实际得到的(rt+γV(st+1)r_t + \gamma V(s_{t+1})rt+γV(st+1),即走出一步的即时奖励+下一步的状态价值),与我们期望得到的(V(st)V(s_t)V(st),即当前状态的价值)之间的差距。
现在,整个拼图完成了:
- 我们只需要训练一个Critic网络来估计V(s)V(s)V(s)。
- 在每一步交互中,Actor产生动作ata_tat,得到rtr_trt和st+1s_{t+1}st+1。
- 我们用Critic网络计算出V(st)V(s_t)V(st)和V(st+1)V(s_{t+1})V(st+1)。
- 我们计算出TD误差作为优势的估计值:A^(t=rt+γV(st+1)−V(st)\hat{A}(t = r_t + \gamma V(s_{t+1}) - V(s_t)A^(t=rt+γV(st+1)−V(st)。
- Actor使用这个A^t\hat{A}_tA^t来更新自己的策略。
- 同时,Critic也需要更新。它的目标是让自己的“估值”V(st)V(s_t)V(st)更接近“真实”的回报。通常,我们用rt+γV(st+1)r_t + \gamma V(s_{t+1})rt+γV(st+1)作为V(st)V(s_t)V(st)的“学习目标”,计算两者的均方误差(MSE Loss)来进行更新。
这个框架,就是大名鼎鼎的Advantage Actor-Critic (A2C) 的核心。
3.3 A2C/A3C:同步与异步的Actor-Critic框架
在2016年,DeepMind的研究者们将Actor-Critic框架与深度神经网络结合,并引入了并行的思想,提出了Asynchronous Advantage Actor-Critic (A3C) 算法,一举在多个Atari游戏和连续控制任务上取得了当时的最佳效果,引发了巨大的轰动。
A3C(Asynchronous,异步)
A3C的核心思想是“众人拾柴火焰高”。它创建了多个并行的“智能体-环境”副本。
- 一个全局网络(Global Network),包含着Actor和Critic的最新权重。
- 多个工作智能体(Worker Agent),每个Worker都有自己独立的网络参数和一个独立的模拟环境。
工作流程如下:
- 每个Worker从Global Network复制一份最新的网络参数。
- 每个Worker在自己的环境中独立探索、收集数据(比如收集n步)。
- 每个Worker独立计算出自己的网络更新梯度。
- 每个Worker将计算好的梯度异步地(Asynchronously) 推送给Global Network,并用它来更新全局参数
- 更新完后,Worker再次从Global Network拉取最新参数,开始新一轮的探索。
A3C的最大优点是打破了数据的相关性。因为每个Worker都在探索环境的不同部分,收集到的数据是多种多样的,这使得训练过程非常稳定和高效。
A2C(Advantage/Synchronous,同步)
然而,研究者们很快发现,A3C的“异步”特性并非成功的关键。我们完全可以实现一个“同步”版本,效果同样出色,甚至更好,且实现起来更简单。这就是A2C。
A2C的架构如下:
- 同样有多个并行的Worker在各自的环境中探索。
- 所有Worker同时(Synchronously) 运行,各自收集一小段轨迹数据。
- 它们不计算梯度,而是将收集到的所有数据(
state,action,reward, …)发送给一个中心学习器(Central Learner)。 - 中心学习器将所有Worker的数据汇总成一个大批次(Batch)。
- 然后,中心学习器在这个大批次上计算损失、计算梯度,并一次性地更新主网络。
- 更新完成后,所有Worker从主网络获取最新的策略,开始下一轮同步的数据收集。
在今天的硬件环境下,尤其是GPU擅长处理大批量数据的特性,A2C往往比A3C更受欢迎。它正是PPO算法在结构上的直系祖先。我们后续手写的代码,也都是基于A2C的同步思想。
3.4 依然存在的问题:步子迈大了,容易“扯到蛋”(更新步长的困境)
从REINFORCE到A2C,我们通过引入Critic和优势函数,极大地降低了梯度估计的方差,让“醉汉下山”的过程变得稳健了许多。他现在有了一个向导,每一步都走得更有依据。
但是,我们还没有解决策略梯度方法的另一个核心顽疾:更新步长(Step Size)的选择。
Actor的更新依赖于学习率(Learning Rate)。如果学习率设置得太大,一次更新可能会让策略网络发生剧变,我们称之为 “破坏性更新”。
想象一下,Actor根据优势函数A(s,a)A(s,a)A(s,a)的指导,认为某个方向是“好的”,于是满怀信心地朝这个方向迈出了一大步。但它不知道的是,策略函数是一个非常复杂的非线性曲面,这一大步可能直接让它跳进了一个很差的“策略洼地”。新的策略表现极差,收集到的数据质量也急剧下降,导致后续的更新错得更离谱,形成恶性循环,最终性能崩溃。
这就是我们常说的:“步子迈大了,容易扯到蛋”。
A2C/A3C框架本身并没有机制来从根本上解决这个问题。我们依然需要小心翼翼地、炼金术般地调整学习率。那么,有没有一种方法,可以让我们在更新策略时,既能充分利用当前的优势信号,又能保证新的策略与旧的策略不会相差太远,从而避免灾难性的崩溃呢?
答案是肯定的,而这,正是PPO算法将要为我们揭晓的“天才简化”之道。 PPO的核心思想——信赖域(Trust Region),就是为了给策略更新这匹“野马”套上缰绳,让它在安全的范围内尽情驰骋。
在我们进入PPO的核心章节之前,让我们先动手实现一个A2C算法,亲身体会一下它相比于REINFORCE的巨大进步。
3.5 本章小结与代码实践:实现一个基础的A2C算法,并与REINFORCE进行对比
本章核心回顾
- Actor-Critic架构:将策略学习(Actor)和价值评估(Critic)分离,由两个网络(或一个网络两个头)分工合作。
- 优势函数A(s,a)A(s, a)A(s,a):通过用动作价值Q(s,a)Q(s,a)Q(s,a)减去状态价值V(s)V(s)V(s)的基线,得到了一个更精确的、低方差的动作评估信号。
- TD误差:我们使用TD误差rt+γV(st+1)−V(st)r_t + \gamma V(s_{t+1}) - V(s_t)rt+γV(st+1)−V(st)作为优势函数的实用估计,这样只需要一个Critic网络来估计V(s)V(s)V(s)。
- A2C/A3C:分别是同步和异步的Actor-Critic实现框架。A2C因其实现简单且与GPU并行特性更契合,成为了现代深度强化学习的基础架构。
- 核心遗留问题:A2C依然没有解决策略更新步长过大可能导致的性能崩溃问题,这为PPO的登场埋下了伏笔。
代码实践:用A2C解决CartPole问题
现在,我们将实现一个精简版的A2C算法。你会发现,代码结构相比REINFORCE会清晰很多,Actor和Critic的职责更加明确。
# 以下为A2C解决CartPole问题的伪代码和关键逻辑解释
# 完整可运行代码请见本书附录GitHub仓库
import torch
import torch.nn as nn
import torch.optim as optim
import gymnasium as gym
from torch.distributions import Categorical
# --- 1. 定义Actor-Critic网络 ---
# 我们使用一个网络,但它有两个输出头,分别用于Actor和Critic
# 这样可以共享底层的特征提取层,训练更高效
class ActorCritic(nn.Module):
def __init__(self, state_dim, action_dim):
super(ActorCritic, self).__init__()
self.shared_layer = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU()
)
# Actor头:输出动作的概率分布
self.actor_head = nn.Linear(128, action_dim)
# Critic头:输出当前状态的价值
self.critic_head = nn.Linear(128, 1)
def forward(self, state):
x = self.shared_layer(state)
# 动作概率
action_logits = self.actor_head(x)
action_probs = torch.softmax(action_logits, dim=-1)
# 状态价值
state_value = self.critic_head(x)
return action_probs, state_value
# --- 2. A2C算法主逻辑 ---
def a2c_train():
# 初始化环境和网络
env = gym.make('CartPole-v1')
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
model = ActorCritic(state_dim, action_dim)
optimizer = optim.Adam(model.parameters(), lr=0.001)
gamma = 0.99
for episode in range(1000):
state, _ = env.reset()
log_probs = []
values = []
rewards = []
done = False
# --- 数据收集 ---
while not done:
state_tensor = torch.FloatTensor(state).unsqueeze(0)
# Actor选择动作,Critic评估价值
action_probs, state_value = model(state_tensor)
dist = Categorical(action_probs)
action = dist.sample()
next_state, reward, terminated, truncated, _ = env.step(action.item())
done = terminated or truncated
# 存储交互数据
log_probs.append(dist.log_prob(action))
values.append(state_value)
rewards.append(reward)
state = next_state
# --- 计算优势和回报 ---
# 这是与REINFORCE最大的不同点
returns = []
Gt = 0
for r in reversed(rewards):
Gt = r + gamma * Gt
returns.insert(0, Gt)
returns = torch.tensor(returns, dtype=torch.float32)
values = torch.cat(values).squeeze()
# 计算优势函数 A(s,a) = Gt - V(s)
# 这里Gt是蒙特卡洛法计算的实际回报,也可以用TD法
advantages = returns - values
# --- 计算损失 ---
# A2C的损失函数是两部分的和
# 1. Actor损失 (策略损失)
# 用.detach()来阻断梯度,因为我们不希望Critic的损失影响Actor
actor_loss = -(torch.stack(log_probs).squeeze() * advantages.detach()).mean()
# 2. Critic损失 (价值损失)
# Critic的目标是让自己的估值V(s)接近真实的回报Gt
critic_loss = nn.functional.mse_loss(returns, values)
# 总损失
total_loss = actor_loss + 0.5 * critic_loss # 0.5是常用系数
# --- 更新网络 ---
optimizer.zero_grad()
total_loss.backward()
optimizer.step()
# 打印日志...
# ...
# 运行训练
# a2c_train()
与REINFORCE的核心对比
- 网络结构:A2C有一个双头网络,同时输出策略和价值;REINFORCE只有一个策略网络。
- 更新信号:A2C使用优势函数(Advantages)
returns - values作为更新Actor的权重,这个信号的方差更低;REINFORCE直接使用总回报(Returns)Gt。 - 损失函数:A2C的损失包含两部分:Actor Loss(策略提升)和Critic Loss(价值评估)。这使得网络有一个额外的、更稳定的监督信号,帮助模型更好地理解状态的好坏。
当你运行这段代码时,你会发现A2C的收敛过程通常比REINFORCE要平滑得多,也更快。我们为“醉汉”找到了一个可靠的“向导”,让他下山的脚步变得稳健而高效。
现在,我们的基础已经无比坚实。我们理解了策略梯度,也掌握了Actor-Critic框架。我们已经站在了通往PPO的最后一道门前。下一章,我们将推开这扇门,真正见识PPO算法那令人着迷的“天才简化”设计。

被折叠的 条评论
为什么被折叠?



