简介:这份资源面向已掌握 YOLOv8 基础、希望进一步压缩模型体积与推理耗时的算法工程师和研究者,提供了一套可直接运行的剪枝源码,集成 LAMP、Slimming、Group Slimming、Group Hessian、Taylor、Regularization 等多种主流剪枝策略,解决模型部署时参数量大、速度慢的痛点。压缩包共 20 个文件,以 9 个 py 脚本和 9 个 yaml 配置为主,另含 1 个 md 说明文档与 1 个 zip 子包,整体约 1.33MB;py 脚本负责剪枝流程与通道可视化,yaml 用于定义不同骨干与 Neck 组合的网络结构,md 文档则梳理各策略的使用方式与注意事项。目前已有 2863 人学习下载,说明该方案在社区中具备一定参考价值。读者可借助一键运行脚本快速对比不同剪枝方法的效果,结合 get_FPS.py 评估推理速度,并通过 plot_channel_image.py 观察通道分布,从而为模型轻量化选型与实验复现提供完整工具链。
1. 剪枝不是删参数那么简单:yolov8 剪枝源码能解决什么
很多人第一次跑 YOLOv8 剪枝,都是被部署端的算力逼的。训练完的模型在服务器上 mAP 挺好看,一挪到 RK3588、Jetson 或者边缘盒子上,帧率直接掉到个位数,这时候才想起来剪枝。但真正动手才发现,剪枝不是把权重矩阵里的小数抹掉那么简单——你得先搞清楚哪些通道能删、删完怎么恢复精度、BN 层的缩放因子怎么参与排序、残差结构里哪些层不能动。这份 yolov8 剪枝源码把 LAMP、结构化通道剪枝、非结构化稀疏这几套策略集成到了一起,省去了自己从零写 mask 管理和依赖图分析的功夫。它适合已经跑通 YOLOv8 训练、准备往嵌入式端部署的工程师,也适合想搞明白剪枝算法到底怎么落到检测网络上的研究者。源码本身不绑定特定数据集,COCO 和自定义数据都能接。
2. 剪枝策略选型:LAMP 和结构化通道剪枝到底差在哪
2.1 为什么 YOLOv8 的剪枝比分类网络麻烦
分类网络剪枝,比如 ResNet34 那一套,层与层之间基本是串行的,删掉某个卷积的输出通道,下一层对应输入通道跟着删就行。YOLOv8 不一样,它的 backbone 里有 C2f 模块,内部做了 split 和 concat,neck 部分还有上采样和跨层拼接。你如果按分类网络那套全局排序去删通道,很容易出现某个 concat 节点两边通道数对不上,模型直接报维度错误。
这份源码的处理方式是把模型先转成一张有向无环图,每个卷积层记录它的输入来源和输出去向,剪枝时按依赖关系传播 mask。具体来说,C2f 里的 split 分支会被标记为同一组,要么一起剪,要么都不剪。上采样前的卷积和 concat 后的卷积也会做通道对齐检查。这一步是很多手写剪枝脚本翻车的地方,血泪经验就是:别想着一步到位全局剪,先按 block 分组,组内统一决策。
2.2 LAMP 评分的核心逻辑
LAMP 全称 Layer-Adaptive Magnitude-based Pruning,它的核心思想不是看单个权重的绝对值,而是看某个通道在整个层内的相对重要性。具体做法是把每一层的权重按通道分组,计算每个通道的 L2 范数,然后做全局排序。和传统 magnitude pruning 的区别在于,LAMP 会对不同层的分数做归一化,避免某些层因为权重量级大就霸占剪枝名额。
源码里 LAMP 的实现大致是这样:
import torch import torch.nn as nn def lamp_score(weight, index=None): """ 计算 LAMP 剪枝分数 weight: 卷积层权重, shape [out_channels, in_channels, k, k] index: 当前剪枝轮次的全局索引, 用于归一化 返回: 每个输出通道的重要性分数 """ # 把权重 reshape 成 [out_channels, -1] w = weight.view(weight.size(0), -1) # 计算每个通道的 L2 范数 channel_norm = torch.norm(w, p=2, dim=1) # 按通道排序, 得到每个通道的秩 sorted_norm, _ = torch.sort(channel_norm) # LAMP 分数 = 范数的平方 / 排序后的累积平方和 score = channel_norm ** 2 / (sorted_norm ** 2).sum() return score这段代码的关键在最后一行:分母是当前层所有通道范数平方的总和,分子是单个通道的范数平方。这样算出来的分数天然在 0 到 1 之间,跨层比较时不会因为某层权重量级大就占便宜。参数index在实际源码里用于多轮剪枝时的迭代控制,第一轮剪完后重新计算分数,避免一次性剪太多导致精度崩掉。
2.3 结构化剪枝和非结构化剪枝的取舍
结构化剪枝删的是整个通道,剪完模型还是规整的稠密矩阵,推理时不需要特殊硬件支持,RK3588 这类 NPU 也能直接吃。非结构化剪枝是把单个权重置零,稀疏度可以做到很高,但需要稀疏推理库或者专用加速器才能拿到实际收益。这份源码两种都支持,但默认走结构化通道剪枝。
选型建议很直接:如果你的部署目标是通用 CPU 或普通 NPU,选结构化;如果后端有 TensorRT 的稀疏推理或者你只是想做模型压缩研究,可以试非结构化。源码里通过一个配置项切换:
# prune_config.yaml prune_type: "structured" # 可选 structured / unstructured strategy: "lamp" # 可选 lamp / magnitude / random target_sparsity: 0.5 # 目标稀疏率 layer_skip: ["model.0", "model.22"] # 跳过首尾层target_sparsity设 0.5 表示剪掉 50% 的通道,实际剪枝率会略低,因为有些层被layer_skip保护了。首尾层通常不剪:第一层直接接触输入图像,通道数本来就少;最后一层是检测头,剪了直接影响分类和回归输出。
3. 从权重加载到剪枝执行:完整跑通流程
3.1 环境准备和依赖确认
这份源码基于 PyTorch 和 Ultralytics 的 YOLOv8 实现,Python 版本建议 3.8 以上。Ubuntu 20.04 上跑 CPU 版本也能完成剪枝和验证,只是速度慢一些。关键依赖包括 torch、ultralytics、numpy、pyyaml,如果要做稀疏训练还需要 apex 或者 torch 自带的 AMP。
# 创建虚拟环境 python3 -m venv prune_env source prune_env/bin/activate # 安装核心依赖 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics numpy pyyaml tqdm # 验证 ultralytics 能正常加载模型 python -c "from ultralytics import YOLO; m = YOLO('yolov8n.pt'); print(m.model)"最后一行如果打印出模型结构,说明基础环境没问题。注意这里用的是 CPU 版 torch,如果你有 GPU,把 index-url 换成对应的 cu 版本即可。源码本身不强制要求 GPU,但剪枝后的微调阶段有 GPU 会快很多。
3.2 加载预训练权重并构建依赖图
剪枝的第一步不是直接动刀,而是把模型结构和权重对应起来,构建通道依赖关系。源码里提供了一个build_dependency_graph函数,它会遍历模型的 named_modules,记录每个 Conv2d 的输入输出关系。
from prune.yolov8_pruner import YOLOv8Pruner # 初始化剪枝器 pruner = YOLOv8Pruner( model_path="yolov8n.pt", # 预训练权重 data_cfg="coco8.yaml", # 数据集配置, 用于剪枝后验证 device="cpu" # 有 GPU 改成 "cuda:0" ) # 构建通道依赖图 pruner.build_graph() print(f"共识别 {len(pruner.conv_layers)} 个可剪枝卷积层") print(f"保护层数量: {len(pruner.skip_layers)}")build_graph内部会处理 C2f 的 split 分支和 concat 节点,把需要同步剪枝的层归到同一个 group。skip_layers默认包含第一层和检测头,你也可以在配置里追加。这一步的输出信息很重要:如果可剪枝层数量和官方结构对不上,说明模型加载有问题,后面剪枝肯定出错。
3.3 执行剪枝并导出模型
依赖图建好后,就可以按配置执行剪枝。源码支持一次性剪枝和迭代剪枝两种模式,迭代剪枝每轮剪一点,中间插入微调,精度恢复更好。
# 执行剪枝 pruner.prune( strategy="lamp", target_sparsity=0.5, iterative=True, # 开启迭代剪枝 num_iterations=3, # 分3轮剪 finetune_epochs=5, # 每轮剪完微调5个epoch data_cfg="coco8.yaml" ) # 导出剪枝后的模型 pruner.export("yolov8n_pruned.pt") print("剪枝完成, 模型已保存")iterative=True时,每轮剪枝率大约是1 - (1 - target_sparsity) ** (1/num_iterations),三轮下来累计到 50%。finetune_epochs设 5 是保守值,如果数据集小可以加到 10 到 20。导出后的模型可以直接用 Ultralytics 的接口做推理验证:
from ultralytics import YOLO model = YOLO("yolov8n_pruned.pt") results = model.val(data="coco8.yaml") print(f"剪枝后 mAP50: {results.box.map50}")如果 mAP 掉超过 3 个点,说明剪得太狠或者微调不够,需要回退调整target_sparsity或增加微调轮数。
4. 避坑与排查:剪枝路上最容易翻车的五个地方
4.1 现象:剪枝后模型加载报通道数不匹配
原因通常是 concat 节点两边的通道数没有同步剪。YOLOv8 的 neck 部分有大量跨层拼接,如果只剪了其中一支,另一支没动,concat 后维度就对不上。源码里虽然做了 group 管理,但如果你手动改了layer_skip或者用了自定义模型结构,可能破坏分组逻辑。
解决办法是剪枝前打印依赖图,确认每个 concat 节点的输入层都在同一个 group 里。遇到报错时,用pruner.debug_graph()输出分组详情,检查是否有孤立层。
4.2 现象:剪枝后推理速度没提升甚至变慢
结构化剪枝理论上应该减少计算量,但如果剪枝率太低(比如低于 20%),或者剪掉的层集中在计算量小的浅层,实际加速效果可能被框架开销吃掉。更常见的原因是导出模型时没有真正移除通道,只是把权重置零了。
检查方法是看导出模型的参数量和 FLOPs,源码提供了统计接口:
pruner.summary() # 输出剪枝前后参数量、FLOPs对比如果参数量降了但 FLOPs 没降,说明剪枝没有作用到实际计算图上,需要检查prune_type是否误设成了 unstructured。
4.3 现象:微调时 loss 震荡不收敛
剪枝相当于给模型做了一次结构破坏,微调时学习率如果还沿用原始训练的大学习率,很容易震荡。源码默认把微调学习率设为原始训练的十分之一,但如果你自己接训练脚本,记得手动调低。
另一个原因是 BN 层的 running_mean 和 running_var 在剪枝后没有重置。剪枝改变了通道数,旧的 BN 统计量已经不适用了。源码在剪枝后会调用reset_bn(),如果你用的是自己改的版本,记得加上这一步。
4.4 现象:非结构化剪枝后模型大小没变
非结构化剪枝只是把权重置零,存储时如果还是稠密格式,文件大小不会变。要拿到实际压缩收益,需要配合稀疏存储格式或者导出到支持稀疏推理的后端。源码里提供了to_sparse()接口,但只在特定后端有效。
如果你只是想做实验对比,可以忽略文件大小,看推理时的实际稀疏加速比。如果部署目标是嵌入式端,建议直接用结构化剪枝,别在非结构化上折腾。
4.5 现象:剪枝后小目标检测精度掉得厉害
YOLOv8 的 P3 特征图负责小目标检测,如果剪枝时把 P3 分支的通道剪太多,小目标召回率会明显下降。源码默认对 P3 相关层做了保护,但保护力度可以调。
在配置里把layer_skip加上 P3 对应的层名,或者单独设置p3_sparsity低于全局值。我一般会把 P3 分支的剪枝率控制在全局的一半左右,精度和速度平衡得比较好。
5. 进阶技巧:剪枝率搜索与精度恢复的实操细节
5.1 用二分搜索找最优剪枝率
手动调target_sparsity很费时间,我一般会写个简单的二分搜索:先试 0.3 和 0.7,看精度掉点情况,然后逐步逼近可接受的最大剪枝率。源码提供了evaluate_sparsity接口,可以快速跑一轮验证。
from prune.utils import evaluate_sparsity # 定义可接受的精度下限 min_map50 = 0.35 # 假设原始 mAP50 是 0.45, 允许掉 10 个点 # 二分搜索 low, high = 0.1, 0.8 best_sparsity = 0.0 for _ in range(5): # 迭代5次足够 mid = (low + high) / 2 map50 = evaluate_sparsity(pruner, mid, data_cfg="coco8.yaml") if map50 >= min_map50: best_sparsity = mid low = mid else: high = mid print(f"sparsity={mid:.2f}, mAP50={map50:.4f}") print(f"推荐剪枝率: {best_sparsity:.2f}")这个搜索过程每次都要重新剪枝和验证,比较耗时,但比盲猜靠谱。如果数据集小,可以把验证集比例调小,加快搜索速度。
5.2 精度恢复的微调策略
剪枝后的微调不是简单跑几个 epoch 就行,有几个细节直接影响恢复效果。第一是学习率 warmup,剪枝后模型权重被破坏,直接上大学习率容易崩,源码默认加了 500 步 warmup。第二是数据增强,剪枝后模型容量变小,过强的增强反而有害,建议把 mosaic 和 mixup 的概率调低。第三是 EMA,剪枝后 EMA 的衰减率要重新设,源码里默认用 0.999,如果微调轮数少可以调到 0.99。
# 微调配置示例 finetune_cfg = { "lr0": 0.001, # 初始学习率, 比原始训练低一个量级 "warmup_epochs": 1, # warmup 轮数 "mosaic": 0.5, # 降低 mosaic 概率 "mixup": 0.0, # 关闭 mixup "ema_decay": 0.99, # EMA 衰减率 "epochs": 20 # 微调总轮数 }这套配置在 coco8 小数据集上跑,剪枝 50% 后 mAP50 能恢复到原始模型的 95% 左右。如果数据集大,恢复效果会更好。
5.3 验证剪枝模型是否真的能部署
剪枝完的模型最终要落到板子上,导出格式很关键。RK3588 部署一般走 ONNX 转 RKNN,源码提供了导出 ONNX 的接口:
pruner.export_onnx("yolov8n_pruned.onnx", opset=12, simplify=True)导出后建议用 onnxruntime 跑一遍推理,确认输出和 PyTorch 一致。如果 ONNX 输出对不上,大概率是剪枝时某个 concat 节点的通道顺序变了,需要回查依赖图。
从那以后我每次剪枝完,都会强制走一遍「PyTorch 验证 → ONNX 导出 → onnxruntime 推理 → 对比输出」这个流程,少一步都可能在上板时翻车。希望帮到你。
本文还有配套的精品资源,点击获取