知识蒸馏这个概念最容易被误解成简单的参数复制,很多实现只把教师模型输出概率最高的类别当作唯一答案,然后让学生模型去拟合这个答案。这样做相当于只迁移了硬标签,把教师网络对错误类别的相对判断完全丢掉了。一个在图像分类上表现很好的大模型,在看到一张猫的照片时,不仅会给猫很高概率,还会给豹、虎、猞猁等相似类别一个略高但不是最高的概率。这个概率分布里包含了类别之间的视觉相似性和易混淆关系。大师带学徒的关键就在这里:大师不会只给一句对错,而是会告诉学徒哪些错误更接近正确答案。神经网络中的这种解释性信息,就藏在软标签里。

一、从大师带学徒到软标签:知识蒸馏的直觉
传统分类训练通常使用独热编码作为标签。假设有四个类别,正确类别索引为0,标签向量就是[1,0,0,0]。这种硬标签对每个错误类别一视同仁,模型无论把猫误判成老虎还是汽车,交叉熵给出的惩罚在错误类别维度上并没有区分。事实上,把猫识别成老虎显然比识别成汽车更值得保留为可接受的误差,因为它们在特征空间里更接近。独热编码无法表达这种相对关系。
知识蒸馏的核心是让教师网络先对输入进行计算,得到原始logits,再通过一个带温度系数的softmax转换为软标签。温度系数T控制分布的平滑程度。T越大,输出概率分布越接近均匀分布,次要类别的概率被抬高;T越小,分布越接近独热编码。教师网络在T大于1的条件下产生的软标签,会保留更多类别间相似性信息。
import torch
import torch.nn.functional as F
def softmax_with_temperature(logits, temperature=1.0):
# temperature 越大,输出分布越平滑;temperature=1 退化为普通softmax
return F.softmax(logits / temperature, dim=-1)
# 模拟教师对4个类别的原始输出
logits = torch.tensor([4.2, 3.8, 1.0, 0.5])
print(softmax_with_temperature(logits, temperature=1.0))
print(softmax_with_temperature(logits, temperature=4.0))
上面的输出可以看到,当温度等于1时,最大概率可能占绝对主导;当温度提高到4以后,第二个类别的概率明显上升,学生模型不再只关注最高分类别,而是会模仿教师对整个概率空间的理解。这就是软标签带来的额外监督信号。需要注意的是,学生模型在推理阶段不使用温度系数,温度只存在于训练阶段用来提取教师知识。
二、温度系数与KL散度:蒸馏损失如何设计
在蒸馏训练中,学生模型通常同时接收两个监督信号。一个是原始硬标签的交叉熵损失,另一个是教师软标签与学生软预测之间的KL散度损失。硬标签损失保证学生最终输出正确的类别,软标签损失则帮助学生捕捉教师对类间关系的判断。二者通过加权系数alpha组合。alpha较大时,模型更偏向真实标签;alpha较小时,模型更偏向模仿教师分布。
教师网络在蒸馏过程中保持冻结,不参与梯度更新。学生模型的前向传播会分别计算普通softmax输出和带温度softmax输出。与硬标签交叉熵不同,软标签KL散度需要将学生和教师的logits都除以同一个温度T,再计算log_softmax和softmax。由于softmax对logits的导数与1除以T有关,反向传播时梯度会缩小T的平方倍,因此通常在KL散度前乘以T的平方进行补偿。这个细节很多第一版实现会漏掉,导致软蒸馏损失过小,训练缓慢。
import torch
import torch.nn.functional as F
def distillation_loss(student_logits, teacher_logits, labels, temperature=4.0, alpha=0.1):
# 教师软标签:温度缩放后计算softmax
soft_teacher = F.softmax(teacher_logits / temperature, dim=1)
# 学生软预测,同样使用温度
soft_student = F.log_softmax(student_logits / temperature, dim=1)
# KL散度损失,注意乘 temperature^2 保持梯度尺度
kd_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (temperature ** 2)
# 学生硬标签交叉熵
ce_loss = F.cross_entropy(student_logits, labels)
return alpha * ce_loss + (1 - alpha) * kd_loss
这段代码中,F.softmax用来生成教师软标签,F.log_softmax用来生成学生软预测。调用F.kl_div时使用batchmean作为归约方式,能避免批次大小对损失数值的影响。温度T平方的乘法是重要一步。另一个容易忽略的点是,教师logits必须在torch.no_grad下获取,否则会更新教师模型,增加计算图内存还不必要。
optimizer = torch.optim.Adam(student.parameters(), lr=1e-3)
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
with torch.no_grad():
teacher_logits = teacher(images)
student_logits = student(images)
loss = distillation_loss(student_logits, teacher_logits, labels, temperature=4.0, alpha=0.1)
optimizer.zero_grad()
loss.backward()
optimizer.step()
完成损失函数后,训练循环保持简短。在每个批次上,先关闭教师参数的梯度,再计算教师logits和学生logits。学生logits同时用于硬标签交叉熵和软标签KL散度。优化器只更新学生参数。实际使用中,教师模型可以预先加载训练好的权重,并在整个蒸馏过程中保持eval状态,关闭Dropout等随机行为。
三、模型压缩中的工程考量与误区
知识蒸馏虽然概念直观,但落地时经常在几个地方走偏。第一个误区是只做最后一层输出蒸馏。对于结构相似的教师与学生,这通常有效;但如果学生模型容量很小,或者教师与学生架构差异很大,单靠输出层软标签可能不足以传递足够信息。此时可以引入中间层蒸馏,例如用均方误差让学生某个隐藏层特征匹配教师对应隐藏层特征,或者蒸馏注意力矩阵。中间层蒸馏能提供更细粒度的监督,但也会增加额外损失项和超参数。
第二个误区是温度设置不当。有些实现把温度固定为20甚至更高,认为越平滑越好。实际上温度过高会使概率分布接近均匀,学生学不到任何判别性信息;温度过低则退化为硬标签,失去类间关系。分类任务常用的温度范围是2到5,生成式任务或类别数很多时可以尝试稍高一些,但需要通过验证集调参。温度与alpha也存在联动,一味增加软蒸馏损失权重并不总能带来精度提升。
硬标签与软标签在信息含量、噪声敏感度和训练难度上差异较大。下面这张表总结了关键区别。
| 对比维度 | 硬标签训练 | 软标签蒸馏 |
|---|---|---|
| 信息量 | 只有正确类别为1 | 所有类别都有概率 |
| 类间关系 | 无法表达相似性 | 能体现易混淆关系 |
| 对噪声标签 | 比较敏感 | 平滑分布更鲁棒 |
| 训练稳定性 | 损失下降快但易过拟合 | 需要平衡硬软损失 |
另一个常被忽略的工程问题是批量归一化和数据增强。学生模型如果使用批量归一化,教师和学生应使用相同的数据增强策略或小心统计量偏移。教师模型在eval状态下预测,通常不对增强后的图像再次做随机裁剪,而学生模型训练时仍使用增强策略。如果两者输入分布差别太大,软标签与硬标签可能不一致,损失优化会变得震荡。
四、从分类任务到生成式模型:蒸馏的延伸应用
知识蒸馏最早在图像分类中取得成功,但思想已经扩展到自然语言处理和生成式模型。以BERT为例,大尺寸预训练模型参数量可能达到数亿,而希望部署的小模型只有几百万参数。如果只做输出层蒸馏,小模型很难逼近大模型在复杂语言理解任务上的表现。因此研究者会同时蒸馏教师模型的隐藏状态、注意力矩阵和最后的预测分布。注意力矩阵蒸馏尤其重要,因为多头注意力的每个头都编码了不同的句法或语义关系,小模型如果只学习最终预测,往往会忽略这些中间表示。
在生成式模型中,蒸馏面临序列输出的挑战。大语言模型逐token生成文本,每个位置都会输出一个词汇表大小的概率分布。学生模型可以在教师生成的语料上做训练,也可以直接对每个有效token位置计算教师与学生logits之间的KL散度。与分类任务不同,生成式蒸馏通常不必加入硬标签交叉熵,因为教师输出本身就是从真实语料学到的分布,可以充当监督信号。下面是一个序列级软蒸馏的简化实现。
import torch.nn.functional as F
def sequence_distillation(student_logits, teacher_logits, mask):
# student_logits: (batch, seq_len, vocab)
# teacher_logits: (batch, seq_len, vocab)
# mask: 有效token位置为1,pad位置为0
T = 2.0
teacher_probs = F.softmax(teacher_logits / T, dim=-1)
student_log_probs = F.log_softmax(student_logits / T, dim=-1)
kld = F.kl_div(student_log_probs, teacher_probs, reduction='none').sum(-1)
return (kld * mask).sum() / mask.sum()
这个函数对每个位置计算KL散度后按有效token求平均。实际生成式模型训练时,还要考虑长度归一化、教师策略采样、动态温度衰减等。温度T同样不宜过高,因为词汇表维度很大时,softmax已经比较平滑,过高温度会让分布过于平坦。生成式蒸馏比分类蒸馏更耗时,因为教师模型需要先在语料上产出logits或采样结果,计算和存储开销都更大。
知识蒸馏的价值不仅在于挤压参数,还在于它能保留大模型学到的结构化知识。一个学生模型通过软标签和中间层匹配,往往比从头用小模型训练获得更高的准确率和更好的泛化性。理解温度系数、KL散度补偿以及硬软损失平衡后,就可以在图像分类、文本分类、序列生成等不同任务上灵活应用。归根到底,蒸馏不是复制教师模型的答案,而是让学生学会教师判断答案的过程。