PPO 完全指南:从 TRPO 到工程落地

一、背景:PPO 从哪里来?

PPO 的前身是 TRPO(Trust Region Policy Optimization)。要理解 PPO 为什么这样设计,必须先理解 TRPO 想解决什么问题。

TRPO 的核心思想

强化学习中,策略梯度方法的核心问题是:每次更新步子多大合适?

步子太小 → 收敛慢;步子太大 → 策略崩溃,难以恢复。

TRPO 的解法是:在优化目标的同时,加一个硬约束,限制新旧策略的 KL 散度不超过阈值

TRPO 能理论保证策略单调改进,但代价沉重:

  • 需要计算 Fisher 信息矩阵的逆,计算量是参数量的平方级
  • 需要共轭梯度迭代求解约束优化
  • 大模型参数动辄几十亿,完全不可行

PPO 的核心改动

PPO 用一个极其简单的想法替代了 TRPO 的二阶约束:

直接把概率比值截断(clip)在 的范围内,从而限制策略更新幅度。

TRPO 的约束是在参数空间上控制 KL 距离( 是 KL 散度上界);PPO 的 是在概率比值空间上直接截断,两者度量体系不同,不能直接比大小。典型取值:

PPO 牺牲了理论上的严格保证,换来了一阶优化、标准反向传播,工程实现大幅简化。

二、PPO 公式详解

2.1 核心目标函数

逐符号解释:

符号 含义
Actor(策略网络)当前参数
对一批采样数据取期望(均值)
概率比率 = ,衡量新旧策略对同一动作的概率之比
优势函数估计值,表示动作 比”平均水平”好多少
截断超参,控制更新幅度,典型值
强制限制在 范围内

2.2 为什么是 clip + min,而不只是 clip?

这是 PPO 设计中最精妙的地方,单独用 clip 是不够的。

场景一:优势 (好动作,应该增大概率)

  • 时:clip 强制截断,梯度为零,不再继续增大比率 ✓
  • 时:min 取 (未截断值更小),仍允许向好方向移动 ✓

场景二:优势 (坏动作,应该减小概率)

  • 时:clip 强制截断,梯度为零,不再继续减小比率 ✓
  • 时(概率不降反升!):
    • 此时 是很负的值
    • 被截断,是一个”没那么负”的值
    • min 会选更负的那个,即 ,产生梯度,强迫把比率拉回来

结论:clip 负责”别走太远”,min 负责”走错了必须拉回来”,二者缺一不可。

2.3 完整损失函数

含义 作用
Actor 策略损失 优化动作选择
Critic 价值损失(MSE) 训练价值估计更准确
策略熵 鼓励探索,防止策略过早收敛
权重系数 平衡各项损失

2.4 优势函数:GAE

PPO 通常使用 GAE(Generalized Advantage Estimation) 来估计

  • :TD 残差,是单步优势的估计
  • :折扣因子,控制未来奖励的权重
  • :GAE 平滑系数,控制方差和偏差的权衡

三、PPO 的系统架构

在大语言模型的 RLHF(基于人类反馈的强化学习)场景中,PPO 同时需要维护 4 个模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  query (q)


┌─────────┐
│ Actor │ ← 唯一被训练的模型(策略网络 π_θ)
│ Policy │
└─────────┘
│ 生成输出 (o)
├──────────────────────┬──────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌───────────┐
│ Reference │ │ Reward │ │ Value │
│ Model │ │ Model │ │ (Critic) │
│ (冻结) │ │ (冻结) │ │ (训练) │
└───────────┘ └──────────┘ └───────────┘
│ │ │
│ KL 惩罚项 │ 奖励分 r │ 价值估计 v
└──────────┬───────────┘ │
▼ │
r(含KL惩罚) v
└──────────────┬───────────────────┘

GAE 计算
┌──────────┐
│ A │ 优势函数 → 更新 Actor
│ (优势) │
└──────────┘
┌──────────┐
│ Returns │ 真实回报 → 更新 Critic
└──────────┘

四个模型的角色

模型 是否训练 作用
Actor(策略网络) ✅ 是 生成回答,是训练的最终目标
Critic(价值网络) ✅ 是 估计当前状态价值 ,计算 GAE,训练完可丢弃
Reference Model ❌ 冻结 SFT 初始模型,计算 KL 散度,防止 Actor 跑偏
Reward Model ❌ 冻结 人类偏好训练的打分模型,给每条输出打分

KL 惩罚机制:

Reference Model 保证了 Actor 不会为了刷高奖励分而产生”奖励黑客”——说出毫无意义但高分的奇怪输出。

四、核心代码讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import torch
import torch.nn as nn
import torch.nn.functional as F

class PPOTrainer:
def __init__(self, clip_epsilon=0.2):
self.clip_epsilon = clip_epsilon

def compute_loss(self, old_log_probs, new_log_probs, advantages, returns, values):
# ----------------------------------------------------------------
# Step 1:计算概率比率 r_t
# ----------------------------------------------------------------
# 用对数差代替直接相除:exp(log π_new - log π_old) = π_new / π_old
# 数值稳定,避免概率值极小时的浮点下溢
ratio = torch.exp(new_log_probs - old_log_probs)
# old_log_probs 必须 detach(),它来自采样时的旧策略,不参与梯度计算
# new_log_probs 保留梯度,是当前 Actor 的输出

# ----------------------------------------------------------------
# Step 2:未截断损失(Surrogate 1)
# ----------------------------------------------------------------
surr1 = ratio * advantages

# ----------------------------------------------------------------
# Step 3:截断损失(Surrogate 2)
# ----------------------------------------------------------------
# torch.clamp 将 ratio 强制限制在 [1-ε, 1+ε]
ratio_clipped = torch.clamp(ratio, 1.0 - self.clip_epsilon, 1.0 + self.clip_epsilon)
surr2 = ratio_clipped * advantages

# ----------------------------------------------------------------
# Step 4:Actor Loss = -min(surr1, surr2)
# ----------------------------------------------------------------
# min:取保守估计,防止过度优化
# 负号:因为 PyTorch 做梯度下降,而我们想最大化目标
policy_loss = -torch.min(surr1, surr2).mean()

# ----------------------------------------------------------------
# Step 5:Critic Loss = MSE(预测值, 真实回报)
# ----------------------------------------------------------------
# values:Critic 对当前状态的价值预测 V(s_t)
# returns:GAE 计算出的真实折扣回报(rewards-to-go)
# 注意:advantages 用于更新 Actor;returns 用于更新 Critic
value_loss = F.mse_loss(values, returns)

# ----------------------------------------------------------------
# Step 6:总损失合并
# ----------------------------------------------------------------
# 0.5 是常用的 Critic 损失系数(也可加熵正则)
total_loss = policy_loss + 0.5 * value_loss

return total_loss

关键实现细节

old_log_probs 必须 detach

old_log_probs 是在数据采集(rollout)阶段用旧策略计算的,它是常数基准,不应该有梯度。如果不 detach,PyTorch 会尝试对它反向传播,导致错误或无意义的梯度。

② 对数差 vs 直接相除

1
2
3
4
5
# ❌ 直接相除:数值不稳定
ratio = new_probs / old_probs

# ✅ 对数差后取指数:数值稳定
ratio = torch.exp(new_log_probs - old_log_probs)

当概率值非常小(如 )时,直接相除会有浮点精度问题;对数空间做减法再取指数则稳定得多。

③ advantages vs returns 的区别

变量 公式 用途
advantages GAE 告诉 Actor”这个动作比均值好多少”,用于更新策略
returns 告诉 Critic”这个状态的真实价值是多少”,用于更新价值估计

五、PPO 的缺点

PPO 虽然工程简洁、效果稳定,但在大语言模型场景下暴露出明显短板:

1. 显存压力极大

同时需要加载 4 个大模型(Actor、Critic、Reference、Reward),且 Actor 和 Critic 通常与原始 LLM 同等体量。以 7B 参数模型为例:

仅模型权重就需要 56GB,还不含激活值、优化器状态,实际需求轻松超过 160GB。

2. On-Policy 导致训练效率低

PPO 是 On-Policy 算法:每次用当前策略生成数据,更新几步后数据就过期,必须重新采样。

1
生成 → 训练几步 → 数据过期 → 重新生成 → 训练几步 → ...

对于 LLM 而言,生成是自回归的,速度远慢于训练。生成成为整个训练流程的瓶颈。

3. Critic 训练带来额外复杂性

Critic 需要与 Actor 同步训练,且本身也是一个 LLM 级别的大模型。如果 Critic 估值不准,会导致 GAE 偏差,进而污染 Actor 的训练信号。

4. 超参数敏感

PPO 有多个超参数需要精心调整:clip 系数 、Critic 系数 、熵系数 、GAE 的 、KL 惩罚系数 ……各超参之间存在耦合,调参成本高。

六、后续改进方向

针对 PPO 的上述缺点,学界提出了几个有影响力的改进方案:

算法 核心改动 解决的问题
ReMax 用 REINFORCE + 贪心基线替代 GAE 去掉 Critic,减少显存
RLOO 组内多条输出互为基线 去掉 Critic,基线更稳定
GRPO 组内平均奖励做基线,去掉 Critic 去掉 Critic,只需 Actor + Reference,DeepSeek-R1 采用

其中 GRPO 目前最为流行。其优势函数估计为:

对同一个 query 采样 条输出,组内标准化得到优势,完全绕开了 Critic 模型。

总结

维度 TRPO PPO GRPO
约束方式 KL 散度硬约束 clip 截断 clip + 组内基线
优化阶数 二阶(共轭梯度) 一阶(Adam) 一阶(Adam)
所需模型数 2 4 2
工程复杂度
理论保证 单调改进 近似保证 无严格保证
大模型适用性 可用

PPO 是强化学习工程化落地的重要里程碑,理解它的公式逻辑(clip + min 的协同)、架构设计(四模型分工)和代码实现,是进入大模型 RLHF 领域的必要基础。

take home message:
PPO arch

「源码解析」合集:本文与 TRPO 解析:从数学原理到代码实现MiniMind 源码阅读与架构分析 同属一个系列。建议先读 TRPO 那篇,PPO 的所有设计都是对它的工程简化。