3. 经典蒸馏损失函数:KL散度与温度系数、均方误差(MSE)与L2损失、对比学习损失(InfoNCE)、蒸馏损失与任务损失的联合优化
损失函数,说白了就是蒸馏过程的指挥棒。你选什么损失,学生模型就朝哪个方向学。我这些年调过不下二十种蒸馏方案,最后发现真正常用的其实就那几类。今天咱们把它们的底牌翻出来看看。
3.1 KL散度与温度系数:软标签的精髓
KL散度是蒸馏的基石。它衡量两个概率分布之间的差异。在蒸馏场景里,就是让学生的输出分布尽量贴近老师的输出分布。
公式长这样:
KL(P || Q) = Σ P(x) * log(P(x) / Q(x))
但直接算KL散度有个问题——老师的输出往往太“硬”了。比如分类任务,老师对正确类别的预测可能是0.99,其他类别接近0。学生根本学不到类别间的相似关系。
温度系数就是来解决这个的。Hinton老爷子在原始论文里提的:把logits除以温度T,再做softmax。T越大,分布越平滑,类别间的相对关系就保留得越好。
核心公式:
软化后的概率:q_i = exp(z_i / T) / Σ_j exp(z_j / T)
蒸馏损失:L_KD = KL(q_teacher || q_student) * T²
注意那个T²,是梯度尺度补偿,很多人会漏掉。
我刚开始做蒸馏时,T设成1,结果学生学出来跟直接训练差不多。后来调到4,效果明显好了。但T也不是越大越好——T太大,所有类别概率都趋同,信息就没了。
| 温度T | 分布特点 | 适用场景 |
|---|---|---|
| T=1 | 原始分布,较尖锐 | 任务损失为主,蒸馏为辅 |
| T=2~5 | 平滑分布,保留类间关系 | 通用蒸馏,大部分场景 |
| T>10 | 接近均匀分布 | 极少使用,信息稀释严重 |
我的经验:T从3开始调,观察学生输出的熵值。如果熵值太小(分布太尖),就增大T;如果熵值太大(太平坦),就减小T。一般3~5之间能找到甜点。
3.2 均方误差(MSE)与L2损失:特征层的对齐
KL散度管的是输出层。但很多时候,我们希望学生在中间特征层也学得像老师。这时候MSE就上场了。
MSE损失就是算老师和学生特征图之间的欧氏距离:
L_MSE = 1/N * Σ ||f_teacher - f_student||²
这里有个坑——老师和学生的特征图尺寸往往不一样。我遇到过好几次,学生网络浅,特征图分辨率大,直接算MSE维度都对不上。
避坑指南:我曾经在一个检测模型蒸馏项目里,直接用MSE对齐特征图,结果训练loss降得很快,但验证指标反而掉了。后来发现是特征图没做归一化——老师和学生的特征尺度差了一个数量级。加上LayerNorm或者BN后再算MSE,问题就解决了。
L2损失和MSE本质是一回事,只是L2不取平均。实践中我更喜欢用MSE,因为梯度尺度更稳定,不用频繁调学习率。
特征层对齐有几个常见策略:
- 直接对齐:选某个中间层,MSE硬对齐。简单,但容易过拟合。
- 注意力对齐:对齐特征图的注意力图,而不是特征本身。更鲁棒。
- 关系对齐:对齐样本间的距离关系矩阵。适合小样本场景。
3.3 对比学习损失(InfoNCE):拉近正样本,推开负样本
InfoNCE损失是从对比学习里借来的。它的思路很直接:让学生的表征和老师的表征在特征空间里靠近,同时和其他样本的表征拉开距离。
公式:
L_InfoNCE = -log( exp(sim(z_s, z_t)/τ) / Σ exp(sim(z_s, z_i)/τ) )
其中sim是余弦相似度,τ是温度系数(注意这个τ和KL散度的T不是一回事)。
我为什么喜欢InfoNCE?因为它比MSE更灵活。MSE强制特征完全一致,但InfoNCE只要求相对关系一致——学生特征和老师特征的距离,要比和其他样本的距离近就行。这给了学生更多自由度。
适用场景对比:
- MSE:特征维度固定,老师和学生结构相似,要求精确对齐。
- InfoNCE:特征维度可不同,老师和学生结构差异大,只要求相对关系。
我在做跨架构蒸馏(比如ResNet蒸馏到MobileNet)时,InfoNCE的效果明显好于MSE。因为两个网络的特征空间本身就不一样,硬对齐反而有害。
3.4 蒸馏损失与任务损失的联合优化
光用蒸馏损失不行,任务损失也得带上。否则学生可能学了一堆特征,但分类/检测任务本身没做好。
联合损失的标准形式:
L_total = α * L_task + β * L_distill
α和β是权重系数。怎么调?我见过有人用网格搜索,有人用贝叶斯优化。我的习惯是:
- 先把α设成1,β设成0,跑一个baseline。
- 然后β从0.1开始试,每次翻倍,观察验证集指标。
- 如果β太大(比如>10),任务损失被淹没,学生学不到任务知识。
- 如果β太小(比如<0.01),蒸馏没效果。
我的经验:大部分场景下,α:β = 1:1 到 1:5 之间效果最好。但如果你用KL散度做输出层蒸馏,记得把KL散度乘以T²,否则梯度尺度会偏小。
还有一个细节:蒸馏损失和任务损失的梯度尺度可能差很多。我遇到过KL散度的梯度是0.01量级,而交叉熵的梯度是1量级。这时候不调整权重,蒸馏根本不起作用。解决办法是:
- 监控两个损失的数值,让它们在同一量级。
- 或者用梯度裁剪,防止某个损失主导。
- 或者用learnable weight,让网络自己学α和β。
嗯,说到learnable weight,我试过一次,效果不稳定。后来还是老老实实手动调了。
最后说一句:损失函数没有银弹。我见过有人用KL散度加MSE加InfoNCE三管齐下,效果反而不如只用KL散度。我的建议是:从最简单的开始,加一个损失看一次效果,不要贪多。