1. YOLOv8预训练模型迁移学习
对于许多刚接触YOLOv8的开发者而言,最令人困扰的往往不是模型架构本身有多复杂,而是从零开始训练一个可用的模型实在过于耗时费力。幸运的是,借助预训练模型进行迁移学习是一条高效捷径——今天就来详细探讨这一方案。
先给出第一个核心结论:采用预训练模型进行迁移学习,无疑是最快速、最稳定的入门路径。
1. YOLOv8源码下载和安装
这一步虽然没有什么特别之处,但确实是整个流程的基础。首先将项目克隆到本地磁盘,例如放在D盘根目录:
git clone https://github.com/ultralytics/ultralytics
接着在你自行创建的虚拟环境中执行安装命令:
cd ultralytics
pip install -e .
随后需要下载预训练权重文件。建议单独创建一个weights文件夹用于存放,例如放在D:ultralyticsultralyticsweights目录下,便于统一管理。
安装完成后可以运行一个小测试,确认环境是否配置正确:
yolo predict model=模型路径 source=图片路径
这里有一个容易踩中的陷阱:如果你使用的是PyTorch 2.6版本,可能会在加载权重时遇到报错问题。原因在于PyTorch 2.6默认启用了weights_only=True参数,只允许加载纯张量数据,而YOLOv8的权重文件中还包含了自定义的类定义。解决方法也很简单,找到ultralytics/ultralytics/nn/tasks.py文件,将原本的加载代码修改一行即可:
# 原始代码(约716行)
ckpt = torch.load(file, map_location="cpu")
# 修改为:
ckpt = torch.load(file, map_location="cpu", weights_only=False)
如果希望将推理结果保存下来,加上sa ve_txt参数即可:
yolo predict model=模型路径 source=图片路径 sa ve_txt
2. 准备自己的数据集
数据准备这一环节,核心工作其实就是完成一次格式转换。通常我们拿到的原始数据是VOC格式,包含JPEGImages/原始图片和Annotations/下的XML标注文件。需要将这些数据转换成YOLOv8能够识别的格式。
具体流程大致如下:使用prepare_data.py脚本,先将XML标注读取进来,通过convert_annotation函数转换为YOLO风格的标签(即类别 x_center y_center width height这种文本格式),存放到临时的YOLOLabels/目录中,再按比例分配到训练集和验证集的labels/train/和labels/val/文件夹下。图片也采用相同方式,按同样比例复制到images/train/和images/val/中。
最终的数据集目录结构应如下所示:
根目录/
├── images/
│ ├── train/ # 训练集图片
│ └── val/ # 验证集图片
└── labels/
├── train/ # 训练集标签
└── val/ # 验证集标签
3. 修改配置文件
这一步相对简单,核心任务是修改cfg/datasets/VOC.yaml文件,将数据集的路径和类别信息正确配置好。
4. 正常训练自己的数据集
一切准备就绪后,就可以启动训练了。训练命令非常直观:
yolo detect train data=配置文件路径 model=模型路径 epochs=100 imgsz=640 batch=16 workers=4
需要留意一点:如果GPU显存不足,最直接的解决办法就是减小batch size。另外,如果训练中途意外中断,Ultralytics支持断点续训,只需加上resume参数就能继续运行:
yolo detect train data=配置文件路径 model=模型路径 epochs=100 imgsz=640 batch=16 workers=4 resume
训练完成后,可以前往ultralytics/runs/detect/train/weights目录查看结果,里面保存了训练过程中表现最优的权重文件。
这里有一个值得关注的细节:YOLOv8的迁移学习流程实际上分为三个步骤:
- 首先使用最小的
YOLOv8n.pt进行一次混合精度测试,这个阶段并非真正训练,而是快速验证整个流程是否畅通; - 正式训练时,系统会将训练集和验证集的标签文件(文本格式)转换为二进制缓存文件,从而显著提升加载和读取速度;
- 随后才是实际的训练、验证和微调工作。
从日志中可以看到如下输出:
Transferred 349/355 items from pretrained weights
Freezing layer 'model.22.dfl.conv.weight'
AMP: running Automatic Mixed Precision (AMP) checks with YOLOv8n...
AMP: checks passed ✅
关于混合精度测试,有必要多说几句。它的核心价值在于低成本验证——用最小的v8n模型跑1-2个epoch,快速检查配置是否正确、数据路径是否对得上、标注文件能否正常解析、CUDA和PyTorch版本是否兼容、显存是否充足。如果直接用大模型跑了几十轮才发现数据有问题,那浪费的时间和计算资源就太可惜了。
此外,所谓的"混合精度测试"也包含了AMP(自动混合精度)是否正常工作的验证。PyTorch的这个特性允许模型同时使用float16和float32精度进行计算,能够显著加快训练速度、节省显存。先用v8n测试一遍,确认没有问题后,就可以放心地对大模型开启amp=True。
5. 测试训练出的网络模型
训练结束后的性能评估同样至关重要。测试命令如下:
yolo detect val model=模型路径 data=配置文件路径 batch=16 conf=0.001 iou=0.6
这里有一个提示:评估结果与conf阈值、iou阈值的设置关系密切,不同的任务场景可能需要微调这些参数。如果要对具体图片进行推理测试:
yolo detect predict model=模型路径 data=配置文件路径 source=测试图片路径
2. 知识点总结
这一部分我整理了DepGraph剪枝中一些最常遇到的关键问题,一次性讲清楚。
1. DepGraph剪枝中的剪枝对象是什么?
主要剪枝的对象是卷积层的输出通道,以及与之对应的依赖参数。
2. 剪枝粒度是什么级别?为什么选择这个粒度?
采用通道级(Channel-level)结构化剪枝。这个粒度能够实现很好的平衡:既保留了结构化剪枝的硬件友好性,又不会像层剪枝那样过于粗糙。
3. 剪枝依据(重要性评估准则)?
使用的是L2幅度重要性(Magnitude Importance)。为什么选择L2?原因有以下几点:
- 简单有效:计算复杂度仅为O(n),对大规模网络非常友好;
- 有理论支撑:大权重通常对应重要特征,这一假设在实践中被反复验证;
- 与训练兼容:经过L2正则化后,不重要的通道权重自然会变小,剪枝时机更加准确;
- 实证成功:在ResNet、YOLO等多种网络上均得到了验证。
4. 剪枝策略是什么?为什么采用这个策略?
采用的是迭代渐进式剪枝 + 依赖感知分组 + 局部微调的三阶段策略。具体来说,分16次小步剪枝,每次剪完后微调30轮。这样做的好处是避免一次性剪掉过多导致精度骤降,让模型有充足的时间去适应和恢复。
5. 如何处理YOLOv8的特殊结构(如C2f模块)?
核心思路是结构转换 + DepGraph自动处理。首先将C2f模块转换成更易于剪枝的C2f_v2版本,然后利用DepGraph自动分析层间依赖关系,确保剪枝后网络结构完整。
6. 如果剪枝后精度损失过大,有哪些恢复策略?
推荐按损失程度分级处理:
- 轻度损失(<5%):短期微调即可;
- 中度损失(5%-10%):强化微调配合知识蒸馏;
- 严重损失(>10%):回退到剪枝前的状态,重新设计剪枝方案。
7. YOLOv8为什么选择DepGraph剪枝方法,而不选择其他的剪枝方法?
简单对比一下几种主流方法就一目了然了:
- L1/L2 Norm:基于权重范数剪枝,方法简单但可能破坏网络结构;
- BN缩放因子:依赖BN层,但YOLOv8某些层没有BN;
- Network Slimming:基于BN层γ系数的通道重要性,忽略层间依赖,YOLOv8的C2f结构会出问题;
- ThiNet:基于下一层重建误差,只考虑局部依赖,忽略跨层连接;
- Channel Pruning:用LASSO回归选择通道,计算复杂,不适合复杂网络;
- DepGraph:构建全局依赖图,确保结构完整性,特别适合YOLOv8的复杂连接结构。
YOLOv8有几个关键结构特性让DepGraph成为了不二之选:多分支结构(C2f模块有并行分支)、跨层连接(PAN-FPN有上下采样和跳跃连接)、权重共享(Detect头共享卷积权重)、多尺度处理(3个不同尺度的检测头)。这些特性别的剪枝方法要么处理不了,要么处理得很勉强。
举个例子,YOLOv8里这样一个依赖链:Conv1 → Split → [Branch1, Branch2] → Concat → Conv2,中间还有Bottleneck的分支连接。DepGraph可以自动发现Conv1和Conv2的依赖关系,识别Split和Concat的通道映射,确保剪枝后网络结构完整。这正是其他方法做不到的。
3. YOLOv8结构及转换过程
这部分涉及较多的代码细节,但核心逻辑其实非常清晰——将YOLOv8中对剪枝不友好的结构,改造成DepGraph能够识别和处理的形式。
3.1 C2f模块转换为C2f_v2
先看原始C2f结构。输入经过一个CBS卷积后,输出2×c2个通道,再通过Split分割成两份:前c2个通道走分支1(直接连接),后c2个通道走分支2(通过多个Bottleneck)。这种设计的优点是计算高效——一次卷积搞定,但缺点是剪枝困难——因为CBS并不知道哪些通道会去哪个分支。
想象一下:CBS输出的通道索引从0到2c2-1,前c2个分配给分支1,后c2个给分支2。如果剪掉通道50(属于分支1),不影响分支2;但如果剪掉通道150(属于分支2),就得同时考虑对分支1的影响。剪枝算法很难知道这个映射关系。
转换后的C2f_v2结构把这个耦合彻底解开了:将原先的一个CBS拆成两个独立的卷积(CV0和CV1),各输出c2个通道,分别服务两个分支。这种改法虽然多了一次卷积、效率略低,但好处非常明显——每个卷积层可以独立评估通道重要性,DepGraph也能构建出清晰的依赖关系,剪枝决策可以沿着依赖图正确传播。
两种结构的核心差异:原结构CBS输出2×c2通道,然后Split,优点是计算高效但剪枝困难;转换后结构用两个独立CBS各输出c2通道,优点是便于独立剪枝但效率略低。换个角度看,这是一个典型的"剪枝便利性"和"计算效率"之间的权衡。
3.2 PAN-FPN颈部转换
PAN-FPN的问题更加明显:存在复杂的跨层连接和权重共享。原结构中,自顶向下路径和自底向上路径共享了卷积权重,一个层的剪枝会影响到多个输出路径。
转换后的结构将共享的卷积层拆分成了独立的实例:原来一个cv_td同时服务于P5→P4和P4→P3两个融合路径,现在拆成cv_p5p4和cv_p4p3两个独立卷积;同理自底向上的cv_bu拆成cv_p3p4和cv_p4p5。上采样和下采样层也做了类似处理。
转换的核心思想非常简单:创建独立的处理路径,避免特征重用。每个CBS只服务于单一输出,避免中间特征的多重使用,让依赖关系变得线性、清晰。
经过这样处理后,DepGraph就能做到:复杂依赖可视化、组剪枝支持、依赖传播的正确性。举个例子,DepGraph需要分析CBS_1的剪枝会不会影响到P3_out、P4_out2甚至P5_out,在转换后的结构中,这种依赖分析变得一目了然。
3.3 Detect检测头转换
原Detect头的问题同样是权重共享——P3、P4、P5三个尺度的分类和回归分支共用了同一组卷积权重。这意味着如果希望对某个尺度做特殊处理,会受到很大的限制。
转换后的独立检测头为每个尺度创建了独立的卷积层:Conv_P3_cls、Conv_P3_reg等。好处非常明显:消除了共享约束,不同尺度特征图可以有不同的剪枝策略,每个头的输入输出关系变得简单明确。
从代码实现上看:
原结构用一个cls_conv = nn.Conv2d(256, num_classes, 1)和reg_conv = nn.Conv2d(256, 4, 1)就处理了所有尺度;转换后变成nn.ModuleList,每个尺度各有独立的cls_conv和reg_conv。
4. DepGraph剪枝优势图示与总结
传统剪枝最大的问题在于:各层之间的依赖关系没有被充分建模,剪掉一层的某些通道后,可能让后面的层完全失效。DepGraph的做法是将每一层当作一个节点,层与层之间的通道依赖关系当作边,构建出一张完整的依赖图。剪枝时,沿着依赖图传播剪枝决策,确保一致性。
打个比方:传统剪枝就像盲人摸象,只关心当前操作的对象;DepGraph则是先画出整头象的骨架,再决定从哪里下手。
3. DepGraph剪枝训练
实际代码实现中,使用的是torch_pruning这个库,DepGraph算法已经集成在其中,不需要从零实现。但要注意,虽然算法是现成的,还有三件事必须自己做:
- YOLOv8特定结构适配——这是核心难点,包括C2f的转换等;
- 完整的剪枝工作流设计——基准测试→迭代剪枝→微调恢复→验证→保存;
- 性能监控与可视化——生成专业的性能对比图表。
训练命令也很简单:
pip install torch-pruning
然后运行专门编写的yolov8_pruning.py脚本。这个脚本具有以下几个特点:完整的C2f结构适配、迭代剪枝策略、自动依赖处理、详细的性能监控、可视化输出。
代码的逻辑结构大致分为三部分:第一部分是导入和配置,第二部分是辅助函数定义(包括可视化函数、C2f结构转换、训练函数重写),第三部分是主剪枝函数。
在C2f结构转换这一步,核心就是前面讲的将YOLOv8的C2f模块转换为C2f_v2。转换完成后,使用tp.pruner.GroupNormPruner来实例化剪枝器,基于L2范数重要性评估通道重要性。
迭代剪枝的策略是这样的:假设目标剪枝比例是50%,分16次迭代完成,那么每次剪枝比例大约是4.4%。这样做的目的是让模型有足够的时间去适应每一次小的结构变化,而不是一次就被砍掉半条命。
每次剪枝后,用少量epoch(比如30轮)进行微调,恢复精度。运行过程中会实时显示MACs、参数量、mAP的变化,最终还会生成性能变化图。
最终输出文件保存在runs/detect/目录下,包括每一步的验证结果和微调过程,以及最终的最优模型权重best_pruned.pt和ONNX格式模型best_pruned.onnx。
4. 最终实验结果
来看看实际效果:
Before Pruning: MACs= 14.27201 G, #Params= 11.13599 M, mAP= 0.82984
14.27G的MACs对应大约28.4G的FLOPs(MACs大约是FLOPs的一半,这是正常比例),11.14M的参数量,0.82984的mAP——这个起点相当不错。
至于剪枝后的结果能达到什么程度,取决于具体的目标压缩率和剪枝策略设置。但可以确定的是,经过DepGraph剪枝后,在模型大小和推理速度上会有一个显著的提升,而精度的损失完全在可控范围内。
```