4. 知识蒸馏(Knowledge Distillation):教师-学生网络架构、软标签与硬标签蒸馏
知识蒸馏,说白了就是「大模型教小模型」。
我刚开始接触这个方向时,觉得挺玄乎的——一个已经训练好的大模型,凭什么能帮小模型提升精度?后来自己动手做了一遍,才明白其中的门道。今天咱们就把这块掰开揉碎了讲清楚。
4.1 教师-学生网络架构
先看整体结构。知识蒸馏的核心,就是两套网络:
- 教师网络(Teacher):大而强,参数量大,精度高。通常是预训练好的,不参与后续训练。
- 学生网络(Student):小而精,参数量小,推理快。是我们真正要部署的模型。
训练时,学生网络同时学两样东西:
- 真实标签(ground truth)
- 教师网络的输出(软标签)
我习惯把教师网络比作「老法师」,学生是「学徒」。老法师不仅告诉学徒正确答案,还把自己的思考过程(输出分布)也教给学徒。这样学徒学到的,就不只是死记硬背了。
核心公式:学生损失 = α × 硬标签损失 + (1-α) × 软标签损失
其中 α 是平衡系数,通常取 0.7 左右。这个值我调过很多次,发现跟任务类型关系很大。
下面这张图,是我自己画的知识蒸馏整体流程:
4.2 软标签与硬标签蒸馏
这里有个关键概念——软标签和硬标签的区别。
硬标签就是 one-hot 向量,比如分类任务中 [0,0,1,0,0]。它只告诉模型「答案是猫」,不告诉模型「猫和狗有点像,但和汽车完全不像」。
软标签就不一样了。教师网络输出的概率分布,比如 [0.05, 0.02, 0.85, 0.06, 0.02]。这里面包含了教师对各类别的「信心程度」。猫和狗的概率都高,说明它们特征相似。这种信息,硬标签是给不了的。
我的经验:软标签里藏着教师网络的「暗知识」。比如在图像分类中,教师可能认为「狼」和「狗」的概率都很高,但「猫」的概率很低。这种类别间的关系,学生网络通过软标签就能学到。
4.2.1 温度系数(Temperature)
软标签不是直接拿教师输出的概率用,而是要先经过一个「温度缩放」。
公式长这样:
soft_label_i = exp(z_i / T) / Σ_j exp(z_j / T)
其中 T 是温度系数。T 越大,概率分布越平滑;T 越小,越接近 one-hot。
我一般这样设置:
| 温度 T | 效果 | 适用场景 |
|---|---|---|
| T = 1 | 原始概率分布 | 默认值,效果一般 |
| T = 3 ~ 5 | 分布更平滑,类别间关系更明显 | 大多数蒸馏任务,推荐 |
| T = 8 ~ 10 | 分布非常平滑,几乎均匀 | 教师网络过强时,防止学生学偏 |
我曾经踩过的坑:温度设太高,学生网络学到的全是「模糊信息」,精度反而下降。后来我总结了一个经验——温度从 4 开始调,每次翻倍或减半,观察验证集精度变化。一般 3~5 之间效果最好。
4.2.2 蒸馏损失函数
蒸馏损失通常用 KL 散度(Kullback-Leibler divergence)来衡量教师和学生输出分布的差异:
L_distill = KL(soft_teacher || soft_student)
= Σ_i soft_teacher_i * log(soft_teacher_i / soft_student_i)
加上硬标签的交叉熵损失,总损失就是:
L_total = α * L_hard + (1-α) * T² * L_distill
注意这里有个 T² 系数。为什么要乘 T²?因为梯度会随着 T 的增大而缩小,乘上 T² 可以保持梯度量级不变。这个细节,很多初学者会忽略。
4.3 蒸馏策略对比
实际项目中,蒸馏策略不止一种。我整理了几种常见方式:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 离线蒸馏 | 教师提前算好软标签,存起来 | 训练快,教师只需推理一次 | 软标签固定,无法适应学生变化 |
| 在线蒸馏 | 教师和学生同时训练,教师实时更新 | 软标签更适配学生 | 训练慢,需要维护两个模型 |
| 自蒸馏 | 自己教自己,用历史版本的输出 | 不需要额外的大模型 | 提升有限,依赖模型自身能力 |
我个人最常用的是离线蒸馏。原因很简单——教师网络通常很大,在线蒸馏的话,显存扛不住。离线蒸馏只需要跑一次推理,后面学生训练时,教师那边完全不占资源。
4.4 实战代码示例
下面给一个 PyTorch 风格的蒸馏训练核心代码。嗯,这里只展示关键部分:
def distill_train_step(teacher, student, dataloader, optimizer, T=4.0, alpha=0.7):
teacher.eval() # 教师不训练
student.train()
for images, labels in dataloader:
optimizer.zero_grad()
# 学生前向
student_logits = student(images)
# 教师前向(不计算梯度)
with torch.no_grad():
teacher_logits = teacher(images)
# 硬标签损失
loss_hard = F.cross_entropy(student_logits, labels)
# 软标签损失(带温度)
soft_teacher = F.softmax(teacher_logits / T, dim=1)
soft_student = F.log_softmax(student_logits / T, dim=1)
loss_distill = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T ** 2)
# 总损失
loss = alpha * loss_hard + (1 - alpha) * loss_distill
loss.backward()
optimizer.step()
关键点:
- 教师网络一定要用
torch.no_grad()包裹,否则显存会爆 - KL 散度用
reduction='batchmean',这样结果跟 batch size 无关 - 别忘了乘 T²,否则温度高了梯度会消失
4.5 避坑指南
做知识蒸馏这几年,我踩过的坑不少。挑几个典型的说说:
- 教师太强,学生学不动:如果教师精度 99%,学生只有 80%,强行蒸馏反而会让学生困惑。我一般会先确保学生 baseline 能达到教师 80% 以上的精度,再开始蒸馏。
- 温度调不好:前面说了,从 4 开始试。如果学生学到的分布太「平」,就降低温度;如果学生只学硬标签,就升高温度。
- α 值选错:α 太大,蒸馏没效果;α 太小,学生学不到真实标签。我习惯从 0.7 开始,然后看验证集精度微调。
说白了,知识蒸馏不是万能药。它适合的场景是:你有一个大模型,但部署条件不允许。这时候,蒸馏就是性价比最高的方案。
好了,这一章的内容就到这。记住一句话:教师教学生的,不只是答案,更是思考过程。
公众号:蓝海资料掘金营,微信deep3321