策略梯度:为什么能对「采样出来的东西」求梯度
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
这一步是整个推导的关键前提,值得停一下。
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 ~ π_θ
这一步是全部的魔法所在。 对比一下前后:
| 之前 | 之后 | |
|---|---|---|
| 形式 | Σ ∇π · R | E[ ∇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 第一节那句大白话:「好的多学一点,坏的少学一点」——现在你知道它对应的式子了。
一条回答是一串 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 条回答的概率都被大幅提高,只是最后一条稍微多一点。
但你真正想要的是:只提高最后那一条,压低前面七条。
「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 以下。
有用的信号(差异)被保留,无用的共同部分(基础分)被消掉。方差大幅下降,偏差为零。
「减一个与动作无关的基线,无偏且降方差」这条性质,几乎所有现代策略梯度方法都建立在它上面。
分歧只在于 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 求梯度」
如果你能对每一行说出它来自推导的哪一步,这一节就算学到了。
log-derivative trick(∇f = f · ∇log f)在这段推导里解决了什么问题?
一组 reward 是 [10, 10, 10, 10, 10, 10, 10, 11]。不减基线的 REINFORCE 会怎么更新?
证明「减基线不引入偏差」的关键一步是什么?
这节课的落点
- 目标是最大化期望 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