4. 关系蒸馏:样本间关系保持、基于距离与角度的蒸馏方法

前面我们聊了特征层怎么对齐,那是让单个样本的中间表示尽量像老师。但有个问题——样本之间的关系,我们还没管。

你想想看,一个学生模型如果只学单样本的特征,它可能记住了「猫长这样」,但不知道「猫和狗比,谁更像老虎」。这种样本间的相对位置,才是知识迁移里更高级的东西。

关系蒸馏(Relational Knowledge Distillation, RKD)就是干这个的。它的核心思想很简单:老师模型内部,样本 A 和样本 B 的距离、角度、拓扑关系,学生模型也要学过来。

一句话总结: 特征蒸馏教学生「这是什么」,关系蒸馏教学生「这个和那个是什么关系」。

4.1 为什么需要关系蒸馏?

我在一个视觉检索项目里踩过坑。当时用特征蒸馏训了一个轻量模型,单样本分类准确率还行,但一做检索(比如找相似图),结果就乱套了。学生模型把两张不同品种的猫图判成不相似,把猫和狗判成相似——因为它没学到样本间的相对距离。

关系蒸馏的价值就在这里:

  • 保持语义结构:老师模型在特征空间里,同类样本聚在一起,不同类分开。学生也要保持这个结构。
  • 提升泛化能力:学关系比学单点特征更鲁棒。即使某个样本特征有偏差,关系结构还能兜底。
  • 适合度量学习任务:人脸识别、行人重识别、图像检索,这些任务本质就是比关系。

我的经验: 如果你的下游任务是分类,特征蒸馏就够了。但如果是检索、聚类、对比学习,一定要加上关系蒸馏。我后来在那个检索项目里加了距离蒸馏,召回率直接涨了 4 个点。

4.2 关系蒸馏的核心框架

关系蒸馏的通用框架分两步:

  1. 定义关系函数:从特征空间中提取样本对(或样本三元组)的关系度量,比如距离、角度、互信息。
  2. 最小化关系差异:让学生模型的关系输出,尽量接近老师模型的关系输出。

用公式表达就是:

L_RKD = Σ_{(i,j) ∈ P} D( ψ_T(f_T(x_i), f_T(x_j)), ψ_S(f_S(x_i), f_S(x_j)) )

其中 ψ 是关系函数,D 是差异度量(比如 L2 损失),P 是样本对集合。

下面这张图展示了关系蒸馏的整体流程:

关系蒸馏核心流程 教师模型 特征提取 f_T 关系函数 ψ_T 学生模型 特征提取 f_S 关系函数 ψ_S 输入样本对 (x_i, x_j) 关系差异损失 L_RKD D( ψ_T, ψ_S ) 关系函数 ψ 可以是距离、角度、互信息等;差异度量 D 常用 L2 或 Huber 损失

4.3 基于距离的关系蒸馏

这是最直观的方法。我们计算样本对在特征空间中的欧氏距离或余弦距离,然后让学生模型的距离分布匹配老师模型。

4.3.1 距离蒸馏损失

给定一个 mini-batch 里的样本对 (i, j),定义距离关系:

d_T(i,j) = || f_T(x_i) - f_T(x_j) ||₂
d_S(i,j) = || f_S(x_i) - f_S(x_j) ||₂

L_dist = (1 / |P|) * Σ_{(i,j) ∈ P} (d_T(i,j) - d_S(i,j))²

这里 P 是所有样本对的集合。如果 batch size 是 N,那就有 N×(N-1)/2 个对。

注意: 距离蒸馏对特征尺度敏感。如果老师模型的特征范数很大,学生模型可能学不过来。我建议先对特征做 L2 归一化,再算距离。这样距离范围固定在 [0, 2] 之间,训练更稳定。

4.3.2 距离蒸馏的变体

方法 关系定义 特点
欧氏距离蒸馏 L2 距离 简单直接,对尺度敏感
余弦距离蒸馏 1 - cos(θ) 对尺度不敏感,关注方向
归一化距离蒸馏 先 L2 归一化再算 L2 综合两者优点,我常用这个
相对距离蒸馏 d(i,j) / mean(d) 关注相对远近,而非绝对值

我的习惯: 在大多数视觉任务里,我优先试归一化距离蒸馏。如果任务对相对顺序更敏感(比如排序学习),我会换成相对距离蒸馏。

4.4 基于角度的关系蒸馏

距离只刻画了样本对的「远近」,但没刻画「方向」。举个例子:三个样本 A、B、C,A 和 B 的距离等于 A 和 C 的距离,但 B 和 C 可能一个在 A 的左边,一个在 A 的右边。角度关系能捕捉这种差异。

4.4.1 角度蒸馏损失

角度蒸馏通常用三元组 (i, j, k),计算以 i 为顶点的夹角:

cos_θ_T(i,j,k) = ( (f_T(x_j) - f_T(x_i)) · (f_T(x_k) - f_T(x_i)) ) / ( ||f_T(x_j) - f_T(x_i)||₂ · ||f_T(x_k) - f_T(x_i)||₂ )

L_angle = (1 / |T|) * Σ_{(i,j,k) ∈ T} (cos_θ_T(i,j,k) - cos_θ_S(i,j,k))²

其中 T 是所有三元组的集合。如果 batch size 是 N,三元组数量是 N×(N-1)×(N-2)/2,计算量比较大。

关键点: 角度蒸馏比距离蒸馏更精细。它保留了样本间的「相对方位」,在高维特征空间中能更好地保持拓扑结构。我在人脸识别任务里试过,角度蒸馏比纯距离蒸馏的验证准确率高 1.2%。

4.4.2 距离 vs 角度:怎么选?

维度 距离蒸馏 角度蒸馏
计算复杂度 O(N²) O(N³)
信息粒度 粗粒度(远近) 细粒度(方位)
对特征尺度 敏感 不敏感(余弦自带归一化)
适用场景 分类、检索 细粒度检索、聚类
训练稳定性 高 中等(三元组选择影响大)

避坑指南: 我曾经在 batch size 只有 16 的时候硬上角度蒸馏,结果损失一直不降。因为三元组太少,角度关系不够丰富。建议 batch size 至少 64 以上再用角度蒸馏,或者用 memory bank 缓存历史特征来扩充三元组。

4.5 关系蒸馏的进阶技巧

4.5.1 多尺度关系融合

单一的关系函数可能不够。我习惯把距离和角度结合起来:

L_RKD = α * L_dist + β * L_angle

α 和 β 是平衡系数。我一般从 α=1.0, β=0.1 开始调,因为距离蒸馏更稳定,角度蒸馏作为辅助。

4.5.2 关系蒸馏 + 特征蒸馏联合训练

别把关系蒸馏和特征蒸馏对立起来。它们互补:

L_total = L_CE + λ1 * L_feature + λ2 * L_RKD

L_CE 是分类损失,L_feature 是特征蒸馏损失(比如前面章节讲的),L_RKD 是关系蒸馏损失。

我的经验: 联合训练时,λ2 不要设太大,0.1 到 0.5 之间比较安全。太大了学生模型会过度关注关系匹配,反而忽略了分类任务本身。

4.5.3 关系蒸馏的采样策略

全量样本对计算量太大。我常用两种采样策略:

  • 随机采样:每个 batch 随机选一部分对,简单但可能漏掉关键关系。
  • 困难对采样:选那些老师模型和学生模型关系差异最大的对,针对性优化。收敛更快,但需要额外计算差异。

我个人更倾向困难对采样。虽然每步计算多一点,但总训练步数能减少 30% 左右。

4.6 实战:关系蒸馏的 PyTorch 实现

下面给一个距离蒸馏的参考实现。代码不长,核心就几十行:

import torch
import torch.nn as nn
import torch.nn.functional as F

class DistanceDistillationLoss(nn.Module):
    def __init__(self, normalize=True, margin=None):
        super().__init__()
        self.normalize = normalize
        self.margin = margin  # 可选:只蒸馏距离大于阈值的对

    def forward(self, feat_s, feat_t):
        # feat_s, feat_t: [batch_size, dim]
        if self.normalize:
            feat_s = F.normalize(feat_s, p=2, dim=1)
            feat_t = F.normalize(feat_t, p=2, dim=1)

        # 计算距离矩阵
        dist_s = torch.cdist(feat_s, feat_s, p=2)  # [N, N]
        dist_t = torch.cdist(feat_t, feat_t, p=2)

        # 取上三角(排除对角线)
        n = dist_s.size(0)
        mask = torch.triu(torch.ones(n, n, device=feat_s.device), diagonal=1).bool()
        d_s = dist_s[mask]
        d_t = dist_t[mask]

        # 可选:只蒸馏距离大于 margin 的对
        if self.margin is not None:
            keep = d_t > self.margin
            d_s = d_s[keep]
            d_t = d_t[keep]

        # MSE 损失
        loss = F.mse_loss(d_s, d_t)
        return loss

使用建议: 这个 loss 可以直接加到你的训练循环里。我一般把它和分类 loss、特征蒸馏 loss 一起用。注意 feat_s 和 feat_t 要来自同一个 batch 的同一个样本。

4.7 关系蒸馏的常见问题与调试

问题 可能原因 解决方案
关系损失不下降 特征尺度不匹配 加 L2 归一化,或调小学习率
分类准确率反而下降 关系损失权重太大 降低 λ2,或先单独训分类再联合
训练速度太慢 全量样本对计算 用随机采样或困难对采样
角度蒸馏不收敛 batch size 太小 增大 batch size 或用 memory bank
检索任务效果差 只用了距离蒸馏 加上角度蒸馏,或换相对距离

最后说一句: 关系蒸馏不是银弹。如果你的老师模型本身就很差(比如分类准确率不到 80%),那它学到的关系结构也是错的,蒸馏过去只会带偏学生。先确保老师模型够强,再谈关系蒸馏。