知识蒸馏的三大范式:从模仿到超越
知识蒸馏听起来很玄乎,说白了就是让一个小模型(学生)去模仿一个大模型(老师)的行为。但模仿也有不同的层次——你是只抄答案,还是连解题思路一起学?这就引出了蒸馏的三大范式。
我个人习惯把这三类蒸馏比作三种教学方式:Logit蒸馏是直接看学生的考卷和老师的考卷哪里不一样;特征蒸馏是盯着学生的解题步骤,看中间哪一步写错了;关系蒸馏则是让学生学会老师处理多个问题时的整体思路。下面我们逐个拆解。
Logit蒸馏:最经典的“软标签”教学
Logit蒸馏是Hinton在2015年提出的原始方法。它的核心思想很简单:学生模型不仅要学习真实标签(硬标签),还要学习老师模型输出的概率分布(软标签)。
为什么会这样?因为硬标签只告诉学生“这是猫”,但软标签会告诉学生“这有80%像猫,15%像狗,5%像兔子”。这种额外的信息量,能让学生学到类别之间的相似性。
核心公式:蒸馏损失 = α × KL散度(学生软输出 || 老师软输出) + (1-α) × 交叉熵(学生硬输出 || 真实标签)
其中α是平衡系数,通常取0.7~0.9。
我在项目中遇到过一个问题:直接用原始概率做KL散度,学生根本学不动。因为老师输出的概率分布太“尖锐”了——比如正确类别的概率是0.99,其他类别接近0。这时候学生看到的软标签和硬标签几乎没区别。
温度系数T:让软标签真正“软”起来
温度系数T就是用来解决上面这个问题的。它通过一个简单的数学变换,把概率分布“软化”:
# 带温度的Softmax
def softmax_with_temperature(logits, T):
# T > 1 时分布变平滑,T < 1 时分布变尖锐
exp_logits = np.exp(logits / T)
return exp_logits / np.sum(exp_logits)
# 举个例子
logits = np.array([5.0, 2.0, 1.0])
print("T=1:", softmax_with_temperature(logits, 1))
print("T=5:", softmax_with_temperature(logits, 5))
print("T=10:", softmax_with_temperature(logits, 10))
输出结果会让你直观感受到T的作用:
| 温度T | 类别A概率 | 类别B概率 | 类别C概率 | 分布特点 |
|---|---|---|---|---|
| 1 | 0.936 | 0.046 | 0.018 | 尖锐,几乎只有正确类 |
| 5 | 0.524 | 0.289 | 0.187 | 平滑,类别间关系清晰 |
| 10 | 0.386 | 0.314 | 0.300 | 非常平滑,接近均匀分布 |
我的经验:T值一般取2~8之间。T太小(<2)软标签不够软,T太大(>10)会丢失类别间的区分度。我习惯先试T=4,然后根据学生收敛速度微调。
KL散度损失:衡量两个分布的“距离”
KL散度(Kullback-Leibler divergence)是蒸馏中最常用的损失函数。它衡量的是两个概率分布之间的差异:
# KL散度的PyTorch实现
import torch
import torch.nn.functional as F
def kl_div_loss(student_logits, teacher_logits, T):
# 先除以温度,再计算KL散度
student_soft = F.log_softmax(student_logits / T, dim=1)
teacher_soft = F.softmax(teacher_logits / T, dim=1)
# KL散度 = sum(teacher * log(teacher/student))
kl_loss = F.kl_div(student_soft, teacher_soft, reduction='batchmean')
# 乘以T^2以补偿梯度缩放
return kl_loss * (T ** 2)
嗯,这里要注意:代码里最后乘了T²。为什么?因为温度缩放后梯度会变小,乘T²是为了让梯度大小和T=1时保持一致。这个细节我踩过坑——第一次写蒸馏代码时忘了乘T²,结果学生怎么训都训不好。
特征蒸馏:盯着中间层学
Logit蒸馏只关注最终输出,但特征蒸馏会让学生去模仿老师中间层的特征图。这就像学画画时,老师不仅看你的成品,还检查你的每一笔素描。
特征蒸馏的损失函数通常写成:
L_feat = || f_s(x) - f_t(x) ||²
其中f_s和f_t分别是学生和老师在某个中间层的特征输出。但这里有个坑:学生和老师的特征维度通常不一样,需要加一个适配层(Adaptation Layer)来对齐。
我曾经踩过的坑:特征蒸馏时直接拿学生最后一层卷积去对齐老师的倒数第二层,结果维度不匹配报错。后来我加了一个1×1卷积做适配层,才把问题解决。另外,特征蒸馏的权重不要设太大,我一般设0.1~0.3,否则学生模型会过度关注中间特征而忽略最终分类。
关系蒸馏:学关系,不学特征
关系蒸馏是相对较新的范式。它不要求学生和老师的特征图一模一样,而是要求学生能学到老师模型中样本之间的关系。
举个例子:老师模型认为样本A和样本B很相似,样本A和样本C差异很大。关系蒸馏就让学生也学会这种相似性关系。常用的关系蒸馏损失包括:
- 距离关系:学生和老师的样本对距离分布一致
- 角度关系:学生和老师的样本对角度分布一致
- 图关系:用图结构表示样本间关系,让学生模仿整张图
关系蒸馏的好处是:它不依赖特征图的具体数值,所以对模型结构差异的容忍度更高。我在做跨架构蒸馏(比如从Transformer蒸馏到CNN)时,关系蒸馏的效果往往比特征蒸馏好。
三大范式对比:什么时候用哪个?
| 蒸馏范式 | 核心思想 | 适用场景 | 我的建议 |
|---|---|---|---|
| Logit蒸馏 | 模仿最终输出分布 | 通用场景,模型结构相近 | 作为基线方法,先跑通 |
| 特征蒸馏 | 模仿中间层特征 | 学生和老师结构相似 | 适合CNN蒸馏,注意维度对齐 |
| 关系蒸馏 | 模仿样本间关系 | 跨架构蒸馏,结构差异大 | Transformer→CNN时优先考虑 |
你想想看,实际项目中很少有人只用一种蒸馏范式。我通常的做法是:先用Logit蒸馏打个底,然后根据模型结构差异决定是否加特征蒸馏或关系蒸馏。三种范式可以组合使用,但要注意权重分配——我一般设Logit蒸馏权重为1,特征蒸馏0.2,关系蒸馏0.1。
最后说一句:别被这些理论吓到。你先把Logit蒸馏跑通,理解温度系数和KL散度怎么调,后面再慢慢加特征蒸馏和关系蒸馏。我在实际项目中,80%的场景只用Logit蒸馏就能拿到不错的收益。剩下的20%才需要上更复杂的方案。