本文转载自:微信公众号「菜菜Vibe Coding」
本文改编自「菜菜Vibe Coding」公众号系列文章,原文发布于 2026-06-11。这是系列第二篇,讲述 SFT、强化学习策略梯度的基础知识和公式推导,为后续的知识蒸馏打下基础。
本文分为 3 个部分:
- 数学基础、信息论基础和 KL 散度(系列第一篇)
- 大模型微调(SFT)、强化学习策略梯度
- OPD 的介绍和遇到的问题
Table of contents
Open Table of contents
第四章:语言模型与自回归分解
这一章建立整个推导的数学语言——符号和分解方式。理解自回归分解才能理解后面为什么 sequence-level log-ratio 可以写成 token-level log-ratio 之和,为什么 softmax 归一化是 policy-gradient 推导的基石。
4.1 符号定义
| 符号 | 含义 | 为什么需要它 |
|---|
| x | prompt(输入提示) | 模型的输入,所有生成都以此为起点 |
| y=(y1,…,yT) | 回答序列 | 模型的输出,是一串 token |
| ct=(x,y<t) | 第 t 步的 prefix | 模型在第 t 步看到的所有已知信息——prompt + 已生成的 t−1 个 token |
| πθ | 学生模型(参数为 θ) | 我们要训练的模型,参数 θ 是被优化的对象 |
| q 或 πT | 教师模型 | 不被训练,提供目标信号 |
| V | 词表(如 50000 个词) | 所有可能 token 的集合,softmax 在词表上做 |
4.2 自回归分解——为什么要这样分解?
问题:模型生成整句话的概率 πθ(y∣x) 是什么?
为什么不能直接算? 一个序列 y=(y1,y2,…,yT) 的概率不是一次性的——模型是”逐步选择”下一个 token 的。每一步的选择依赖于之前已经生成的所有 token。
分解过程:
模型生成第 1 个 token:
P(y1∣x)=πθ(y1∣c1),c1=x
模型生成第 2 个 token(需要看第 1 个):
P(y2∣x,y1)=πθ(y2∣c2),c2=(x,y1)
模型生成第 t 个 token(需要看前 t−1 个):
P(yt∣x,y<t)=πθ(yt∣ct),ct=(x,y<t)
链式法则(概率论基本规则):联合概率 = 条件概率之积:
P(y1,y2,…,yT)=P(y1)⋅P(y2∣y1)⋅⋯⋅P(yT∣y<T)
应用到语言模型:
πθ(y∣x)=t=1∏Tπθ(yt∣ct)
为什么要取 log? 乘法在数学上不方便:
- 多个小数相乘容易数值溢出/归零(如 0.01×0.01×⋯→ 机器精度下变成 0)
- 乘法求导需要用乘积法则,复杂
取 log 后乘法变加法,解决这些问题:
logπθ(y∣x)=t=1∑Tlogπθ(yt∣ct)
- Sequence-level log-ratio logq(y)πθ(y) 可以分解成 token-level log-ratio 之和 ∑trt
- Sequence-level score gradient ∇logπθ(y) 可以分解成 token-level score 之和 ∑tgt
- SFT loss 是对每个 token 的 logprob 求和
- 如果没有这个分解,我们无法把高方差的 sequence-level 目标降到 token-level
Teacher 同理:
logq(y∣x)=t=1∑Tlogq(yt∣ct)
两者的差——log-ratio:
logq(y∣x)πθ(y∣x)=t=1∑T[logπθ(yt∣ct)−logq(yt∣ct)]=t=1∑Trt
这就是为什么 log-ratio 可以按 token 分解——自回归分解让我们能把整条序列的 “student vs teacher” 差异,拆成每个位置上的差异之和。
4.3 Softmax 保证归一——为什么这是整个推导的基石?
每个时间步,模型对词表中每个 token 输出一个实数分数 zv(称为 logit),然后通过 softmax 转成概率:
πθ(v∣ct)=∑u∈Vezu(ct)ezv(ct)
为什么需要 softmax? 模型的输出 zv 是任意实数(可正可负,没有上界),不能直接当概率用。Softmax 的功能:
- 把所有值变成正数(ezv>0)
- 把所有值缩到 0~1 之间
- 让所有值加起来等于 1
证明归一性:
v∈V∑πθ(v∣ct)=v∈V∑∑uezuezv=∑uezu∑vezv=1
分母和分子是同一个东西(对所有词表元素求和),所以比值 = 1。
从单步归一到联合分布归一:
每个时间步的概率都归一(和为 1),所以联合概率也归一:
y∑πθ(y∣x)=y1∑y2∑⋯yT∑t∏πθ(yt∣ct)=1
为什么归一性是 policy-gradient 推导的基石?
在第 10 章的推导中,关键步骤是:
Ey∼πθ[∇logπθ(y)]=∑yπθ(y)∇logπθ(y)=∇(∑yπθ(y))=∇1=0
如果 ∑yπθ(y)=1,这一步就不成立,整个 policy-gradient 推导就崩了。
Softmax 是归一性的保障——它让每步概率和为 1,进而让联合概率和为 1,进而让梯度推导的第二项消零成立。
PyTorch 中 softmax 的实现:
import torch.nn.functional as F
# 假设模型输出的logits,shape [B, L, V]
logits = torch.randn(2, 10, 50000) # batch=2, seq_len=10, vocab=50000
probs = F.softmax(logits, dim=-1) # shape [2, 10, 50000]
print(probs.sum(dim=-1)) # 每个位置的概率和,应该全是1.0
# log_softmax(更稳定,避免数值问题)
log_probs = F.log_softmax(logits, dim=-1)
# 验证: exp(log_probs) ≈ probs
print(torch.exp(log_probs) - probs) # 应该接近0
第五章:SFT(监督微调)
SFT 是大模型训练的起点。理解 SFT 的数学本质(它到底在优化什么),才能理解后面 KD、OPD 为什么需要改进、改进了什么。
5.1 什么是 SFT?为什么要做 SFT?
大模型训练的三个阶段:
- 预训练:让模型学会语言的”语法”和”常识”(读海量文本,预测下一个词)
- SFT:让模型学会”怎么对话”——给它看人类写的高质量问答,让它模仿
- RL/Distillation:让模型变得更精准、更可靠
为什么需要 SFT? 预训练后的模型能”说话”,但不会”对话”——它可能续写一段新闻,但不会回答你的问题。SFT 教它:
- 看到问题 → 给出回答(而不是续写问题)
- 回答要有结构、有逻辑
- 某些格式要求(如代码要用 markdown 包裹)
类比:预训练像学会了中文语法,SFT 像学会了”怎么写商务邮件”——语法对了,但格式和内容风格需要专门训练。
5.2 SFT Loss 的完整推导——从目标到公式
训练数据:(x,ydata),其中 x 是 prompt,ydata=(y1data,…,yTdata) 是人类写好的标准回答。
目标:让模型尽可能生成和标准答案一样的回答。
第一步:写出目标
模型对标准答案的生成概率(用自回归分解):
πθ(ydata∣x)=t=1∏Tπθ(ytdata∣ctdata)
我们希望这个概率尽可能大。
为什么最大化概率? 如果模型认为标准答案的概率很高,说明模型”认同”这个答案——它倾向于生成同样的回答。这正是我们想要的。
第二步:取 log,乘法变加法
logπθ(ydata∣x)=t=1∑Tlogπθ(ytdata∣ctdata)
取 log 的原因(第四章已讲):乘法数值不稳定、求导复杂,加法更方便。
第三步:从最大化变成最小化
机器学习习惯写最小化问题(因为优化器是做梯度下降的)。最大化 logπ 等价于最小化 −logπ:
LSFT=−t=1∑Tlogπθ(ytdata∣ctdata)
这就是 SFT Loss(也叫 NLL Loss,Negative Log-Likelihood)。
第四步:逐项理解
每个时间步 t 的 loss 是:
LSFT,t=−logπθ(ytdata∣ctdata)
- πθ(ytdata∣ctdata):模型在看到前缀 ctdata 后,认为标准答案的下一个 token ytdata 的概率
- −log:概率越高 → loss 越小;概率越低 → loss 越大
- 模型被”逼”着在每个位置都给标准答案的 token 高概率
直觉:就像考试——每道题都有标准答案,你答对了(给了高概率)就不扣分,答错了(给了低概率)就扣分。
5.3 SFT 与信息论的联系——SFT 到底在优化什么?
关键问题:SFT loss 看起来只是”让概率变大”,但它的数学本质是什么?
推导:每个时间步,标准答案的真实分布是 one-hot——只有 ytdata 这个 token 概率为 1,其他所有 token 概率为 0:
Pdata(v∣ct)={10if v=ytdataotherwise
SFT loss 就是这个 one-hot 分布和 student 分布的交叉熵:
LSFT,t=H(Pdata,PS)=−v∑Pdata(v)logPS(v)=−logPS(ytdata∣ct)
为什么交叉熵只剩一项? 因为 Pdata 是 one-hot——只有 v=ytdata 时 Pdata(v)=1,其他都是 0。0 乘任何数都是 0,所以求和只剩一项。
再用第二章的公式 H(P,Q)=H(P)+DKL(P∥Q):
LSFT,t=H(Pdata)+DKL(Pdata∥PS)
One-hot 分布的熵 H(Pdata)=−1⋅log1−0⋅log0=0(完全确定,没有不确定性):
LSFT,t=0+DKL(Pdata∥PS)=DKL(Pdata∥PS)
结论:
SFT=在数据 prefix 上,最小化 student 与 one-hot 分布的 forward KL
- SFT 的目标是让 student 完全模仿标准答案(one-hot = 100% 确定只有一个正确答案)
- KL 方向是 forward(DKL(Pdata∥PS)),因为标准答案说”只有这个 token 是对的”,student 不能给其他 token 太高概率
- 这是一种硬监督——每个位置只有一个正确答案,没有”软信号”
5.4 SFT 的根本局限——Exposure Bias
问题:SFT 在数据的前缀上训练:
ct=(x,y<tdata)
但推理时,模型在自己生成的前缀上运行:
ct=(x,y<tstudent)
一旦 student 在某个位置生成了和标准答案不同的 token,后续的前缀就进入了训练时从未见过的区域。模型在这个陌生区域没有受过训练,容易继续犯错——错误连锁放大。
这就是 Exposure Bias——训练看到的”世界”和推理看到的”世界”不一致。
SFT 像只在阳光明媚的公路上练车。考试时遇到雨天、夜路,你就慌了——因为你从未在这些条件下练过。
5.5 SFT 的 PyTorch 实现
import torch.nn.functional as F
def sft_loss(student_logits, labels, ignore_index=-100):
student_logits: student模型输出的logits, shape [B, L, V]
labels: 标准答案的token id, shape [B, L]
ignore_index: 忽略的label值(如padding token)
# shift: logits的第t步对应labels的第t+1步(因为logits预测下一个token)
shift_logits = student_logits[:, :-1, :].contiguous() # [B, L-1, V]
shift_labels = labels[:, 1:].contiguous() # [B, L-1]
# 交叉熵 = NLL = -log P_S(y_t^data | c_t^data)
shift_logits.view(-1, shift_logits.size(-1)), # [B*(L-1), V]
shift_labels.view(-1), # [B*(L-1)]
ignore_index=ignore_index
logits = torch.randn(4, 128, 50000) # batch=4, seq_len=128, vocab=50000
labels = torch.randint(0, 50000, (4, 128)) # 标准答案token ids
loss = sft_loss(logits, labels)
print(f"SFT loss: {loss.item():.4f}")
第六章:强化学习基础——策略梯度
本章是整个 OPD 推导的”引擎”——理解策略梯度,才能理解第 10 章 OPD 的梯度推导为什么是那个形式、为什么第二项能消零、为什么可以加 baseline。
6.1 什么是 RL?为什么要引入 RL?
SFT 的局限:SFT 只能告诉模型”标准答案是什么”,但不能告诉模型”你自己的回答好不好”。
类比:
- SFT 像老师批改作业——每题有标准答案,学生照着抄
- RL 像学生自己探索——做完后老师只给总分(reward),不说哪题对哪题错
为什么需要 RL? 因为很多场景没有标准答案:
- 数学题有多种解法,哪种更好?
- 对话有多种回复方式,哪种更自然?
- 代码有多种实现,哪种更高效?
SFT 只能学一种固定答案,RL 让模型自己探索并发现更好的策略。
核心要素:
- 策略(policy) πθ:模型的”行为规则”——给定状态,选择什么动作
- 奖励(reward) R:做完一件事后得到的分数
- 目标:调整策略参数 θ,使得期望奖励最大
6.2 策略梯度推导——为什么要这样推导?
目标:最大化期望奖励
J(θ)=Ey∼πθ[R(y)]=y∑πθ(y)⋅R(y)
问题:πθ(y) 是一个概率值(经过 softmax),我们不能直接对它求导——因为概率值和参数 θ 的关系是通过 softmax 的复杂映射,直接求导极其困难。
策略梯度的核心思路:不直接对 πθ(y) 求导,而是通过 log-derivative trick 把 ∇πθ(y) 转化成更容易处理的形式。
推导过程:
第一步:写出目标函数
J(θ)=y∑πθ(y)⋅R(y)
第二步:对 θ 求导
∇J=y∑∇πθ(y)⋅R(y)
为什么只对 πθ 求导? 因为 R(y) 是 reward,它不依赖 θ(reward 是外部给的,不是模型参数决定的)。所以 R(y) 对 θ 的导数为 0。
第三步:用 log-derivative trick
∇πθ(y)=πθ(y)⋅∇logπθ(y)
为什么这个 trick 成立? 因为 πθ(y)=elogπθ(y),对 ef(θ) 求导 = ef⋅∇f,所以 ∇πθ(y)=πθ(y)⋅∇logπθ(y)。
为什么需要这个 trick? 直接对 softmax 输出的概率 πθ(y)=∑uezθ(u)ezθ(y) 求导需要用商的求导法则,还要处理分母(所有词表的指数之和),极其复杂。Log-derivative trick 避开了这个困难——它把 ∇πθ 转化成 πθ⋅∇logπθ,后者只需要对 log 概率求导,简单得多。
第四步:代入
∇J=y∑πθ(y)⋅∇logπθ(y)⋅R(y)=Ey∼πθ[R(y)⋅∇logπθ(y)]
结果的含义:
∇J=Ey∼πθ[R(y)⋅∇logπθ(y)]
| 组成部分 | 含义 |
|---|
| ∇logπθ(y) | Score function:参数怎么调才能让模型更多生成这条轨迹 |
| R(y) | Reward:这条轨迹好不好 |
| 乘积 | 好轨迹 → 提高概率;坏轨迹 → 降低概率 |
就像生活中的学习:做了一件事,得了高分(reward 高),以后多做(提高概率);得了低分,以后少做(降低概率)。
为什么这是采样估计? 因为 Ey∼πθ 的意思是”从模型分布中采样 y“,我们让模型生成几条轨迹,算每条的 R(y)⋅∇logπθ(y),取平均。不需要遍历所有可能的 y。
6.3 加 baseline——为什么可以减?减了有什么好处?
原始策略梯度方差很高。可以减去一个 baseline b:
∇J=Ey∼πθ[(R(y)−b)⋅∇logπθ(y)]
为什么可以减 baseline? 逐步证明:
Ey∼πθ[b⋅∇logπθ(y)]
把 b 提出来(常数):
=b⋅Ey∼πθ[∇logπθ(y)]
展开期望:
=b⋅y∑πθ(y)∇logπθ(y)
用 log-derivative trick 反向写 πθ(y)∇logπθ(y)=∇πθ(y):
=b⋅y∑∇πθ(y)
求和与梯度交换:
=b⋅∇(y∑πθ(y))
归一性 ∑yπθ(y)=1:
=b⋅∇1=b⋅0=0
∑yπθ(y)=1,∇1=0。这和第 10 章 OPD 推导中”第二项消零”用的是同一个性质。
减 baseline 有什么好处?
- 不改变期望(无偏):E[(R−b)⋅∇logπθ]=E[R⋅∇logπθ]−0=∇J
- 降低方差:如果 reward R(y) 的值在 0
10 之间波动,减去均值 b=E[R] 后,(R−b) 的波动范围变成 -55,方差大幅降低
常见 baseline 选择:reward 的均值 b=E[R],或用另一个模型(critic/reference model)的 logprob 作为 baseline。
PyTorch 实现:
def policy_gradient_loss(logprobs, rewards):
# 策略梯度: -reward * logprob
# 最大化期望奖励 = 最小化 -reward * logprob
loss = -rewards * logprobs
def policy_gradient_with_baseline(logprobs, rewards, baseline):
# advantage = reward - baseline
advantages = rewards - baseline
loss = -advantages * logprobs
# 示例: PPO中的baseline就是reference model的logprob
# advantage = reward + ref_logprob - logprob
# 这和OPD中的 sampled-token advantage: A_t = logq - logpi 是同一个思路
6.4 策略梯度与 OPD 的联系
在第 10 章,OPD 的目标函数 J(θ)=Ey∼πθ[logπθ(y)−logq(y)] 中:
- “reward” R(y) 被替换成了 log-ratio logπθ(y)−logq(y)
- 推导过程完全一样:乘积求导 → log-derivative trick → 归一性消零
- 最终形式:∇J=E[(logπθ−logq)⋅∇logπθ]
唯一的区别:reward 从外部给的 R(y) 变成了内部计算的 log-ratio。这意味着 OPD 不需要外部 reward 函数,teacher 的 logprob 就是 reward。