2. 软标签与硬标签:温度系数、KL散度与蒸馏损失调节

知识蒸馏里,最核心的一步就是怎么把大模型(Teacher)的知识传给小模型(Student)。说白了,就是让Student学Teacher的输出。但这里有个关键问题——学什么?是学它预测的类别,还是学它预测时的“犹豫”和“倾向”?

这就引出了我们今天的主角:硬标签和软标签。

2.1 硬标签 vs 软标签:一个例子就懂

先看一个直观的例子。假设我们做一个情感分类,输入是“这部电影太棒了”。

  • 硬标签:直接告诉你“这是正面”。(one-hot向量:[1, 0])
  • 软标签:告诉你“我90%确定是正面,10%觉得是负面”。(概率分布:[0.9, 0.1])

你想想看,哪个信息量更大?显然是软标签。它不光告诉了你答案,还告诉了你模型在决策时的“置信度”和“类间关系”。

核心观点:蒸馏的本质,就是让Student去模仿Teacher在软标签里表达的“暗知识”。

我在项目里遇到过一种情况:直接用硬标签训Student,效果很差。后来换成软标签,准确率直接涨了3个点。为什么?因为硬标签把Teacher的“犹豫”信息全丢了。

2.2 温度系数T:让软标签更“软”

那软标签怎么生成呢?直接用Softmax的输出?

嗯,这里要注意。原始的Softmax输出往往太“尖锐”了。比如一个分类任务,Teacher可能对正确类输出0.99,对其他类输出0.01。这种分布,Student学起来跟学硬标签区别不大。

所以我们需要一个温度系数T来调节。

公式很简单:

q_i = exp(z_i / T) / Σ_j exp(z_j / T)

其中z_i是logits(模型最后一层的输出,还没过Softmax)。T就是温度系数。

T的作用:

  • T=1:就是标准的Softmax。
  • T>1:概率分布变得更平滑,小概率类的值被放大,类间关系更清晰。
  • T→0:趋近于one-hot,也就是硬标签。

我的经验:T一般取2-8之间。我习惯先从T=4开始试。如果Student太小,可以适当提高T,让Teacher的“暗知识”更明显。

举个例子,假设Teacher对三个类输出的logits是[10, 3, 1]:

T值 类别A 类别B 类别C 特点
T=1 0.999 0.001 0.000 太尖锐,几乎就是硬标签
T=4 0.836 0.114 0.050 平滑,保留了类间关系
T=8 0.635 0.229 0.136 更平滑,但可能丢失主要信息

你看,T=4的时候,类别B和C的概率明显提升了。Student就能学到“类别A和B更相似,和C差异更大”这种知识。

2.3 KL散度损失函数:衡量两个分布的差异

有了Teacher的软标签,怎么让Student去学呢?我们需要一个损失函数来衡量两个概率分布之间的差异。

这里用的就是KL散度(Kullback-Leibler Divergence)。

公式:

KL(P || Q) = Σ P(x) * log(P(x) / Q(x))

其中P是Teacher的分布,Q是Student的分布。

KL散度的特点:

  • 非对称性:KL(P||Q) ≠ KL(Q||P)。蒸馏时我们用KL(Teacher||Student),意思是“让Student去拟合Teacher”。
  • 值非负:当两个分布完全一致时,KL=0。
  • 对概率敏感:Teacher认为概率大的类,Student必须学得准,否则惩罚很大。

我曾经踩过的坑:直接用KL散度训Student,发现loss降不下去。后来检查发现,Teacher的logits里有些类概率极小(接近0),导致log(P/Q)出现数值不稳定。解决办法是给logits加一个很小的epsilon(比如1e-7)。

在PyTorch里实现蒸馏的KL损失,代码很简单:

import torch
import torch.nn.functional as F

def distillation_loss(student_logits, teacher_logits, T=4.0):
    """
    计算蒸馏损失
    Args:
        student_logits: Student模型的logits
        teacher_logits: Teacher模型的logits
        T: 温度系数
    Returns:
        loss: KL散度损失
    """
    # 用温度T软化两个分布
    soft_student = F.log_softmax(student_logits / T, dim=1)
    soft_teacher = F.softmax(teacher_logits / T, dim=1)
    
    # 计算KL散度,然后乘以T^2
    loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T ** 2)
    return loss

注意最后乘了T²。这是Hinton论文里的做法,目的是让梯度的大小不受T的影响。我刚开始做蒸馏时没注意这个细节,调T半天没效果,后来才发现是忘了乘T²。

2.4 蒸馏损失的权重调节

实际训练时,我们通常不只使用蒸馏损失。还会加上Student和真实标签之间的交叉熵损失。

总损失公式:

L_total = α * L_CE(student, true_labels) + (1 - α) * L_KL(student, teacher, T)

这里的α就是蒸馏损失的权重调节参数。

α怎么选?我个人的经验:

  • α=0.7:适合大部分场景,蒸馏损失占主导,但保留一部分真实标签信息。
  • α=0.5:两者均衡,适合Teacher和Student规模差距不大的情况。
  • α=0.9:几乎只用真实标签,蒸馏效果弱。适合Teacher质量不高时。
  • α=0.1:几乎全靠蒸馏,适合Teacher非常强、数据量少的情况。

我的建议:先固定T=4,α=0.7跑一个baseline。然后调α,观察验证集效果。如果Student过拟合了,就增大α(多用真实标签);如果欠拟合,就减小α(多学Teacher)。

另外,α和T是联动的。T越大,软标签越平滑,蒸馏损失的影响相对变小。所以调大T时,可以适当减小α来补偿。

2.5 本章知识结构图

下面这张图总结了软标签蒸馏的核心流程:

知识蒸馏核心流程:软标签与损失调节 Teacher模型 Student模型 Logits (z_t) Logits (z_s) 温度 T Softmax(z / T) → 软标签分布 KL散度损失 L_KL 交叉熵损失 L_CE 总损失 = α·L_CE + (1-α)·L_KL α控制蒸馏强度

2.6 实践中的几个要点

最后,总结几个我在项目中反复踩过的坑:

  1. Teacher和Student的logits尺度要匹配:如果Teacher的logits数值范围很大(比如几百),直接除以T可能效果不好。可以先对Teacher的logits做LayerNorm或标准化。
  2. 温度T和α要一起调:不要只调一个。我习惯用网格搜索,T在[2,4,6,8]里选,α在[0.3,0.5,0.7]里选。
  3. 蒸馏不是万能的:如果Teacher本身效果就不好,蒸馏出来的Student也不会好。先确保Teacher的质量。
  4. 注意数值稳定性:KL散度里log(0)的问题,记得加epsilon。

一句话总结:软标签是Teacher的“暗知识”,温度T让暗知识更清晰,KL散度让Student去模仿,α调节模仿的力度。这四个东西配合好了,蒸馏就成功了一大半。