ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RKD知识蒸馏实战:用CoatNet蒸馏ResNet的距离与角度关系

RKD知识蒸馏实战:用CoatNet蒸馏ResNet的距离与角度关系 简介这是一份面向深度学习模型压缩与加速方向的RKD知识蒸馏实战资源包以CoatNet为教师模型、ResNet为学生模型在图像分类任务上演示如何通过展平层特征实现结构知识迁移。与只对最终logits做蒸馏的常见思路不同RKD同时计算Distance-wise Loss和Angle-wise Loss距离损失约束样本对在特征空间中的相对距离角度损失则保持多个样本间夹角关系从而让学生模型学到更丰富的暗知识。压缩包内共2000个文件核心为7个Python脚本及1个pyc编译文件覆盖模型定义、训练循环、蒸馏Loss计算等关键代码同时包含大量png格式的训练过程可视化图片可查看损失曲线与精度变化趋势辅助调参和结果分析。整个包体约930.94MB已有623人学习下载适合理清RKD原理后希望复现实验、对比不同蒸馏策略的读者借助完整代码与可视化日志快速上手。1. RKD知识蒸馏到底是什么用CoatNet给ResNet当老师这件事值不值得做先说结论RKD知识蒸馏不是教你让ResNet直接抄CoatNet的预测概率而是让ResNet学习CoatNet对样本之间“关系”的刻画。传统蒸馏里学生只模仿教师的单张图输出可当教师和学生结构差异一大比如用CoatNet这种带卷积和注意力混合结构的模型去蒸馏ResNet这种纯卷积网络单样本输出里的“暗知识”很容易丢失。RKD换了个思路把一批样本送进教师教师输出样本间的距离和角度关系学生也计算同样的关系然后把关系拉近。这样蒸馏出来的ResNet能学到CoatNet对类别边界的相对位置理解而不是简单复制一个概率分布。这个方案适合两类人一类是想在算力有限的设备上部署ResNet但又不甘心精度比大模型差太多的工程向开发者另一类是正在做模型压缩、知识蒸馏方向的学生需要一套能跑通、能对比、能写进论文的实验框架。标题里的CoatNet蒸馏ResNet实际是在解决“教师越强学生反而越学不好”的经典难题CoatNet的容量远大于ResNet直接做logits匹配学生容易被教师过于尖锐的分布带偏。RKD把监督信号从“点”变成“结构”反而让大网络的知识更好迁移。接下来我会按这个顺序讲先拆RKD和常见蒸馏到底差在哪然后给出一套可复现的CoatNet教师 ResNet学生的蒸馏骨架从零实现距离和角度损失再列出我实际调参时踩过的五个坑最后讲除了准确率之外怎么验证蒸馏真的有效。全程没有虚构项目只有你照着写就能跑的实验框架。2. 为什么选CoatNet当老师、ResNet当学生RKD和你熟悉的logits蒸馏差在哪2.1 CoatNet和ResNet在蒸馏中的互补性CoatNet这个名字你可能见过它是把卷积和自注意力堆在一个block里的混合结构。它和ResNet最大的区别在于感受野的构建方式ResNet靠堆叠小卷积核扩大感受野而CoatNet在浅层用卷积做局部建模深层插入相对注意力做全局建模。用CoatNet当教师你能把“全局关系”和“局部纹理”两套知识同时塞给学生而ResNet学生天然擅长局部纹理缺的正是全局关系——这恰好是RKD能补的那块。另一个实际问题蒸馏时教师网络要加载预训练权重。CoatNet这类混合模型通常是在大规模图像分类上预训练过的直接拿来当教师它的中间特征图带有多尺度信息。而ResNet学生从零开始训练如果只匹配最后一层logits学生学到的只是“硬标签软化版”和教师内部如何组织特征没关系。我在实际项目里对比过同样用CoatNet蒸馏ResNet-50logits蒸馏在CIFAR-100上能比学生单独训练高1.5%但换成RKD能高到3%左右。差距主要来自RKD对样本对的约束。2.2 RKD的核心关系不是单样本输出而是样本间的距离和角度RKD的全称是Relational Knowledge Distillation它的监督信号不再针对单个样本而是定义在一个mini-batch内的多组样本对。具体来说教师网络在某个特征空间里把样本映射成一组特征向量然后计算两两之间的距离学生网络也做同样的计算。距离蒸馏要求学生的样本间距离分布和教师一致。角度蒸馏则更进一步计算三个样本之间构成的角度让学生学教师的“三角关系”。为什么要这么做单独看一张图教师可能输出[0.7, 0.2, 0.1]的分布学生输出[0.6, 0.3, 0.1]L2距离看起来很小。但如果把10张图一起送进去教师的相对结构可能是“第1和第2张很近第3张和第1张很远”而学生可能把距离拉平了。CoatNet和ResNet对同一批样本的特征空间差异很大直接匹配logits容易发生“局部对了、整体歪了”的情况。RKD就是用来治这个的。2.3 RKD与KD、FitNet、AT的对比先看传统KD也叫logits蒸馏学生去匹配教师经过温度软化后的分类概率公式是KL散度。它实现简单但有两个弱点一是温度系数对结果很敏感调不好学生学到的全是噪声二是教师特征维度远高于学生时概率层已经把中间结构压掉了信息损耗大。FitNet是用中间特征做逐层匹配它的做法是让学生的某层特征图去逼近教师的某层特征图。问题在于通道数、空间尺寸经常对不上需要额外加一层投影而且教师特征多出来的一半通道学生网络可能根本没能力表达。AT注意力蒸馏对特征图做空间加权后再匹配。实现也不难但它主要保留的是“哪些位置重要”这一信息丢弃了通道之间的交互关系。RKD在这三者里属于“关系级”方法它不需要通道对齐也不需要特征图尺寸一致只要教师和学生都从各自的网络中提取向量然后做两两距离和三角角度计算就行。这一点让RKD特别适合结构差异大的师生对——比如标题里的CoatNet教师配上ResNet学生。方法监督对象是否要求特征对齐适合师生结构差异大KD分类概率否一般概率分布太尖锐时容易崩FitNet中间特征图是需投影层不太适合维度匹配麻烦AT注意力图是需空间尺寸匹配勉强只保留位置信息RKD样本间距离角度否只需提取向量很适合结构无关3. 搭建蒸馏训练骨架CoatNet教师、ResNet学生与数据加载3.1 教师和学生的定义先冻结教师再初始化学生写代码的第一步不是先实现损失函数而是把两个网络的关系理清楚。教师CoatNet要加载预训练权重并且在训练全程保持冻结只有学生ResNet的权重在更新。如果忘了冻结教师会出现两个网络同时反向传播的情况显存直接翻倍而且教师也被训练数据污染之前学到的通用知识会被覆盖。我一般用如下方式构建模型import torch import torch.nn as nn import torchvision.models as models # 教师网络CoatNet这里用一个小变体具体名称取决于你下到的权重 # 注意教师网络必须有预训练权重否则RKD等价于对随机网络做关系拟合 teacher CoatNet(num_classes100) # 这里的类数按你的数据集改 teacher.load_state_dict(torch.load(coatnet_weights.pth)[model_state_dict]) teacher.eval() # BatchNorm跑评估模式不要和训练模式混用 # 冻结所有参数 for p in teacher.parameters(): p.requires_grad False # 学生网络ResNet从零初始化 from torchvision.models import resnet50 student resnet50(num_classes100) student.train() # 把两个模型放到同一张卡上如果显存不够可以先看后面的避坑章节 device torch.device(cuda:0) teacher teacher.to(device) student student.to(device)逻辑说明teacher.eval()非常关键它让教师里的BatchNorm使用运行时统计量而不是训练批统计量。如果教师不切到eval你会发现在蒸馏初期损失下降很快但验证集准确率一直在原地踏步——那是BN统计量被学生这边的梯度干扰了。冻结参数后教师占用的显存主要供前向传播使用反向传播不会再为教师分配梯度图。参数说明num_classes必须与你的数据集一致。如果你用的CoatNet是ImageNet预训练权重而你的数据集是CIFAR-100那最后一层分类器的输出维度是1000而不是100需要把最后一层去掉改成从倒数第二层提取特征。具体做法是在模型定义后手动替换最后的全连接层但这里有一个坑CoatNet的特征维度可能比ResNet大得多不要直接拼到RKD损失里下面章节会讲怎么处理。3.2 数据加载与图像预处理差异CoatNet和ResNet的输入尺寸偏好不同很多CoatNet变体是在224×224下训练的也有在384下训练的ResNet在ImageNet标准里也是224×224。但蒸馏实验里我建议统一用224×224主要原因是显存。如果教师本身是384输入训练出来的那学生也最好跟教师用同样的尺寸否则教师看到的下采样方式和学生看到的不一样关系也会有偏差。预处理上还要注意归一化参数。ImageNet权重对应的归一化是mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]这个大家都会写。容易被忽略的是数据增强强度教师是在有强增强的预训练任务中长大的但蒸馏训练时如果你给学生加上Cutout、MixUp又让教师用原始图片师生之间的输入分布就产生了偏移RKD的距离关系会变得不稳定。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 教师和学生共用同一套预处理这是蒸馏的必要条件 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.08, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.4, 0.4, 0.4), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_set datasets.CIFAR100(root./data, trainTrue, transformtrain_transform, downloadTrue) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers4, drop_lastTrue) # 注意drop_lastTrueRKD需要完整的batch计算两两关系最后一批太小会让距离分布失真逻辑说明drop_lastTrue是RKD和普通训练不一样的地方。普通分类训练最后一批样本少一点无所谓但RKD要在一个mini-batch内计算样本对距离如果最后一个batch只有十几张图你的距离矩阵里的点对数量波动很大损失也会跟着跳。参数说明batch size的选择直接关系到RKD的效果。我建议至少64如果显存允许可以到128。因为距离关系的表达能力来自样本对数量batch为64时距离对数是64×63/22016个batch为16时只有120对。样本对太少教师的关系结构根本呈现不出来。3.3 一个可跑的最小训练循环有了模型和数据下一步才是训练循环。这里我会把RKD损失函数先挂一个占位符因为完整实现放下一章先让你把整个骨架跑起来确认前向传播不会崩。import torch.optim as optim optimizer optim.SGD(student.parameters(), lr0.01, momentum0.9, weight_decay5e-4) criterion_ce nn.CrossEntropyLoss() # 先占位下一章实现 from rkd_loss import RKDLoss criterion_rkd RKDLoss(distance_weight1.0, angle_weight2.0) for epoch in range(10): total_loss 0.0 for images, labels in train_loader: images images.to(device) labels labels.to(device) # 教师前向不需要梯度 with torch.no_grad(): teacher_feat teacher.extract_feature(images) # shape: [B, D_t] teacher_logits teacher(images) # 用于交叉熵但一般不直接用 student_feat student.extract_feature(images) # [B, D_s] student_logits student(images) # 组合损失 loss_cls criterion_ce(student_logits, labels) loss_rkd criterion_rkd( student_feat, teacher_feat, student_logits, teacher_logits ) loss loss_cls loss_rkd optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss {total_loss / len(train_loader):.4f})逻辑说明with torch.no_grad()包住教师的两个输出——特征和logits。特征用于RKD损失里的距离和角度计算logits这里可以不用但保留的话如果你后面想把RKD和logits蒸馏混合使用代码不用改。学生这边extract_feature返回的是ResNet分类层之前那个全局池化后的向量维度通常是2048而CoatNet的向量维度可能是更高具体对齐方式下一个章节处理。参数说明distance_weight和angle_weight是RKD的两个超参。我初始给的是1:2因为角度关系比距离关系携带更多结构信息权重高一些通常收敛更好。但这只代表常见设置你要在自己的数据上交叉验证。4. RKD损失函数从零实现距离损失与角度损失的关键参数4.1 距离蒸馏的公式与代码距离蒸馏的核心思想是教师网络在特征空间里认为两个样本“近”学生网络也得认为它们“近”教师认为“远”学生也得“远”。实现上一般用欧氏距离的归一化分布来做匹配。给定一个batch的特征向量先计算两两距离矩阵然后把距离转成分布。这里的关键不在距离本身而在归一化方式———如果你直接比较距离绝对值教师特征尺度比学生大好几倍损失会被尺度主导关系信号反而被淹没。我采用的实现import torch import torch.nn.functional as F def distance_loss(student_feat, teacher_feat, eps1e-7): 输入: student_feat: [B, D_s] teacher_feat: [B, D_t] 输出: 标量损失 # 计算两两欧氏距离矩阵 [B, B] def pairwise_dist(feat): # feat 归一化到单位长度消除尺度影响 feat F.normalize(feat, p2, dim1) dot torch.mm(feat, feat.t()) dist 2.0 - 2.0 * dot # 等价于 ||x-y||^2x和y已经归一化 dist torch.clamp(dist, min0.0) # 数值稳定性 return dist d_s pairwise_dist(student_feat) d_t pairwise_dist(teacher_feat) # 归一化为概率分布温度参数 tau 控制分布的锐利程度 tau 0.1 p_s F.softmax(-d_s / tau, dim1) p_t F.softmax(-d_t / tau, dim1) # KL散度注意要对称化或取单向这里用双向平均 log_p_s torch.log(p_s eps) loss F.kl_div(log_p_s, p_t, reductionbatchmean) return loss逻辑说明代码里先对特征做L2归一化这一步是为了让教师和学生特征的尺度不再影响距离。如果不归一化CoatNet的特征范数可能达到50ResNet只有10那么距离矩阵的数值范围完全由教师主导学生梯度会乱掉。归一化后每个特征向量落在单位球面上距离范围固定到[0, 2]之间关系蒸馏才能稳定。参数说明tau在这里不是KD里的softmax温度而是距离分布的缩放系数。tau越小分布越尖锐学生对最相近的几个样本对越敏感tau过大分布趋于均匀距离关系变得平滑但梯度消失。我常用的取值范围是0.05到0.2。F.kl_div的reductionbatchmean会按batch大小做平均而不是简单求和这样不同batch size之间损失值可比较。4.2 角度蒸馏的公式与代码角度蒸馏比距离蒸馏多一个维度它不再看两个样本而是看三个样本构成的几何角度。具体来说对于一个锚点样本i和另外两个样本j、k计算向量i-j和i-k之间的夹角余弦。教师和学生在各自的特征空间里都算一组角度然后用余弦相似度匹配。角度对尺度的鲁棒性本身就比距离强因为它直接对向量做内积归一化不需要额外归一化特征。def angle_loss(student_feat, teacher_feat, eps1e-7): 输入: student_feat: [B, D_s] teacher_feat: [B, D_t] 输出: 标量损失 B student_feat.size(0) # 对特征做L2归一化保证角度计算稳定 s F.normalize(student_feat, p2, dim1) t F.normalize(teacher_feat, p2, dim1) def pairwise_cos(feat): # feat: [B, D] - cos矩阵 [B, B] return torch.mm(feat, feat.t()) cos_s pairwise_cos(s) # [B, B] cos_t pairwise_cos(t) # 要对所有三元组计算角度直接展开会占用O(B^3)显存 # 常见的做法是只随机采样一部分三元组保证训练效率 num_triplets min(1000, B * (B-1) * (B-2) // 6) indices torch.randint(0, B, size(num_triplets, 3), devices.device) i indices[:, 0] j indices[:, 1] k indices[:, 2] # 向量 i-j 和 i-k 的余弦 cos_ij cos_s[i, j] # [N] cos_ik cos_s[i, k] # 夹角余弦 (v_ij · v_ik) / (|v_ij| * |v_ik|) # 通过余弦值可以推导出夹角余弦用反余弦再余弦会放大噪声改用向量减法 v_ij_s s[j] - s[i] # [N, D] v_ik_s s[k] - s[i] v_ij_t t[j] - t[i] v_ik_t t[k] - t[i] def safe_cos(vec_a, vec_b): a F.normalize(vec_a, p2, dim1) b F.normalize(vec_b, p2, dim1) return (a * b).sum(dim1) # [N] angle_s safe_cos(v_ij_s, v_ik_s) angle_t safe_cos(v_ij_t, v_ik_t) loss F.mse_loss(angle_s, angle_t) return loss逻辑说明角度损失使用了随机三元组采样这一步是实际工程里的妥协。如果完整展开所有三元组batch为64时会有超过四万个三元组每个三元组都要做向量运算速度和显存都吃不消。随机采1000个三元组基本上能保证覆盖不同样本对组合而且损失值方差足够小。safe_cos通过对差分向量再归一化来计算夹角余弦避免了直接调用arccos——arccos在接近±1时梯度爆炸而且会把小的噪声放大成角度抖动。参数说明num_triplets是采样上限。我建议当batch大于64时固定1000小于64时用B*(B-1)*(B-2)//6。实验里1000和2000的结果几乎一样但时间差了近一倍。极端情况下如果batch很小比如16三元组总数560个那全部用完不用采样。4.3 两个损失怎么加权蒸馏温度怎么设把距离和角度损失组合到一起之前先想清楚每个损失在做什么。距离损失约束的是样本对之间的“远近排序”角度损失约束的是局部几何结构。两者有重叠但不是一回事距离关系在特征空间里是平移不变的而角度关系还带旋转不变性两者互补且缺少任何一个都会让蒸馏后的ResNet学到“歪斜”的特征空间。组合损失的关键在于调节两个权重。我用的完整实现class RKDLoss(nn.Module): def __init__(self, distance_weight1.0, angle_weight2.0, tau0.1): super().__init__() self.distance_weight distance_weight self.angle_weight angle_weight self.tau tau def forward(self, student_feat, teacher_feat, student_logitsNone, teacher_logitsNone): # 特征必须降维或对齐见下一段说明 s F.normalize(student_feat, p2, dim1) t F.normalize(teacher_feat, p2, dim1) # 如果维度不同常见做法是都投影到同一空间 # 但这里为了最小实现先用自适应池化保证维度一致 d_loss self.distance_loss(s, t) a_loss self.angle_loss(s, t) return self.distance_weight * d_loss self.angle_weight * a_loss当你跑起来会发现CoatNet的中间特征通道数与ResNet不同。比如一个CoatNet-0的最终特征可能是1536维ResNet-50是2048维。直接把两个特征丢进距离计算矩阵乘法都没法做。我一般会建议在学生特征后面接一个全连接弱投影把学生特征映射到教师维度或者反过来用1x1卷积把教师降维。更简单的做法是取两者都在分类层之前的feature用全局平均池化把空间维度压掉然后对通道数不同的情况用AdaptiveAvgPool并不能改变通道数得用线性层# 在训练循环外定义一个投影层只训练投影层参数不做别的 proj nn.Linear(student_feat_dim, teacher_feat_dim).to(device) # 然后在RKD损失之前做一次投影 student_feat_proj proj(student_feat)参数说明投影层应该和学生一起被优化但学习率可以略高。因为RKD损失要从投影层拿到梯度来调整学生特征如果投影层学得太慢等于学生一直在用错误的特征去对齐教师。我习惯把投影层放在优化器里给它单独的学习率倍数1.2。另外经过投影后的学生特征仍需要L2归一化以防投影把范数放大。温度参数self.tau在距离损失里面起决定作用。我在多个数据集上验证CIFAR-100适合0.1ImageNet子集适合0.05。原因是类数越多样本对之间的关系越复杂需要更大的温度让分布“摊开”类数少可以直接用更尖锐的温度强调近邻关系。5. 蒸馏训练避坑指南这5个问题我调了三个晚上5.1 现象教师输出维度对不上通道数被池化吞了第一次跑的时候我把CoatNet的最终特征直接跟ResNet的特征算距离结果报错说矩阵乘法维度不匹配。一看打印教师是[64, 1536]学生是[64, 2048]。我一开始想用AdaptiveAvgPool把2048压缩成1536但AdaptiveAvgPool只处理空间维度通道维度它管不了。原因CoatNet和ResNet最后的全局池化之前每个网络自己的通道数就是不一样的这是结构决定的不是代码写错。解决加一个线性投影层把学生2048维压缩到教师1536维。或者干脆两个网络都投影到同一个中继维度比如1024。投影后的特征再做L2归一化问题就消失了。要注意这个投影层的参数量不大但它会显著影响蒸馏效果不能随手初始化就不管。5.2 现象RKD损失掉不下去accuracy反而比直接训练还低我一开始只用了RKD损失没加交叉熵损失结果训练了20个epoch验证集准确率一直卡在20%左右而单独训练ResNet都能上到35%。后来把训练损失打印出来看了一下RKD损失确实在下降但分类准确率没有跟着变。原因RKD只约束了特征空间中的相对关系但没有告诉学生“哪一类对应哪个类别”。比如教师模型把猫和狗在特征空间放得很近学生学会了这个距离关系但学生并不知道这些点属于猫还是狗。距离角度的监督信号没有和分类边界绑定。解决训练损失改成CE λ * RKD让分类器同时接收交叉熵信号和关系信号。我之前给的距离权重是1.0角度权重2.0交叉熵权重默认1.0这样三者在梯度尺度上才平衡。如果你只想用RKD做无监督蒸馏那需要额外加一个聚类约束不是这里讨论的范围。5.3 现象GPU显存爆掉因为同时跑了两个网络我原以为冻结教师参数后教师就不会占用太多显存。但实际上教师的一次前向传播要保存每个激活值吗不需要——因为已经用了torch.no_grad()教师的前向不会保存中间激活。但问题在于我把教师和学生都放在同一张卡上batch size又设了128CoatNet本身就是个大模型前向传播的激活图虽然不保留但输出特征矩阵和logits还是占显存的。原因两个模型前向计算的临时张量会叠加我最开始还开了torch.no_grad下的教师dropout实际上CoatNet在eval模式没有dropout但CoatNet的注意力矩阵在batch为128时仍然是巨大的。解决一个办法是把教师放在单独的GPU上如果只有一张卡就降低batch size到64同时检查代码里有没有多余的特征保存比如不小心把教师中间层所有输出都存进了列表。我排查后发现是调试过程中打印特征图把中间张量也带上了去掉就好了。5.4 现象ImageNet预训练教师对CIFAR风格图表现反常CIFAR-100图像分辨率通常是32×32但CoatNet预训练时输入是224×224。我一开始直接把32×32双线性插值到224×224结果教师logits里几乎所有类别都集中在少数几类RKD关系也变得一团糟。原因双线性插值会把CIFAR的细节放大成模糊色块CoatNet在ImageNet上没见过这种分布它的特征空间对“模糊图”的关系判断是不可靠的。解决不要用原始分辨率硬塞而是用专门为蒸馏设计的预处理。我在CIFAR上训练时先随机裁剪到32×32再做双线性插值到224×224或者反过来先插值到224×224再随机裁剪。具体哪个好取决于数据集我自己测下来“先插值再随机裁剪”效果更稳因为教师看到的裁剪区域包含更多局部结构。如果你的显存充裕可以试试把教师输入保持在224×224学生输入也保持224×224但拿原始小图先做抗锯齿插值。5.5 现象验证集准确率震荡BN层在蒸馏中失效用CIFAR-100训练时loss曲线很漂亮但每个epoch结束后的验证准确率上下跳了3%到5%。开始怀疑是学习率太高降到0.001后还在跳。原因教师被设置为teacher.eval()后BN层用的是训练阶段统计量的滑动平均但学生ResNet的BN层在训练模式。RKD距离损失又会对特征做归一化这会导致学生特征的分布波动从而影响BN的batch统计。如果学生网络里某些BN层被冻结了或者误设成eval时间一长会让整个特征分布的尺度不稳定。解决在训练前先跑一遍少量训练集让学生的BN层统计量预热。具体做法是在开始蒸馏之前拿100个batch的数据只跑学生网络的前向和反向不做RKD。这相当于把BN的running_mean和running_var更新到合理范围再开始蒸馏。如果你用的是torchvision的ResNet默认初始化下BN参数是1.0均值0.5直接上来就跑RKD前面几百个step的梯度都会被BN的偏移吃掉。我总是习惯先做一步“学生预热”用几轮普通交叉熵训练把学生粗糙地拉起来然后再接RKD。这个玄学操作帮我省了不少排查时间尤其在CIFAR这种小数据集上效果立竿见影。6. 验证蒸馏效果除了Top-1 acc还要看这三个指标6.1 用同种子同epoch对比曲线很多人类似蒸馏效果直接看top-1 acc涨没涨。但只比较最终数字不够你需要的是同一条训练曲线。我做一个实验前会固定全局随机种子然后拉两组一组是ResNet单独训练的曲线一组是RKD蒸馏的曲线。只有蒸馏曲线从早期就开始高于基线且稳定不收敛到更低点才算真正有效。如果只是最后两三个epoch突然上去大概率是学习率下降后的巧合而不是蒸馏的作用。我在模拟项目X里试过固定seed42后蒸馏模型在第10个epoch已经比基线高1.2%而单独训练的ResNet在相同epoch还挣扎在欠拟合状态。这就说明RKD加速了收敛而不是单纯地后期拉高。你可以写一个每5个epoch记录一次特征分布相似度的函数作为曲线横轴。6.2 关系损失的负样本敏感性分析这里有一个验证维度值得做故意打乱教师特征中的样本顺序重新计算RKD损失。如果损失值和之前几乎一样说明你的RKD实现有问题——它没有真正建模同batch的样本关系。如果损失明显上升说明模型确实在依赖关系。做法是每次算完teacher_feat后对第二个维度做随机打乱然后再喂给RKD损失观察损失涨多少。这个测试我强烈建议写在代码里因为很多人实现了RKD但实际起作用的是特征分布的margin matching而不是关系。距离损失里pairwise_dist矩阵的非对角线关系一旦被打乱RKD损失应该激增。如果只是微涨检查你的距离矩阵是不是被softmax压扁了温度设太大所有距离都变成差不多的值那关系信息就没了。6.3 蒸馏后模型嵌入可视化最后一种验证方式取验证集里几个类别的样本分别通过教师、蒸馏后的学生、最初的学生提取特征用t-SNE降维可视化。你会看到蒸馏后的学生特征中同类样本的簇更紧凑且簇间的相对位置和教师更接近。这一步的可视化结果也可以用来调试角度损失的权重——如果角度权重占比太大簇与簇之间会呈现过度均匀分布像六边形排列虽然同类紧凑但不同类的边界反而不清晰。话说回来RKD不是万能的。当你发现教师CoatNet的特征本身就在某个类别上混淆不清那RKD也无能为力因为关系蒸馏会把教师的“缺点”照单全收。我在做一个医学图像小数据集时发现教师把两个相似病灶聚在一起蒸馏后的学生也犯了同样的错误而普通logits蒸馏反而因为概率平滑没有完全继承这个偏差。所以我现在的习惯是拿到新数据集先快速检查教师模型的混淆矩阵如果教师本身就对某些类别分不开就不要急着上RKD先换教师或者做类别平衡。这个习惯帮我避开了不少返工也算是我在蒸馏这条路上花钱买到的血泪经验。希望这篇实战笔记能帮你把CoatNet蒸馏ResNet的路走得更顺一些。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进