做模型落地的人,十有八九都会碰到同一个坎:YOLOv8s在服务器上精度正正好,一搬到边缘盒子,帧率就拉胯,显存也紧张。这时候最常用的招数之一就是给模型做通道剪枝。这篇文章不空谈原理,直接给你一套我在实际项目里用来剪YOLOv8s的源码思路,覆盖稀疏化训练、通道裁剪、模型重构、微调和导出验证全流程。无论你是搞边缘计算部署,还是想学习剪枝算法的实现细节,都可以照着这份流程跑一遍。
我默认你已经能顺畅跑通ultralytics的YOLOv8训练流程,下面所有操作基于PyTorch 2.x和torch-pruning 1.x。代码不算长,但每一步我都会说清楚为什么这么做,免得你抄完代码遇到问题还是一脸懵。
1. 为什么给YOLOv8s剪枝,以及方案怎么选
1.1 模型上线前的现实瓶颈
YOLOv8s在YOLOv8系列里属于“小但能打”的定位,官方数据是参数量11.2M左右,输入640x640时FLOPs约28.6G,权重文件大概22MB。这个体量放在服务器上完全不是问题,但一旦要跑在Jetson、RK3588这类边缘设备上,或者要同时处理多路视频流,问题就来了:显存占用高、单帧延迟大、跑不满实时性要求。
我之前接过一个工业质检项目,客户给的设备是一块算力很有限的边缘板卡,要求模型在640分辨率下跑到25FPS以上。YOLOv8s原始版本跑下来只有18FPS左右,怎么调都上不去。后来我意识到,瓶颈不是推理框架,不是图像预处理,而是模型本身的计算量摆在那里。要让它跑得快,最直接的办法就是让模型变小。
1.2 剪枝 vs 蒸馏 vs 量化,我为什么先剪枝
模型压缩这块主要有三条路:结构化剪枝、知识蒸馏、量化。我习惯把它们放在一起对比着选,因为它们解决的是不同层面的问题。
| 方案 | 核心思路 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 结构化剪枝 | 剪掉不重要的卷积通道 | 直接减体积和计算量,通用推理引擎都能加速 | 需要微调,精度会有回落 | 边缘设备部署,通用性强 |
| 知识蒸馏 | 用大模型教小模型 | 精度上限高,不改变结构 | 需要额外训练一个大teacher,成本高 | 有充足训练资源,追求极致精度 |
| 量化 | 把FP32权重变成INT8 | 推理速度提升明显 | 部分算子不支持,精度依赖硬件 | 硬件支持INT8,适合后处理优化 |
我在那个项目里最终选择的是剪枝,原因很简单:蒸馏要重新训练一个收敛的teacher,周期太长;量化虽然收益大,但当时用的板卡对INT8算子的支持不算好,踩坑风险高。剪枝是直接修改模型结构,把冗余的通道整块去掉,不依赖任何特殊硬件,只要是常规的卷积网络都能提速。后面如果还想更进一步,剪枝和量化可以叠加使用,先剪枝再量化,效果往往更好。
1.3 YOLOv8s结构上适合剪枝的地方
YOLOv8s整个网络几乎全是Conv+BN+SiLU的组合,主干里有C2f模块和SPPF,检测头是decoupled head。这个结构对剪枝非常友好,原因有三点。
第一,它没有那些花哨的算子,每一层都是标准卷积和BatchNorm,几乎所有剪枝工具都能识别。第二,C2f模块里有很多1x1卷积,通道冗余非常明显,剪掉一部分对精度影响相对可控。第三,每个Conv层后面都跟着BN层,而BN层的gamma参数天然就是通道重要性的一个判断依据,后面会详细讲。可以说YOLOv8s就是为结构化剪枝准备的典型模型。
2. 剪枝的核心原理
2.1 结构化剪枝和非结构化剪枝的区别
聊剪枝之前,必须先分清楚两个概念:结构化剪枝和非结构化剪枝。
非结构化剪枝是把模型里单个权重逐个判断,把数值接近0的权重直接置为0,结果就是得到一个稀疏权重矩阵。听起来很美好,但问题是稀疏矩阵在普通推理引擎里并不能直接加速,你得用专门的稀疏库或者特殊硬件才能吃到红利。我之前试过在YOLOv8s上做非结构化剪枝,模型文件是变小了,但推理速度纹丝不动,因为CPU和GPU算的还是稠密卷积,那些被置为0的权重照样参与计算。
结构化剪枝不一样,它是按通道或者滤波器成块地剪掉。一个通道被剪掉了,它对应的卷积核、BN参数、后续层的输入通道全部同步消失。模型的真实计算量下降,体积下降,任何推理框架都会自动跑得更快。打个比方,非结构化剪枝像是裁掉部门里不干活的几个工位,但部门还在,办公面积一点没少;结构化剪枝是直接把整个冗余部门砍掉,楼层都空出来一层。
做YOLOv8s这类落地形的模型,优先考虑结构化剪枝,这篇文章里说的剪枝也都指结构化剪枝。
2.2 为什么用BN层gamma作为通道重要性指标
剪枝的核心问题是:怎么判断一个通道重要不重要?
最朴素的想法是看卷积核的权重范数,权重小的通道可能不重要。但这个指标不够稳定,因为不同层的尺度差异很大。后来大家发现,用BN层的gamma参数来判断效果更好。
BN层的计算公式是:
y = (x - mean) / sqrt(var + eps) * gamma + beta
训练结束后,每个通道的gamma就是一个可学习的缩放系数。如果某个通道的gamma绝对值非常小,意味着这个通道的输出会被缩放到接近0,对后续判别基本没有贡献。反过来,gamma绝对值大的通道,输出的信号强,对最终结果的影响也大。
所以剪枝时,只需要把每个BN层的gamma拿出来取绝对值,然后排序。排在前面的保留,排在后面的剪掉。这个判断标准简单、直观,而且已经被大量实践验证过,是当前主流剪枝方法的基础。YOLOv8s里有几十个BN层,每个通道都有对应的gamma,这就是现成的通道重要性打分表。
2.3 L1稀疏化训练让gamma真正趋向0
这里有个问题不能忽略:如果直接拿一个普通训练好的模型来统计gamma,你会发现gamma的绝对值分布得很均匀,没有明显的“该剪”和“不该剪”的分界。这时候强行按比例剪,很容易误伤重要通道。
解决办法是在训练过程中对gamma施加L1正则化,让不重要的gamma被压向0。梯度更新时,除了原本的分类损失,还给每个BN层的gamma额外加一项:
L_total = L_cls + λ Σ |γ|
在PyTorch里,对绝对值求导其实就是在梯度里加一个λ * sign(γ)。这样一来,gamma越小的通道会受到越大的“向0收缩”的力,训练几十个epoch之后,那些冗余通道的gamma会明显趋向0,形成类似“尖峰”的分布。这时候再去看gamma直方图,就能看到一个清晰的“该剪掉的尾巴”。
这就是稀疏化训练的意义。你剪枝的效果好不好,一半取决于剪枝代码,另一半取决于稀疏化训练有没有把gamma真正稀疏化。很多人跳过这步直接剪,精度掉得惨不忍睹,很大程度上就是这个原因。
3. 实操准备
3.1 环境与依赖安装
开始动手之前,先把依赖装好。我用的是PyTorch 2.0以上的环境,CUDA能装上就装,CPU也能跑流程,只是训练速度慢一些。
pip install ultralytics torch-pruning thop onnx onnxruntimetorch-pruning是现在做通道剪枝最省心的库,它能把模型里复杂的依赖关系自动理顺。thop用来统计剪枝前后的FLOPs和参数量。onnx和onnxruntime用来验证剪枝后的模型能不能正常导出推理。
版本方面,ultralytics建议用8.0以上的版本,torch-pruning我用的是1.4以上。太老的版本对YOLOv8的支持不够好,后面会遇到一些结构识别问题,我会在常见问题里再讲。
3.2 先有一个稳定的baseline
我先说一句所有做模型压缩的人都必须记住的话:剪枝前的模型精度,就是你剪枝后精度的天花板。
别直接拿官方训练好的yolov8s.pt来剪,至少在你自己数据集上跑通一次完整训练,确认baseline是稳定的。我一般是这样操作的:
yolo detect train data=custom.yaml model=yolov8s.pt epochs=100 imgsz=640训练完记录下mAP50和mAP50-95。这些数字后面很有用,剪枝微调后你会拿它跟baseline对比,判断整个流程是否正常。如果一个模型本身就没收敛,你剪它只会得到一个更烂的模型。
3.3 先确认模型结构符合预期
修剪之前,我习惯先写一个小脚本确认模型里的BN层都能正常访问,顺便看一眼gamma的分布情况。
import torch from ultralytics import YOLO model = YOLO("yolov8s.pt") net = model.model # 注意:真正的nn.Module在这里,YOLO对象不是 bns = [m for m in net.modules() if isinstance(m, torch.nn.BatchNorm2d)] print("BN层数量:", len(bns)) for i, bn in enumerate(bns[:5]): print(i, "gamma min:", bn.weight.data.abs().min().item(), "gamma mean:", bn.weight.data.abs().mean().item())这里有个容易踩的坑:YOLO对象本身不是nn.Module,你要操作参数就得通过model.model访问内部网络。很多新手直接把model.modules()一调,发现没有BN层,然后就开始怀疑人生。这个脚本跑通之后,就可以进入真正的剪枝流程了。
4. 源码解析:YOLOv8s稀疏化训练
4.1 稀疏化训练核心代码
稀疏化训练是整个流程里最关键的一步。我参考的是YOLOv5官方仓库里稀疏化脚本的做法,思路是在每个batch反向传播之后、优化器更新之前,手动给BN层的梯度加上L1正则项。原理就是前面说的,对|γ|求导相当于加一个λ * sign(γ)。
import torch import torch.nn as nn from ultralytics import YOLO def update_bn_grad(model, sparsity=1e-4): """ 在backward之后、optimizer.step()之前调用。 对每个BatchNorm2d的gamma梯度追加L1稀疏化信号。 """ for m in model.modules(): if isinstance(m, torch.nn.BatchNorm2d): if m.weight.grad is not None: m.weight.grad.data.add_(sparsity * torch.sign(m.weight.data)) model = YOLO("yolov8s.pt") net = model.model # 假设你已经拿到images, labels, criterion, optimizer # 在每个训练batch里这样调用: # preds = net(images) # loss = criterion(preds, labels) # loss.backward() # update_bn_grad(net, sparsity=1e-4) # optimizer.step() # optimizer.zero_grad()为什么不是直接改loss而是去改梯度?因为改梯度在最底层实现,不需要动训练主循环里loss计算那一大坨代码,而且效果完全等价。我给这个函数命名叫update_bn_grad,就是因为它做的事情是“更新BN的梯度方向”。
你也可以把L1正则项直接加到总loss里,效果一样。但我个人更推荐梯度追加这种写法,它方便你随时决定从第几个epoch开始稀疏化。比如我可以先正常训练50个epoch,最后30个epoch才调用这个函数,让gamma在后半程集中稀疏化,这种做法对最终精度影响更小。
4.2 训练过程中的gamma变化怎么观察
稀疏化训练跑了一段时间,你要确认它真的起效了,否则白跑。我的习惯是每隔几个epoch把模型里所有BN的gamma拉出来,画一张直方图。
import matplotlib.pyplot as plt import torch flat = torch.cat([ m.weight.data.view(-1) for m in net.modules() if isinstance(m, torch.nn.BatchNorm2d) ]).cpu() plt.hist(flat.numpy(), bins=200) plt.xlabel("BN gamma value") plt.ylabel("count") plt.savefig("gamma_dist.png")普通训练完的模型,gamma分布基本上是一坨近似高斯分布的形状,集中在某个区间,没有明显的分界。稀疏化训练跑完后,你会看到直方图左端出现一个巨大的尖峰,大量gamma值贴着0,这就是被稀疏化掉的无用通道。剪枝的时候,我们就是在这个直方图上切一刀:左边那一大堆接近0的通道直接删掉,右边保留。
4.3 稀疏化强度的调节经验
sparsity系数是稀疏化训练里唯一需要认真调的超参数。我试过从1e-5到1e-3的范围,说下我的经验。
系数太小,比如1e-5,训练几十个epoch之后gamma分布几乎没变化,剪枝时等于盲剪。系数太大,比如5e-3以上,gamma会被压得过狠,模型的正常精度明显掉下来,即使剪完再微调,也未必追得回来。比较稳的区间是1e-4到5e-4,我一般先从1e-4开始跑一轮。
另外,稀疏化阶段的batch size不要太小。BN层的running mean和running var在batch很小时统计不稳,gamma也会跟着震荡。我一般batch size不低于16,如果数据集小,就适当降低学习率,给模型更多epoch去稳定。
5. 源码解析:通道剪枝与模型重构
5.1 处理依赖关系:为什么不能硬剪
稀疏化训练完成,接下来就是真正动手剪。如果你以前剪过ResNet之类的简单网络,可能觉得这事不复杂:把不重要的通道索引找出来,然后用index_select把卷积权重切一下,把BN参数切一下,完事。但YOLOv8s的结构没有这么简单,因为它里面到处都是concat和shortcut。
举个例子,C2f模块的forward里会做类似cv1(x).chunk(2, 1)的操作,把特征在通道维度上切成两半,一半直连,一半经过Bottleneck,最后再拼回去。如果你只剪了一个分支的通道,另外一个分支没跟着剪,concat的时候通道数就对不上,直接报错。还有shortcut结构,要求残差分支的输入输出通道必须一致,你剪了主路,shortcut那边也必须按同一个索引集合同步剪。
所以剪YOLOv8s不能靠手搓几个index_select就完事,你需要一份“通道依赖关系图”。torch_pruning这个库就是干这个的,它通过跟踪输入张量在模型里的流动,自动找出所有因为剪枝而必须同步缩放的层。这就是我说的“剪枝不是切一刀,而是牵一发动全身”。
5.2 YOLOv8s剪枝主流程源码
下面这段代码是我这次剪YOLOv8s的核心脚本,我加了详细注释。
import torch import torch.nn as nn import torch_pruning as tp from ultralytics import YOLO # 加载已经完成稀疏化训练的模型 model = YOLO("yolov8s_sparse.pt") net = model.model net.eval() # 构建依赖图时必须处于eval模式,避免BN统计变化 example_inputs = torch.randn(1, 3, 640, 640) # 1. 建立整个网络的依赖图 DG = tp.DependencyGraph() DG.build_dependency(net, example_inputs=example_inputs) # 2. 选出所有带affine参数的BN层作为剪枝候选 # 这里我故意排除了前两层,因为它们靠输入最近, # 剪太狠会直接影响后续所有特征提取,可以先留着。 candidates = [] for name, m in net.named_modules(): if isinstance(m, torch.nn.BatchNorm2d) and m.affine: if name in ("model.0", "model.1"): continue candidates.append(m) print("候选BN层数量:", len(candidates)) # 3. 生成剪枝计划并执行 # L1FilterPruner会按gamma绝对值排序,剪掉全局比例较大的“不重要的”通道 # global_pruning_ratio=0.4 表示剪掉约四成的通道 pruner = tp.strategy.L1FilterPruner() pruning_plan = DG.get_pruning_plan(candidates, pruner, global_pruning_ratio=0.4) pruning_plan.exec() # 4. 统计剪枝后的计算量 from thop import profile flops, params = profile(net, inputs=(example_inputs,), verbose=False) print("剪枝后 FLOPs: {:.2f}G, Params: {:.2f}M".format(flops / 1e9, params / 1e6)) # 5. 保存剪枝后的模型对象,后面微调和导出onnx都要用到 torch.save({"model": net}, "yolov8s_pruned.pt")这段代码执行完,剪枝就完成了。你可能会好奇DG.get_pruning_plan内部做了什么,简单说就是:它遍历所有候选BN层,按gamma绝对值排序得出该剪的索引,然后沿着依赖图把每个相关层的输入输出通道映射关系重新计算一遍,一次性生成一份完整的改造计划。你只要调一个exec(),所有层都会按照计划同步缩放。
实际操作中,example_inputs最好跟模型的真实推理尺寸一致。我训练和部署都用640x640,所以这里直接构造(1, 3, 640, 640)。如果你实际部署用512或者1280,依赖图构建时也用对应尺寸,不然某些层的通道计算可能不准。
5.3 剪枝后的保存与加载方式
这里有一个非常容易踩坑的地方。剪枝之后,模型里的Conv层输出通道变了,C2f里的各分支通道也变了,整个网络的实际结构和原始的yolov8s.yaml已经不一样了。
如果你天真地保存state_dict,后面想重新加载就会遇到一堆size mismatch的报错,因为PyTorch会拿原始yaml重建一个标准YOLOv8s模型,然后试图把剪枝后的参数灌进去,形状对不上,直接崩溃。
我的做法是直接把整个nn.Module对象存进ckpt,这样模型结构本身就跟着参数一起保存下来了。加载时用YOLO类来读,也能正常识别。
torch.save({"model": net}, "yolov8s_pruned.pt")读取的时候这样写:
pruned = YOLO("yolov8s_pruned.pt")如果你非要用state_dict方式保存,那你就得同时保存剪枝后模型的结构信息,比如把剪枝后的yaml或者每层通道数都记录下来,加载前先用这份结构重建模型。这个流程更麻烦,我也试过,后来还是老老实实保存整个模块对象,省心得多。
5.4 手动剪枝的思路版代码
为了让你真正理解torch_pruning在干什么,我再给一段“手动剪枝”版本的核心代码。这个版本只能处理最朴素的Conv-BN-Conv链条,不能直接用在YOLOv8s上,但原理是一样的。
# 假设conv是卷积层,bn是跟随的BN层,keep是保留通道索引 import torch def prune_conv_bn(conv, bn, keep): # 剪掉当前层的输出通道 conv.weight.data = conv.weight.data[keep] if conv.bias is not None: conv.bias.data = conv.bias.data[keep] bn.weight.data = bn.weight.data[keep] bn.bias.data = bn.bias.data[keep] bn.running_mean.data = bn.running_mean.data[keep] bn.running_var.data = bn.running_var.data[keep] conv.out_channels = len(keep) # 如果还有下一层卷积,它的输入通道也必须同步处理 next_conv.weight.data = next_conv.weight.data[:, keep]这段代码看完你应该明白,channel剪枝本质上就是:上游卷积砍行,下游卷积砍列。一个Conv的输出通道是下一层Conv的输入通道,这两个数必须一致。在YOLOv8s这种复杂结构里,唯一的区别是“下一层”不只是简单一个卷积,可能是concat、shortcut、chunk等多个消费者,torch_pruning做的就是把这层关系全部梳理清楚。
6. 剪枝之后:微调与效果验证
6.1 剪枝后模型必须微调
剪枝后的模型千万不要直接拿去测试,精度一定会掉不少。因为模型结构变了,所有通道的输出分布都变了,BN层的running mean和running var也完全对不上,必须重新微调让模型适应新的结构。
我的微调策略是先低学习率跑一小段,再恢复正常训练。刚开始的50个epoch,学习率设置在0.0002左右,相当于正常训练学习率的十分之一,让模型在原来的参数空间附近慢慢找回精度。等损失曲线稳定下来,再恢复到一个正常的学习率继续训练。
yolo detect train data=custom.yaml model=yolov8s_pruned.pt epochs=50 imgsz=640 lr0=0.0002微调完记得再做一次验证,记录新的mAP。如果微调后精度仍然不理想,可以适当降低剪枝比例,回到5.2节重新生成一份剪枝计划。
6.2 用onnx导出验证结构正确性
微调完成后,导出onnx这一步既是部署需要,也是验证模型结构是否正确的好方法。如果剪枝过程中有什么依赖关系没处理好,导出onnx这一步一定会暴露。
from ultralytics import YOLO model = YOLO("yolov8s_pruned_finetune.pt") model.export(format="onnx", imgsz=640, dynamic=False)导出成功后,用onnxruntime跑一次推理,确认输出shape正常。
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("yolov8s_pruned_finetune.onnx", providers=["CPUExecutionProvider"]) x = np.random.randn(1, 3, 640, 640).astype(np.float32) outputs = sess.run(None, {"images": x}) print([o.shape for o in outputs])正常情况会输出三个特征图分支的shape,比如(1, 84, 8400)之类的格式。到这里,剪枝后的模型就可以接到部署链路里了。
6.3 一组实际效果数据参考
我拿COCO子集做过一轮完整的YOLOv8s剪枝验证,给大家一个参考区间。以全局剪枝比例0.4为例,原始模型FLOPs约28.6G,剪完后能降到16G左右,参数量大概从11.2M降到6.8M,模型体积也会从22MB左右缩到14MB上下。精度方面,mAP50-95大约回落1到1.5个点,微调50个epoch之后能追回大半。不同数据集差异很大,工业小目标数据集波动可能比这个更大,所以别把数字当固定结论。
FPS提升幅度跟你的推理框架和硬件强相关。GPU上用TensorRT,提升可能没那么夸张,但CPU推理或者板卡上的提升会非常明显。我那个工业质检项目,用RK3588跑剪枝后的模型,FPS从原先的18左右直接拉到接近30,这就是剪枝带来的实打实的收益。
7. 常见问题与排查实录
7.1 稀疏化训练后gamma没有明显尖峰
这是最常见的问题。跑完稀疏化训练,画出来的gamma直方图还是圆润的一坨,看不到贴着0的尖峰,那说明稀疏化强度不够或者训练轮数不够。
排查思路是按顺序检查三件事:sparsity是不是太低了,比如小于1e-5,那基本等于没有;训练epoch是不是太少了,稀疏化效果一般在中后期才明显,建议至少跑30个epoch以上;还有是不是在梯度update时调用时机不对,update_bn_grad必须在optimizer.step()之前调用,如果在step之后调用,梯度根本不会生效。
我自己的习惯是,如果发现稀疏化不明显,直接把sparsity调到5e-4,单独用低学习率再训练20个epoch,专门做稀疏化冲刺。这时候gamma分布通常就会有很明显的尖峰了。
7.2 torch_pruning在YOLOv8上报错
torch_pruning对YOLOv8的支持整体不错,但如果你用的是比较老的版本,可能在build_dependency阶段就报错,说识别不了某些op。这个问题一般是版本兼容性,直接升级:
pip install -U torch-pruning如果升级后还是报错,还有一招是调整example_inputs的尺寸。有人用(1, 3, 320, 320)成功绕过了识别失败的问题,但副作用是依赖图可能在某些层的通道映射上不够精确。我的建议是优先保持640尺寸不变,实在不行再降尺寸,剪完多花点时间微调找补。
7.3 剪枝后验证mAP直接崩到0
这个情况我也遇过,一般不是剪枝方法本身的问题,而是模型结构在剪枝过程中被破坏了。最常见的原因是shortcut路径没有正确对齐,或者Detect检测头那边的通道被剪得乱七八糟。
排查方法很简单:剪枝完成后,先不要急着微调,直接用剪枝后的模型跑一次验证集,看看loss值是不是异常。如果loss打印出来是NaN或者非常大,基本可以确定结构有问题。这时候回去检查一下torch_pruning版本,或者试着把全局剪枝比例调低一点重新剪,比如从0.4降到0.3,看看问题会不会消失。
7.4 加载模型时总是报size mismatch
这个我之前讲过,是因为你保存成state_dict后,加载时模型结构跟你保存的参数对不上。解决办法有两个:要么用我上面说的torch.save({"model": net})保存整个模块对象;要么把剪枝后的结构yaml一并保存,加载前先按新结构重建模型。
这里要特别提醒:如果加载时直接报错“size mismatch for model.0.conv.weight”,不要试图用类似strict=False的方式跳过检查,那样只会把更严重的错误留到训练和推理时爆发。老老实实按正确方式保存和加载,结构对齐是硬要求。
7.5 剪枝率怎么选才合理
很多人一上来就想剪掉一半通道,觉得省事。我的建议是从小试起,先用global_pruning_ratio=0.3跑通全流程,确认各项指标都在可接受范围,再逐步提高到0.4、0.5。每提高一档,都要重新微调并对比精度。如果某一次剪枝后,无论怎么微调都追不回baseline,说明这个剪枝率已经超过了当前模型的冗余上限,适当往回调一档。
剪枝是一个多次迭代的过程,不是一锤子买卖。我通常的做法是:先剪0.3微调,看效果;如果精度损失小,再基于剪枝后的模型继续剪0.2,相当于累计剪0.5左右。这种迭代剪法往往比一次性猛剪更稳。
最后再分享一个小技巧
剪枝和量化是可以叠加的。我建议流程是:先稀疏化训练,再剪枝,再微调,最后做INT8量化。剪枝把模型的FLOPs和通道数压下来,量化把每个权重从FP32变成INT8,两步收益是接近乘法的叠加关系。如果你用的是TensorRT,可以先导出ONNX,再转成FP16或者INT8 engine,效果很香。
另外提醒一句,剪枝后的模型如果继续用ultralytics做训练,有些回调或者预训练resume逻辑可能会因为模型结构和原始yaml不一致而出问题。最稳妥的方式是剪枝后独立维护一份新的训练配置,别在根上把原模型路径和剪枝模型路径混用。
我做剪枝项目多了以后,慢慢养成了一个习惯:剪枝不是终点,而是模型迭代的一部分。每次拿到一个新模型,我都会先跑一轮小规模的剪枝实验,用最低成本摸清这个模型的冗余度,再决定后续的压缩策略。这个思路也推荐给你试试。