Actor-Critic算法详解:从策略梯度到PPO的强化学习核心框架

发布时间:2026/10/20 2:06:53
Actor-Critic算法详解:从策略梯度到PPO的强化学习核心框架
做强化学习有一段时间的人大概率都绕不开 Actor-Critic 这个名字。我第一次接触到这个概念时正好是在调一个策略梯度算法模型死活不收敛奖励曲线像心电图一样乱跳。后来把 Critic 加进去训练过程肉眼可见地稳定下来。从那时起我就意识到Actor-Critic 不只是一个算法更是一套理解强化学习底层逻辑的框架。这篇文章把 Actor-Critic 方法从原理到代码、从坑到变体完整拆开讲。适合已经开始学强化学习、手写过策略梯度或者 DQN、但想进一步理解主流算法A2C、PPO、SAC底层逻辑的读者。如果只看懂一个强化学习算法我会建议你看懂 Actor-Critic它几乎是所有现代深度强化学习算法的地基。1. 为什么需要两个“演员”和一个“评委”1.1 纯策略梯度方法的最大痛点在 Actor-Critic 出现之前最朴素的策略梯度方法是 REINFORCE。它的思路很直接让策略网络Actor与环境交互收集一整条轨迹然后用这条轨迹的总回报去更新策略——回报高的动作增大概率回报低的动作减小概率。听起来没问题但实际跑起来非常难受。问题出在“用总回报当信号”上。想象一次完整回合从开始到结束可能有几百步中间动作的好坏参差不齐而总回报是这一整条轨迹的累计结果。你很难判断到底是哪一步动作导致了最终的高分或低分。这就像一场球赛打了两个小时你只盯着最终比分去评价每个球员的每一次传球噪声大得离谱。更麻烦的是更新时机。REINFORCE 必须等一个完整回合结束才能更新如果回合特别长、或者根本没有明确的回合边界算法就完全没法用。早期我做实验时经常跑十几分钟环境还在转参数一动没动那种感觉非常煎熬。专业点说REINFORCE 的梯度估计方差太大。数学上它的更新量是回报乘以策略的梯度回报本身就包含大量随机性相当于给每个梯度分量加上了一个不确定的放大系数。方差的代价就是训练效率低、不稳定而 Actor-Critic 的核心思路就是要用一个可学习的“预测器”来替代这个高方差的回报信号。1.2 Actor 和 Critic 各自管什么从角色分工来看Actor 和 Critic 各司其职。Actor 就是策略网络输入状态输出动作概率分布离散动作或动作均值方差连续动作它的职责是决定“该做出什么样的行为”。Critic 是价值网络输入状态或者状态加动作输出一个预估的价值分数它的职责是评价“当前这个局面到底有多好”。两个网络通常都是小型全连接网络结构上并不复杂。Actor 的更新通过策略梯度完成Critic 的更新通过时序差分TD完成。二者交替训练Critic 越来越会打分Actor 则依靠 Critic 给出的分数不断优化动作策略。这个“互相促进”的循环是 Actor-Critic 最优雅的地方——这也是它被叫做 Actor-Critic 的原因一个表演一个评论。值得强调的是Critic 的价值预测是随训练动态变化的。训练初期Critic 的分数可能完全不准但即使不准它提供的也是一个相对连续的、可以实时更新的信号。比起等待完整回报这种“即时的反馈”本身就是巨大的进步。1.3 从“批改试卷”看 Actor-Critic 的协作逻辑用一个生活中常见的类比帮助理解。假设你在备考数学Actor 是你做题的策略——选什么方法、先做哪一步Critic 是你的老师每次你做完一道题老师立刻给你批改并指出哪里扣分、哪里加分。注意这时候老师不是等到期末考试才给你总成绩而是每道题、甚至每一步都给一个参考分。这个“每一步都给分”的能力来自 Critic 的价值函数。价值函数 V(s) 预测的是“从当前状态出发未来能拿到多少累计奖励”。有了它就能算出一个叫 TD 误差的东西拿到实际奖励后比较一下“预期的未来价值”和“实际奖励加下一步的预期价值”差多少。这个差值就是 Critic 给 Actor 的批改意见也是 Actor 参数更新的方向指引。老师一开始批得不准没关系但每改一题就进步一点最后会越批越准。Actor 跟着老师的意见调策略老师自己也从实际结果里学着批得更准最终一起变强。理解了这张“互相批改”的关系网Actor-Critic 的运作逻辑就已经掌握了大半。2. 核心原理解析动态规划视角下的状态价值2.1 Critic 学的究竟是什么想深入理解 Actor-Critic关键是要看懂 Critic 的价值函数到底在算什么。它计算的核心是状态价值 V(s)定义为从状态 s 出发在策略 π 作用下未来折扣回报的期望。数学表达为 V^π(s) E[∑γ^t r_t | s_0 s]其中 γ 是折扣因子越往后的奖励权重越低。折扣因子的设置很讲究。γ 0.99 意味着大约 100 步之后的奖励对当前决策仍有一定影响γ 越小模型越“短视”。这是一个直接影响训练效果的超参数我在实际项目中一般起点选 0.99任务需要长期规划就调到 0.995 甚至更高任务本身比较短期则可以用 0.95。Critic 就是用神经网络去逼近这个 V 函数。网络参数 φ输入状态 s输出一个标量 V_φ(s)。更新的目标很简单让 V_φ(s) 与实际观察到的“真实折扣回报”尽量一致。但问题是我们没有“真实折扣回报”——这正是 TD 方法存在的意义它用一步实际奖励 r 加上下一步的状态价值预测 γV_φ(s′) 来当作 V_φ(s) 的更新目标。这个组合量叫 TD 目标Critic 的损失函数就是预测值和 TD 目标的均方误差。这里有个循环依赖V_φ(s) 的目标里包含着 V_φ(s′) 的预测。这就是所谓“自举”bootstrapping。好处是方差低、可以单步更新潜台词是误差会沿着时间反向传播稍有不慎就容易累积偏差。这也是后面所有稳定性技巧目标网络、双 Q 网络的源头。2.2 TD 误差是怎么算出来的TD 误差的完整计算路径如下。在状态 s 下Actor 依据策略采样一个动作 a 并执行环境回传奖励 r 和新状态 s′Critic 分别预测 V_φ(s) 和 V_φ(s′)于是δ r γV_φ(s′) - V_φ(s)这个 δ 就是 TD 误差。它同时承担两个使命。对 Critic 来说δ 是更新方向Critic 参数向缩小 δ 的方向调整可以理解成修正考试批改分数。对 Actor 来说δ 代替了 REINFORCE 里的总回报作为动作好坏的即时评判如果 δ 0说明当前动作带来的结果比预期好对应的动作概率应该提高如果 δ 0则应该降低。注意一个易混点TD 误差中 δ 0并不一定意味着奖励为正而是“实际奖励比预期好”。哪怕奖励是负数只要 Critic 预测得更差δ 也可能为正。这种相对评价的本质决定了 Actor-Critic 对环境的奖励尺度不那么敏感这是它比 REINFORCE 稳健的重要来源。把这个逻辑落到更新公式上以 Actor、Critic 均使用参数化网络为例Critic 更新φ ← φ - α_c · δ · ∇_φ V_φ(s)Actor 更新θ ← θ α_a · δ · ∇_θ log π_θ(a|s)Actor 更新的本质就是带着符号的策略梯度。δ 的正负决定了梯度方向∇log π_θ 保证了高概率的动作更新幅度小、低概率的动作更新幅度大刚好符合直觉。2.3 为什么这套设计能降低方差对比一下 REINFORCE 和 Actor-Critic 的差别就能理解方差降低的来源。REINFORCE 的更新权重是整回合折扣回报这个数字包含了大量随机噪声而 Actor-Critic 的更新权重是 TD 误差 δ它只包含环境单步的随机性信息更集中、更贴近“当前决策”。用统计的语言解释就是REINFORCE 用一个蒙特卡洛样本去估计回报方差是正比于轨迹长度的Actor-Critic 用单步时序差分去估计优势方差大幅减小代价是一点点偏差。强化学习中方差和偏差的均衡在这个框架里体现得淋漓尽致。这个“方差换偏差”的取舍带来了实际操作上的两个直接好处。第一Actor 可以在未结束的中间状态就进行更新不再需要等待完整轨迹天然支持在线学习。第二Critic 的价值预测可以作为不同状态之间比较的共同基准相当于给所有动作的评估做了归一化更新信号不再依赖全局得分的高低Actor 学起来自然快得多。3. 动手实现一个 Actor-Critic 模型3.1 网络结构设计以经典的连续控制任务比如平衡杆一类的标准基准环境为例状态通常是若干维度的观测值动作是左右两档的离散选择。这种情况下Actor 和 Critic 的网络结构可以设计成如下样式。Actor 网络输入状态维度中间经过两层带 ReLU 激活的全连接层最后一层输出两个动作的 logits再接 Softmax 得到动作概率分布。Critic 网络同样输入状态中间隐藏层结构与 Actor 基本相同但输出层只有一个神经元不加激活函数直接输出 V 值。为什么不加激活因为 V 值理论上可以是任意实数有正有负你加一个 ReLU 或 Tanh 反而限制了它。两个网络可以共享底层特征提取层也可以完全独立。我的经验是任务本身简单时独立网络更稳定训练更省心任务输入复杂比如图像时共享层能减少计算量、帮助特征复用。初学阶段建议先用独立网络排查问题更容易。PyTorch 里实现如下import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim128): super(Actor, self).__init__() self.fc1 nn.Linear(state_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, action_dim) def forward(self, x): x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return torch.softmax(self.fc3(x), dim-1) class Critic(nn.Module): def __init__(self, state_dim, hidden_dim128): super(Critic, self).__init__() self.fc1 nn.Linear(state_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, x): x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return self.fc3(x)隐藏层维度取 128 在多数简单任务上绰绰有余。如果你的任务状态维度特别高可以适当加宽到 256 或叠加更多层。但别一上来搞得很大Actor-Critic 的超参调优本来就敏感网络本身大反而更容易过拟合噪声。3.2 训练循环的完整流程接下来是完整训练循环。核心流程分五步进入状态 sActor 按当前策略采样动作 a执行动作拿到奖励 r 和下一个状态 s′Critic 计算两个状态的 V 值并得到 TD 误差最后按各自的规则更新参数。这里的更新是单步在线更新不需要回合结束。每步更新的完整代码如下def update(actor, critic, optimizer_actor, optimizer_critic, state, action, reward, next_state, done, gamma0.99): state torch.FloatTensor(state).unsqueeze(0) next_state torch.FloatTensor(next_state).unsqueeze(0) action torch.LongTensor([action]) reward torch.FloatTensor([reward]) done torch.FloatTensor([done]) # 当前状态与下一状态的 V 值 v_s critic(state) v_next critic(next_state) # TD 目标实际奖励 折扣后的下一状态价值 td_target reward gamma * v_next * (1 - done) # TD 误差 delta td_target - v_s.detach() # Critic 损失V(s) 尽量接近 TD 目标 critic_loss torch.mean((v_s - td_target.detach()) ** 2) optimizer_critic.zero_grad() critic_loss.backward() optimizer_critic.step() # 策略概率与 log 概率 probs actor(state) dist torch.distributions.Categorical(probs) log_prob dist.log_prob(action) # Actor 损失负的策略梯度带上 detach 的 TD 误差 actor_loss -torch.mean(log_prob * delta.detach()) optimizer_actor.zero_grad() actor_loss.backward() optimizer_actor.step() return delta.item()两个细节值得单独说明。一个是done的处理当回合结束时下一状态不存在它的 V 值应被强行置 0所以乘以(1 - done)。漏掉这一步会让 Critic 在回合终止点误认为未来还有奖励导致边界状态的价值估计偏高。另一个是 detach 的时机Actor 的损失里 δ 是当作常数来用的不能让它反向传播到 Critic 网络否则两条更新路径会纠缠在一起产生奇怪的梯度耦合。主循环就是反复采样、调 update。如果环境本身提供了标准的step和reset接口代码骨架大致如下任何标准强化学习接口的环境都能直接套用。for episode in range(total_episodes): state, _ env.reset() total_reward 0 while True: state_tensor torch.FloatTensor(state).unsqueeze(0) probs actor(state_tensor) action torch.distributions.Categorical(probs).sample().item() next_state, reward, done, truncated, _ env.step(action) done bool(done or truncated) update(actor, critic, optimizer_actor, optimizer_critic, state, action, reward, next_state, done) total_reward reward state next_state if done: break这里把truncated也当成结束处理因为超时截断后继续自举会让 Critic 学到错误的时间因果。3.3 训练启动前的关键超参数设置超参数是 Actor-Critic 最容易翻车的地方。学习率方面我通常给 Actor 和 Critic 分配不同的学习率Actor 用偏小的学习率比如 3e-4Critic 可以稍微大一点比如 1e-3。原因是 Actor 需要稳定、保守地改变策略而 Critic 需要相对快速地修正价值估计。折扣因子 γ 的设置逻辑前面已经提过。离策略的轨迹回报接近的时候γ 可以小任务时间跨度长则必须加大。比如训练典型平衡任务γ 0.99 起步没问题如果是稀疏奖励的探索型任务可能用到 0.995 甚至 0.999。批大小是个微妙的东西。在线单步更新理论可行但实际中我很少真的每步只更新一次通常攒一小批32~64 条转移再更新一次梯度噪声会小很多。这其实已经是在往“mini-batch”方向靠了。后期 PPO 把这套逻辑发扬光大用一个大 batch 的多轮更新来控制更新幅度值得在学完基础版后进一步深入。4. 踩坑实录与常见问题排查4.1 训练不收敛的几种典型表现跑过 Actor-Critic 的人大概率见过以下几种症状。第一种是训练刚开始奖励曲线一路高歌然后突然断崖式下跌并再也爬不起来。这种通常是因为 Actor 更新太快把策略推向了一个“极端化”的分布——某个动作概率逼近 1另一个接近 0回不到探索状态。解决办法是降低 Actor 学习率、在 loss 里加熵正则项鼓励探索、或者限制单步更新幅度。第二种是训练曲线始终在一个低水平徘徊偶尔蹦出几个高分又迅速回落。这种情况多数是 Critic 没学好给出的 δ 信号是乱码。检查办法很简单在训练过程中打印 δ 的均值和标准差。正常训练时 δ 应该逐渐变小、围绕着 0 附近波动如果 δ 长期是大的正值或负值说明 Critic 的预测和真实回报差距一直没有收敛问题出在 Critic 的学习率太低或者网络表达能力不足。第三种是奖励曲线持续震荡、没有明显上升趋势。这多数是探索与利用的平衡出了问题。刚学完基础版本时我习惯加一个熵系数到 Actor 的损失里- log_prob * delta - beta * entropy。beta 初期取 0.01随训练推进可以衰减。这个小改动往往能让训练过程的稳定性上一个台阶。4.2 Critic 高估问题怎么防高估问题是价值学习类的通病。Critic 依赖自我预测去更新目标TD 目标本身带着自身近似误差当误差为正向时V 值会被不断抬高。表现为 Critic 输出值远大于实际能拿到的长期回报最终导致 Actor 被错误的信号误导。高估最明显的征兆是训练中 Critic loss 不停下降但实际奖励没涨。因为 Critic 在“自我圆谎”预测值和自我生成的 TD 目标保持一致地虚高。验证方式不复杂训练结束后单独算几条轨迹的真实打折回报跟 Critic 预测值对比。预测值系统性偏高就说明高估了。缓解高估有两条实际路线。一条是加目标网络复制一份 Critic参数定期软更新或硬复制TD 目标用目标网络计算降低“在线网络和目标同步膨胀”的风险这是 DQN 时代就很成熟的技巧。另一条是双 Critic维护两套价值网络取两者较小值作为目标从机制上抑制正偏差。后面讲主流算法时会提到这已经成了现代强化学习算法的基础组件。4.3 探索失衡和奖励尺度问题探索不足是 Actor-Critic 的隐性问题。Critic 对未探索过的区域没有准确价值Actor 也就没有动机去那些区域形成恶性循环。最直接的手段是在动作分布上做手脚离散动作可以用 ε 贪心或加熵正则连续动作可以在输出均值上叠加噪声。我在实际训练中发现初期加一点随机噪声对跳出价值网络的盲区非常有效。奖励尺度问题则更隐蔽。不同任务的奖励范围差出几个数量级是常有的事而 TD 误差的绝对大小又直接决定了 Actor 的更新步长。同一个 δ1对奖励范围 0~1 的任务来说影响巨大对奖励范围在 0~1000 的任务来说却没什么感觉。实际操作时我会周期性记录 δ 的范围如果发现相对设置的学习率而言太大或太小就手动对整个奖励序列做归一化。最简单的方案是除以一个固定估计值或者用近期奖励的移动平均做减除把奖励变成一个相对量。受益于 δ 是相对偏差而非绝对值Actor-Critic 比纯 REINFORCE 对奖励尺度敏感得多。但别掉以轻心尺度跨度过大时仍然要处理极端情况下干脆把奖励做标准化。这个习惯在后续用 PPO、SAC 时会打包在库里但自己手写一遍理解得更透。5. 从基础 Actor-Critic 到主流算法5.1 必要的升级A2C 与优势函数基础的 Actor-Critic 已经能跑但离好用的算法还有差距。第一个自然升级就是优势函数Advantage Function定义为动作的实际价值 Q(s,a) 与状态平均价值 V(s) 之间的差A(s,a) Q(s,a) - V(s)。它回答的问题是“相对于平均水准这个动作究竟有多好”。标准的 TD 误差 δ其实可以看作是优势函数的一个单步采样估计。直觉上δ 0 意味着“动作带来的回报高于状态平均值”这正是要在策略梯度里放大信号的条件。于是策略梯度的更新权重从 Q 换成了优势估计Actor 更新时用 δ 就已经在隐式地利用优势了。当单步优势不够稳的时候可以用多步优势估计即 GAE。GAE 将多种不同长度的优势估计加权组合参数 λ 控制权重λ 越大越依赖长期信息偏差小方差大λ 越小越依赖短期信息偏差大方差小。A2C 这个名字里的“Advantage”指的就是这套优势设计。想从基础 AC 进阶到现代算法理解优势函数是必然一步。5.2 稳定性增益PPO 与目标网络PPO 是目前最常用的强化学习算法之一它的骨架就是 Actor-Critic核心改造在于限制策略更新的幅度。普通的策略梯度一步更新可能把策略推得过远PPO 在目标函数里加入了新旧策略的比率裁剪让每次更新都在一个可信范围内。PPO 损失函数的核心是裁剪项min(ratio * A, clip(ratio, 1-ε, 1ε) * A)。ratio 是当前策略与旧策略在给定动作下的概率比。如果优势为正就希望 ratio 尽量大但不能超过 1ε如果优势为负就希望 ratio 尽量小但不能低于 1-ε。这个裁剪让 PPO 在训练中保持极高的数据利用效率——一批数据被反复用多轮更新这是它比基础 AC 高效的一个关键原因。同时PPO 里的 Critic 也沿用了目标网络和优势估计的手法。本质上PPO 就是给 Actor-Critic 装上了“限速器”和“刹车系统”让它又稳又快地学习。5.3 连续动作空间的天花板DDPG/SAC如果面对的是连续动作空间比如机器人关节力矩控制离散 softmax 的策略方法会失灵。DDPG 用确定性策略 经验回放Actor 直接输出动作值而非概率分布。它的 Critic 是一个 Q 函数网络输入状态与动作输出 Q 值。DDPG 的训练稳定性对超参数极敏感是出了名的难调。SAC 在 DDPG 基础上引入了最大熵框架目标除了最大化累计奖励还要最大化每一步策略的熵。这意味着策略在得到相同奖励时会尽量保持随机天然对抗探索不足问题。SAC 的训练曲线通常比 DDPG 平稳很多且对学习率不那么敏感因此已成为连续控制任务的首选。回过头来你会看到DDPG、SAC、PPO、A2C 这些名字虽然层出不穷但内里构造都绕不开一个表演者、一个评论家、一个误差信号。理解了 Actor-Critic 这个地基再去看任何现代算法都不会再觉得是一团迷雾。我个人在实际训练中体会最深的一件事是永远不要迷信“算法一定有效”这个假设。Actor-Critic 给出了极好的框架但每个任务都有它独特的坑。调试时少走弯路的关键是时刻盯着 δ 的分布、Critic loss 的趋势和策略的熵值——这三者比最终奖励曲线更早告诉你模型是否健康。希望这篇文章能帮你把基础打牢也让你在以后调试任何一个强化学习算法时至少心里有底。