4. 模型剪枝技术
模型剪枝,说白了就是给神经网络“瘦身”。
我刚开始接触剪枝时,总觉得这玩意儿挺玄乎。把权重砍掉一部分,模型还能干活?后来踩过几次坑才明白——剪枝不是乱砍,而是有策略地“去芜存菁”。今天咱们就聊聊,怎么剪才能既保精度,又提速度。
4.1 非结构化剪枝:精细但“难伺候”
非结构化剪枝,也叫细粒度剪枝。它直接干掉单个权重,哪个权重值接近0,我就把它置零。
举个例子,一个卷积核是3x3的矩阵:
原始权重:
[[0.1, 0.8, 0.02],
[0.9, 0.01, 0.7],
[0.03, 0.6, 0.05]]
剪枝阈值 = 0.05,干掉绝对值小于0.05的权重:
剪枝后:
[[0.1, 0.8, 0.0],
[0.9, 0.0, 0.7],
[0.0, 0.6, 0.0]]
你看,权重矩阵变得稀疏了。但问题来了——NPU硬件喜欢规整的数据。稀疏矩阵在GPU上可以用cuSPARSE加速,但在NPU上,很多硬件根本没法直接利用这种稀疏性。
在某个NPU平台上做非结构化剪枝,剪完模型小了,但推理速度反而慢了。为什么?因为NPU没有专门的稀疏计算单元,零权重照样参与乘法运算,白忙活一场。
所以,非结构化剪枝更适合存储压缩,或者配合专用稀疏加速硬件使用。如果你用的NPU不支持稀疏计算,我建议你慎重考虑。
4.2 结构化剪枝:NPU的“真香”选择
结构化剪枝,是整块整块地砍。比如砍掉整个通道、整个卷积核,甚至整个层。
你想想看,NPU做卷积时,输入通道和输出通道都是并行计算的。砍掉一个通道,计算量直接减少,硬件利用率反而更高。
常见的结构化剪枝方式:
- 通道剪枝:砍掉不重要的特征图通道
- 滤波器剪枝:砍掉整个卷积核
- 层剪枝:直接跳过某些冗余层(比如ResNet中的某些残差块)
我个人习惯用通道剪枝。在项目中,我一般先用BN层的缩放因子γ来评估通道重要性。γ值接近0的通道,说明它对后续层贡献很小,可以安全剪掉。
举个例子,假设某卷积层有64个输出通道,经过重要性评估后,发现其中16个通道的γ值小于0.01:
# 伪代码示意
importance = abs(bn_layer.weight) # BN层的γ参数
threshold = 0.01
mask = importance > threshold # 保留48个通道
pruned_weight = conv_weight[mask] # 只保留重要通道
剪完后,模型体积变小,NPU上的计算量也实打实地减少了。嗯,这才是NPU喜欢的剪枝方式。
4.3 剪枝策略:怎么剪才不翻车?
剪枝不是一锤子买卖。我见过不少同学,上来就剪掉50%的通道,结果模型直接崩了。正确的做法是迭代式剪枝。
我常用的策略有三种:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 一次性剪枝 | 训练→剪枝→微调 | 剪枝比例小(<20%) |
| 迭代剪枝 | 训练→剪枝→微调→再剪枝→再微调... | 剪枝比例大(20%~80%) |
| 渐进式剪枝 | 训练过程中逐步增加剪枝比例 | 需要端到端训练 |
我个人推荐迭代剪枝。每次只剪掉5%~10%的通道,然后微调几个epoch,让模型慢慢适应。这样精度损失最小。
💡 避坑指南:
我曾经在剪枝后直接做大量微调,结果过拟合了。后来发现,剪枝后的微调学习率要调低,一般是原学习率的0.1倍。另外,微调轮数不用太多,5~10个epoch就够了。
4.4 效果评估:剪得好不好,得看指标
剪枝效果不能只看精度。我一般从三个维度评估:
- 精度损失:剪枝后模型在验证集上的准确率下降了多少?一般控制在1%以内算合格。
- 计算量减少:用FLOPs或MACs衡量。通道剪枝后,FLOPs应该成比例下降。
- 实际推理速度:在目标NPU上跑一遍,看延迟有没有降低。这是最实在的指标。
举个例子,我在某个项目中的评估结果:
| 指标 | 剪枝前 | 剪枝后(通道剪枝50%) | 变化 |
|---|---|---|---|
| Top-1 准确率 | 76.3% | 75.8% | -0.5% |
| FLOPs | 3.2G | 1.6G | -50% |
| NPU推理延迟 | 12ms | 7ms | -41.7% |
你看,精度只掉了0.5%,但推理速度提升了近42%。这就是结构化剪枝的魅力。
⚠️ 注意:FLOPs减少不等于实际速度提升。有些NPU对通道数有对齐要求(比如16的倍数),剪枝后通道数不对齐,反而会引入填充开销。所以,一定要在目标硬件上实测。
4.5 知识体系总览
下面这张图,帮你理清剪枝技术的核心脉络:
这张图把剪枝的三大分支和评估闭环都串起来了。你可以在实际项目中,对照这张图来设计你的剪枝方案。
好了,关于模型剪枝,核心就是这些。记住:结构化剪枝是NPU上的首选,迭代剪枝是保精度的关键,实测延迟才是硬道理。