4. 知识蒸馏(Knowledge Distillation):教师-学生网络架构、软标签与硬标签蒸馏

知识蒸馏,说白了就是「大模型教小模型」。

我刚开始接触这个方向时,觉得挺玄乎的——一个已经训练好的大模型,凭什么能帮小模型提升精度?后来自己动手做了一遍,才明白其中的门道。今天咱们就把这块掰开揉碎了讲清楚。

4.1 教师-学生网络架构

先看整体结构。知识蒸馏的核心,就是两套网络:

  • 教师网络(Teacher):大而强,参数量大,精度高。通常是预训练好的,不参与后续训练。
  • 学生网络(Student):小而精,参数量小,推理快。是我们真正要部署的模型。

训练时,学生网络同时学两样东西:

  • 真实标签(ground truth)
  • 教师网络的输出(软标签)

我习惯把教师网络比作「老法师」,学生是「学徒」。老法师不仅告诉学徒正确答案,还把自己的思考过程(输出分布)也教给学徒。这样学徒学到的,就不只是死记硬背了。

核心公式:学生损失 = α × 硬标签损失 + (1-α) × 软标签损失

其中 α 是平衡系数,通常取 0.7 左右。这个值我调过很多次,发现跟任务类型关系很大。

下面这张图,是我自己画的知识蒸馏整体流程:

教师网络 大模型(已冻结) 学生网络 小模型(训练中) 输入数据(图片/文本) 软标签(Soft Label) 概率分布(含温度系数) 学生输出 预测概率分布 蒸馏损失(KL散度) 真实标签(Hard Label) 交叉熵损失 总损失 = α·CE + (1-α)·KL 蒸馏路径 硬标签路径

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