4、离线蒸馏实战:教师模型预训练、学生模型架构选择、蒸馏训练流程详解

好,咱们今天聊点实在的。

离线蒸馏,说白了就是「先有个好老师,再带个聪明学生」。这个流程我跑了不下几十次,踩过的坑比代码行数还多。今天我把整个套路拆开揉碎了讲给你听。

4.1 教师模型预训练:别急着当老师

教师模型的质量,直接决定了蒸馏的天花板。

我见过不少团队,随便拿个预训练模型就当老师。结果学生学完,精度还不如自己从头训。为什么?因为老师自己都没学明白,怎么教学生?

⚠️ 注意: 教师模型必须经过充分微调,达到当前任务的最佳性能。否则,蒸馏出来的学生模型会继承教师的「坏习惯」。

我个人习惯的做法是:

  • 先在大规模通用数据上预训练(比如 ImageNet 或 JFT-300M)
  • 再在目标任务数据上微调,直到验证集精度不再提升
  • 最后做一次模型集成,把多个 checkpoint 平均一下,作为最终教师

举个例子,我在做图像分类蒸馏时,教师用的是 EfficientNet-B7。先在 ImageNet 上训了 350 个 epoch,然后在自有的工业缺陷数据集上又微调了 50 个 epoch。最后 Top-1 精度到了 84.3%。这个精度,说实话已经接近当时的 SOTA 了。

嗯,这里要注意:教师模型不要太大。我见过有人用 ViT-G 当老师,参数量 18 亿,学生才 100 万参数。结果学生根本学不动,因为教师的知识分布太复杂了。我个人建议,教师和学生的参数量差距控制在 10 倍以内比较合适。

4.2 学生模型架构选择:小,但要巧

学生模型选什么?这是个灵魂拷问。

你想想看,学生模型太简单,学不到教师的知识;太复杂,又失去了蒸馏的意义。我踩过最深的坑,就是选了一个 MobileNetV3-Small 当学生,结果精度比教师低了 12 个点。后来换成 MobileNetV3-Large,差距缩小到 3 个点,推理速度只慢了 15%。

我的经验是:

  • 优先选择与教师同族的轻量架构(比如教师是 ResNet-50,学生选 ResNet-18)
  • 如果追求极致速度,可以考虑 MobileNet、ShuffleNet 或 EfficientNet-Lite
  • 如果追求精度,可以用 TinyViT 或轻量级 Transformer

这里我整理了一个常用搭配表,供你参考:

教师模型 推荐学生模型 参数量比 适用场景
ResNet-50 ResNet-18 / MobileNetV3 ~4:1 通用图像分类
BERT-Base TinyBERT / DistilBERT ~6:1 文本分类、NER
ViT-Base TinyViT-5M / DeiT-Tiny ~10:1 视觉 Transformer 任务
YOLOv8-L YOLOv8-N / YOLOv5-S ~5:1 目标检测
💡 小技巧: 我习惯先跑一次「盲测」—— 让学生模型从头训练,看它能达到什么精度。如果盲测精度离教师差距超过 5 个点,那这个学生架构可能不太适合蒸馏。

4.3 蒸馏训练流程详解:三步走,稳得很

好,老师和学生都选好了,接下来就是核心环节 —— 蒸馏训练。

整个流程我分成三步:

4.3.1 第一步:准备软标签

教师模型在推理时,输出的不是 hard label(硬标签,比如类别 0 或 1),而是 soft label(软标签,比如 [0.1, 0.7, 0.2])。

这里有个关键参数叫 温度 T。温度越高,软标签越「软」,类别间的差异越小。我一般设 T=4,效果比较均衡。

import torch
import torch.nn.functional as F

def soft_label_with_temperature(logits, temperature=4.0):
    """生成软标签,温度越高,分布越平滑"""
    return F.softmax(logits / temperature, dim=-1)

# 教师模型推理
teacher_logits = teacher_model(images)
soft_targets = soft_label_with_temperature(teacher_logits, T=4)

🔑 关键点: 软标签要提前保存到磁盘,不要每次训练都重新推理。我试过在线生成软标签,训练速度直接慢了 3 倍。离线蒸馏嘛,就该把「离线」做到极致。

4.3.2 第二步:设计损失函数

蒸馏损失通常由两部分组成:

  • 蒸馏损失(KL 散度):让学生模型的软输出逼近教师的软标签
  • 硬标签损失(交叉熵):让学生模型直接学习真实标签

我常用的公式是:

loss = alpha * KL_div(student_soft, teacher_soft) + (1 - alpha) * CE(student_logits, hard_labels)

其中 alpha 控制蒸馏的强度。我一般设 alpha=0.7,温度 T=4。为什么?因为软标签包含的信息量更大,应该给更高权重。当然,这个值不是固定的,我建议你在验证集上调一下。

⚠️ 避坑指南: 我曾经把 alpha 设成 0.9,结果学生模型完全忽略了真实标签,学了一堆教师的「偏见」。后来改成 0.7,精度直接涨了 2 个点。

4.3.3 第三步:联合训练

把学生模型、软标签、硬标签一起喂进训练循环。这里有个细节:学生模型的输出也要除以温度 T,这样才能和软标签在同一个尺度上计算 KL 散度。

def distillation_loss(student_logits, teacher_soft_targets, hard_labels, T=4.0, alpha=0.7):
    # 学生输出也要除以温度
    student_soft = F.log_softmax(student_logits / T, dim=-1)
    
    # KL 散度损失
    kl_loss = F.kl_div(student_soft, teacher_soft_targets, reduction='batchmean') * (T * T)
    
    # 硬标签交叉熵损失
    ce_loss = F.cross_entropy(student_logits, hard_labels)
    
    # 联合损失
    return alpha * kl_loss + (1 - alpha) * ce_loss

# 训练循环
for images, labels in dataloader:
    student_logits = student_model(images)
    soft_targets = precomputed_soft_targets[batch_idx]  # 从磁盘加载
    
    loss = distillation_loss(student_logits, soft_targets, labels)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

嗯,这里要注意:训练初期,学生模型输出和教师软标签差距很大,KL 散度会很大。我建议先用较小的学习率(比如 1e-4)预热 5 个 epoch,再恢复正常学习率。

4.4 实战经验总结

说了这么多,我总结几条血泪教训:

  • 教师模型别太大:参数量差距超过 20 倍,学生基本学不动
  • 温度 T 别乱设:我试过 T=1(太硬)和 T=20(太软),效果都不如 T=4
  • 软标签要离线保存:在线生成太慢,而且浪费 GPU 资源
  • alpha 值要调:0.7 是个不错的起点,但具体任务要具体分析

💡 最后一个小建议: 蒸馏完成后,别忘了做一次「学生模型自检」—— 用验证集跑一遍,看看精度是否达到预期。如果差距太大,回头检查教师模型的质量或者学生架构的选择。

好了,离线蒸馏的实战流程就讲到这里。下一章咱们聊聊在线蒸馏和自蒸馏,那又是另一番天地了。