2. Logits蒸馏:KL散度损失函数、温度缩放策略、蒸馏权重α的调参经验

Logits蒸馏,说白了就是让“学生模型”去模仿“老师模型”的软输出。你想想看,老师教学生,不只是给个标准答案(硬标签),更重要的是教解题思路(软概率分布)。这个思路,就是Hinton在2015年那篇经典论文里提出的核心思想。

我个人觉得,Logits蒸馏是知识蒸馏里最基础也最实用的一招。它不需要动模型内部结构,只改损失函数和输出层。今天咱们就把KL散度、温度T、权重α这三个核心参数彻底聊透。

2.1 KL散度损失函数:到底在算啥?

KL散度,全称Kullback-Leibler divergence,用来衡量两个概率分布之间的差异。在蒸馏场景里,就是老师模型的软输出和学生模型的软输出之间的“距离”。

公式长这样:

KL(P || Q) = Σ P(x) * log(P(x) / Q(x))

其中P是老师分布,Q是学生分布。值越小,说明学生越像老师。

为什么不用交叉熵? 我刚开始做蒸馏时也困惑过。后来发现,KL散度天然适合做“模仿学习”——它不对称,惩罚的是学生分布偏离老师分布的程度。交叉熵虽然数学上等价,但KL散度在梯度回传时更稳定,尤其当温度较高时。

关键理解: KL散度不是对称的。KL(P||Q) ≠ KL(Q||P)。蒸馏时我们用老师当P,学生当Q,意思是“学生要尽量靠近老师”。反过来用就错了。

2.2 温度缩放策略:这个T到底怎么调?

温度T是Logits蒸馏的灵魂参数。它控制着软标签的“平滑程度”。

公式很简单:

softmax(z_i / T)

其中z_i是logits,T是温度。

温度T的作用:

  • T=1:标准softmax,输出就是硬概率分布
  • T>1:分布变平滑,小概率类别的信息被放大
  • T<1:分布变尖锐,接近one-hot

我在项目中遇到过一个问题:温度设太高,学生模型学到的全是噪声;温度设太低,又跟硬标签训练没区别。后来总结出一套经验:

温度范围 适用场景 我的经验
T=1~3 分类数少(10类以内) 图像分类常用,稳定
T=3~8 分类数中等(10~100类) NLP任务常见,能学到类间关系
T=8~20 分类数多(100类以上) 大规模分类,需要平滑信息

调参技巧: 我习惯先固定T=4跑一轮,观察学生模型的收敛速度和验证集精度。如果loss下降太快但精度上不去,说明温度太高;如果loss下降慢且精度跟硬标签训练差不多,说明温度太低。

2.3 蒸馏权重α:软硬兼施的艺术

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

L_total = α * L_soft + (1-α) * L_hard

其中L_soft是KL散度(学生vs老师),L_hard是交叉熵(学生vs真实标签)。α控制着软标签和硬标签的权重比例。

α的调参经验:

  • α=0.7~0.9:老师主导,适合老师模型很强、学生模型容量够大的情况
  • α=0.3~0.5:平衡策略,适合大多数通用场景
  • α=0.1~0.3:硬标签主导,适合学生模型容量小、容易过拟合的情况

我曾经踩过一个坑:在某个文本分类任务中,我把α设成0.9,结果学生模型完全跟着老师走,连老师的错误都学过来了。后来改成α=0.5,同时把温度从8降到4,效果反而更好。

避坑指南: 我曾经在CIFAR-100上做实验,发现α=0.9时学生模型精度反而比α=0.5低。原因是老师模型在少数类别上也有错误,学生学得太“像”老师,反而把错误也继承了。建议先跑一组α=[0.3, 0.5, 0.7]的对比实验,找到最优值。

2.4 核心流程:一张图看懂Logits蒸馏

下面这张图展示了Logits蒸馏的完整流程,从输入到损失计算:

Logits蒸馏核心流程 输入数据 x 老师模型(大) 学生模型(小) 老师 Logits 学生 Logits Softmax(T=4) Softmax(T=4) KL散度损失 蒸馏损失 = α × KL(老师软分布 || 学生软分布) + (1-α) × 交叉熵(学生输出, 真实标签)

2.5 完整训练流程与代码示例

下面是一个完整的Logits蒸馏训练代码片段,包含了温度缩放和KL散度计算:

import torch
import torch.nn.functional as F

def distillation_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.5):
    """
    Logits蒸馏损失函数
    Args:
        student_logits: 学生模型输出
        teacher_logits: 老师模型输出(不计算梯度)
        labels: 真实标签
        T: 温度
        alpha: 蒸馏权重
    """
    # 软标签损失:KL散度
    soft_student = F.log_softmax(student_logits / T, dim=1)
    soft_teacher = F.softmax(teacher_logits / T, dim=1)
    kl_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean')
    kl_loss = kl_loss * (T * T)  # 温度补偿
    
    # 硬标签损失:交叉熵
    ce_loss = F.cross_entropy(student_logits, labels)
    
    # 总损失
    total_loss = alpha * kl_loss + (1 - alpha) * ce_loss
    return total_loss

# 训练循环示例
for batch in dataloader:
    inputs, labels = batch
    inputs, labels = inputs.to(device), labels.to(device)
    
    # 老师模型不更新梯度
    with torch.no_grad():
        teacher_logits = teacher_model(inputs)
    
    # 学生模型前向
    student_logits = student_model(inputs)
    
    # 计算蒸馏损失
    loss = distillation_loss(
        student_logits, teacher_logits, labels,
        T=4.0, alpha=0.5
    )
    
    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

温度补偿: 代码中kl_loss乘以T²是因为梯度会随T缩放,补偿后梯度大小与T无关,方便调参。这个细节我一开始没注意,导致不同温度下学习率需要重新调,后来加上补偿就稳定多了。

2.6 调参经验总结

我把Logits蒸馏的三个核心参数总结成一张速查表,方便你快速定位问题:

现象 可能原因 我的建议
学生精度比硬标签训练还差 α太大,老师错误被继承 降低α到0.3~0.5,同时检查老师精度
训练loss下降快但验证集不涨 温度太高,学到噪声 降低T到2~4,或增加硬标签权重
学生模型收敛太慢 温度太低,软标签信息不足 提高T到6~8,或增大α
学生模型过拟合 α太小,硬标签主导 增大α到0.7~0.9,利用老师做正则化

嗯,这里要注意:没有万能参数组合。我每次接到新任务,都会先跑一组小规模对比实验,用网格搜索找T和α的最优值。虽然费点时间,但比凭感觉调参靠谱得多。

Logits蒸馏的核心就这些。说白了,KL散度告诉你“差多少”,温度T控制“看多细”,权重α决定“听谁的多”。三个参数配合好了,学生模型就能在保持轻量的同时,学到老师模型的精髓。