Actor-Critic框架详解:从策略梯度到RLHF的PPO实现
2026/8/30 8:24:21 网站建设 项目流程

这次要解决的问题很具体:如何把 Actor-Critic(演员-评论家)算法从数学推导到代码实现完整讲清楚,并且把它和 RLHF(基于人类反馈的强化学习)这条链路串起来。无论你是准备算法面试、想读懂 PPO 的论文,还是打算在机械臂控制、推荐系统或大模型对齐里落地策略优化,这节内容都建议完整过一遍。

先给结论:Actor-Critic 不是某一个具体算法,而是一套“策略网络 + 价值网络”的框架。策略网络负责选择动作,价值网络负责评估当前状态或动作的好坏,两者通过 TD 误差(时序差分误差)耦合起来。而 RLHF 里的 PPO 阶段,本质上就是一个带 KL 约束的 Actor-Critic 变体。理解了 Actor-Critic,后面看 PPO 和 RLHF 都会轻松很多。

这篇文章会包含四部分内容:核心概念与数学公式推导、PyTorch 最小可运行代码、从 AC 到 PPO 再到 RLHF 的推演路径、还有常见训练问题排查。阅读门槛不高,需要一点概率论和微积分基础,代码部分在 CPU 上就能跑完。

1. 内容速览与学习地图

先给一张学习地图,知道每一步在解决什么问题。

学习主题核心概念数学符号最终目标
回报与价值函数折扣回报、状态价值、动作价值$G_t, V(s), Q(s,a)$知道“好”和“坏”如何量化
贝尔曼方程状态价值递推关系$V(s) = \mathbb{E}[r + \gamma V(s')]$理解 Critic 的训练依据
策略梯度定理用梯度直接优化策略$\nabla J(\theta)$知道 Actor 的更新方向
基线函数降低方差$b(s)$推导出优势函数
TD 误差时序差分目标$\delta_t$理解 Critic 怎么学
Actor-Critic双网络协作$\theta, \omega$完成算法闭环
PPO 与 RLHF截断目标、KL 约束$L^{CLIP}$从单智能体扩展到 LLM 对齐

建议按表格顺序学习,不要跳。尤其是从“策略梯度定理”到“优势函数”这段推导,是整个 Actor-Critic 的数学核心,也是最容易卡住的地方。

2. 适用场景与学习路线

2.1 这套内容适合谁

  • 正在准备算法岗面试的人:Actor-Critic、PPO、RLHF 是高频考点。
  • 刚入门强化学习的研究生或工程师:需要从理论推导到代码实现完整走一遍。
  • 想理解 LLM 对齐原理的人:RLHF 是当前大模型训练绕不开的一环。
  • 做机器人控制、推荐系统或游戏 AI 的人:AC 框架是很多落地算法的底层结构。

2.2 能解决什么问题

  • 解决“策略梯度方差过大、训练不稳定”的问题。
  • 解决“如何同时优化策略和价值估计”的问题。
  • 解决“如何理解 PPO 和 RLHF 的数学目标”的问题。
  • 给后续阅读 TRPO、PPO、DPO、离线 RLHF 论文打基础。

2.3 不适合什么场景

  • 如果完全没有 MDP(马尔可夫决策过程)基础,建议先补 Sutton 的《强化学习》前四章。
  • 如果只是想用现成 RLHF 工具而不关心原理,本篇文章偏理论,可能不是最高效路径。
  • 如果目标是工业级大规模分布式训练,这篇文章不涉及分布式框架细节,只讲单机算法。

2.4 合规与使用边界

涉及 RLHF 训练、大模型对齐时,需要注意三点:第一,使用真实用户反馈数据必须确认数据来源合规,涉及个人信息时要脱敏并遵守隐私保护要求;第二,训练出的模型如果用于对外服务,需要做内容安全评估;第三,涉及人脸、声音、版权素材时,必须提前获得授权。技术本身是中性的,但数据、部署和应用边界必须把住。

3. 前置知识:回报、策略、价值函数与贝尔曼方程

3.1 回报与折扣因子

强化学习的目标是最大化期望累计回报。一个回合从时间 $t$ 开始,折扣回报定义为:

$$ G_t = r_t + \gamma r_{t+1} + \gamma^2 r_{t+2} + \cdots = \sum_{k=0}^{\infty} \gamma^k r_{t+k} $$

其中 $\gamma \in [0,1]$ 是折扣因子。$\gamma$ 越小,智能体越关注短期收益;$\gamma$ 越大,越关注长期收益。

3.2 策略与价值函数

策略 $\pi(a|s)$ 表示在状态 $s$ 下选择动作 $a$ 的概率分布。策略可以是表格形式,也可以是神经网络参数化形式 $\pi_\theta(a|s)$。

状态价值函数表示“从这个状态出发,按照策略 $\pi$ 行动,后续能拿到的期望回报”:

$$ V_\pi(s) = \mathbb{E}_\pi \left[ G_t \mid S_t = s \right] $$

动作价值函数表示“在状态 $s$ 先执行动作 $a$,之后按照策略 $\pi$ 行动”的期望回报:

$$ Q_\pi(s, a) = \mathbb{E}_\pi \left[ G_t \mid S_t = s, A_t = a \right] $$

两者关系是:

$$ V_\pi(s) = \sum_a \pi(a|s) Q_\pi(s, a) $$

3.3 贝尔曼方程

价值函数可以用下一时刻的价值递归表示,这就是贝尔曼方程:

$$ V_\pi(s) = \sum_a \pi(a|s) \sum_{s', r} P(s', r | s, a) \left[ r + \gamma V_\pi(s') \right] $$

这个公式的意义非常直接:当前状态的价值等于“下一时刻的即时奖励 + 下一状态价值乘以折扣因子”的期望。Critic 网络的训练目标,就是让估计的 $V_\omega(s)$ 尽量靠近这个递归关系。

4. 从策略梯度到 Actor-Critic:数学公式推导

4.1 策略目标函数

策略参数为 $\theta$,目标函数是期望回报:

$$ J(\theta) = \mathbb{E}{\tau \sim \pi\theta} \left[ R(\tau) \right] $$

轨迹 $\tau = (s_0, a_0, r_0, s_1, a_1, r_1, \dots)$ 的回报是 $R(\tau)$。

4.2 策略梯度定理

对目标函数求梯度,得到策略梯度定理:

$$ \nabla_\theta J(\theta) = \mathbb{E}{\tau} \left[ \sum{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t | s_t) , R(\tau) \right] $$

由于当前时刻的动作不会影响过去的奖励,可以用动作价值函数 $Q_\pi(s_t, a_t)$ 替代整条轨迹的回报:

$$ \nabla_\theta J(\theta) = \mathbb{E}{s_t, a_t} \left[ \nabla\theta \log \pi_\theta(a_t | s_t) , Q_\pi(s_t, a_t) \right] $$

这就是 REINFORCE 算法的核心,也是 Actor-Critic 的出发点。

4.3 基线函数与优势函数

REINFORCE 的一个主要问题是方差大。一个常用改进是引入基线函数 $b(s)$,它不改变梯度期望:

$$ \mathbb{E}{a \sim \pi} \left[ \nabla\theta \log \pi_\theta(a|s) , b(s) \right] = b(s) \nabla_\theta \sum_a \pi_\theta(a|s) = b(s) \nabla_\theta 1 = 0 $$

因为概率和恒等于 1,梯度为 0。所以梯度可以改写为:

$$ \nabla_\theta J(\theta) = \mathbb{E}{s,a} \left[ \nabla\theta \log \pi_\theta(a|s) \left( Q_\pi(s,a) - b(s) \right) \right] $$

如果选择状态价值函数 $V_\pi(s)$ 作为基线,就得到优势函数:

$$ A_\pi(s, a) = Q_\pi(s, a) - V_\pi(s) $$

优势函数衡量的是:在状态 $s$ 下,采取动作 $a$ 比“平均水平”好多少。如果 $A > 0$,说明这个动作优于平均水平,策略应该增加这个动作的概率;如果 $A < 0$,则降低概率。

4.4 Actor 与 Critic 的分工

到这里,“演员”和“评论家”的角色就很清晰了:

  • Actor(策略网络):参数 $\theta$,负责输出动作概率分布,按照优势函数的正负方向更新策略。
  • Critic(价值网络):参数 $\omega$,负责估计状态价值 $V_\omega(s)$,用来计算优势函数。

Actor 的更新公式:

$$ \theta \leftarrow \theta + \alpha \nabla_\theta \log \pi_\theta(a_t|s_t) , A_t $$

Critic 的更新就是让 $V_\omega(s_t)$ 逼近真实回报,或者说逼近一个更可靠的 TD 目标。

5. TD 误差与优势函数:Critic 的训练数学推导

5.1 从蒙特卡洛到 TD 目标

最简单的价值估计是用整条回合的回报 $G_t$ 做回归:

$$ L(\omega) = \frac{1}{2} \mathbb{E}\left[ \left( G_t - V_\omega(s_t) \right)^2 \right] $$

这种蒙特卡洛方式无偏但方差大,而且需要等回合结束才能更新。

TD(Temporal Difference)方法用一步的 bootstrap 替代整条回报:

$$ \text{TD target} = r_t + \gamma V_\omega(s_{t+1}) $$

TD 误差定义为:

$$ \delta_t = r_t + \gamma V_\omega(s_{t+1}) - V_\omega(s_t) $$

TD 误差的期望与优势函数在统计上是相通的。用 TD 误差代替蒙特卡洛回报,可以显著降低方差。Critic 的损失函数变为:

$$ L(\omega) = \mathbb{E}\left[ \left( r_t + \gamma V_\omega(s_{t+1}) - V_\omega(s_t) \right)^2 \right] $$

对应 Critic 的参数更新:

$$ \omega \leftarrow \omega - \beta \nabla_\omega L(\omega) $$

5.2 GAE 广义优势估计

单步 TD 偏差小但偏差可能较大,蒙特卡洛无偏但方差大。GAE(Generalized Advantage Estimation)通过一个参数 $\lambda$ 在偏差和方差之间做权衡,把多步 TD 误差加权求和:

$$ \delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) $$

$$ A_t^{GAE} = \delta_t + (\gamma \lambda) \delta_{t+1} + (\gamma \lambda)^2 \delta_{t+2} + \cdots = \sum_{l=0}^{\infty} (\gamma \lambda)^l \delta_{t+l} $$

当 $\lambda = 0$ 时,GAE 退化为单步 TD;当 $\lambda = 1$ 时,近似蒙特卡洛回报。实际训练中,$\lambda$ 通常取 0.95 左右。GAE 是 PPO 实现里不可或缺的组件,理解了 TD 误差,GAE 就只是“加权累加”这一步。

5.3 为什么说 TD 误差连接了 Actor 和 Critic

很多资料把 Actor-Critic 简写成“策略梯度 + TD 更新”,但更准确的理解是:Critic 用 TD 误差训练,然后 TD 误差构成优势函数,优势函数再指导 Actor 更新。Critic 训练得越准,Actor 的梯度方向越可靠;Actor 更新后,策略变了,Critic 又要重新追。这就是两个网络交替优化、互相依赖的协作关系。

6. Actor-Critic 算法实现:PyTorch 最小可运行代码

6.1 实验环境

这里用一个极简环境验证原理:CartPole 平衡杆问题(版本为 CartPole-v1)。CPU 即可运行,单个回合的交互成本很低,非常适合用来观测 AC 算法是否收敛。

依赖库:

pip install gymnasium torch

6.2 Actor-Critic 网络定义

用一个两层隐藏层的网络,同时输出策略 logits 和状态价值:

import torch import torch.nn as nn import torch.optim as optim import gymnasium as gym import numpy as np class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim=128): super().__init__() self.shared = nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), ) self.policy_head = nn.Linear(hidden_dim, act_dim) self.value_head = nn.Linear(hidden_dim, 1) def forward(self, x): h = self.shared(x) logits = self.policy_head(h) value = self.value_head(h).squeeze(-1) return logits, value

6.3 GAE 计算函数

GAE 是连接 TD 误差和优势函数的关键实现:

def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95): """ 输入: rewards: 一回合内的即时奖励列表 values: 当前 Critic 对各状态的预测值 dones: 是否为终止状态 返回: advantages: GAE 优势估计 returns: 用于 Critic 回归的回报目标 """ advantages = [] gae = 0.0 returns = [] for t in reversed(range(len(rewards))): if t == len(rewards) - 1: next_value = 0.0 else: next_value = values[t + 1] delta = rewards[t] + gamma * next_value * (1 - int(dones[t])) - values[t] gae = delta + gamma * lam * (1 - int(dones[t])) * gae advantages.insert(0, gae) returns.insert(0, gae + values[t]) return torch.tensor(advantages, dtype=torch.float32), torch.tensor(returns, dtype=torch.float32)

6.4 训练主循环

def train_one_episode(env, model, optimizer, gamma=0.99, lam=0.95): obs, _ = env.reset(seed=0) log_probs = [] values = [] rewards = [] dones = [] while True: obs_t = torch.as_tensor(obs, dtype=torch.float32) logits, value = model(obs_t) dist = torch.distributions.Categorical(logits=logits) action = dist.sample() log_probs.append(dist.log_prob(action)) values.append(value) dones.append(False) next_obs, reward, terminated, truncated, _ = env.step(action.item()) rewards.append(reward) done = terminated or truncated dones.append(done) if done: break obs = next_obs advantages, returns = compute_gae(rewards, values, dones, gamma, lam) policy_loss = 0.0 value_loss = 0.0 for log_p, adv in zip(log_probs, advantages): policy_loss = policy_loss - log_p * adv value_loss = nn.functional.mse_loss(torch.stack(values), returns) optimizer.zero_grad() (policy_loss + value_loss).backward() optimizer.step() return len(rewards) env = gym.make("CartPole-v1") model = ActorCritic(obs_dim=4, act_dim=2, hidden_dim=128) optimizer = optim.Adam(model.parameters(), lr=3e-3) for i in range(300): total_steps = train_one_episode(env, model, optimizer) if (i + 1) % 50 == 0: print(f"episode {i + 1}, steps = {total_steps}")

6.5 预期结果与判断标准

  • CartPole-v1 存活步数上限是 500 步。
  • 如果算法正常收敛,训练后期steps会逐渐接近 500。
  • 如果一直徘徊在 20 步左右,大概率是学习率或网络初始化问题。
  • CPU 上训练 300 轮通常只需要几分钟,时间长短取决于机器配置,不需要 GPU。

6.6 失败时排查什么

  • 检查dones是否在 GAE 计算里正确处理,终止状态后不应该把下一状态价值算进去。
  • 检查advantages是否出现剧大数值,如果出现,先用标准化处理。
  • 检查损失是否变成 NaN,如果是,降低学习率并检查数值稳定性。
  • 检查是否每个回合都重置了环境。

7. 从 AC 到 PPO 再到 RLHF:奖励模型与策略对齐

7.1 PPO 为什么要截断

Actor-Critic 的问题在于:策略网络更新后,收集数据的旧策略和当前策略就不同了,用来训练的数据分布发生了偏移,容易导致训练震荡。PPO(Proximal Policy Optimization)的核心思路是限制策略更新的幅度。设概率比为:

$$ r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)} $$

PPO 的截断目标:

$$ L^{CLIP}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta) A_t, , \operatorname{clip}\left(r_t(\theta), 1-\epsilon, 1+\epsilon\right) A_t \right) \right] $$

其中 $\epsilon$ 通常取 0.2。当优势 $A_t > 0$ 时,即使概率比超过 $1+\epsilon$,也会被截断;当 $A_t < 0$ 时,概率比低于 $1-\epsilon$ 的部分会被截断。这相当于给策略更新加了一个“信任区域”,既保留 Actor-Critic 的思想,又让训练更稳定。

7.2 RLHF 的三阶段流程

RLHF(Reinforcement Learning from Human Feedback)是目前大模型对齐的主流做法,核心流程可以拆成三阶段:

  1. 监督微调(SFT):在高质量指令数据上微调基础模型,得到一个具备基本对话能力的策略模型。
  2. 训练奖励模型(Reward Model):让人类标注者对同一提示词的多个回答排序,训练一个打分模型。
  3. 策略优化(PPO 对齐):用奖励模型输出作为奖励信号,在 KL 约束下用 PPO 进一步优化策略模型。

7.3 奖励模型训练公式

奖励模型通常基于 SFT 模型,把语言模型最后一个 token 的 hidden state 接一个线性层,输出标量奖励分数。对同一个提示词 $x$,有两个回答 $y_w$ 和 $y_l$,其中 $y_w$ 是人类更偏好的一项,训练目标是最小化 Bradley-Terry 排序损失:

$$ L(\phi) = - \mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma\left( r\phi(x, y_w) - r_\phi(x, y_l) \right) \right] $$

其中 $\sigma$ 是 sigmoid 函数。这个公式鼓励奖励模型给偏好回答更高的分,给非偏好回答更低的分。训练数据需要经过授权和脱敏处理,避免利用真实用户数据时产生隐私风险。

7.4 RLHF 策略优化目标

在 RLHF 的 PPO 阶段,语言模型就是 Actor,奖励模型近似充当 Critic 给出的奖励信号。由于奖励模型并非从真实环境中获得,而是拟合人类偏好,所以需要加上 KL 惩罚项,防止策略模型偏离 SFT 模型太远:

$$ \max_\theta \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} \left[ r_\phi(x, y) \right] - \beta , \mathbb{E}{x \sim \mathcal{D}} \left[ \operatorname{KL}\left( \pi\theta(\cdot|x) ,|, \pi_{\text{ref}}(\cdot|x) \right) \right] $$

这里的 $\pi_{\text{ref}}$ 通常是 SFT 阶段的模型,$\beta$ 控制偏离程度。KL 约束的作用是:在追求奖励最大化的同时,不牺牲语言模型原本的表达能力和连贯性。PPO 内部仍然需要价值网络估计 state value,用来计算 GAE 优势,这和前面 CartPole 代码里的结构一致,只是状态变成了 prompt 和生成的 token 序列。

7.5 RLHF PPO 训练伪代码

# RLHF 训练伪代码,需要根据实际框架调整 for prompt in dataloader: # 1. 用当前策略模型生成回答 response = policy_model.generate(prompt) # 2. 用奖励模型打分,并计算对参考模型的 KL 惩罚 reward = reward_model(prompt, response) kl_penalty = kl_divergence(policy_model(prompt), ref_model(prompt)) normalized_reward = reward - beta * kl_penalty # 3. 计算优势函数 advantage = compute_gae(reward, value_pred, done) # 4. PPO 更新 policy_loss = clipped_ppo_loss(log_probs, old_log_probs, advantage) value_loss = mse_loss(value_pred, return_target)

这套流程和单智能体 AC 框架是相通的。价值网络和策略网络可以部分共享参数,也可以分开训练,具体取决于模型规模和训练资源。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
训练不收敛,回合步数一直很低学习率过大或过小观察 loss 曲线变化调整学习率,优先用 3e-4 到 3e-3 区间
优势函数数值波动很大GAE 中 lambda 或 done 处理错误打印 advantage 分布,检查终止状态修正 done 处理逻辑,或对 advantage 做标准化
损失变成 NaN计算中出现 log 0 或梯度爆炸检查 log_prob 和 reward 范围加 epsilon 防止 log 0,使用梯度裁剪
PPO 更新后奖励突然暴跌概率比超出截断范围检查 KL 散度的变化幅度调小学习率,增大 KL 惩罚系数
RLHF 奖励模型过拟合训练数据规模小或标注噪声大在验证集上观察排序准确率增加数据多样性,使用多个标注员一致性校验
价值网络与策略网络收敛速度不匹配两个网络共用学习率或共用优化器分别观察 policy loss 和 value loss分开设置学习率,价值网络通常需要更低学习率
并行采样时数据分布不一致多个 worker 没有同步策略版本检查 worker 中的模型更新机制统一在训练端更新后广播策略参数
显存或内存不足批量生成长度过长样本过多观察峰值显存占用减小 batch size,或使用梯度累积

9. 最佳实践与下一步学习路径

9.1 工程建议

第一,先小参数跑通,再放大。第一次训练固定 seed,让结果可复现。把env.reset(seed=0)和所有随机数种子固定后,调试会快很多。

第二,保留一套最小可运行配置。比如 CartPole 上 128 隐藏层、学习率 3e-3、300 轮训练,就作为 AC 代码的基线模板,之后调参都基于这套模板改。

第三,模型文件、训练数据、日志输出分目录管理。建议使用如下目录结构:

project/ ├── configs/ # 超参数配置 ├── data/ # 训练数据 ├── models/ # 模型权重 ├── logs/ # TensorBoard 或文本日志 └── scripts/ # 训练脚本

第四,批处理任务要加日志和失败重试。强化学习训练经常跑几个小时后发现中间某一步出了 NaN,建议定期保存 checkpoint,并且把每个 epoch 的奖励均值、方差写入日志。

第五,接口服务训练完成后要限制访问范围。模型如果对外提供服务,需要先做内容安全测试,加上输入输出过滤,并且控制访问频率。

第六,涉及人脸、声音、版权素材时,必须提前获得授权。这条在 RLHF 数据采集阶段尤其重要。

9.2 学习路线建议

  • 第一步:把第 6 节的 CartPole 代码跑通,最好能自己复现 GAE 函数。
  • 第二步:比较 AC 和 REINFORCE 的训练曲线,观察引入价值网络后方差是否下降。
  • 第三步:用同样的环境替换成 PPO 目标,对比 AC 和 PPO 的稳定性。
  • 第四步:读 PPO 原始论文和 GAE 论文,用代码反向对照公式。
  • 第五步:读 RLHF 相关论文,用第 7 节的奖励模型公式复现一个小的排序模型。
  • 第六步:了解 DPO、KTO 等不需要 PPO 的偏好优化方法,理解不同对齐路径的取舍。

建议今天就做三件事:跑通 CartPole 上的 AC 代码,在训练日志里画出每轮步数曲线,然后用 GAE 替换掉单步 TD 误差观察效果。这三个动作做完,你不仅理解了 Actor-Critic 的数学原理,也具备了自己改算法和排查训练问题的能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询