检测模型做压缩,剪枝是条性价比极高的路子。别的不说,光是不用换推理框架、不用动硬件,就能白拿一波推理速度,这点就足够让人优先考虑它了。我当时接手的一个目标检测项目,骨干网络用的就是ResNet34,视频流任务对延迟卡得很死,GPU上跑到40 FPS没用,换成边缘设备立刻跌到15 FPS,根本没法上线。折腾一圈之后,是靠剪枝算法把模型瘦下来的,之后又叠了一层量化,最终体积砍掉一半多,帧率翻了两倍多,精度只掉了一个点左右。这篇就把整个思路、原理、实操流程和踩过坑的细节都复盘一遍,适合正在做检测模型部署、被推理性能卡住、又不想换网络架构的人参考。
1. 先想明白一件事:检测模型到底是被什么拖慢的
1.1 部署场景里的三重约束
训练阶段跑模型,我们关心的是精度和收敛速度,但部署阶段完全是另一套逻辑。端侧设备、边缘盒子、车载平台,这些地方要同时面对三重约束:
第一是算力。GPU上的CUDA核心和边缘芯片的NPU完全是两种东西,模型里的卷积操作在GPU上可能被高效并行,到了端侧就是老老实实逐层算,浮点运算量大一点都看得见。第二是显存带宽。检测模型不仅仅是参数量大,中间特征图的尺寸也很大,一张1080P的图走到特征金字塔,每层特征图都要占内存,带宽不够的时候,数据搬运时间比计算时间还长。第三是延迟上限。视频流每秒25帧,一帧的推理预算只有40毫秒,去掉前后处理,真正给到模型的可能就20毫秒,稍微慢一点就直接掉帧。
剪枝解决的正是这三重问题。通道剪枝直接让卷积层的输入输出通道变少,FLOPs降下来,特征图通道数也降下来,内存占用跟着降,延迟自然就下来了。它不改变数值精度,也不依赖特殊硬件,在常规推理框架里就能看到加速效果,这也是剪枝在模型压缩技术里特别实用的原因。
1.2 模型压缩技术全家桶里,剪枝处在什么位置
很多文章喜欢把压缩技术分成量化、剪枝、知识蒸馏、低秩分解四类,但实际项目里它们不是竞争关系,而是互补关系。
量化的思路是降低数值精度,FP32变FP16或者INT8,优点是直接白嫖加速,缺点是敏感层经常掉点,需要校准甚至重训练。知识蒸馏是让一个大的teacher模型把知识教给小的student模型,精度上限高,但训练复杂度大,得先有个好teacher。低秩分解是把权重矩阵分解成两个小矩阵,理论压缩率高,但卷积实现和硬件优化都不好做,实际收益常常打折扣。这里反而就属剪枝直接、可控性强。剪枝是先把结构变稀疏,和量化完全不冲突,两个叠加起来就是常见的“剪枝+量化”组合拳。
提示:剪枝和量化经常一起做,因为它们分别从“结构”和“精度”两个维度瘦身,互不干扰。除非设备对INT8支持太差,否则推荐两个都上。
2. 剪枝原理:网络减掉一部分参数,凭什么不影响精度
2.1 模型里的冗余是常态,不是巧合
神经网络训练完之后,参数并不是全都“有用”的。你用L1范数或者L2范数把所有卷积核过一遍,会发现大量卷积核的权重值趋近于0,它们对输出的贡献非常小。原因也好理解:训练过程中,网络为了拟合数据,会把特征表达分散到很多神经元上,部分神经元承担主路径,部分神经元只是在某个样本上有微弱激活,统计意义上看就是冗余的。
学术上有过一个常见结论:训练好的网络里可能有50%甚至更多的权重可以被置零而不产生精度损失。这个数字听起来夸张,但做剪枝实验时你会真实感受到。我用ResNet34的backbone做敏感性分析时,把backbone最后一个stage里30%的通道直接删掉,mAP只掉了0.3%,那个瞬间你就明白,之前的算力确实是浪费了不少。
2.2 剪枝算法三步走:评估、删除、微调
剪枝不是一个动作,是一个流程,核心是三步:
第一步是评估重要性。给每个可剪单元(通道、滤波器或单个权重)算一个重要程度。常见指标有:权重绝对值范数、BN层缩放因子γ、基于梯度的泰勒展开值。以通道剪枝为例,如果通道对应的BN层γ值很小,说明这个通道的输出会被缩放得很小,删掉它对后续层的影响就有限。这个思想来自Network Slimming那篇工作,实操里特别好用。
第二步是删除。有两种做法,一种是直接物理删除通道,网络结构变了,输出通道数变了;另一种是先用mask把不重要的通道置零,网络结构不变,但计算图中产生了很多零值。前者是真正的结构化剪枝,后者是伪稀疏,后面要配合真正裁掉才能提速。
第三步是微调。删除参数之后,网络精度一定是掉的,需要重新训练几步甚至几十步,让剩下的参数重新适应。微调质量直接决定最终精度,很多时候不是剪枝本身掉点,而是微调没做好。
2.3 结构化剪枝 vs 非结构化剪枝:现实和论文要分开看
这里专门说说“非结构化剪枝”。它在学术论文里很常见,方法是把不重要的单个权重置零,不要求整个通道或者整个滤波器被删掉。好处是细粒度,精度损失小,高稀疏度下依然能保持很好的效果。但问题也很现实:大多数推理框架和硬件对稀疏矩阵的支持并不好,你剪完之后如果不配套稀疏卷积库,模型文件虽然变小了,推理延迟却几乎没变化。
结构化剪枝就不一样了,它按通道或滤波器整体删,网络变成一个小一点的密集网络,任何框架都能跑,速度提升明显。缺点是因为粒度粗,同参数压缩率下精度下降会更多一些。
我自己的选择很明确:部署项目优先结构化剪枝。对ResNet34这种标准结构,通道剪枝够用了,没必要为了多保零点几个点的精度去折腾稀疏推理支持。
| 对比项 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 整个通道/滤波器 |
| 精度保持 | 更好 | 略差,需更精细的策略 |
| 实际加速 | 依赖稀疏库 | 直接加速 |
| 部署通用性 | 差 | 好 |
| 典型场景 | 论文研究、专用硬件 | 工程落地、边缘部署 |
3. 检测模型剪枝:跟分类模型完全不是一回事
3.1 backbone、neck、head三部分要区别对待
很多人拿分类模型那套剪枝流程直接套到检测模型上,结果一剪就崩。分类模型整体都用于提取语义特征,剪哪里相对平等;检测模型结构上分为backbone、neck、head三个部分,敏感度差太多了。
backbone承担特征提取,冗余度最高,也最能剪。我当时对ResNet34的四个stage都做了剪枝测试,stage3和stage4可以剪到40%,stage1只能剪20%以内,越低层越敏感。原因是浅层特征偏向边缘、纹理这些通用特征,删多了底层信息就断了;深层通道更偏语义,冗余更多。
neck部分我做的是FPN,特征融合结构,这些层相当敏感。每一层都在做不同尺度特征的上采样和融合,通道删多了,跨层信息就对不齐了。neck部分我的建议是保守,最多不要超过20%。
head就是检测头,包括分类分支和回归分支,这部分我几乎不动。检测头的参数量虽然不大,但直接决定输出质量,尤其回归分支涉及坐标的精细调整,剪坏一个通道就可能导致大量框的位置偏移。
3.2 检测任务里的专有坑:小目标、正负样本不均、多尺度融合
检测模型剪枝还有一个分类模型从来没有的问题:目标有尺度差异。COCO数据集里AP_S、AP_M、AP_L是要分别看的。我在实验里就遇到过,整体mAP看起来只掉了0.8%,拆开一看AP_S掉了2.1%,小目标本来特征就弱,剪枝后特征图分辨率又降了,小目标直接就被放过去了。
正负样本不均也是一个问题。检测任务里绝大多数anchor区域是背景,如果剪枝让某些负责背景抑制的特征通道失效,网络就会输出大量假阳性框,导致precision下降比recall明显,这个在剪枝后很容易被忽略。
所以评估检测模型剪枝效果时,不仅要看mAP,最好拆开看AP50、AP75、AP_S、AP_M、AP_L,再配合PR曲线看整体形状有没有明显劣化。不看分项指标,很容易被一个总分糊弄过去。
3.3 剪枝之后,评价体系要跟着换
分类模型剪枝后,用Top-1 Accuracy一个指标就够了;检测模型可不行。除了上面说的尺度分项,还需要关注推理延迟是不是真的降了。理想情况下FLOPs和延迟应该同步下降,但实际里如果剪枝层正好不是计算瓶颈,延迟可能没变化。
具体到ResNet34这种结构,计算密集部分是stage3和stage4,通道剪这里,FLOPs减少非常明显,延迟也能体现。如果只减stage1或者只减head的通道,FLOPs降了但延迟几乎不动,因为浅层卷积在GPU上并行度高,减几个通道根本看不出来。所以检测模型里的剪枝,本质上是“把钱花在刀刃上”,选择哪些层的通道进行剪枝,比设定多少剪枝率更重要。
4. 实操记录:ResNet34检测模型从剪枝到量化全流程
4.1 第一步:基线先打牢,敏感性分析比拍脑袋靠谱
切不可上来就定剪枝率。建议先跑一遍完整基线,记录原始模型的参数量、FLOPs、内存占用、单帧延迟,以及mAP、AP50、AP_S、AP_M、AP_L这些指标。有了基线,后面每一步改动都有参照。
接着做敏感性分析。我当时的方法很简单:把backbone按stage分成几个区域,每个区域单独以10%、20%、30%的比例通道剪枝,不做微调直接评估精度掉落情况。这个实验结果能给出一个大概的“安全边界”。比如我看到ResNet34的stage3剪20%掉0.2%,剪30%掉0.8%,那就说明20%安全,30%勉强;而stage1剪20%就掉1.5%,这条信息直接指导后续全局剪枝率怎么安排。
这个步骤看起来很笨,但非常值得做。它避免了“全局统一剪枝率”这种省事但危险的做法。我见过太多人直接全局剪50%,结果neck或者浅层被剪崩了,然后再反过来找人帮忙调,费劲程度远大于先花半天做敏感性实验。
4.2 第二步:设计剪枝方案,这里有几个关键参数
我的方案是以结构化通道剪枝为主,重要性指标用BN层γ值。具体流程是:
- 先做稀疏化训练。给BN层的γ添加L1正则,让γ值变得稀疏,也就是一部分γ趋近于0,这部分通道就是候选删除对象。正则系数一般取1e-4到1e-3,太大精度崩,太小稀疏化效果不明显。我这边取的是5e-4,训练80个epoch,前面50个epoch正常训练,后面30个epoch打开正则,让γ慢慢稀疏下来。
- 然后按通道重要性做全局排序。全局排序比逐层排序效果好,因为它自动把“不重要”的通道分配到各个层,而不是每层都硬剪到同样的比例。
- 设置目标剪枝率。这里要分模块定,不能全局一个数。我的实际设置是:backbone整体剪35%,其中stage1只剪15%,stage2剪30%,stage3剪40%,stage4剪40%;neck剪20%;head完全不剪。
全部流程里最重要的是:不要边训练边物理删通道。先训练出稀疏化的γ,再生成mask,然后用mask把不重要的通道置零,再做短窗微调,最后才是真正导出小模型。直接删通道再从头训练,训练不稳定,精度也很难回来。
4.3 第三步:微调策略决定最终精度
剪枝后的微调是最容易被低估的一步。常见的错误是微调只跑几个epoch、学习率开得还很大,结果精度一直回不来。我在实操中总结出几个有效做法:
一是学习率要小。原始训练用0.01的SGD,微调时reducelr到0.001甚至0.0005,因为剩下参数基本已经具备特征提取能力,用大学习率会冲乱现有特征空间。二是加长微调时间。不要少于40个epoch,我这边跑了60个epoch才让mAP稳定回来。三是配合知识蒸馏。用未剪枝模型当teacher,对剪枝后的student做蒸馏,这个操作对检测模型尤其有效,每次能多拉回0.5到1个点的mAP。蒸馏损失权重可以放在0.1到0.3之间,取0.2时效果最好。
还一个细节是BN层统计量的重置。剪枝后BN层对应的通道变了,最好在微调开始时用一小批数据重新估计一下running mean和running variance,不然前几个epoch会因为统计量错乱出现精度跳水。
4.4 第四步:量化衔接实操
模型结构瘦下来之后,接着做量化。ReLU后的激活值分布、conv的权重分布都在FP32范围里,用INT8表示就得做min-max映射,这一步处理不好,剪枝省下来的精度空间可能会全亏进去。
我的做法是先做PTQ,也就是训练后量化。用500到1000张验证集图片做校准,统计每个激活层的min和max,然后映射到INT8。PTQ做完通常会有0.3到0.5个点左右的精度下降,可以接受。如果某些层掉点严重,就把敏感层保留FP16,也就是混合精度量化,而不是强行全部INT8。
要警惕的是直接在剪枝前做量化。剪枝改变的是网络结构,原本量化的统计量全都不准了。所以顺序一定是:先剪枝微调,再量化校准。另外,如果PTQ掉点超过1个百分点,那就要考虑QAT(量化感知训练),在训练时就模拟INT8的量化误差。我这边最终是用PTQ的,因为硬件支持比较好,但如果换更激进的INT8推理芯片,QAT会更稳。
最终结果可以给大家一个参考:原始模型参数约28MB,剪枝后约17MB,再量化后约4.5MB,压缩比接近6倍;GPU上单帧延迟从35ms降到18ms;mAP从36.4降到35.2,掉了1.2个点,但AP_S只掉了0.8,这个结果我认为是可接受的,因为换来的是部署端帧率从15 FPS升到32 FPS。
5. 预剪枝与后剪枝:两个经典概念怎么迁移到深度网络
5.1 预剪枝和后剪枝原本解决的是什么问题
预剪枝和后剪枝这两个名字最早出现在决策树算法里。很多机器学习的教材里都会提到:构建决策树的时候,如果边建树边判断当前节点分裂是否能带来泛化增益,不能就停止分裂,这叫预剪枝;另一种是把树完整建立起来,再自底向上把贡献不大的子树替换成叶子节点,这叫后剪枝。
在决策树场景里,预剪枝训练时间短、不容易过拟合,但可能欠拟合,因为“当前增益不大”不代表“后续子树增益不大”;后剪枝效果通常更好,因为它是基于完整结构做的评估,但训练成本更高。这个权衡逻辑,放到深度网络里其实也是成立的。
5.2 深度网络里两种思想的对应关系
深度网络的剪枝世界里,预剪枝思想体现在训练过程中。比如NAS或One-shot模型搜索,一开始就设计一个小网络,或者规定哪些层不参与训练,直接用搜索或训练策略决定最终结构,这种“边训练边定结构”的思路就是预剪枝。再比如前面提到的稀疏化训练,让γ趋近于0的过程,其实也在“训练过程中”悄悄进行结构选择,只是没有真正删除物理结构。
后剪枝就是最常见的流程:训练一个完整大模型,评估参数重要性,把不重要的删掉,再微调恢复精度。我们做检测模型压缩,用的就是后剪枝。这个思路的好处是:模型先用完整能力学到足够的特征表达,剪枝时知道哪些东西是冗余的,决策依据更可靠;缺点是前期的训练成本省不下来。
5.3 实际项目到底选预剪枝还是后剪枝
从我的实践看,常规部署项目基本选后剪枝。原因是深度学习模型的表达能力复杂,很难在训练一开始就准确判断哪些通道最终有用;预剪枝的“边训练边剪”策略在工程上也不容易实现,训练不稳定,还要额外调试很多超参。后剪枝反而稳定:模型已经训练好,做敏感性分析、做稀疏化微调,每一步都能看到指标变化,排查问题也方便。
如果你是为了探索极限压缩比、有充足训练资源,可以考虑预剪枝思路,比如更极端的稀疏正则或者渐进式剪枝,但需要做好掉点修复的成本准备。
6. 踩坑实录与常见问题速查
6.1 剪枝之后精度骤降,先检查BN层
这个坑我印象太深了。第一次剪完ResNet34的stage3,mAP直接掉了3个点,比敏感性分析时预估的掉点严重得多。排查半天,最后发现问题出在BN层:稀疏化训练时BN层的running mean和running variance还是原始模型的,通道删掉之后,剩余通道的统计量对应的分布变了,前几层输出的尺度被错误放大。解决办法就是在微调前用一批真实数据reset统计量,非常有效。
6.2 剪完掉的主要是AP_S,注意浅层特征保留
搞检测的应该有同感:普通分类任务剪枝后最多掉1个点,但检测模型一剪,小目标先崩。AP_S掉的比AP_M和AP_L多,是因为小目标本身在特征图上占据的像素少,能用的有效信息本来就集中在浅层高分辨率特征图上。所以剪枝策略里必须对浅层和下采样少的层更保守。我们后来在stage1只剪10%,FPN的P2层完全不动,AP_S的掉点就控制住了。
6.3 FLOPs降了很多,但帧率没提升
FLOPs和延迟不是一回事。剪枝如果只剪了参数量大但计算不密集的层,比如1x1卷积的前后过渡层,FLOPs看起来降了,但GPU实际耗时没变化。ResNet34里最耗时的其实是stage3和stage4的3x3卷积,剪这里才有效果。建议剪枝前用profiler看每层实际耗时,找到计算热点,然后针对热点层做剪枝。
6.4 先量化后剪枝是个大坑
我一个同事踩过这个坑,他先做了INT8量化,然后才剪枝,结果精度崩得没法看。原因很简单:量化是在固定网络结构上做统计映射,剪枝之后结构变了,通道少了,统计量全失效,之前校准结果全作废。正确顺序就是剪枝-微调-量化-校准,这个顺序别乱改。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| mAP整体掉点超过预期 | BN统计量未重置或微调学习率过大 | 重置BN统计量,降低学习率到0.001以下,延长微调 |
| AP_S单独掉点严重 | 浅层特征通道被剪过多 | 降低浅层剪枝比例,保留高分辨率特征图通道 |
| FLOPs降了延迟没降 | 剪枝层不是计算热点 | 用profiler定位热点层,优先剪stage3/stage4 |
| 量化后精度崩 | 量化顺序不对或敏感层被强行INT8 | 先剪枝再量化;对敏感层做混合精度 |
| 剪枝效果很好,但模型文件体积变化不大 | 没有物理删除通道,只是mask置零 | 导出模型时做真实结构重写,删掉零通道 |
| 微调过程模型发散 | 学习率过大或剪枝率过高 | 检查剪枝率是否超出敏感性安全边界,降低lr |
最后再分享一个我自己的体会:剪枝不是单纯的模型瘦身,而是一个系统工程,需要数据、训练、部署三端配合。检测模型比分类模型复杂,敏感性分析、分模块剪枝率、微调策略这些环节都不能省。如果你刚好在项目里被模型体积和延迟卡住了,按照这个流程先跑一遍,大概率不会走歪路。