4. 特征层蒸馏:中间层特征对齐、Attention Transfer、FitNets与NST方法对比

前面我们聊了logits层面的蒸馏,说白了就是让学生的软输出模仿老师的软输出。但有个问题——你想想看,如果老师模型很深,学生模型很浅,光靠最后那层logits,学生很难学到老师中间那些抽象特征。这时候,特征层蒸馏就派上用场了。

我个人习惯把特征层蒸馏分成四类:FitNets、Attention Transfer、NST(Neural Selective Transfer),以及最朴素的中间层特征对齐。它们的目标都一样——让学生模型的中间层特征图,尽量接近老师模型的中间层特征图。但具体怎么对齐、对齐哪一层、用什么损失函数,差别还挺大的。

4.1 为什么需要特征层蒸馏?

先看一个我踩过的坑。几年前我做一个小型语音识别模型的蒸馏,直接用KD(知识蒸馏)训练,学生模型精度死活上不去。后来我把老师模型中间层的特征图可视化出来,发现学生模型根本学不到那些高频细节的激活模式。说白了,logits层面的信息太“浓缩”了,学生消化不了。

特征层蒸馏的核心思路很简单:

  • 选层:从老师模型中挑出若干中间层
  • 对齐:让学生模型对应层的特征图与老师接近
  • 损失:用L2、L1或更复杂的度量计算差异

但这里有个关键问题——老师和学生的特征图尺寸往往不一样。怎么办?加一个适配层(adaptation layer),把学生的特征图映射到和老师一样的维度。

核心公式:特征蒸馏损失 = Σ || f_t(x) - g(f_s(x)) ||²

其中 f_t 是老师特征,f_s 是学生特征,g 是适配层(通常是1x1卷积或全连接层)

4.2 FitNets:最早的特征蒸馏方法

FitNets是2015年提出的,算是特征层蒸馏的开山之作。它的做法很直接——选老师模型的某几个中间层,让学生模型对应层的特征图去拟合。

我记得第一次实现FitNets时,遇到一个坑:适配层的初始化。如果适配层初始化不好,训练初期梯度就爆炸了。后来我改用Xavier初始化,才稳定下来。

FitNets的关键设计:

  • 使用L2损失作为特征对齐的度量
  • 引入适配层(adaptation layer)解决维度不匹配
  • 通常只对齐1-2个中间层,避免训练不稳定
  • 训练分两阶段:先特征蒸馏预训练,再logits蒸馏微调

我的经验:FitNets对适配层的设计很敏感。如果适配层太深(比如3层以上),学生模型容易过拟合到适配层上,反而学不到老师的特征。我一般只用1层1x1卷积或1层全连接。

4.3 Attention Transfer:让注意力图对齐

Attention Transfer(AT)是2017年提出的,思路比FitNets更巧妙——不直接对齐特征图,而是对齐特征图的注意力图。说白了,就是让学生的“注意力分布”和老师一样。

注意力图怎么算?很简单:对特征图在通道维度上做sum或mean,得到一个二维的注意力热力图。然后让学生的热力图和老师的热力图尽量接近。

# 注意力图计算示例
def attention_map(feature_map, method='sum'):
    # feature_map shape: [B, C, H, W]
    if method == 'sum':
        # 通道维度求和
        attn = torch.sum(feature_map ** 2, dim=1)  # [B, H, W]
    elif method == 'mean':
        # 通道维度求平均
        attn = torch.mean(feature_map ** 2, dim=1)  # [B, H, W]
    # 归一化
    attn = attn / attn.view(attn.size(0), -1).sum(dim=1).view(-1, 1, 1)
    return attn

# AT损失
def at_loss(student_feat, teacher_feat):
    student_attn = attention_map(student_feat)
    teacher_attn = attention_map(teacher_feat)
    return F.mse_loss(student_attn, teacher_attn)

AT相比FitNets的优势:

  • 注意力图是二维的,维度远小于特征图,训练更稳定
  • 不需要适配层(因为注意力图尺寸天然一致)
  • 对特征图的噪声更鲁棒

注意:AT虽然稳定,但信息量也少了。因为注意力图丢失了通道维度的信息。我做过实验,在图像分类任务上,AT单独使用效果不如FitNets+KD的组合。建议AT作为辅助损失,不要单独用。

4.4 NST:选择性传递关键神经元

NST(Neural Selective Transfer)是2019年提出的,思路更激进——不是所有神经元都值得传递,只传递那些“激活值高”的神经元。

为什么这么做?因为老师模型的特征图里,大部分神经元激活值很低(接近0),这些神经元对最终决策贡献很小。如果强制学生去拟合这些低激活值,反而会引入噪声。

NST的做法:

  1. 计算老师特征图每个位置的激活值
  2. 选出激活值最高的k%位置(比如top 10%)
  3. 只在这些位置上计算蒸馏损失
  4. 其他位置忽略(或者给很小的权重)
# NST选择性掩码计算
def nst_mask(teacher_feat, keep_ratio=0.1):
    # teacher_feat: [B, C, H, W]
    # 计算每个位置的激活强度
    act_strength = torch.sum(teacher_feat ** 2, dim=1)  # [B, H, W]
    # 展平
    B, H, W = act_strength.shape
    flat = act_strength.view(B, -1)  # [B, H*W]
    # 计算阈值
    k = int(H * W * keep_ratio)
    threshold = torch.topk(flat, k, dim=1)[0][:, -1:]  # [B, 1]
    # 生成掩码
    mask = (flat > threshold).float()  # [B, H*W]
    mask = mask.view(B, 1, H, W)
    return mask

# NST损失
def nst_loss(student_feat, teacher_feat, mask):
    diff = (student_feat - teacher_feat) ** 2
    masked_diff = diff * mask
    return masked_diff.sum() / mask.sum()

我的经验:keep_ratio这个超参数很关键。我试过0.05到0.5之间的值,发现0.1-0.2效果最好。如果keep_ratio太小(比如0.01),学生学不到足够信息;如果太大(比如0.5),又退化成普通特征对齐。

4.5 四种方法对比

下面这个表格是我在实际项目中总结的对比,供你参考:

方法 对齐目标 是否需要适配层 训练稳定性 信息量 推荐场景
FitNets 特征图(L2) 是 中等 高 学生模型较深时
Attention Transfer 注意力图 否 高 中 轻量级模型、快速实验
NST 选择性特征图 是 中等 中高 特征图稀疏、噪声大时
朴素特征对齐 特征图(L1/L2) 是 低 高 老师和学生结构相似时

4.6 特征层蒸馏的整体流程

下面这张图展示了特征层蒸馏的完整流程,从老师模型提取中间特征,经过适配层映射,再与学生特征计算损失:

特征层蒸馏流程 特征层蒸馏流程 老师模型 学生模型 中间特征 f_t 中间特征 f_s 适配层 g(·) 蒸馏损失 L_feat L_feat = Σ || f_t(x) - g(f_s(x)) ||² | 适配层用于对齐维度 可选方法:FitNets(L2) / AT(注意力图) / NST(选择性掩码)

4.7 实践中的选择建议

说了这么多,到底该用哪种?我根据实际项目经验,给你几个建议:

  • 快速验证阶段:先用Attention Transfer,因为它不需要适配层,训练稳定,代码量最少
  • 追求精度:用FitNets + KD的组合,但要注意适配层的设计和训练稳定性
  • 特征图稀疏场景(比如检测模型、分割模型):用NST,它能过滤掉大量无效区域
  • 老师和学生结构相似:直接用朴素特征对齐,省掉适配层的麻烦

我曾经踩过的坑:有一次我同时用了FitNets、AT和KD三个损失,结果训练时损失函数互相打架,模型精度反而下降了。后来我用了梯度裁剪和损失权重调整(给特征蒸馏损失0.1的权重,KD损失0.9),才稳定下来。记住:不是损失越多越好,关键是平衡。

4.8 小结

特征层蒸馏的核心,就是让学生模型在中间层就学会老师模型的“思维方式”。FitNets直接对齐特征图,AT对齐注意力分布,NST选择性对齐关键区域——各有各的适用场景。

我个人建议:先从AT入手,因为它最简单、最稳定。如果精度不够,再换成FitNets或NST。记住,特征蒸馏通常要和logits蒸馏配合使用,单独用效果往往不够好。