3. 知识蒸馏原理:软标签、温度与损失函数
知识蒸馏这个概念,我第一次接触是在做移动端模型部署的时候。当时要在手机上跑一个图像分类模型,但大模型实在太慢了。我试过直接剪枝、量化,效果都不太理想。后来读到 Hinton 那篇经典论文,才明白——原来我们可以让大模型当老师,手把手教小模型。
说白了,知识蒸馏就是「师生学习」。老师是一个大而准的模型,学生是一个小而快的模型。学生不光看真实标签,还要模仿老师的输出分布。这样学出来的小模型,往往比直接训练的效果好不少。
什么是知识蒸馏
知识蒸馏的核心思想很简单:让学生模型学习教师模型的「思考方式」,而不仅仅是最终的分类结果。
举个例子,你训练一个 ResNet-152 做猫狗分类。对于一张猫的图片,真实标签是「猫」。但教师模型输出的概率分布可能是:猫 0.95,狗 0.04,兔子 0.01。这个分布里藏着信息——模型觉得这张猫图「有点像狗」。这种模糊的判断,恰恰是学生模型需要学习的「暗知识」。
核心观点:教师模型的软输出比硬标签包含更丰富的类间关系信息。学生模型通过模仿这些软输出,能学到更泛化的特征表示。
我在项目中遇到过这样的情况:直接用小模型训练,准确率卡在 72% 上不去。用了蒸馏之后,同样的模型结构,准确率直接跳到 78%。提升很明显,而且没有增加任何推理成本。
软标签与硬标签
这里需要区分两个概念:硬标签和软标签。
| 类型 | 定义 | 例子 | 信息量 |
|---|---|---|---|
| 硬标签 | one-hot 编码的真实类别 | [1, 0, 0] | 低(只告诉你是哪一类) |
| 软标签 | 模型输出的概率分布 | [0.85, 0.10, 0.05] | 高(包含类间相似度信息) |
硬标签就是标准的 one-hot 向量。比如三分类问题,真实类别是第 0 类,硬标签就是 [1, 0, 0]。它只告诉你「这是猫」,不告诉你「它和狗有多像」。
软标签则是教师模型经过 softmax 后的输出。比如 [0.85, 0.10, 0.05]。这个分布里,0.10 给了狗,0.05 给了兔子。这说明教师模型认为这张猫图和狗有几分相似,和兔子不太像。这种类间关系,就是学生模型要学的「暗知识」。
我的经验:软标签的质量直接决定蒸馏效果。如果教师模型训练不充分,软标签的分布太平滑,反而会引入噪声。我一般会先确保教师模型在验证集上达到 95% 以上的准确率,再开始蒸馏。
温度参数 T 的作用
这里有个关键问题:教师模型的软标签往往太「尖锐」了。比如猫的概率 0.99,狗的概率 0.009,兔子 0.001。这种分布几乎和硬标签没区别,类间关系的信息被淹没了。
怎么办?Hinton 提出了一个巧妙的办法——温度参数 T。
标准的 softmax 公式是:
p_i = exp(z_i) / Σ_j exp(z_j)
加入温度 T 后变成:
p_i = exp(z_i / T) / Σ_j exp(z_j / T)
当 T=1 时,就是标准的 softmax。T 越大,输出的分布越平滑;T 越小,分布越尖锐。
| 温度 T | 分布特点 | 适用场景 |
|---|---|---|
| T → 0 | 极端尖锐,接近 one-hot | 几乎不用 |
| T = 1 | 标准 softmax | 常规分类 |
| T = 2~5 | 适度平滑,保留类间关系 | 知识蒸馏(常用) |
| T → ∞ | 均匀分布,信息完全丢失 | 无意义 |
我习惯把 T 设置在 3~5 之间。T 太小,软标签太尖锐,学生学不到暗知识;T 太大,分布太平滑,学生学到的全是噪声。具体值需要根据任务调,我一般会在验证集上扫一遍 T=[1, 2, 3, 4, 5, 7, 10]。
注意:训练学生模型时,教师模型和学生模型要用相同的 T 值计算 softmax。但学生模型在推理时,T 要设回 1。这个细节我踩过坑——忘了改 T,结果推理时概率分布全是平的,分类完全不准。
蒸馏损失函数设计
蒸馏的损失函数由两部分组成:
- 蒸馏损失:学生模型和教师模型软标签之间的差异
- 学生损失:学生模型和真实硬标签之间的差异
总损失是两者的加权和:
L_total = α * L_distill + (1 - α) * L_student
其中 α 是平衡系数,一般在 0.5~0.9 之间。
蒸馏损失通常用 KL 散度(Kullback-Leibler divergence)来衡量:
L_distill = KL(p_teacher || p_student) = Σ p_teacher * log(p_teacher / p_student)
学生损失就是标准的交叉熵损失:
L_student = CE(y_true, p_student)
下面是一个 PyTorch 风格的蒸馏损失实现:
def distillation_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.7):
# 用温度 T 软化教师和学生的输出
soft_teacher = F.softmax(teacher_logits / T, dim=1)
soft_student = F.softmax(student_logits / T, dim=1)
# 蒸馏损失:KL 散度
distill_loss = F.kl_div(
F.log_softmax(student_logits / T, dim=1),
soft_teacher,
reduction='batchmean'
) * (T * T) # 乘以 T^2 保持梯度尺度
# 学生损失:标准交叉熵
student_loss = F.cross_entropy(student_logits, labels)
# 总损失
total_loss = alpha * distill_loss + (1 - alpha) * student_loss
return total_loss
这里有个细节:KL 散度计算后要乘以 T * T。为什么?因为用温度 T 软化后,梯度会缩小 T² 倍。乘回去是为了保持梯度尺度,让训练更稳定。这个技巧是 Hinton 论文里提到的,我一开始没注意,结果训练总是不收敛。
调参建议:α 控制蒸馏损失和学生损失的权重。如果学生模型容量很小(比如 MobileNet),我建议 α 设大一点(0.7~0.9),多依赖教师的知识。如果学生模型容量还行(比如 ResNet-18),α 可以设小一点(0.5~0.7),让学生自己也能学一些。
知识蒸馏整体流程
下面这张图展示了知识蒸馏的完整流程:
整个流程可以概括为:
- 输入数据同时送入教师模型和学生模型
- 教师模型和学生模型都用温度 T 计算 softmax
- 计算蒸馏损失(KL 散度)和学生损失(交叉熵)
- 加权求和得到总损失,反向传播更新学生模型参数
- 推理时,学生模型用 T=1 做标准分类
总结一下:知识蒸馏的核心就是「温度软化 + 双损失」。温度 T 让教师的知识变得可学习,蒸馏损失传递类间关系,学生损失保证分类准确。两者配合,小模型就能学到远超自身容量的知识。