端侧AI ISP推理性能优化实战 - 第3节
3. 模型剪枝技术
模型剪枝,说白了就是给神经网络“瘦身”。你想想看,一个训练好的大模型里,其实有很多参数是冗余的。把它们去掉,模型照样能干活,但体积更小、跑得更快。我在端侧部署项目里,剪枝几乎是必做的一步。
剪枝主要分两大类:结构化剪枝和非结构化剪枝。这两者的区别,我一开始也搞混过,后来踩了坑才真正明白。
非结构化剪枝:细粒度但“不友好”
非结构化剪枝,就是对单个权重下手。它会找出那些值很小的权重,直接置为零。这样做的好处是压缩率可以很高,理论上能去掉90%甚至更多的参数。
但问题来了——硬件不买账。你想想看,剪完之后权重矩阵变得稀疏了,到处都是零。普通的CPU和NPU在做矩阵乘法时,并不会跳过这些零值。我曾经在手机上试过,剪完后的模型文件是小了,但推理速度几乎没变,甚至还慢了一点。因为稀疏矩阵的存储和计算都需要特殊处理,通用硬件不支持。
核心结论:非结构化剪枝适合学术研究或配合专用稀疏计算库(如NVIDIA的cuSPARSE),但在端侧推理中,除非你有专门的稀疏加速硬件,否则不推荐。
结构化剪枝:端侧部署的“真香”选择
结构化剪枝就不一样了。它剪的不是单个权重,而是整个通道(Channel)、滤波器(Filter)或者层(Layer)。剪完之后,模型结构还是规整的,没有稀疏性,硬件可以照常计算。
我个人习惯在端侧项目里优先用结构化剪枝。举个例子,一个卷积层有64个输出通道,通过剪枝发现其中12个通道的贡献很小,那就直接把这12个通道连同对应的卷积核一起删掉。模型宽度从64变成52,计算量直接减少,而且不需要任何特殊硬件支持。
| 对比维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 通道/滤波器/层 |
| 模型结构 | 稀疏,不规则 | 规整,无稀疏性 |
| 硬件加速 | 需要专用库/硬件 | 通用硬件直接加速 |
| 压缩率 | 高(可达90%+) | 中等(通常30%-50%) |
| 端侧适用性 | 低 | 高 |
剪枝策略:L1/L2范数与通道剪枝
知道了剪什么,接下来就是怎么剪。这里我重点讲两种常用策略:基于范数的剪枝和通道剪枝。
基于L1/L2范数的剪枝
这个方法很直观。每个卷积核或者权重矩阵,我们计算它的L1范数(值之和)或者L2范数(平方和的平方根)。范数越小,说明这个卷积核的“能量”越低,对输出的贡献就越小。
具体做法是:
- 计算每个卷积核的L1或L2范数
- 对所有范数排序
- 设定一个剪枝比例(比如剪掉20%),把范数最小的那些卷积核删掉
我在项目中遇到过一个问题:直接用L1范数剪枝,有时候会把某些层剪得太狠,导致精度骤降。后来我加了一个层敏感度分析——先逐层测试,看看每层能承受多大的剪枝比例,再分别设定阈值。这样剪出来的模型更稳。
小技巧:L1范数倾向于产生稀疏解,适合快速筛选;L2范数对异常值更敏感,适合保留那些虽然值小但分布均匀的卷积核。我一般先用L1快速剪一轮,再用L2做精细调整。
通道剪枝:更“暴力”但更有效
通道剪枝是结构化剪枝里最常用的方法。它直接对特征图的通道维度下手。怎么做呢?
一种经典做法是:利用BN层(Batch Normalization)的缩放因子γ。训练时给每个通道的γ加一个L1正则化,让不重要的通道的γ趋近于零。剪枝时,直接去掉γ值很小的通道,以及对应的输入和输出连接。
嗯,这里要注意:通道剪枝会改变网络宽度,所以后续的层也要跟着调整。比如你剪掉了第3层的16个输出通道,那第4层的输入通道数也要从64变成48。这个联动关系一定要处理好,否则模型结构就乱了。
# 伪代码示例:基于BN层γ值的通道剪枝
def channel_prune(model, prune_ratio):
# 收集所有BN层的γ值
gamma_list = []
for module in model.modules():
if isinstance(module, nn.BatchNorm2d):
gamma_list.extend(module.weight.data.abs().cpu().numpy())
# 计算阈值
threshold = np.percentile(gamma_list, prune_ratio * 100)
# 对每一层,保留γ大于阈值的通道
for name, module in model.named_modules():
if isinstance(module, nn.BatchNorm2d):
mask = module.weight.data.abs() > threshold
# 根据mask裁剪当前层和后续层
# ...(具体裁剪逻辑略)
剪枝后的微调技巧
剪完枝,模型精度肯定会掉一点。这时候就需要微调(Fine-tuning)。我见过有人剪完直接部署,结果精度掉了5个点,用户反馈直接炸了。微调这一步,不能省。
分享几个我常用的微调技巧:
- 学习率要小:剪枝后的模型已经接近收敛,学习率设太大容易破坏已有特征。我一般用原训练学习率的1/10,比如原先是1e-3,微调就用1e-4。
- 先冻结再解冻:刚开始微调时,先把被剪的层冻结,只训练剩下的层。等损失稳定了,再解冻所有层一起训练。这样能避免刚剪完的“震荡期”。
- 知识蒸馏辅助:如果精度要求很高,我会用原始大模型作为教师,剪枝后的模型作为学生,做蒸馏微调。效果比单纯微调好不少。
避坑指南:我曾经在微调时犯过一个错——剪枝比例设得太大(超过50%),结果微调了100个epoch精度都回不来。后来我改成“渐进式剪枝+微调”,每次只剪10%,微调几个epoch,再剪10%,循环往复。这样精度损失小很多,最终模型也更稳定。
另外,微调时别忘了重新统计BN层的均值和方差。剪枝改变了特征分布,旧的BN统计量已经不适用了。我习惯在微调前先跑几个batch的数据,重新校准BN参数。
本章知识体系
下面这张图总结了本章的核心逻辑,从剪枝分类到策略选择,再到微调流程,一目了然。
总结一下:端侧推理场景下,结构化剪枝是首选,通道剪枝配合BN层的γ值是实操中最稳定的方案。剪完之后别急着部署,花点时间微调,精度能回来不少。记住,剪枝不是一锤子买卖,渐进式地剪、边剪边调,效果最好。