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 | 目标检测 |
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。为什么?因为软标签包含的信息量更大,应该给更高权重。当然,这个值不是固定的,我建议你在验证集上调一下。
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 是个不错的起点,但具体任务要具体分析
💡 最后一个小建议: 蒸馏完成后,别忘了做一次「学生模型自检」—— 用验证集跑一遍,看看精度是否达到预期。如果差距太大,回头检查教师模型的质量或者学生架构的选择。
好了,离线蒸馏的实战流程就讲到这里。下一章咱们聊聊在线蒸馏和自蒸馏,那又是另一番天地了。