Back

04 | 强化学习核心:从MDP到PPO

June 29, 202611 min read
Agent

04 | 强化学习核心:从MDP到PPO

核心问题:PPO为什么是RLHF的标配?它到底解决了什么问题?


开篇:一个让人上瘾的小游戏

想象你面前有一个简单的游戏:

┌─────────────────────────────────────────────────────┐
│                                                     │
│   状态:[小车位置, 小车速度, 杆子角度, 杆子角速度]    │
│                                                     │
│   动作:向左推 / 向右推                               │
│                                                     │
│   目标:让杆子保持直立,越久越好                       │
│                                                     │
│   奖励:每存活一步 +1                                 │
│                                                     │
└─────────────────────────────────────────────────────┘

这就是经典的 CartPole 环境。规则简单到小学生都能理解,但让一个AI从零学会它,需要经历一个精妙的过程。

我写了一个最朴素的算法来训练这个任务。先让Agent随机乱动,记录每次的总奖励,然后"鼓励"那些表现好的动作。具体来说:

第1局:随机动作,杆子2步就倒了,奖励=2
第2局:随机动作,杆子8步倒了,奖励=8  ← 这局不错,记住这些动作!
第3局:随机动作,杆子3步倒了,奖励=3
...
第100局:平均奖励=15
第500局:平均奖励=45
第1000局:平均奖励=180
第2000局:平均奖励=350 ← 接近满分!

训练曲线对比

这就是强化学习的魔力:没有标注数据,没有"正确答案",只有"做得好不好"的信号,Agent就能从一片混沌中学会最优策略。

但问题来了——上面的朴素算法(叫REINFORCE)收敛慢、方差大、样本效率低。实际工程中几乎没人用它。工业界的标准答案是 PPO(Proximal Policy Optimization),也就是ChatGPT用来做RLHF的那个算法。

这篇文章,我们从最底层的MDP出发,一步步推导出PPO,搞清楚它到底解决了什么问题,为什么能成为RLHF的标配。

预警:这是本系列数学最多的一篇。 但别慌——每个公式之前,我都会先用大白话和类比解释直觉。你可以先读直觉,再看公式,两遍下来一定懂。


4.1 MDP:把世界装进数学框架

先讲个故事

你训练一只小狗"坐下"。

  • 你说"坐下"(状态 s
  • 小狗坐下(动作 a
  • 你给一块饼干(奖励 r
  • 小狗下次听到"坐下"更可能坐下(策略更新

这个过程,就是强化学习的核心。我们需要一个数学框架来精确描述它——这就是 马尔可夫决策过程(MDP)

MDP的五元组

一个MDP可以写成 (S, A, P, R, γ),每个字母代表什么:

| 符号 | 名称 | 直觉 | CartPole例子 | |------|------|------|-------------| | S | 状态空间 | 世界的所有可能情况 | 所有(位置,速度,角度,角速度)的组合 | | A | 动作空间 | Agent能做的所有动作 | {向左推, 向右推} | | P | 转移概率 | 做了动作后世界怎么变 | 向左推后,杆子角度怎么变 | | R | 奖励函数 | 每一步的即时反馈 | 杆子没倒→+1,倒了→0 | | γ | 折扣因子 | 未来奖励的重要程度 | γ=0.99:100步后的奖励≈现在的37% |

马尔可夫性:一个关键的"偷懒"假设

MDP的名字里有个"马尔可夫",它说的是:

未来只取决于现在,跟过去无关。

用数学写:P(s_{t+1} | s_t, a_t, s_{t-1}, a_{t-1}, ...) = P(s_{t+1} | s_t, a_t)

类比:你在下棋。MDP假设"下一步的局面只取决于当前棋盘和你这一步的走法",不需要知道前10步是怎么走到这个局面的。

这当然是个简化——现实中很多情况不满足马尔可夫性。但如果我们把"状态"定义得足够丰富(比如包含所有历史信息),这个假设基本成立。

CartPole的MDP,具体长什么样?

状态 s = [x, ẋ, θ, θ̇]
    x   = 小车位置(-4.8 ~ 4.8)
    ẋ   = 小车速度(-∞ ~ ∞)
    θ   = 杆子角度(-41.8° ~ 41.8°)
    θ̇   = 杆子角速度(-∞ ~ ∞)

动作 a ∈ {0: 向左推, 1: 向右推}

转移 P(s'|s,a):
    由物理引擎决定(重力、质量、力的大小)
    我们不需要知道具体公式,只需要能"模拟"

奖励 R(s,a,s'):
    如果杆子角度 < 阈值 且 小车没出界:+1
    否则:0(episode结束)

折扣 γ = 0.99

我们要解什么?

给定一个MDP,我们的目标是找到一个 策略 π(a|s)——在每个状态下选择什么动作——使得累积回报最大

         ∞
G_t = Σ  γ^k · r_{t+k+1}
        k=0

目标:找到 π* 使得 E[G_t] 最大

用大白话说:策略就是一个"决策规则",告诉你在每种情况下该怎么做。我们要找到那个让你"一辈子"得分最高的规则。

折扣因子 γ 的意义:

γ = 0    → 只看眼前(短视)
γ = 0.5  → 10步后的奖励只有现在的 1/1024
γ = 0.99 → 100步后的奖励还有现在的 37%(有远见)
γ = 1.0  → 所有奖励同等重要(可能发散)

4.2 策略梯度:从"碰运气"到"有方向地学"

先讲直觉:训练鹦鹉

想象你在训练鹦鹉做动作。

  1. 鹦鹉随机做动作
  2. 做得好→给零食(正强化)
  3. 做得差→不给零食(自然淘汰)
  4. 久而久之,鹦鹉学会了好动作的概率越来越高

策略梯度就是这个思路:好的动作,提高它被选中的概率;差的动作,降低概率。

数学推导:为什么这个直觉是对的

设策略 π_θ(a|s) 是一个参数化模型(比如神经网络),参数是 θ。

我们的目标是最大化期望回报 J(θ):

J(θ) = E_π[G] = E_π[Σ γ^t · r_t]

我们要计算 J 对 θ 的梯度——"往哪个方向调参数能让回报增大"。

核心技巧(log-derivative trick)

∇_θ J(θ) = E_π[G · ∇_θ log π_θ(a|s)]

这个公式怎么来的? 用三步:

步骤1:写出期望
  J(θ) = Σ_τ P(τ;θ) · G(τ)
  其中 τ 是一条完整的轨迹 (s₀,a₀,s₁,a₁,...)

步骤2:对 P(τ;θ) 求梯度
  ∇_θ P(τ;θ) = P(τ;θ) · ∇_θ log P(τ;θ)
  (这是数学恒等式:∇f = f · ∇log f)

步骤3:代入期望
  ∇_θ J(θ) = E_π[G(τ) · ∇_θ log P(τ;θ)]
  
  由于 P(τ;θ) = Π π_θ(aₜ|sₜ)
  所以 ∇log P(τ;θ) = Σ ∇log π_θ(aₜ|sₜ)

最终公式的直觉

∇_θ J(θ) = E[回报 × "这个动作有多出人意料"]
                ↑            ↑
                G(τ)      ∇log π(a|s)
  • 如果 G 很大(这局打得好),就增大好动作的概率
  • 如果 G 很小(这局打得差),就减小差动作的概率
  • ∇log π 是"调整方向"——让好的动作更可能被选中

REINFORCE算法:最简单的策略梯度

REINFORCE算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
对于每个episode:
  1. 用当前策略 π_θ 跑一局,得到轨迹 τ
  2. 计算这局的总回报 G
  3. 更新参数:θ ← θ + α · G · Σ ∇log π_θ(aₜ|sₜ)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

问题:这个算法有一个致命弱点——方差极大

方差问题:为什么REINFORCE学这么慢?

想象你在教一个蒙着眼睛的人走迷宫:

方案A(REINFORCE):
  让他随机走100次,每次走完告诉他"这次得了多少分"
  得分高的那次,他记住路径
  问题:100次里可能99次都是死路,有效信息极少

方案B(更好的方法):
  让他走一步就告诉他"这步好不好"
  不需要等走完整个迷宫
  有效信息多得多,学得快

方差的本质:REINFORCE用一整条轨迹的总回报 G 来更新每一步的动作。但 G 里面混杂了太多噪音——有些步骤做得好,有些做得差,但你给它们都乘了同一个 G。

类比:期末考试你总分85分。老师说"你每道题都做得不错"——但其实你选择题满分,大题只拿了一半分。用总分来评价每道题,太粗糙了。

解决方案:引入一个"基准线"(baseline),把绝对分数变成相对分数。

原来:∇J = E[G · ∇log π(a|s)]
改进:∇J = E[(G - b) · ∇log π(a|s)]

b = 平均回报(baseline)
效果:高于平均的动作→正梯度→概率增大
      低于平均的动作→负梯度→概率减小

数学上可以证明,减去baseline不影响梯度的期望(无偏),但大幅降低方差。


4.3 Actor-Critic:两全其美的方案

从方差问题到Actor-Critic

baseline虽然能降低方差,但它只是一个常数——对所有状态一视同仁。更好的做法是:对每个状态都有一个专门的baseline

这就是 状态价值函数 V(s):在状态 s 下,按当前策略行动,期望能拿多少分。

V(s) = E[G_t | s_t = s]

"在这个局面下,我大概能拿多少分?"

有了 V(s),我们可以定义 优势函数(Advantage)

A(s,a) = Q(s,a) - V(s)

"在这个局面下选这个动作,比平均水平好多少?"

| A(s,a) | 含义 | 梯度方向 | |--------|------|---------| | A > 0 | 这个动作比平均好 | 增大该动作概率 | | A = 0 | 这个动作就是平均水平 | 不更新 | | A < 0 | 这个动作比平均差 | 减小该动作概率 |

Actor-Critic架构

现在我们有两个组件:

┌───────────────────────────────────────────────────────┐
│                                                       │
│    ┌─────────────┐         ┌─────────────┐           │
│    │   Actor     │         │   Critic    │           │
│    │  π_θ(a|s)   │         │   V_φ(s)    │           │
│    │             │         │             │           │
│    │ "选什么动作" │         │ "这步好不好" │           │
│    └──────┬──────┘         └──────┬──────┘           │
│           │                       │                   │
│           │    动作 a             │  优势 A(s,a)      │
│           ├──────────────────────▶│                   │
│           │                       │                   │
│           │◀──────────────────────┤                   │
│           │    优势 A(s,a)        │                   │
│           │                       │                   │
│    更新 θ:                   更新 φ:                 │
│    θ ← θ + α·A·∇logπ       φ ← φ + β·TD_error       │
│                                                       │
└───────────────────────────────────────────────────────┘

Actor(演员):负责选动作,用策略梯度更新 Critic(评论家):负责评价动作好不好,用TD误差更新

Bias-Variance权衡:Critic不准怎么办?

Actor-Critic有一个微妙的问题:

如果 Critic 完全准确(A(s,a) 精确):
  → 方差最小,梯度最准
  → 但需要 Critic 完美收敛,几乎不可能

如果不用 Critic(REINFORCE):
  → 无偏(用真实回报 G)
  → 但方差极大

Actor-Critic 居中:
  → 用 Critic 的估计代替真实值
  → 有偏(Critic 不完美)但方差小
  → 实践中效果好得多

类比

REINFORCE = 问100个路人"这道菜好不好吃",取平均
Actor-Critic = 问一个美食评论家"这道菜好不好吃"

路人多但噪音大(高方差)
评论家有偏见但稳定(低方差,可能有偏)
实践中,一个好的评论家比100个路人有用

TD误差:Critic怎么更新?

Critic不需要等一局结束才更新。它可以用 时序差分(Temporal Difference, TD) 来逐步学习:

TD目标:r + γ·V(s')
TD误差:δ = r + γ·V(s') - V(s)

直觉:"我原来以为这个局面值V(s)分,
      但现在拿到r分,而且下一步的局面值V(s')分。
      我的估计差了 δ = r + γ·V(s') - V(s),
      所以应该调整一下。"

Bellman方程的直觉版本

V(s) = 当前奖励 + 折扣后的未来价值
     = r + γ · V(s')

"一个局面的价值 = 马上拿到的 + 下一步的价值(打个折)"

这就是著名的 Bellman方程——强化学习的基石。它把"长期价值"分解成了"即时奖励 + 未来价值"的递归关系。


4.4 PPO深入解析:工业界的标准答案

先理解问题:为什么需要PPO?

策略梯度有一个致命问题:更新步长不可控

假设当前策略在状态s选动作a的概率是 π(a|s) = 0.6

情况1:A(s,a) = 10(这个动作很好!)
梯度更新后:π(a|s) = 0.6 + 0.5 × 10 = 5.6 ???

概率不能超过1!但梯度更新不管这个。
实际上,一步更新可能让策略发生剧变:
  之前:80%概率选左
  更新后:95%概率选左(变化太大,可能错过更好的策略)

类比:你在调收音机。如果旋钮太灵敏,轻轻一碰就从FM97.4跳到FM88.7——你根本找不到想要的台。PPO就是给旋钮加了一个"限位器"。

TRPO:第一个解决方案(但太复杂)

2015年,Schulman等人提出了 TRPO(Trust Region Policy Optimization)

TRPO的思路:
  限制新旧策略之间的"距离",不让策略变化太大

  max  E[A(s,a) · π_new(a|s)/π_old(a|s)]
  s.t. E[KL(π_old || π_new)] ≤ δ

  其中 π_new/π_old 的比值叫"重要性采样比率"
  KL散度衡量新旧策略的"距离"

TRPO效果好,但实现极其复杂——需要计算二阶优化(Fisher信息矩阵的逆),计算量大,工程实现困难。

PPO:TRPO的简化版,但效果更好

2017年,Schulman提出了 PPO——用一种更简单的方式达到类似TRPO的效果。

PPO的核心是 clip机制

定义比率:r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)

如果 r_t = 1.0 → 新旧策略在这个动作上完全一样
如果 r_t = 1.5 → 新策略选这个动作的概率比旧策略高50%
如果 r_t = 0.5 → 新策略选这个动作的概率比旧策略低50%

PPO的目标函数:
L^CLIP(θ) = E[min(r_t·A_t, clip(r_t, 1-ε, 1+ε)·A_t)]

其中 ε 通常取 0.1 或 0.2

clip到底在干什么? 用一张图说清楚:

PPO Clip机制

当 A > 0(好动作,想增大概率):
  r_t · A 随 r_t 线性增长
  但 clip(r_t, 0.8, 1.2) · A 在 r_t=1.2 时封顶
  → 最多只允许概率比提高20%(ε=0.2时)
  → 防止"太激动",把一个好动作的概率推得太高

当 A < 0(差动作,想减小概率):
  r_t · A 随 r_t 增大而减小(更想减小r_t)
  但 clip(r_t, 0.8, 1.2) · A 在 r_t=0.8 时封顶
  → 最多只允许概率比降到0.8
  → 防止"太嫌弃",把差动作的概率压得太低

min操作的意义:
  取两者的较小值 → 保守更新
  如果新策略偏离太远,梯度变成0(不再更新)

用大白话说:PPO给策略更新设了一个"安全区间"。新策略可以比旧策略好一点,但不能好太多(防止过拟合到少量样本);可以比旧策略差一点,但不能差太多(防止突然变蠢)。

为什么clip比TRPO的KL约束更好?

| 维度 | TRPO | PPO | |------|------|-----| | 约束方式 | KL散度硬约束 | clip软约束 | | 优化方法 | 二阶优化(共轭梯度) | 一阶优化(Adam/SGD) | | 实现复杂度 | 高(需要Hessian向量积) | 低(几十行核心代码) | | 超参数敏感 | δ(KL上界) | ε(clip范围),通常0.1-0.2 | | 实际效果 | 好 | 相当或更好 | | 工程友好度 | 差 | 极好 |

PPO的成功哲学:与其用复杂的数学约束来限制策略变化,不如用一个简单的clip来"截断"梯度。效果差不多,但实现简单一个数量级。

GAE:优势估计的艺术

前面我们说优势函数 A(s,a) = Q(s,a) - V(s)。但怎么估计它?

方法1:蒙特卡洛(MC)

A(s_t, a_t) = r_t + γr_{t+1} + γ²r_{t+2} + ... - V(s_t)
             (用实际回报代替Q值)

优点:无偏
缺点:方差大(要等一整局结束)

方法2:TD(0)

A(s_t, a_t) = r_t + γV(s_{t+1}) - V(s_t)
             (一步TD误差)

优点:方差小(只看一步)
缺点:有偏(V的估计不准就全错了)

GAE:两者的加权平均

A^GAE(γ,λ) = Σ_{l=0}^{∞} (γλ)^l · δ_{t+l}

其中 δ_t = r_t + γV(s_{t+1}) - V(s_t) 是TD误差

展开写:
A^GAE = δ_t + γλ·δ_{t+1} + (γλ)²·δ_{t+2} + ...

λ 的作用

λ = 0  → A = δ_t(纯TD,低方差,高偏差)
λ = 1  → A = MC回报 - V(s_t)(纯MC,高方差,无偏)
λ = 0.95 → 折中(实践中最常用)

类比:你在预测明天的天气。

λ = 0:只看今天的天气(简单但可能不准)
λ = 1:看未来所有天的天气(信息多但噪音大)
λ = 0.95:主要看近几天,远处的天气仅作参考(最佳平衡)

GAE的精妙之处在于:用一个超参数 λ 来控制 bias-variance 权衡。实践中 λ=0.95 几乎总是最好的起点。

PPO完整算法

PPO算法(PPO-Clip版本):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始化策略网络 π_θ 和价值网络 V_φ

对于每次迭代:
  1. 用 π_θ_old 与环境交互,收集一批数据 {s_t, a_t, r_t, s_{t+1}}
  
  2. 计算GAE优势估计:
     δ_t = r_t + γ·V_φ(s_{t+1}) - V_φ(s_t)
     A_t = Σ_l (γλ)^l · δ_{t+l}
  
  3. 计算回报目标:
     R_t = A_t + V_φ(s_t)
  
  4. 在收集的数据上,进行K个epoch的 mini-batch 更新:
     
     比率:r_t = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)
     
     策略损失:L^CLIP = -E[min(r_t·A_t, clip(r_t,1-ε,1+ε)·A_t)]
     价值损失:L^VF = E[(V_φ(s_t) - R_t)²]
     熵奖励:S = -E[π_θ·log π_θ](鼓励探索)
     
     总损失:L = L^CLIP + c₁·L^VF - c₂·S
     
     用Adam优化器更新 θ 和 φ
  
  5. θ_old ← θ
  
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

关键超参数

| 参数 | 典型值 | 作用 | |------|--------|------| | ε (clip范围) | 0.1~0.2 | 控制策略更新幅度 | | γ (折扣因子) | 0.99 | 对未来奖励的重视程度 | | λ (GAE参数) | 0.95 | bias-variance权衡 | | K (更新epoch数) | 3~10 | 每批数据用几次 | | c₁ (价值损失权重) | 0.5~1.0 | Critic的学习强度 | | c₂ (熵奖励权重) | 0.001~0.01 | 探索的激励强度 |

On-Policy意味着什么?

PPO是一个 on-policy 算法。这意味着:

On-policy(PPO):
  收集数据的策略 = 被更新的策略
  每次更新后,必须用新策略重新收集数据
  → 样本效率低(数据用完就扔)
  → 但训练稳定

Off-policy(DQN):
  收集数据的策略 ≠ 被更新的策略
  可以用历史数据反复训练
  → 样本效率高(经验回放)
  → 但可能不稳定

类比

On-policy = 学开车时,每次练车都用最新的驾驶技术
  → 安全,但费油(每次都要重新上路)

Off-policy = 学开车时,可以看别人开车的录像来学习
  → 省油,但可能学到过时的习惯

这就是为什么PPO在RLHF中特别合适:LLM生成回复的成本虽然高,但每次生成的数据质量更好(因为用的是最新的策略),训练更稳定。


4.5 PPO与RLHF:为什么是标配?

RLHF的完整流程

┌─────────────────────────────────────────────────────────────────┐
│  RLHF三阶段流程                                                  │
│                                                                 │
│  阶段1:SFT(监督微调)                                          │
│  ┌──────────┐    高质量示范数据    ┌──────────────┐             │
│  │ 预训练模型 │ ──────────────────▶ │ SFT模型       │             │
│  └──────────┘                      └──────┬───────┘             │
│                                           │                      │
│  阶段2:训练奖励模型(RM)                   │                      │
│  ┌──────────┐    人类偏好对比数据    ┌──────┴───────┐            │
│  │ SFT模型   │ ──────────────────▶  │ 奖励模型 RM   │            │
│  └──────────┘                       └──────┬───────┘            │
│                                            │                     │
│  阶段3:PPO训练                             │                     │
│  ┌──────────┐    RM作为奖励信号     ┌──────┴───────┐            │
│  │ SFT模型   │ ──────────────────▶  │ PPO优化后的   │            │
│  │(初始策略)│                      │ 对齐模型      │            │
│  └──────────┘                       └──────────────┘            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

在阶段3中,PPO的角色是:

状态 s   = 当前已生成的文本(prompt + 已生成的tokens)
动作 a   = 下一个token
策略 π   = 正在训练的LLM
奖励 r   = 生成完整回复后,RM给的打分

为什么PPO适合RLHF?

原因1:处理离散动作空间

LLM的每个token都是从词表中离散的采样,PPO天然处理离散动作。

原因2:稳定的训练

RLHF中奖励模型本身有噪声(人类标注不一致),PPO的clip机制能防止模型被噪声奖励带偏。

没有clip:RM给了一个异常高分 → 策略剧变 → 模型崩了
有clip  :RM给了一个异常高分 → 策略最多变20% → 安全

原因3:KL约束防止偏离太远

PPO训练中通常还会加一个KL惩罚项:

r_total = r_RM - β · KL(π_θ || π_ref)

π_ref = SFT模型(参考策略)
效果:防止模型为了刷RM的分而"跑偏"(reward hacking)

原因4:工程成熟度

OpenAI在InstructGPT中验证了PPO的有效性,之后DeepSpeed-Chat、TRL等框架都提供了成熟的PPO实现。

RLHF中PPO的特殊挑战

| 挑战 | 说明 | 解决方案 | |------|------|---------| | 奖励稀疏 | 只在生成完整个回复后才有奖励 | 使用过程奖励模型(PRM) | | KL散度计算 | 需要在token级别计算 | 使用log概率差的滑动平均 | | 模型规模大 | LLM有数十亿参数 | DeepSpeed ZeRO、模型并行 | | Reward Hacking | 模型找到RM的漏洞 | KL约束、reward ensembling |


4.6 实验:REINFORCE vs PPO

我们用配套代码 code/04_ppo_demo.py 在 CartPole 环境上对比两种算法。

实验设置

环境:CartPole-v1(OpenAI Gymnasium)
  - 状态维度:4(位置、速度、角度、角速度)
  - 动作空间:2(左推、右推)
  - 最大步数:500

REINFORCE配置:
  - 隐藏层:[64, 64]
  - 学习率:0.01
  - baseline:移动平均回报

PPO配置:
  - 隐藏层:[64, 64]
  - 学习率:0.003
  - clip ε:0.2
  - GAE λ:0.95
  - 更新epoch:4
  - mini-batch size:64

每种算法跑5个随机种子,取平均。

实验结果

训练曲线对比

| 指标 | REINFORCE | PPO | |------|-----------|-----| | 达到200分的训练步数 | ~3000 episodes | ~800 episodes | | 最终平均奖励 | 320 ± 45 | 470 ± 25 | | 训练稳定性(标准差) | 大 | 小 | | 收敛速度 | 慢 | 快(约3.5倍) |

关键发现

发现1:PPO的样本效率远高于REINFORCE

REINFORCE:需要~3000个episode才能稳定达到200分
PPO     :只需~800个episode

原因:PPO用GAE降低了方差,每次更新更精准

发现2:PPO的上限更高

REINFORCE最终稳定在~320分
PPO最终稳定在~470分(接近满分500)

原因:REINFORCE的高方差导致策略在最优解附近震荡
      PPO的clip机制让策略稳定收敛

发现3:PPO的训练曲线更平滑

REINFORCE的5个种子标准差:±45
PPO的5个种子标准差:±25

原因:PPO的Critic有效降低了方差

代码核心片段(完整代码见 code/04_ppo_demo.py):

def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
    """GAE优势估计"""
    advantages = np.zeros_like(rewards)
    last_gae = 0
    for t in reversed(range(len(rewards))):
        if dones[t]:
            next_value = 0
        else:
            next_value = values[t + 1] if t + 1 < len(values) else 0
        delta = rewards[t] + gamma * next_value - values[t]
        advantages[t] = last_gae = delta + gamma * lam * last_gae
    return advantages

4.7 踩坑实录

坑1:GAE计算中的"边界bug"

场景:我的PPO实现训练曲线震荡严重,怎么调参都不稳定。

根因:GAE计算中,episode结束时没有正确处理 done 信号。

# 错误写法:
for t in reversed(range(len(rewards))):
    delta = rewards[t] + gamma * values[t + 1] - values[t]
    advantages[t] = delta + gamma * lam * advantages[t + 1]

# 正确写法:
for t in reversed(range(len(rewards))):
    if dones[t]:
        next_value = 0  # episode结束,没有"下一步"
    else:
        next_value = values[t + 1]
    delta = rewards[t] + gamma * next_value - values[t]
    advantages[t] = delta + gamma * lam * (0 if dones[t] else advantages[t + 1])

教训:RL的bug往往不是报错,而是"训练不稳定"。当你的训练曲线震荡时,优先检查 reward shaping 和 GAE 计算。

坑2:clip范围ε设太大,PPO退化成REINFORCE

场景:我设 ε=0.5,觉得"给策略更大的自由度应该学得更快"。

结果

ε = 0.1  → 稳定收敛,470分
ε = 0.2  → 稍快收敛,450分
ε = 0.5  → 剧烈震荡,平均只有280分
ε = 1.0  → 基本等于没clip,跟REINFORCE一样差

原因:ε 太大时,clip 形同虚设,策略更新不受约束,又回到了高方差的老路。

教训:ε=0.1~0.2 是经过大量实验验证的"甜蜜区间"。不要随意调大。

坑3:忽略熵奖励,策略过早收敛到次优解

场景:PPO训练到一半,策略固定在"永远选左",杆子只能撑50步。

根因:没有加熵奖励(entropy bonus),策略过早丧失了探索能力。

# 缺失熵奖励:
loss = policy_loss + value_loss

# 加上熵奖励:
entropy = -np.sum(pi_probs * np.log(pi_probs + 1e-8))
loss = policy_loss + 0.5 * value_loss - 0.01 * entropy
#                                         ^^^^^^^^^^^^^^^^
#                                         鼓励探索,防止过早收敛

教训:熵奖励是PPO的"探索引擎"。没有它,策略会迅速坍缩到一个局部最优解,再也出不来。系数通常设 0.001~0.01。


4.8 一张图总结:从MDP到PPO的演进

MDP(马尔可夫决策过程)
 │
 ├── 目标:找到最优策略 π* 最大化 E[Σ γ^t · r_t]
 │
 ├── 策略梯度(REINFORCE)
 │    │
 │    ├── 直觉:好的动作提高概率,差的动作降低概率
 │    ├── 公式:∇J = E[G · ∇log π(a|s)]
 │    └── 问题:方差太大
 │         │
 │         ├── 加Baseline:∇J = E[(G-b) · ∇log π]
 │         │    └── 好一点,但baseline太粗糙
 │         │
 │         └── Actor-Critic
 │              │
 │              ├── Actor选动作,Critic评价
 │              ├── 优势函数 A(s,a) = Q(s,a) - V(s)
 │              └── 问题:Critic估计不准 → bias
 │                   │
 │                   ├── TRPO(2015)
 │                   │    ├── KL散度硬约束
 │                   │    ├── 效果好但实现复杂
 │                   │    └── 需要二阶优化
 │                   │
 │                   └── PPO(2017)★
 │                        ├── Clip软约束:min(r·A, clip(r,1±ε)·A)
 │                        ├── GAE:用λ平衡bias-variance
 │                        ├── 一阶优化,实现简单
 │                        └── RLHF的标配算法
 │
 └── 在RLHF中的角色
      ├── 状态 = 已生成的文本
      ├── 动作 = 下一个token
      ├── 奖励 = 奖励模型打分
      └── KL约束防止reward hacking

本章小结

  1. MDP是强化学习的语言:状态、动作、奖励、转移、折扣——五个元素定义了一个决策问题。

  2. 策略梯度让学习有了方向∇J = E[G · ∇log π],好动作增大概率,差动作减小概率。但方差是核心问题。

  3. Actor-Critic用Critic降低方差:优势函数 A(s,a) = Q(s,a) - V(s) 告诉Actor"这个动作比平均好多少"。

  4. PPO用clip解决策略更新失控min(r·A, clip(r, 1±ε)·A) 限制了新旧策略的偏离程度,简单但有效。

  5. GAE用λ平衡bias和variance:λ=0是纯TD(低方差高偏差),λ=1是纯MC(高方差无偏),λ=0.95是最佳折中。

  6. PPO成为RLHF标配的原因:clip机制提供稳定训练、KL约束防止reward hacking、工程实现简单、OpenAI验证有效。


下一篇预告

05 | Prompt Engineering的科学:CoT、ReAct、ToT的原理与实验

Prompt不只是"写几句话"——它是一门有理论支撑的工程。下一篇我们会深入:

  • Chain-of-Thought为什么能提升推理能力?(不是玄学,有数学解释)
  • ReAct的"思考-行动"交替如何减少幻觉?
  • Tree-of-Thoughts的搜索策略:什么时候值得"多想几步"?
  • 我们自己的实验:不同Prompt策略在数学推理任务上的定量对比

参考资料

  1. Proximal Policy Optimization Algorithms - Schulman et al., 2017. PPO原论文
  2. Trust Region Policy Optimization - Schulman et al., 2015. TRPO原论文
  3. High-Dimensional Continuous Control Using Generalized Advantage Estimation - Schulman et al., 2015. GAE原论文
  4. Asynchronous Methods for Deep Reinforcement Learning - Mnih et al., 2016. A3C,Actor-Critic的经典实现
  5. Playing Atari with Deep Reinforcement Learning - Mnih et al., 2013. DQN,深度RL的开创性工作
  6. Training language models to follow instructions with human feedback - Ouyang et al., 2022. InstructGPT,RLHF的里程碑
  7. DeepSeek-V3 Technical Report - DeepSeek, 2024. 大规模PPO训练实践

配套代码:code/04_ppo_demo.py(REINFORCE与PPO的从头实现,纯numpy)

图表生成:code/generate_charts_04.py(运行后生成所有图表到 assets/

有问题或建议?欢迎在Issues区讨论。