简介:一套基于YOLOv5目标检测模型实现知识蒸馏的完整项目源码,适合高校人工智能、通信工程、自动化等专业学生用于毕业设计、课程设计或项目初期演示,也适合对模型压缩与加速感兴趣的进阶学习者。项目将YOLOv5检测框架与知识蒸馏技术融合,提供从数据集配置、模型定义、训练检测到蒸馏调优的完整代码链路,含蒸馏脚本、模型结构配置、导出推理等模块,并附有详细文档,便于按图索骥。压缩包共包含55个文件,核心为27个Python源码文件,涵盖训练、检测、蒸馏与工具函数,另有19个YAML配置文件负责模型结构和超参数设定,以及Shell脚本、Markdown文档、Dockerfile等辅助内容,整体仅138KB。目前已有214人学习下载,适合快速上手与二次开发。代码均经过运行验证,作为高分项目具备可靠性与完整性,蒸馏脚本实现了关键逻辑,配合清晰的目录结构,可以帮助读者复现完整实验流程,深入理解知识蒸馏的工作原理,也可在此基础扩展其他检测任务。
1. 当 YOLOv5 跑得动却塞不进边缘设备,知识蒸馏是比换模型更稳的解
YOLOv5 的 mAP 确实够用,但真正让它进落地流程的却不是精度,而是推理速度是否撑得住你的业务场景。如果你已经跑通了 YOLOv5 的目标检测流程,却卡在模型体积、显存占用和边缘设备实时性这三件事上,那么基于 YOLOv5 的目标检测模型知识蒸馏,就是一条不需要重写检测头、不需要重新标注数据、不需要动后处理逻辑的压缩路线。它和剪枝、量化最大的差别在于:蒸馏不修改推理时模型本身的算子,也不依赖硬件对低比特位宽的支持,它只是把大模型的“判断经验”迁移到小模型里。这篇内容适合的是这样的人:已经从 GitHub 拉过 YOLOv5 源码、训练过自己的数据集、被显存和速度卡过脖子。下面我不去介绍那个压缩包里有什么文件,而是直接讲这套学生模型改造、蒸馏损失注入、训练配置和验证流程应当怎样完整落地。
2. 知识蒸馏作用于 YOLOv5 的哪些层,教师模型到底在教什么
2.1 检测任务的蒸馏不是把标签换成 soft label 那么简单
图像分类里的知识蒸馏,核心操作是让学生模型去拟合教师模型的 softmax 输出,温度 T 把概率分布拉平,学生从而学到类别之间的相似关系。但目标检测是复合任务,一个样本同时有分类置信度、边界框回归偏移量、目标是否存在三个维度的信息。YOLOv5 的损失函数由loss_cls、loss_box、loss_obj三部分组成,蒸馏如果只模仿分类那一支的概率分布,学生就只能学到“这是什么”,学不到“这个东西在哪里”。
因此 YOLOv5 蒸馏实践中,教师模型至少要提供三类中间信息:最后一层分类 logits、边界框回归的预测偏移量、以及 feature map 层面的空间注意力响应。前两者对应响应蒸馏,后者对应特征蒸馏。知识蒸馏在这个框架里更像是一种多任务学习约束——学生不仅要对数据集标签负责,还要对教师输出的分布负责。
2.2 响应蒸馏与特征蒸馏的边界在哪里
响应蒸馏的计算位置在检测头的输出端。对 YOLOv5 来说,输出是一个 tensor,形状为[batch, anchor_num * (5 + num_classes), grid_h, grid_w],里面 4 个值是回归偏移,1 个值是 objectness,剩下的是类别 logits。对于正样本位置的回归和分类分支,可以分别做蒸馏损失;对于负样本位置,通常只对 objectness 分支做蒸馏,因为背景区域没有明确的类别逻辑可学,硬拉只会让小模型去匹配教师在高 IoU 阈值下也模棱两可的预测。
特征蒸馏则作用在 backbone 的中间层。常见做法是选 C3 模块输出的 3 个有效特征层,学生去拟合教师在对应层上的空间注意力图或通道注意力图。这里要特别注意:教师和学生 backbone 的通道数通常不一致,直接算 MSE 前需要加 1x1 卷积对齐维度,或者用自适应池化把特征图压到同一分辨率再做 L2 loss。直接对原始 feature 做 MSE 是新手最常犯的错,结果就是 loss 震荡、梯度爆炸,被误判成蒸馏没效果。
提示:响应蒸馏负责“结果对齐”,特征蒸馏负责“过程对齐”。如果只做响应蒸馏,小模型大概率能保住 mAP 但中间层语义和教师差异很大;如果只做特征蒸馏,loss 降得很快但最终 mAP 提升不明显。两者组合使用才是检测场景的常规做法。
2.3 蒸馏损失如何与 YOLOv5 原生损失拼接成一个可回传的标量
YOLOv5 在训练时对每个 head 分别计算置信度、分类、回归损失,然后求和。加入蒸馏后,总损失的组织方式常见的有三种,我整理成一张表方便对比:
| 损失组织方式 | 计算位置 | 适合场景 | 注意事项 |
|---|---|---|---|
| 全响应蒸馏 | 检测头三个输出层全部参与 | 教师与学生结构差异大 | 需要在每层损失前给不同权重 |
| 前景区域蒸馏 | 只在有目标的 grid cell 位置计算蒸馏损失 | 数据集中小目标多 | 需要从教师输出中过滤负样本 |
| 特征对齐蒸馏 | backbone 中间三层特征图 | 希望学生学到整体语义表达 | 需要加入对齐模块,训练参数变多 |
多数工程落地方案是第 1 种和第 3 种混合。蒸馏损失权重 alpha 从 0.1 起步,前 5 个 epoch 做 warm-up,因为早期学生模型自身的检测损失还没降下来,如果蒸馏权重太大,学生会一味模仿教师的输出分布,导致收敛极慢。后面到了训练中段再把 alpha 提到 0.5 到 1.0 之间,让蒸馏信号占主导。
3. 在 YOLOv5 源码里注入知识蒸馏的最小改造方案
3.1 加载教师模型并把所有 BN 层设为 eval 模式
知识蒸馏训练的第一步是搞到教师模型。教师模型不需要一开始就加载进同一个显存,实践中通常在train.py主流程里创建学生模型,然后单独加载教师权重。教师模型的入口定义如下:
import torch from models.experimental import attempt_load def build_teacher(teacher_weights_path, device): teacher = attempt_load(teacher_weights_path, map_location=device) teacher.eval() for m in teacher.modules(): if isinstance(m, torch.nn.BatchNorm2d): m.eval() m.requires_grad_(False) return teacher关键逻辑说明:教师模型必须整体处于 eval 模式,尤其是 BatchNorm 层。如果教师 BN 还处于训练模式,批统计量会随着蒸馏训练继续漂移,相当于教师模型本身的目标函数一直在变化,学生学到的分布也就不稳定。requires_grad_(False)是确保反向传播时不会把梯度传到教师网络参数里。
教师模型的每一层输出在蒸馏过程中需要被反复使用,因此建议在训练循环前先把教师模型放在no_grad上下文里推理一次,把中间层输出缓存下来。但要注意:YOLOv5 训练时图片会做 Mosaic 增强,每张图的拼接方式不同,所以不能把缓存做到样本级别,只能在每个 step 内复用。
3.2 在学生模型的 forward 中额外返回中间特征
YOLOv5 的Detect层 forward 返回的是检测输出,但蒸馏需要 backbone 的中间特征,因此常见做法是改造Model.forward,把原来的三层特征额外加进返回值:
def forward_once(self, x, profile=False, feature_out=False): y = [] for m in self.model: if m.f != -1: x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] x = m(x) y.append(x if m.i in self.save else None) if feature_out: return x, [y[4], y[6], y[9]] return x参数说明:feature_out控制是否返回中间特征,[y[4], y[6], y[9]]对应 YOLOv5 三个不同尺度的特征层索引。改造后原训练流程不受影响,只有蒸馏训练脚本里显式传feature_out=True。
这个方案比专门写一个教师分支高效得多,因为它不复制学生模型的 forward 逻辑,所有特征提取计算都在已有 forward 里完成,不会出现两份模型权重参数不同步的问题。
3.3 蒸馏损失函数如何写才算贴合检测任务
蒸馏损失在这个场景中建议用Distribution Focal Loss的思想,而不是直接对 logits 做 KL 散度。检测输出的 logits 在不同 anchor 位置差异极大,背景 anchor 的 logits 几乎全为负数,KL 散度在负数区域会产生梯度消失。我对蒸馏损失的实现通常拆成分类和回归两段:
def distill_loss(teacher_feat, student_feat, teacher_pred, student_pred, target_boxes, temperature=4.0): loss = 0.0 # 对每个特征尺度的预测结果分别处理 for t_pred, s_pred, t_feat, s_feat in zip( teacher_pred, student_pred, teacher_feat, student_feat ): # 分类蒸馏:只对前景 anchor 计算 obj_mask = (t_pred[..., 4] > 0.5).detach() if obj_mask.sum() > 0: t_cls = torch.softmax(t_pred[..., 5:] / temperature, dim=-1) s_cls = torch.log_softmax(s_pred[..., 5:] / temperature, dim=-1) loss_cls = -(t_cls * s_cls).sum(-1)[obj_mask].mean() loss += loss_cls * 0.5 # 特征蒸馏:取前景区域对应的特征做 L2 对齐 feat_loss = torch.nn.functional.mse_loss(s_feat, t_feat.detach()) loss += feat_loss * 0.2 return loss逻辑说明:obj_mask从教师预测中直接筛出它有把握的目标区域,这样的锚点位置保留分类蒸馏,避免背景位置把大量负样本的 logits 拉向一个错误的分布。回归分支没有单独做蒸馏,因为特征对齐已经隐含了定位信息,检测头里再做框回归蒸馏容易和原生 GIoU 损失互相干扰,反而拉低小模型的定位精度。
参数说明:temperature默认设为 4.0 而不是分类任务常用的 3.0。目标检测输出 channels 更多,logits 分布更分散,温度太低接近 one-hot 编码,蒸馏出的就是一个几乎硬标签的分布;太高则所有类别趋向均匀,学生学不到类别间的细粒度区别。
4. 训练流程与超参数调优:把蒸馏训练跑通的关键配置
4.1 在 YOLOv5 的训练主循环里插入蒸馏 step
将蒸馏逻辑接入训练循环时,需要改动utils/loss.py和train.py两处。主流程中设置两个优化器:一个是学生模型的正常优化器,一个是教师停更的参数。教师参数不需要梯度,因此不需要额外优化器。训练 step 的逻辑框架如下:
for batch_i, (imgs, targets, paths, _) in enumerate(train_loader): imgs = imgs.to(device) targets = targets.to(device) with torch.no_grad(): t_pred, t_feats = teacher(imgs, feature_out=True) s_pred, s_feats = student(imgs, feature_out=True) loss, loss_items = compute_loss(s_pred, targets, model=student) d_loss = distill_loss(t_feats, s_feats, t_pred, s_pred, targets) total_loss = loss + d_loss * alpha total_loss.backward() optimizer.step() optimizer.zero_grad()训练过程中的几个关键点:第一,教师模型的推理必须在no_grad上下文中,否则显存直接翻倍;第二,蒸馏损失和原生损失的 backward 必须在同一个计算图内完成,不能分开 backward,否则学生网络中间层的梯度被重复累积;第三,EMA 权重只在学生模型上维护,不能把教师模型也放进 EMA。
alpha的值随着训练进度变化,这里用线性 warm-up 方式实现:
alpha = min(1.0, max(0.1, (epoch - 5) / 20))前 5 个 epoch alpha 保持 0.1,此后每个 epoch 增加约 0.045,到第 25 个 epoch 时 alpha 达到 1.0。这个曲线比固定 0.5 省事,且不容易在训练中段出现 loss 突然反弹。
4.2 数据增强与蒸馏同时开启时的两个隐性坑
YOLOv5 默认开启 Mosaic、MixUp、HSV 扰动等增强策略。蒸馏训练中,教师和学生在同一个 step 输入是同一张增强后的图,这是正确的。但 Mosaic 增强会切分四张图拼接,导致教师模型的感受野发生了畸变,如果教师是从 COCO 上预训练的权重,它在畸变图上的预测置信度会整体偏低。此时用固定阈值 0.5 去筛前景 anchor,会把大量真实的物体区域当成背景,蒸馏效果直接减半。
我采用的缓解方案是:蒸馏前 5 个 epoch 关闭 Mosaic 增强,或者把 obj_mask 的阈值从 0.5 降到 0.3,让教师更多的低置信度预测也参与蒸馏。训练中段以后,学生已经学到基本的目标感知能力,此时再开启 Mosaic 增强,共同参与蒸馏。
第二个坑是训练卡在显存溢出上。一张 640x640 输入下,教师 YOLOv5x 和学生 YOLOv5s 同时前向,batch size 为 16 时显存占用超过 24 GB,但这还没算特征缓存。自由落体式减少 batch size 会让 BN 统计量不稳定,更合理的方案是开启梯度累积:
python train.py \ --data your_dataset.yaml \ --weights yolov5s.pt \ --teacher-weights yolov5x.pt \ --batch-size 8 \ --accumulate 4 \ --device 0--batch-size 8配合--accumulate 4,等效 batch 32,每 4 个 step 做一次优化器更新。这样 BN 统计量在每 32 张图上统计,不掉精度。教师模型需要常驻显存的部分包括特征图缓存和检测头 logits,所以学生模型可以先加载到 CPU 再移动到 GPU,避免显存峰值重叠。
4.3 超参数表:一份可以直接照抄的初始配置
蒸馏训练和普通 YOLOv5 训练的超参数大部分共用,但有几个专项超参需要单独设置。下面是完整的一套初始配置参考:
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| alpha | 0.1 → 1.0 warm-up | 蒸馏损失权重,前 5 epoch 固定 0.1 |
| temperature | 4.0 | 分类蒸馏温度,类别多时可降到 2.0 |
| obj_mask_thres | 0.5(Mosaic 关闭) | 前景筛选阈值,低阈值让更多教师预测参与 |
| feature_loss_weight | 0.2 | 特征蒸馏损失相对于响应蒸馏的权重 |
| freeze_backbone | 前 10 epoch | 冻结学生 backbone,只用检测头训练,减少震荡 |
| learning_rate | 0.002 到 0.01 | YOLOv5 默认 0.01,蒸馏建议降低到 0.005 以下 |
| 优化器 | SGD 或 AdamW | SGD 稳定;AdamW 收敛快但 mAP 波动大 |
freeze_backbone是蒸馏中容易被忽略的参数。学生模型如果从随机初始化开始,backbone 特征和教师差异巨大,特征蒸馏损失会在前几个 epoch 主导梯度,导致检测头学到的是混乱语义。常见的做法是前 10 个 epoch 冻结学生 backbone 参数,只让检测头先拟合一批数据,等到检测损失降到一定阈值再解冻 backbone,此时特征蒸馏才能起到精细校准的作用。
4.4 训练曲线怎么看:蒸馏 loss 不等于 mAP
很多人看到蒸馏训练日志里总 loss 很高就开始调参,这是误区。蒸馏 loss 本身是额外叠加的损失项,它的绝对值没有任何参考意义,真正要看的是三件事:原生检测 loss 是否仍在下降、蒸馏 loss 是否在 20 个 epoch 内降低到初始值的一半以下、验证集的 mAP 曲线是否出现明显平台期。
如果 mAP 在训练前几个 epoch 一直不动,同时蒸馏 loss 降得很快,这说明学生模型在拿“死记硬背”的方式模仿教师的输出,而没有真正理解目标检测任务。这时候需要立刻调低 alpha,把 0.5 降到 0.3,或者把 feature_loss_weight 从 0.2 提到 0.4,让特征层面的对齐信号超过响应层面的信号,学生模型会更主动地提取空间语义信息。
5. 蒸馏完成后怎么验证,以及三个能提升稳定性的训练技巧
5.1 验证时需要对比三组模型而不是一组
蒸馏项目报告里如果只放学生的 mAP 对比,说服力会弱很多。更扎实的验证方案是:同时评估教师模型、学生模型(未蒸馏直接训练)、学生模型(蒸馏后)在一套测试集上的 mAP、FPS 和显存占用。三种模型用同一份测试集和同一个置信度阈值跑结果。学生蒸馏后通常能达到教师 mAP 的 95% 到 98%,但推理速度翻倍,这个数据就是项目验收时的核心结论。如果蒸馏后 mAP 反而低于未蒸馏学生,优先检查教师模型是否已经过拟合训练集,而不是怀疑蒸馏代码有 bug。
验证脚本里要注意 YOLOv5 的val.py默认会做 NMS 和类别过滤,需手动关闭与模型无关的增强,否则蒸馏模型在验证时会被数据增强干扰,导致 mAP 虚低。
5.2 温度参数的动态退火策略
固定温度 4.0 只在蒸馏中前期有效。训练进入后段,学生的分类置信度已经接近教师,此时温度仍然过高会导致分布过于均匀,学生无法精炼出高置信度的类别边界。常见的做法是让温度随训练进程动态衰减:
temperature = max(1.5, 4.0 * (1 - epoch / total_epochs))前 30% 的 epoch 从 4.0 开始,逐步下降到 1.5 后保持。这样前期学到的是类别之间的相对关系,后期则强化对不同类别差异的精确感知。需要注意:温度不能降到 1.0 以下,否则蒸馏 loss 的梯度方向会和学生原生交叉熵损失冲突,两个目标函数互相打架。
5.3 分 head 打印蒸馏曲线,定位性能瓶颈
YOLOv5 有三个不同尺度的检测头,分别负责大、中、小目标。蒸馏时三者的收敛速度可能差异很大,常见的情况是负责小目标的 P3 head 蒸馏 loss 收敛快,大目标的 P5 head 却迟迟不降。在日志打印中加入分 head 的蒸馏 loss 统计,可以快速定位问题:
print(f"P3_loss: {loss_p3:.4f} P4_loss: {loss_p4:.4f} P5_loss: {loss_p5:.4f}")如果某一条曲线长时间不降,对应的做法不是盲调整体的 alpha,而是单独给那个 head 的蒸馏权重乘上一个 1.5 到 2.0 的系数。检测头蒸馏的收益通常集中在 P3(小目标)上,如果你的数据集小目标占比高,就应该把特征蒸馏的权重按[0.3, 0.3, 0.4]分配而不是默认均匀。这个技巧对提升在低 IoU 阈值下的 recall 值有明显作用,也是让蒸馏实验结果在报告中更有说服力的一个关键调优点。
本文还有配套的精品资源,点击获取