简介知识蒸馏IRG算法实战代码包面向希望掌握特征图蒸馏、用轻量模型替代重型网络的算法工程师与研究生提供基于ResNet50蒸馏ResNet18的完整工程实现可结合原理解析逐步复现训练与表征蒸馏流程。压缩包共约2000个文件以2400余张PNG过程图为主涵盖师生模型特征图、损失曲线、精度对比等可视化结果另有7个Python脚本、4个JSON结果文件、3个pyc及1个txt说明整体约930.95MB目录按代码、结果与说明分层便于快速定位核心模块和产出。已有720人学习下载适合作为课程实验、毕业设计或模型压缩预研的实战参考。从源码中可提取IRG算法模块、训练与推理脚本、ResNet50/ResNet18权重状态及蒸馏结果JSON为后续迁移到其他分类任务、设计特征对齐损失提供了直接可改的工程底座配合过程图还能直观检查蒸馏中间表示是否符合预期。1. 知识蒸馏 IRG 实战为什么非要用 ResNet50 去压榨 ResNet18知识蒸馏早就不只是“soft label 对 logit”那一套了。IRGInstance Relationship Graph实例关系图把蒸馏从“答案对齐”推进到“思考过程对齐”teacher 和 student 不光要输出接近特征空间里的实例关系也得接近。这份源码做的事很具体——用 ResNet50 当 teacher把 ResNet18 蒸馏到接近 teacher 的精度。包里三份 jsonresult_kd.json、result.json、result_student.json分别对应蒸馏训练、teacher 单独训练、student 裸训练 baseline配 6 张可视化图蒸馏效果一眼能看明白。适合两类人做模型压缩但发现 logit 蒸馏精度上不去的以及想复现 IRG 论文又不想从零搭实验的。2. IRG 核心原理与源码结构先把蒸馏的账算清楚2.1 IRG 和 logit 蒸馏的差别关系图到底在蒸馏什么Hinton 那篇经典 KD 的做法是把 teacher 的 logits 除以温度 T 变成软标签学生用 KL 散度去拟合。这套思路有个被反复吐槽的短板软标签只编码了类别概率层面的信息特征空间里样本之间的相对位置、类内紧凑程度、类间间距这些结构信息在 softmax 之后基本丢光了。你让学生学一个“答案”它并不知道 teacher 为什么觉得这两张图像、那两张图不像。IRG 的思路是绕开 logits直接在特征空间里做对齐而且不是一对一对齐是“关系对关系”对齐。具体拆成两个 loss。第一个是特征图对齐 loss。取 teacher 某一层的特征图和学生同语义深度的特征图做逐位置 L2 对齐让学生特征在数值分布上逼近 teacher。这个 loss 的作用是定锚点保证学生特征不会跑到 teacher 特征分布太远的地方。第二个是实例关系图 loss也就是 IRG 名字的出处。在一个 batch 内把每个样本当节点样本两两之间的余弦相似度当边构成一张全连接关系图teacher 和 student 各算一张然后让两张图尽量一致。关系图只约束“相对结构”teacher 认为相近的样本学生也认为相近其它自由度全留给学生自己发挥。为什么要“关系对关系”而不是只做特征图对齐因为学生容量小硬拟合 teacher 特征会出现两个问题一是特征完全复刻会让学生丢掉自己的归纳偏好在分布外数据上更脆二是 teacher 特征里有些维度本身就是冗余的学生把容量花在复刻冗余上得不偿失。关系图对齐相当于只传“骨架”不传“皮肉”这是 IRG 在同样容量下普遍比直接特征蒸馏多涨点的原因。提示IRG 里说的“关系”指的就是 batch 内两两样本的相似度矩阵。这个矩阵的维度是 batch_size × batch_size所以 batch 大小直接决定关系图的精度后面第 4 章会专门说这个坑。2.2 源码文件组织拿到的包里都有什么解压之后核心是实验产物而不是堆成山的脚本三份 json 结果文件、一份 class.json、6 张 png 图。我拿到手第一件事是先用 jq 把每份 json 的字段摸一遍确认记录粒度再谈复现。class.json 存的是类别 id 到类名的映射。这类蒸馏实验常用 CIFAR-100 或 ImageNet 子集class.json 的作用是在评估和画混淆矩阵时把预测 id 翻译回可读类名。result.json 是 teacher ResNet50 单独训练的完整过程记录result_student.json 是 student ResNet18 不做任何蒸馏硬训的记录result_kd.json 是加了 IRG 蒸馏之后的记录。三份记录字段一致才能做同尺度对比。# 先看 json 顶层结构确认有哪些字段 jq keys result_kd.json # 按 epoch 看最后一轮记录确认训练是否跑完 jq .[-1] result_kd.json6 张 png 图我归类成三类训练 loss 曲线、验证精度曲线、IRG 关系图热力图。关系图热力图一般把 teacher 关系矩阵和 student 关系矩阵并排画颜色越接近说明对齐越好。有的图还会把 cls loss、feature loss、graph loss 拆开画方便观察三个分量是否同步收敛。这些图就是判断蒸馏有没有生效的第一手证据第 5 章我会教你怎么读。3. 复现训练完整流程、命令与参数解读3.1 数据准备与 teacher 预训练检查蒸馏实验有个硬前提teacher 必须足够强。如果 ResNet50 自己在验证集上只有 60% 的精度那蒸馏出来的学生上限就被锁死了——你不可能让学生学到 teacher 都不会的东西。所以拿到包之后第一件事不是直接跑蒸馏而是先把 teacher 训到符合预期的水平CIFAR-100 上 ResNet50 常见能到 75% 以上或者加载一份验证过精度的预训练权重。这一步如果跳过后面所有对比都失去参照物这是最典型的无效实验。解压和准备数据的步骤# 解压到工作目录 unzip 知识蒸馏IRG算法实战使用ResNet50蒸馏ResNet18.zip -d irg_distill cd irg_distill # 创建数据目录CIFAR-100 第一次运行会触发下载 mkdir -p data数据加载按 CIFAR-100 的常见写法来import torchvision from torchvision import transforms transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.5071, 0.4865, 0.4409], std[0.2673, 0.2565, 0.2762]), ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.5071, 0.4865, 0.4409], std[0.2673, 0.2565, 0.2762]), ]) trainset torchvision.datasets.CIFAR100( root./data, trainTrue, downloadTrue, transformtransform_train) valset torchvision.datasets.CIFAR100( root./data, trainFalse, downloadTrue, transformtransform_test) trainloader torch.utils.data.DataLoader( trainset, batch_size128, shuffleTrue, num_workers4) valloader torch.utils.data.DataLoader( valset, batch_size128, shuffleFalse, num_workers4)逻辑说明CIFAR-100 的 mean/std 是公开标准值直接拿来用不要套 ImageNet 那套归一化参数输入分布对不上精度会掉一截。batch_size128 是刻意选的原因在关系图 loss 那块——batch 太小关系图就是噪声。downloadTrue 只在第一次生效第二次跑建议去掉避免每次都检查网络。3.2 蒸馏训练核心逻辑与 loss 权重设置IRG 一个训练 step 的核心逻辑如下我把关键的 detach 和投影层都写进去了teacher.eval() # teacher 固定不更新 for batch_idx, (inputs, targets) in enumerate(trainloader): inputs, targets inputs.cuda(), targets.cuda() # teacher 只前向且不保留梯度 with torch.no_grad(): t_logits, t_feat teacher(inputs, out_featureTrue) # student 正常前向特征要参与梯度 s_logits, s_feat student(inputs, out_featureTrue) # 三个 loss 分量 loss_cls F.cross_entropy(s_logits, targets) # 分类主 loss loss_fm F.mse_loss(proj(s_feat), t_feat) # 特征图对齐 loss_irg relation_loss(s_feat, t_feat) # 关系图对齐 # alpha/beta 是蒸馏权重要按量级单独调 loss loss_cls alpha * loss_fm beta * loss_irg optimizer.zero_grad() loss.backward() optimizer.step()几个关键点展开说。teacher.eval() 和 torch.no_grad() 缺一不可前者保证 teacher 的 BN 统计不被污染后者保证梯度不往 teacher 里传既省显存又防止 teacher 权重被意外更新。t_feat 和 s_feat 必须是同一语义层级的特征ResNet50 和 ResNet18 的 layer4 输出通道分别是 2048 和 512直接做 mse 会 shape 报错所以 s_feat 要先过一个 1x1 卷积投影层把通道数对齐这个 proj 层跟着 student 一起训练。relation_loss 内部是先把特征归一化算两两余弦相似度矩阵再做 mse余弦对特征尺度不敏感比欧氏距离稳得多。超参怎么给我按常见做法列一个起步表具体值跑完第一个 epoch 看 loss 量级再调超参数常见取值调参方向alpha特征图对齐权重0.1 ~ 0.5过大时学生特征被 teacher 锁死val acc 上不去beta关系图权重0.01 ~ 0.1过大时关系结构主导分类主 loss 被架空batch_size64 ~ 128关系图需要足够样本形成两两关系不建议低于 64初始学习率0.02 ~ 0.1蒸馏比普通训练保守从 0.05 起步比较稳学习率调度cosinestep decay 后期容易截断特征对齐的梯度信号注意alpha 和 beta 不是越大越好。蒸馏 loss 占比过高学生会被 teacher 的特征分布“绑架”在验证集上表现为精度涨不动、loss 降不下去。3.3 训练评估与结果输出格式每个 epoch 结束时要在验证集上全量评估一次把 val_acc 连同三个 loss 分量一起写进 json。这份源码里的 result_kd.json 大概率就是这种结构。写入的常见做法是 json lines一个 epoch 一行import json record { epoch: epoch, train_loss: float(train_loss), cls_loss: float(cls_loss), fm_loss: float(fm_loss), irg_loss: float(irg_loss), val_acc: float(val_acc), } with open(result_kd.json, a, encodingutf-8) as f: f.write(json.dumps(record) \n)说明每行一个 json 对象的好处是训练中断不丢历史脚本可以用逐行读取的方式增量分析。这里刻意存了三个 loss 分量的历史值就是为了后面画分解曲线判断 IRG 两个蒸馏 loss 是不是真的在同步下降。很多复现失败的人只存 total loss结果训练崩了都不知道是哪一个分量出了问题后悔药都没地方吃。4. IRG 训练避坑实录最容易翻车的 5 个细节这一章我从自己复现 IRG 的血泪经验里挑了 5 个最常见的坑。每一个都是先看到现象、再定位原因、最后给解决按这个顺序排查能帮你省掉至少一半的调试时间。4.1 batch size 太小关系图变成噪声现象蒸馏 loss 震荡剧烈student 最终精度反而比裸训练还低。原因IRG 的关系图是 batch 内样本两两相似度构成的。batch size 只有 16 或 32 时两两关系对只有一百多个关系图里绝大多数边都是噪声teacher 的结构信息根本传不过去学生等于在跟着噪声学。解决把 batch size 提到 64 以上CIFAR-100 上我一般直接开 128。显存不够就先降输入分辨率或者临时用 teacher 的小分支代替别动 batch size动了等于白蒸馏。4.2 teacher 没 detach梯度污染 BN 统计现象训练时显存比预期多出一倍或者 teacher 的 BN running_mean 越跑越偏teacher 精度中途掉点。原因teacher 是固定不更新的但如果没对 teacher 输出 detachbackward 时梯度会一路回传到 teacher 网络。teacher 参数虽然不会真的更新因为没挂 optimizer但中间变量的显存占用会翻倍更严重的是 teacher 处于训练模式时BN 统计会被当前 batch 污染teacher 的特征分布越跑越偏。解决teacher 一定要 eval()前向包在 torch.no_grad() 里所有 teacher 输出在进 loss 前都.detach()。这是 IRG 代码里最常见的低级翻车点没有之一。我在第 3 章代码里已经写好了照抄就行。4.3 蒸馏 loss 权重失衡学生退化成裸训练现象加了 IRG 之后精度跟 baseline 一模一样曲线走势都重合调参怎么调都没反应。原因alpha 和 beta 设得太小两个蒸馏 loss 加起来在总 loss 里占比不到 5%梯度信号完全被 cls loss 淹没学生实际上还是在裸训练。这种失败最坑人因为它不报错曲线正常就是没效果。解决训练前先单独打印三个 loss 的初始量级。常见做法是让两个蒸馏 loss 的初始值加起来占总 loss 的 30% 到 50%再根据验证集精度微调。判断标准很简单第一个 epoch 结束后看一眼 fm_loss 和 irg_loss 有没有明显下降如果纹丝不动就是权重太小。4.4 特征层没对齐shape 报错或 loss 为 nan现象训练一开始就报 shape mismatch或者 mse loss 直接算出 nan程序跑不下去。原因ResNet50 和 ResNet18 对应 stage 的输出通道数不一样layer4 一个 2048 一个 512直接做 mse 必崩。nan 则通常是特征里有极端值比如 BN 还没热身就跑蒸馏或者初始化方式不对。解决在 student 特征进 loss 前加 1x1 卷积投影层把通道投影到 teacher 的通道数这是 IRG 实现里的标准操作。如果是 nan先让模型在每个 epoch 前多跑几个空 forward 让 BN 统计稳定或者把投影层初始化换成 kaiming 初始化。4.5 学习率调度太激进后期精度停滞现象前 60 个 epoch 精度正常爬升后 30 个 epoch 完全停滞student 距离 teacher 还差一大截怎么等都不涨。原因step decay 把学习率降太狠之后cls loss 的梯度还在但特征对齐那部分梯度已经小到可以忽略关系图 loss 几乎停止更新。蒸馏后期的精度主要靠特征和关系图 loss 推着走调度器等于把路堵死了。解决学习率调度换成 cosine初始学习率从 0.1 降到 0.05 或 0.02。这个组合下蒸馏 loss 在整个训练周期都保持有效梯度后期还能稳定涨点。5. 结果解读三份 json 和三组曲线怎么读5.1 三份 json 的对比逻辑先明确三份 json 各自代表谁文件对应模型含义result.jsonResNet50teacher教师网络单独训练的上限参照result_student.jsonResNet18 无蒸馏学生裸训练的 baselineresult_kd.jsonResNet18 IRG加了 IRG 蒸馏后的学生对比时重点看两个数验证集 top-1 精度、训练末尾的 loss 值。有效蒸馏的判断标准是 result_kd 的精度明显高于 result_student并且尽量逼近 result。如果 result_kd 只比 baseline 高 0.5 个点说明蒸馏权重没调到位或者特征层选得不对如果 result_kd 和 result 差在 2 个点以内说明这套配置已经接近这个容量组合的蒸馏上限。我把三份 json 拉到一起画对比曲线的写法import json import matplotlib.pyplot as plt def load_acc(path): with open(path, encodingutf-8) as f: rows [json.loads(line) for line in f if line.strip()] return [r[val_acc] for r in rows] teacher load_acc(result.json) student load_acc(result_student.json) kd load_acc(result_kd.json) plt.plot(teacher, labelResNet50 teacher) plt.plot(kd, labelResNet18 IRG) plt.plot(student, labelResNet18 baseline) plt.legend() plt.xlabel(epoch) plt.ylabel(val acc (%)) plt.savefig(compare_acc.png, dpi150)说明这里按 json lines 格式逐行读兼容训练中断的增量写入。如果三份 json 的 epoch 数不一致取最小的公共长度再画否则末尾会错位。画完这张图蒸馏有没有效就一目了然了不需要再看任何别的指标。5.2 训练曲线和关系图热力图怎么判定蒸馏生效第 2 章说过包里 6 张 png 图分成三组这里给一个可执行的读图顺序。第一组看 loss 曲线。重点关注 result_kd 的 train_loss 是否比 result_student 更平滑。IRG 有 teacher 特征当锚点loss 一般不会像裸训练那样大起大落如果 result_kd 的 loss 照样剧烈震荡先回去查 batch size 和权重衰减。第二组看精度曲线。重点看收敛速度和最终值result_kd 的收敛速度通常比裸训练快 20 到 30 个 epoch这是 IRG 早期就传入结构信息的直接体现。如果两条曲线收敛速度几乎一样说明蒸馏 loss 大概率没起作用。第三组看关系图热力图。teacher 的关系矩阵和 student 的关系矩阵并排颜色分布越接近说明关系对齐越好如果 student 那张图颜色明显发虚、对比度低说明关系蒸馏没生效常见原因是 beta 太小。读图的习惯我建议固定下来每次实验结束先把三份 json 的 val_acc 对齐画一张图再单独画一张 fm_loss 和 irg_loss 的分解图。前者判断“有没有涨点”后者判断“涨点来自蒸馏还是来自训练本身”缺一个都容易被曲线骗到。6. 进阶用法把 IRG 迁移到你自己的模型组合上如果你手上不是 ResNet50 蒸馏 ResNet18而是想换成 ResNet34 蒸馏 MobileNetV2或者别的 teacher/student 组合IRG 这套逻辑完全能迁移但要改三个地方。第一特征层配对。teacher 和 student 不一定要用最后一个 stage常见做法是各取倒数第二个 stage。最后一个 stage 的特征太接近 logits语义太抽象关系图里 discriminative 信息反而少换到 layer3 这类中层特征关系结构更丰富蒸馏效果往往更好。第二投影层维度。student 特征通道数和 teacher 不同proj 层的输出通道改成 teacher 对应特征的通道数即可kernel size 保持 1x1不要引入额外下采样。bias 设 False因为后面接的是 mse lossbias 会影响特征对齐的精度这是我实测过的细节。第三loss 权重重调。换模型组合之后teacher 特征分布完全不同alpha 和 beta 必须重新按量级调不要沿用旧值。这是我换组合时踩过最深的坑——沿用 ResNet 对蒸馏的权重跑 MobileNet 蒸馏精度比 baseline 还低折腾了两天。迁移的核心代码骨架# 1. 替换 teacher 和 student teacher resnet50(pretrainedTrue) student mobilenet_v2(num_classes100) # 2. proj 层输出维度 teacher 对应 stage 的通道数 proj nn.Conv2d(student_feat_channels, teacher_feat_channels, kernel_size1, biasFalse) # 3. 冒烟实验只跑 5 个 epoch打印三个 loss 的量级 # 目的确认蒸馏 loss 占比在 30% 左右shape 无误迁移验证我有个固定套路先只跑 5 个 epoch 的冒烟实验打印每个 loss 分量的初值和末值确认量级和下降趋势对得上再放全长训练。全量训练跑到一半时再抽一次关系图热力图出来看确认结构对齐确实在往好的方向走而不是随着训练悄悄退化。这三步能砍掉大半无效训练时间。这里有个很玄学但反复验证有效的观察关系图 loss 的下降曲线如果中途出现平台期通常不是你调参的问题而是这个 teacher/student 组合的关系结构本身就不匹配换个特征层配对往往比调 beta 更管用。刚开始我以为是自己 loss 写错了折腾了一周后来把配对层从 layer4 换到 layer3平台期直接消失。从那以后我每次跑新组合的蒸馏实验都强制走一遍三件事先打印 loss 量级再跑 5 epoch 冒烟最后中途抽一次关系图热力图。希望帮到你。本文还有配套的精品资源点击获取