动机与历史

问题:朴素的策略梯度更新对步长没有任何约束。仅仅一次倒霉的 batch 就可能把策略推入一个生成垃圾文本的区域 → 垃圾文本获得低奖励 → 下一次梯度让情况更糟 → 不可挽回地崩溃。

解决方案的演进:

  1. TRPO[317](2015):对新旧策略之间的 KL 散度施加约束。效果完美,但需要昂贵的二阶优化(Fisher 信息矩阵、共轭梯度)。
  2. PPO(2017)[319]:用一个简单的一阶 clipped 目标实现类似的稳定性。实现复杂度低了 10×,效果几乎一样,且能轻松扩展到分布式训练。

Clipped 目标

PPO 的核心创新是一个 clipped surrogate 目标:它能阻止破坏性的大幅策略更新,同时保持实现简单。

LCLIP​(θ)=𝔼t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ),1−ϵ,1+ϵ)​A^t)](5.1)

其中 rt​(θ)=πθ​(at|st)πθold​(at|st) 是概率比。

完整的 PPO Loss

L=LCLIP−c1​(Vθ​(st)−Vttarget)2⏟value loss+c2​H[πθ(⋅|st)]⏟entropy bonus(5.2)
  • 价值损失(c1=0.1):训练评论家(critic)去预测回报。同样被 clip 以保持稳定。
  • 熵奖励(c2=0.01):防止过早收敛到确定性策略,对探索至关重要。

PPO Gradient 与更新规则的推导

本节追溯从 RL 目标到 PPO 更新规则的数学路径,说明 clipped surrogate 为何有效。

第 1 步:RL 目标

目标是在策略下最大化期望累计奖励:

J⁡(θ)=𝔼τ∼πθ​[∑t=0Trt](5.3)

第 2 步:Policy Gradient 定理

J⁡(θ) 关于策略参数的梯度:

∇θJ​(θ)=𝔼πθ​[∑t=0T∇θ​log​πθ​(at|st)⋅A^t](5.4)

其中 A^t 是优势函数(即在状态 st 下动作 at 相对平均动作的好坏程度)。用优势替代完整回报是为了降低方差。

第 3 步:离策略数据的重要性采样

PPO 使用 πθold 收集数据,却更新 πθ。为修正这种分布失配,需应用重要性采样:

∇θJ​(θ)=𝔼πθold​[πθ​(at|st)πθold​(at|st)​∇θ​log⁡πθ​(at|st)⋅A^t](5.5)

定义概率比 rt​(θ)=πθ​(at|st)πθold​(at|st)。利用恒等式 ∇θ​log​f=∇θff,可得:

∇θJ​(θ)=𝔼πθold​[∇θrt​(θ)⋅A^t](5.6)

这意味着最大化 surrogate 目标:

LCPI​(θ)=𝔼t​[rt​(θ)⋅A^t](5.7)

第 4 步:无约束 Surrogate 的问题

LCPI 是一个合法的目标,但若无约束,单次梯度步就可能让 rt​(θ) 远离 1.0,导致:

  • 重要性权重变得极端 → 方差升高
  • 策略进入未经检验的区域 → 奖励模型给出不可靠的分数
  • 灾难性崩溃:策略生成垃圾,且无法恢复

TRPO 方案:约束 DKL(πθold∥πθ)≤δ。需要二阶方法(成本高)。

第 5 步:PPO 的 Clipped Surrogate(一阶近似)

PPO 用一个 clipped 目标取代硬性 KL 约束,仅使用一阶梯度即可达到类似行为:

LCLIP​(θ)=𝔼t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ),1−ϵ,1+ϵ)​A^t)](5.8)

梯度的推导:

令 Lt=min⁡(rt​A^t,r¯t​A^t),其中 r¯t=clip​(rt,1−ϵ,1+ϵ)。

∇θLt={∇θrt​(θ)⋅A^tif ​rt​A^t<r¯t​A^t​ (unclipped term is smaller)0if ​rt​A^t≥r¯t​A^t​ (clipped term is smaller, gradient = 0)(5.9)

展开各条件:

  • 当 A^t>0 且 rt<1+ϵ:梯度正常传播——策略被鼓励提升 πθ​(at|st)。
  • 当 A^t>0 且 rt≥1+ϵ:梯度为 零——策略提升已足够,停止继续推。
  • 当 A^t<0 且 rt>1−ϵ:梯度正常传播——策略被鼓励降低 πθ​(at|st)。
  • 当 A^t<0 且 rt≤1−ϵ:梯度为 零——策略降低已足够,停止继续推。

第 6 步:完整的 PPO 更新规则

将 clipped 策略损失、价值损失与熵奖励组合起来:

θk+1=θk+α⋅∇θ[LCLIP​(θ)−c1​LVF​(θ)+c2​H​[πθ]](5.10)

其中:

Rollout Buffer 与 Rollout

在 PPO 中,数据管理依赖于一个专门的短期存储系统,称为 Rollout Buffer。与把经验无限期保存在回放缓冲区中的 off-policy 算法(DQN)不同,PPO 需要一个临时结构来满足其 on-policy 的数学约束。

什么是 Rollout?

一次 rollout(轨迹)是指智能体在环境中运行其当前策略所生成的一串交互:

  • 该过程:智能体观察状态、选择动作、接收奖励,并转移到下一个状态。它会重复固定数量的步数,直至回合结束。
  • 在 LLM/RLHF 中:一次 rollout 就是从一个数据集中取出一条 prompt,让语言模型逐 token 生成完整序列,直到命中文本结束标记。每个 token 就是一次「step」。

Rollout Buffer

Rollout Buffer 临时保存 rollout 阶段收集到的全部数据。对每个生成的 token / 每一步,它记录:

ℬ={(st,at,log⁡πθold​(at|st),rt,V⁡(st))}t=1T(5.13)
  • st,at,rt:第 t 步的状态、所采取的动作与奖励。
  • log⁡πθold​(at|st):在生成该动作的那份策略下采取该动作的对数概率(计算概率比时需要)。
  • V⁡(st):价值函数给出的基线预测(计算 GAE 优势时需要)。

Rollout Buffer 的生命周期

该缓冲区按照严格的三阶段周期循环运转:

  1. 收集:当前策略与环境交互,用新鲜轨迹填满缓冲区(对于 batch=128、max_tokens=512 的 70B 模型:每次 rollout 最多 65K 条 token 级转移)。
  2. 训练:跨轨迹计算 GAE 优势。用 clipped 目标做 K 个 epoch(通常 3–10)的 mini-batch 梯度下降,以更新策略权重。
  3. 清空:整个缓冲区会被彻底清空。因为 PPO 是 on-policy 的,旧策略生成的数据无法安全地用于下一个更新周期——比率 rt​(θ) 会变陈旧,clipping 的保证也会失效。

用于 RLHF 的 PPO:完整循环

细节机制:Logits 与 Policy 更新

图 5.1:PPO 端到端流程:从 prompt batch 出发,经由生成、奖励打分、KL 计算、优势估计,直至 clipped 策略更新。反馈回路展示了更新后的策略被用于下一轮生成。
图 5.1:PPO 端到端流程:从 prompt batch 出发,经由生成、奖励打分、KL 计算、优势估计,直至 clipped 策略更新。反馈回路展示了更新后的策略被用于下一轮生成。

PPO 在内存中维护两个不同的参数状态,它们共享相同的神经网络拓扑,但在优化过程中持有不同的权重值:

阶段 1:Rollout(数据收集)

在数据收集期间,智能体与环境交互 T 步。在每一时间步 t:

  1. 环境给出当前状态/观测 st(对 LLM 而言:prompt 加上目前已生成的 token)。
  2. 状态 st 被送入当前网络快照(θold)。
  3. 网络输出原始未归一化的值——logitszold——一个大小为 |V| 的向量(词表规模 32K–128K)。
  4. 概率通过 Softmax 计算:P⁡(a∣st)=Softmax​(zold)=exp⁡(zold,a)∑j=1|V|exp⁡(zold,j)(5.14)
  5. 从 P⁡(a∣st) 中采样得到动作 at(下一个 token),并把转移元组 ⟨st,at,rt,st+1⟩ 连同 log⁡πθold​(at∣st) 一起存入 rollout buffer。

阶段 2:优化循环(Mini-Batch 更新)

一旦 rollout buffer 填满,PPO 就会在 mini-batch 上跑 K 个 epoch(通常 3–10)。每次梯度步都会用存储的状态 st 为两个策略生成 logits:

旧策略评估(冻结):

zold=f⁡(st,θold)⟶log⁡πθold​(at∣st)=LogSoftmax​(zold)​[at](5.15)

实现捷径:直接复用 rollout 时存下的标量,而不重新计算。

在线策略评估(更新中):

znew=f⁡(st,θ)⟶log⁡πθ​(at∣st)=LogSoftmax​(znew)​[at](5.16)

由于 θ 在每个 mini-batch 梯度步之后都会更新,znew 在整个优化循环中持续变化,而 zold 始终保持完全静止。

从 Logits 到概率比

PPO 的核心比率衡量的是:同一动作在新策略下比在旧策略下更可能还是更不可能:

rt​(θ)=πθ​(at∣st)πθold​(at∣st)(5.17)

为避免直接相除原始概率造成灾难性的数值下溢/上溢,计算在 对数空间中进行:

对比值取差值的指数即可还原出比率:

rt​(θ)=exp⁡(log⁡πθ​(at∣st)−log⁡πθold​(at∣st))(5.20)

该比率被注入 PPO 的 clipping 目标:

ℒCLIP​(θ)=𝔼^t​[min⁡(rt​(θ)​A^t,clip​(rt​(θ), 1−ϵ, 1+ϵ)​A^t)](5.21)

PPO 权重生命周期

阶段在线 θ旧 θold比率 rt​(θ)
1. Rollout 开始活跃副本同一个活跃副本始终为 1.0(按同一性)
2. Batch 第 1 步计算梯度冻结1.0(初始步骤)
3. Batch 第 N 步修改中(θ≠θold)冻结偏离 1.0(例如 1.06、0.94)
4. Clipping 生效被 ϵ 约束冻结被困在边界(1±ϵ)
5. 优化结束高度优化被丢弃N/A
6. 下一个周期θ→θold接收新的 θ重置回 1.0

表 5.1:在 PPO 训练各阶段中 θ 与 θold 的演化。

连续 Action 空间扩展

对于连续动作空间(对 LLM 并不典型,但对机器人 RL 很重要),网络输出的不是离散 logits,而是分布参数:

  • 预测的均值向量 μ
  • 预测的标准差向量 σ

对数概率通过高斯对数 PDF 计算:

log⁡π⁡(at∣st)=−12​(at−μσ)2−log⁡(σ)−12​log⁡(2​π)(5.22)

随后比率 rt​(θ)=exp⁡(log⁡πθ−log⁡πθold) 的计算方式完全相同,并送入同一个 clipping 目标。

TRL 实现

HuggingFace TRL 库 [376] 为 LLM 的所有主流 RL 方法提供了生产级实现。

from trl import PPOConfig, PPOTrainer, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
from peft import LoraConfig
 
# Model setup
model = AutoModelForCausalLMWithValueHead.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    torch_dtype=torch.bfloat16, device_map="auto",
    peft_config=LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj","v_proj","k_proj","o_proj"])
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
 
# PPO config with all critical hyperparameters
ppo_config = PPOConfig(
    learning_rate=1.5e-6,        # Low LR for stability
    batch_size=128,              # Prompts per step
    mini_batch_size=16,          # Gradient accumulation unit
    ppo_epochs=4,                # Epochs per batch (reuse data)
    gamma=1.0,                   # No discounting (single turn)
    lam=0.95,                    # GAE lambda
    cliprange=0.2,               # PPO epsilon
    cliprange_value=0.2,         # Value function clipping
    vf_coef=0.1,                 # Value loss coefficient
    init_kl_coef=0.05,           # Initial KL penalty
    target_kl=6.0,               # Adaptive KL target
    whiten_rewards=True,         # Normalize advantages
    gradient_accumulation_steps=4,
    max_grad_norm=1.0,
)
 
ppo_trainer = PPOTrainer(config=ppo_config, model=model, tokenizer=tokenizer,
    dataset=prompt_dataset, data_collator=collator)
 
# Training loop
for batch in ppo_trainer.dataloader:
    # 1. Generate responses
    query_tensors = batch["input_ids"]
    response_tensors = ppo_trainer.generate(
        query_tensors, max_new_tokens=512, temperature=0.7, top_p=0.9, do_sample=True
    )
    # 2. Score with reward model
    texts = [tokenizer.decode(r, skip_special_tokens=True) for r in response_tensors]
    rewards = [torch.tensor(reward_model.score(q, r)) for q, r in zip(batch["query"], texts)]
    # 3. PPO update (handles KL, GAE, clipping internally)
    stats = ppo_trainer.step(query_tensors, response_tensors, rewards)
    # Monitor: stats["ppo/mean_scores"], stats["ppo/policy/approx_kl"]

关键超参

参数典型取值设置错误的后果cliprange0.2过低:不学习。过高:不稳定。init_kl_coef0.01–0.1过低:奖励作弊。过高:卡在 SFT 阶段。target_kl4–8自适应控制器的目标值。越低越保守。ppo_epochs4过多:过拟合到 batch。过少:浪费生成算力。learning_rate1−5×10−6过高:灾难性遗忘。batch_size64–256越大:梯度越平滑,生成算力消耗越多。temperature0.7–1.0越低:探索更少。越高:优势噪声更大。