17 分钟
1 次观看

对比SFT

SFT、离线 KD 与 OPD:核心区别在于训练前缀的来源

1. 先用一句话说明这页的核心

SFT、离线蒸馏和 OPD,不只是损失函数不同,更重要的是:训练时,模型根据谁已经说出来的话,预测下一步?

对于自回归 ASR:

pS(y∣x)=∏tpS(yt∣x,y<t)p_S(y\mid x)=\prod_t p_S(y_t\mid x,y_{<t})

其中:

  • x:输入音频。
  • y_t:第 t 个输出 token。
  • y_{<t}:此前的输出,也就是“前缀”。
  • p_S:学生模型的概率分布。

例如识别“明天去南京南站”,预测“京”时,模型不仅看音频,还看此前的“明天去南”。

同一段音频,前缀不同,下一 token 的概率也可能不同。 这就是为什么训练前缀的来源很重要。

下面用字或词作教学单位,不代表真实 tokenizer 的切分。


2. SFT:沿正确答案的前缀学习

SFT 使用人工参考转写(Ground Truth,GT)作为训练目标。

损失函数:

LCE(y∗)=−1N∗∑tmt∗log⁡pS(yt∗∣x,y<t∗)L_{\mathrm{CE}}(y^*) = -\frac{1}{N^*} \sum_t m_t^* \log p_S(y_t^*\mid x,y_{<t}^*)

这里:

  • y^*:人工参考转写,即 GT。
  • y_t^*:当前位置的正确 token。
  • y_{<t}^*:当前位置之前的正确参考前缀。
  • m_t^*:位置掩码,参与损失为 1,否则为 0。
  • N^*:有效 token 数。
  • 负对数:正确 token 概率越低,惩罚越大。

有效 token 数:

N∗=∑tmt∗N^*=\sum_t m_t^*

例如:

音频:明天去南京南站
训练输入前缀:明天去
当前位置目标:南

如果学生给“南”的概率是 0.3,该位置的损失为:

−log⁡0.3≈1.204-\log 0.3\approx 1.204

提高到 0.8 后:

−log⁡0.8≈0.223-\log 0.8\approx 0.223

因此,CE 推动模型提高正确 token 的概率。

但是,即使学生本来会把“南”识别成“难”,标准 SFT 在训练后续位置时,仍然给它正确前缀“明天去南”,不会把它自己的“明天去难”接进去。

这种训练方式称为 Teacher Forcing。

可以口头解释为:

SFT 在正确路线上的每一步教模型怎么走,但模型部署时一旦走错,后面遇到的状态可能是训练时很少见过的。

这不代表 SFT 无效,而是在说明训练与自由生成之间的前缀分布差异,即 Exposure Bias。


3. 离线 KD:沿教师答案或固定数据前缀学习

图中展示的是序列级蒸馏(Sequence-Level KD):教师先生成一份答案 y^T,缓存下来,学生再用 CE 学习:

LCE(yT)=−1NT∑tmtTlog⁡pS(ytT∣x,y<tT)L_{\mathrm{CE}}(y^T) = -\frac{1}{N_T} \sum_t m_t^T \log p_S(y_t^T\mid x,y_{<t}^T)

它和 SFT 的形式基本一样,变化是:

标签从人工 GT 换成教师生成的文本。

例如教师输出“明天去南京南站”,学生训练时就沿着这条固定答案学习。即使当前学生已经会输出其他内容,缓存的教师前缀也不会随它变化。

3.1 离线 KD 不一定使用硬标签

这里需要口头补充:离线 KD 不只有硬标签 CE,也可以蒸馏教师的软概率分布。

CE 本身也支持概率形式的目标。

参考:PyTorch CrossEntropyLoss 文档

对固定前缀,软标签交叉熵为:

H(pT,pS)=−∑vpT(v)log⁡pS(v)H(p_T,p_S) = -\sum_v p_T(v)\log p_S(v)

而 Forward KL 为:

KL(pT∥pS)=H(pT,pS)−H(pT)\mathrm{KL}(p_T\Vert p_S) = H(p_T,p_S)-H(p_T)

其中,教师分布的熵为:

H(pT)=−∑vpT(v)log⁡pT(v)H(p_T) = -\sum_v p_T(v)\log p_T(v)

当教师分布停止梯度、前缀固定时,教师熵不随学生参数变化,所以两者对学生产生相同的梯度。

也就是:

∇θSH(pT)=0\nabla_{\theta_S}H(p_T)=0

因此:

∇θSKL(pT∥pS)=∇θSH(pT,pS)\nabla_{\theta_S}\mathrm{KL}(p_T\Vert p_S) = \nabla_{\theta_S}H(p_T,p_S)

所以,这页不能讲成“离线 KD 用 CE,OPD 用 KL”。

正确的区别是:

离线蒸馏主要沿固定数据前缀训练;OPD 沿当前学生生成的前缀训练。

需要注意,离线 KD 也可以沿 GT 前缀或其他预先收集的前缀计算教师软分布,不必一定使用教师生成的文本作为前缀。


4. OPD:沿学生实际生成的前缀,向教师请教

OPD(On-Policy Distillation)的核心:

学生先使用当前策略生成序列,教师再在学生实际生成的前缀上提供概率分布监督。

统一的蒸馏目标可以写为:

LOPD=Ey^∼pS(⋅∣x)[1N∑tmtD(pT,t,pS,t)]L_{\mathrm{OPD}} = \mathbb{E}_{\hat y\sim p_S(\cdot\mid x)} \left[ \frac{1}{N} \sum_t m_tD(p_{T,t},p_{S,t}) \right]

可以从外到内读。

4.1 第一层:学生先生成

y^∼pS(⋅∣x)\hat y\sim p_S(\cdot\mid x)

其中:

  • \hat y:当前学生根据音频生成的序列。
  • p_S:当前学生策略。
  • x:输入音频。

这个序列不是 GT,也不是教师生成的答案。

随着学生更新,它生成的序列及前缀分布也会变化。

4.2 第二层:教师和学生在同一个前缀上打分

学生下一 token 概率:

pS,t(v)=pS(v∣x,y^<t)p_{S,t}(v) = p_S(v\mid x,\hat y_{<t})

教师下一 token 概率:

pT,t(v)=pT(v∣x,y^<t,c)p_{T,t}(v) = p_T(v\mid x,\hat y_{<t},c)

其中:

  • v:词表中的候选 token。
  • p_{S,t}:学生对下一 token 的完整分布。
  • p_{T,t}:教师对同一位置的完整分布。
  • c:教师可选的额外条件。

重点:教师和学生必须基于相同的学生前缀打分。

但教师可以拥有额外条件 c。

例如:

普通强教师 OPD

  • 学生输入:音频 + 学生生成前缀。
  • 教师输入:音频 + 相同的学生生成前缀。
  • 教师不一定需要额外条件。

Keywords-GT OPSD

  • 学生输入:音频 + 学生生成前缀。
  • 教师输入:音频 + 学生生成前缀 + GT 槽位信息。
  • 学生不直接看到 GT。

这种额外条件使教师有机会提供更准确的指导,但不保证教师在每种错误前缀下都能正确恢复。

4.3 第三层:逐位置比较分布,再平均

D 表示教师和学生之间的分布差异,可以选择:

  • Forward KL:教师分布到学生分布。
  • Reverse KL:学生分布到教师分布。
  • JSD:Jensen-Shannon Divergence。
  • 其他合适的分布匹配目标。

m_t 用于排除不参与训练的位置。

有效 token 数:

N=∑tmtN=\sum_t m_t

外面的期望:

Ey^∼pS(⋅∣x)\mathbb{E}_{\hat y\sim p_S(\cdot\mid x)}

可以理解为“对学生可能生成的轨迹求平均”。

实际训练时,通常用采样出的有限数量轨迹估计。

学生轨迹及停止采样梯度的设计,与 GKD 的 On-Policy 蒸馏实现一致。

参考:GKD 原论文

需要注意:

上述目标描述的是在学生当前策略产生的前缀分布上进行蒸馏。

常见的 stop-gradient OPD 实现不会穿过离散 token 采样反向传播,而是将采样后的前缀视为固定输入,再对学生的预测分布求梯度。


5. 一个可以现场口头讲的数值例子

以下概率都是教学构造,不是实测结果。

音频 / GT:明天去南京南站

学生已经输出:明天去难京

现在需要预测下一步。

教师和学生都必须基于“明天去难京”打分,不能一个看正确前缀,另一个看错误前缀。

把候选词表简化为三项:

下一 token教师 p_T学生 p_S
南0.800.30
站0.150.30
EOS,结束0.050.40

在这个人为构造的状态下:

  • 教师认为应该继续输出“南”。
  • 学生却比较倾向于提前结束。

5.1 Forward KL 如何计算

采用 Forward KL:

D=KL(pT∥pS)D=\mathrm{KL}(p_T\Vert p_S)

定义:

D=∑vpT(v)log⁡pT(v)pS(v)D = \sum_v p_T(v)\log\frac{p_T(v)}{p_S(v)}

代入数值:

D=0.80log⁡0.800.30+0.15log⁡0.150.30+0.05log⁡0.050.40≈0.577\begin{aligned} D &=0.80\log\frac{0.80}{0.30}\\ &\quad+0.15\log\frac{0.15}{0.30}\\ &\quad+0.05\log\frac{0.05}{0.40}\\ &\approx 0.577 \end{aligned}

这个值衡量当前教师和学生的下一 token 分布差异。

5.2 KL 如何更新学生

假设学生的 logit 为 z_v。

在温度为 1、教师固定的情况下,对学生 logit 的梯度是:

∂D∂zv=pS(v)−pT(v)\frac{\partial D}{\partial z_v} = p_S(v)-p_T(v)

所以:

对“南”:

gsouth=0.30−0.80=−0.50g_{\mathrm{south}}=0.30-0.80=-0.50

梯度下降会提高“南”的 logit。

对“站”:

gstation=0.30−0.15=+0.15g_{\mathrm{station}}=0.30-0.15=+0.15

梯度下降会降低“站”的 logit。

对 EOS:

gEOS=0.40−0.05=+0.35g_{\mathrm{EOS}}=0.40-0.05=+0.35

梯度下降会降低提前结束的倾向。

5.3 软分布为什么比硬标签信息更多

如果只有硬标签,监督信号只是:

正确 token = 南

但是教师软分布提供了更多信息:

南:0.80
站:0.15
EOS:0.05

它不仅告诉学生“南”更合适,还告诉学生:

  • “站”具有一定概率,但不应该这么高。
  • EOS 概率应该很低,不应该提前结束。
  • 不同错误候选的惩罚程度并不相同。

可以口头总结:

教师不仅告诉学生“下一步选南”,还告诉它:不要直接跳到站,更不要现在结束。这就是软分布比单个硬标签多出的信息。

这里需要强调:该例子的教师分布是人为假设的。

现实中,当学生前缀已经严重偏离音频内容时,教师可能也无法提供正确的恢复指导。


6. 最容易讲错的两个地方

6.1 第一,OPD 不会直接修改已经生成的错误

上例中,“难”已经在固定前缀里,当前位置的蒸馏不会把它改成“南”。

但在这条轨迹更早的位置,前缀还是“明天去”时,教师也会打分。

如果教师更支持“南”而不是“难”,更新就能降低学生以后重新识别时犯同类错误的概率。

所以 OPD 可以学习两件事:

  1. 减少错误发生:在错误出现之前,提高正确 token 的概率。
  2. 错误后继续生成:在已经出现错误的前缀下,学习更合理的后续输出。

但不保证教师在所有错误前缀下都能提供正确指导。

需要区分:

训练时更新模型参数,不等于直接修改本次已经生成的 token。

6.2 第二,rollout 不反传,不等于学生不训练

常见的 OPD 训练过程:

text16 行
Step 1:学生无梯度生成序列
         |
         v
Step 2:固定生成前缀
         |
         v
Step 3:教师无梯度评分
         |
         v
Step 4:学生带梯度重新评分
         |
         v
Step 5:计算蒸馏损失
         |
         v
Step 6:反向传播,更新学生

对应的简化伪代码:

python31 行
# Step 1:学生生成 rollout,不计算梯度
with torch.no_grad():
    sampled_tokens = student.generate(audio)

# Step 2:固定学生生成的序列
tokens = sampled_tokens.detach()

# Step 3:教师在学生前缀上评分
with torch.no_grad():
    teacher_logits = teacher(
        audio=audio,
        tokens=tokens,
    )

# Step 4:学生重新评分,保留梯度
student_logits = student(
    audio=audio,
    tokens=tokens,
)

# Step 5:计算 token-level 分布蒸馏损失
loss = distillation_loss(
    student_logits,
    teacher_logits,
    mask,
)

# Step 6:更新学生参数
optimizer.zero_grad()
loss.backward()
optimizer.step()

说明:以上是流程伪代码。实际实现必须处理自回归 shift、EOS、padding mask、loss mask,以及教师和学生 logits 的位置对齐。

关键是:

Rollout 阶段不需要对离散采样反向传播,但评分阶段必须重新计算带梯度的学生概率分布。

梯度通过学生重新计算的 logits 传播,不穿过离散采样过程。

教师只需要在学生前缀上打分,不需要重新生成一整句答案。

需要区分两种梯度:

第一种:固定前缀上的分布蒸馏梯度

来自学生重新评分得到的 logits。

第二种:学生策略改变引起的未来采样轨迹分布变化

涉及轨迹概率对学生参数的依赖。

常见 stop-gradient OPD/GKD 实现直接使用第一种,不显式计算第二种策略梯度项。


7. SFT、离线 KD 与 OPD 的最终对比

对比维度SFT离线 KDOPD
训练前缀来源人工 GT固定教师输出或数据前缀当前学生 rollout
下一 token 监督GT token教师硬标签或软分布通常使用教师软分布
常用损失CECE、KLKL、JSD 等
是否依赖当前学生输出轨迹否通常否是
前缀是否随学生更新变化否通常否是
是否覆盖学生错误前缀标准 SFT 不直接覆盖取决于固定数据直接覆盖采样到的错误前缀
能否学习错误后恢复缺乏直接覆盖取决于数据可以学习采样到的错误状态
主要限制Exposure Bias前缀分布不匹配Rollout 成本、教师质量、错误轨迹覆盖

7.1 数学上的核心区别

SFT:沿参考答案前缀学习

y<t=y<t∗y_{<t}=y_{<t}^{*}

离线 KD:沿固定教师或数据前缀学习

y<t=y<tTy_{<t}=y_{<t}^{T}

其中也可以使用其他预先确定的固定前缀。

OPD:沿当前学生生成的前缀学习

y<t=y^<t,y^∼pS(⋅∣x)y_{<t}=\hat y_{<t}, \qquad \hat y\sim p_S(\cdot\mid x)

因此,真正重要的训练范式差异是:

训练监督发生在哪种前缀分布上,而不是单纯使用 CE、KL 还是 JSD。

7.2 从状态分布角度理解

可以把自回归生成中的每个前缀视为一个状态:

SFT

监督主要来自参考答案访问过的状态。

离线 KD

监督主要来自预先确定的教师轨迹或固定数据状态。

OPD

监督来自当前学生策略实际访问的状态。

这也是 On-Policy 的核心含义。


8. 这页最后可以这样收尾

SFT 是在正确参考前缀上学习正确答案;离线蒸馏是在固定前缀上模仿教师;OPD 则让当前学生先走一遍,再让教师在学生实际到达的每个状态上提供指导。

关键不是用了 CE 还是 KL,而是监督发生在谁的轨迹上。

这样做的目的,是让训练时模型面对的前缀,更接近它真实推理时会遇到的前缀。

下一页再讲这套流程如何落实到训练代码。


9. 最终核心结论

SFT:

沿参考答案前缀训练。

Offline KD:

沿固定数据或教师前缀训练。

OPD:

沿当前学生生成的前缀训练。

可以用三个公式总结:

SFT:

LSFT=−E(x,y∗)[∑tlog⁡pS(yt∗∣x,y<t∗)]L_{\mathrm{SFT}} = -\mathbb{E}_{(x,y^*)} \left[ \sum_t \log p_S(y_t^*\mid x,y_{<t}^*) \right]

Offline KD(序列级):

LKD=−E(x,yT)[∑tlog⁡pS(ytT∣x,y<tT)]L_{\mathrm{KD}} = -\mathbb{E}_{(x,y^T)} \left[ \sum_t \log p_S(y_t^T\mid x,y_{<t}^T) \right]

OPD:

LOPD=Ey^∼pS(⋅∣x)[1N∑tmtD(pT,t,pS,t)]L_{\mathrm{OPD}} = \mathbb{E}_{\hat y\sim p_S(\cdot\mid x)} \left[ \frac{1}{N} \sum_t m_t D(p_{T,t},p_{S,t}) \right]

最终结论:训练范式的关键差异,是监督发生在哪种前缀分布上,而不仅仅是损失函数使用 CE、KL 还是 JSD。


10. 参考资料

  1. GKD:On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes
    https://arxiv.org/abs/2306.13649

  2. PyTorch CrossEntropyLoss
    https://docs.pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html

正在连接 GitHub 评论…