RLforge
原理预计 40 分钟

策略梯度:为什么能对「采样出来的东西」求梯度

RL 唯一绕不开的一段数学。这一节只推一个公式,但推完之后 GRPO 的代码你会觉得理所当然。

学完这节你能做到

  • 解释 log-derivative trick:为什么梯度里出现了 log π
  • 说清基线(baseline)为什么能降方差却不引入偏差
  • 把 REINFORCE 的一行公式对应到代码里的 logprob * advantage

整条路径唯一一段数学

只推一个式子。推完之后,L2 那二十行代码你会觉得理所当然——因为每一行都能对回这个式子的一部分。

站点里没有公式排版,所以下面全部用等宽代码块写。符号约定:

θ        模型参数
π_θ(y|x) 参数为 θ 的模型,在输入 x 下生成回答 y 的概率
R(y)     回答 y 的得分(reward)
E[·]     期望,也就是"平均而言"
∇        对 θ 求梯度

目标:最大化期望 reward

我们想要的东西写出来很朴素:

J(θ) = E          [ R(y) ]
        y ~ π_θ(·|x)

读法:从当前模型采样一条回答,它的得分平均下来是多少。 我们希望这个平均分越高越好。

展开成求和形式(把期望写成"概率乘取值再求和"):

J(θ) = Σ  π_θ(y|x) · R(y)
       y

这里 Σ 是对所有可能的回答求和——一个天文数字(词表 15 万,长度 100,可能性是 150000^100)。所以这个式子没法直接算,只能采样估计。记住这一点,它是后面所有麻烦的根源。

第一步:梯度往哪走

要用梯度上升最大化 J(θ),就得求 ∇J(θ):

∇J(θ) = ∇ Σ  π_θ(y|x) · R(y)
          y

      = Σ  ∇π_θ(y|x) · R(y)          ← 求和与求导交换,R(y) 与 θ 无关
        y
iR(y) 为什么与 θ 无关

这一步是整个推导的关键前提,值得停一下。

R(y) 是「给定这条回答 y,它得几分」。它是一个对 y 的函数,和模型参数没有关系——同一条回答,无论哪个模型生成的,得分都一样。

θ 影响的是哪些 y 会被采样出来,不是「y 值多少分」。所以求导时 R(y) 就是个常数系数。

这也解释了 L1 第一节说的「reward 不可导」:R 对 y 不可导(y 是离散 token),但我们根本不需要对它求导。

第二步:log-derivative trick

现在的问题是 ∇π_θ(y|x) 不好用——它是概率的梯度,但我们采样时拿到的是具体的一条 y,没法用它做蒙特卡洛估计。

这里用一个恒等式。从对数函数的求导规则出发:

∇ log f  =  ∇f / f          (链式法则)

两边乘 f:

∇f  =  f · ∇ log f

把 f 换成 π_θ(y|x):

∇π_θ(y|x)  =  π_θ(y|x) · ∇ log π_θ(y|x)

这个恒等式叫 log-derivative trick(也叫 score function trick)。它本身没有任何深意,就是乘一个 π/π。但代进去之后,整个式子的性质变了:

∇J(θ) = Σ  π_θ(y|x) · ∇ log π_θ(y|x) · R(y)
        y

      = E          [ ∇ log π_θ(y|x) · R(y) ]
         y ~ π_θ

这一步是全部的魔法所在。 对比一下前后:

之前之后
形式Σ ∇π · RE[ ∇log π · R ]
能采样估计吗❌ 求和号在外面,要遍历所有 y✅ 是个期望,采样求平均就行
要求 R 可导吗❌ 不要求❌ 不要求
∇log π 能算吗—✅ 模型前向就能拿到

第二列每一行都是"能做",于是这个式子就能变成代码。

第三步:变成能跑的估计

期望用采样平均来估:采 N 条回答,算平均。

∇J(θ) ≈ (1/N) Σ  ∇ log π_θ(y_i|x) · R(y_i)
                i=1..N

这就是 REINFORCE 算法。 整个 RL 的地基。

直觉解读:

R(y) 大   →  系数为正、且大  →  提高这条回答的概率
R(y) 小   →  系数小或为负    →  降低这条回答的概率

也就是 L1 第一节那句大白话:「好的多学一点,坏的少学一点」——现在你知道它对应的式子了。

✓log π 在 LLM 里怎么算

一条回答是一串 token,概率是逐 token 相乘:

π_θ(y|x) = Π  π_θ(y_t | x, y_<t)
           t

取对数之后乘变成加:

log π_θ(y|x) = Σ  log π_θ(y_t | x, y_<t)
               t

右边每一项就是「模型在位置 t 给实际采样出的那个 token 的对数概率」——正是 L2 里 sequence_logprobs() 干的事:取 logits、log_softmax、gather 出目标 token 那一列。

这也顺便解释了 L0「token 与 chat template」那节强调的错一位对齐:log π_θ(y_t | ...) 里的条件是 y_<t,所以要用位置 t-1 的 logits。

麻烦来了:REINFORCE 的方差

REINFORCE 数学上是对的(无偏:估计的期望等于真实梯度),但方差大到不能用。

举个具体例子。假设 reward 全是正数,8 条回答得分是:

R = [10, 10, 10, 10, 10, 10, 10, 11]

按 REINFORCE,梯度是:

(1/8) · [ 10·∇logπ(y₁) + 10·∇logπ(y₂) + ... + 11·∇logπ(y₈) ]

所有系数都是正的,所以全部 8 条回答的概率都被大幅提高,只是最后一条稍微多一点。

但你真正想要的是:只提高最后那一条,压低前面七条。

×问题的本质:reward 的绝对值没有参照

「10 分」是好还是坏?取决于同伴考了多少。

REINFORCE 只看绝对值,于是把「大家都及格」误读成「大家都很棒,一起加强」。有用的信息(第 8 条比别人好 1 分)被淹没在共同的基础分里。

数值上的表现:梯度的量级由 reward 的绝对值决定,而有效信号只是它们之间的差。信噪比极低——这就是"方差大"的具体含义。

极端一点:如果你把 reward 全都加上 1000,梯度会大 100 倍,而"哪条更好"这个信息一点没变。

第四步:减掉一个基线

解法出人意料地简单:减掉一个和 y 无关的量。

∇J(θ) = E[ ∇ log π_θ(y|x) · ( R(y) − b ) ]
                              └────┬────┘
                                advantage

b 叫基线(baseline)。

为什么减了不影响正确性

需要证明减掉 b 之后梯度的期望不变,也就是多出来的那一项为 0:

E[ ∇ log π_θ(y|x) · b ]
  = b · E[ ∇ log π_θ(y|x) ]          ← b 是常数,提出来
  = b · Σ  π_θ(y|x) · ∇ log π_θ(y|x)
        y
  = b · Σ  ∇π_θ(y|x)                 ← log-derivative trick 反着用
        y
  = b · ∇ Σ  π_θ(y|x)                ← 求导与求和交换
          y
  = b · ∇ 1                          ← 概率之和恒为 1
  = 0

最后两步是要点:概率归一化保证了这一项恒为 0。 所以减基线不引入偏差——梯度指向的方向没变。

为什么减了能降方差

回到那个例子,取 b = 均值 = 10.125:

R      = [10,     10,     10,     10,     10,     10,     10,     11    ]
R − b  = [−0.125, −0.125, −0.125, −0.125, −0.125, −0.125, −0.125, +0.875]

现在方向清楚了:前七条压低,第八条提高。 而且系数量级从 10 降到 1 以下。

有用的信号(差异)被保留,无用的共同部分(基础分)被消掉。方差大幅下降,偏差为零。

i这是 RL 里最重要的一个技巧

「减一个与动作无关的基线,无偏且降方差」这条性质,几乎所有现代策略梯度方法都建立在它上面。

分歧只在于 b 怎么来:

  • PPO:训一个模型来预测它(critic / 价值网络)。准,但要多一份权重加一份优化器状态。
  • GRPO:同一个 prompt 采 G 条,用这 G 条的平均分当 b。一分显存都不用多花。

下一节就讲这个分歧。而你现在应该能看出来:GRPO 不是什么新数学,它只是给 b 选了一个便宜的估计方式。

优势函数

R(y) − b 这个量有个正式名字:优势(advantage),记作 A(y)。

它的含义就是字面意思:这条回答比基线好多少。

A(y) > 0    比平均好  →  提高它的概率
A(y) = 0    就是平均  →  不动
A(y) < 0    比平均差  →  降低它的概率

最终形式:

∇J(θ) ≈ (1/N) Σ  A(y_i) · ∇ log π_θ(y_i|x)
                i

对应的损失函数(梯度上升改成梯度下降,加负号):

L(θ) = − (1/N) Σ  A(y_i) · log π_θ(y_i|x)
                i

代码对照

这个式子在 PyTorch 里就是三行:

# 1. log π_θ(y|x) —— 取出实际采样 token 的对数概率
logprobs = torch.log_softmax(logits.float(), dim=-1)
token_logprobs = torch.gather(logprobs, -1, targets.unsqueeze(-1)).squeeze(-1)

# 2. A(y) —— 优势(这里用组内均值当基线,也就是 GRPO)
advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-4)

# 3. L(θ) = −A · log π
loss = -(advantages.unsqueeze(1) * token_logprobs * mask).sum() / mask.sum()

就是这样。 L2 那二十行代码里,除了 clip 和 KL 这两道护栏(下一节讲),核心就是这三行。

✓对着这三行回看整段推导
  • 第 1 行是 log-derivative trick 的产物:我们求梯度的对象是 log π,不是 reward
  • 第 2 行是基线,把「绝对分数」变成「相对好坏」
  • 第 3 行的负号是「梯度上升改成下降」,mask 是「只对 completion 求梯度」

如果你能对每一行说出它来自推导的哪一步,这一节就算学到了。

Checkpoint单选

log-derivative trick(∇f = f · ∇log f)在这段推导里解决了什么问题?

Checkpoint单选

一组 reward 是 [10, 10, 10, 10, 10, 10, 10, 11]。不减基线的 REINFORCE 会怎么更新?

Checkpoint单选

证明「减基线不引入偏差」的关键一步是什么?

这节课的落点

  • 目标是最大化期望 reward;R(y) 与 θ 无关,所以求导时是常数系数
  • log-derivative trick ∇f = f·∇log f 把「对所有 y 求和」变成期望,从而可采样估计
  • 得到 REINFORCE:∇J ≈ (1/N) Σ R(y_i) · ∇log π(y_i)
  • REINFORCE 无偏但方差大,因为 reward 的绝对值没有参照,共同基础分淹没了差异
  • 减基线 A = R − b:无偏(靠概率归一化,∇Σπ = ∇1 = 0)且降方差
  • 分歧只在 b 怎么估:PPO 学一个 critic,GRPO 用组内均值
  • 落到代码就三行:log_softmax + gather / 算优势 / −A · logπ 加 mask

延伸资料