3. 剪枝分类体系:结构化剪枝与非结构化剪枝、细粒度与粗粒度剪枝
各位同学,咱们今天聊聊剪枝的分类。说实话,我刚入行那会儿,看到各种剪枝名词——结构化、非结构化、细粒度、粗粒度——头都大了。后来在NPU上踩了几个坑,才真正搞明白它们之间的区别。
说白了,剪枝就是给神经网络“瘦身”。但怎么瘦,瘦到什么程度,直接决定了你的模型在NPU上跑得快不快、省不省电。
3.1 非结构化剪枝:最灵活,但NPU不买账
非结构化剪枝,也叫细粒度剪枝。它允许你把权重矩阵中任意一个参数置零。举个例子:
# 非结构化剪枝示例
import torch
import torch.nn.utils.prune as prune
model = torch.nn.Linear(10, 10)
# 随机剪掉20%的权重
prune.random_unstructured(model, name='weight', amount=0.2)
print(model.weight)
# 输出中会有很多零值,但位置是随机的
你看,这种剪枝方式很自由。每个权重都可以独立决定去留。理论上,它能达到最高的压缩率。我在项目中试过,把ResNet-50剪掉90%的参数,精度只掉了不到1%。
但是! 问题来了。NPU的硬件设计,说白了就是做矩阵乘法。它希望数据是规整的、连续的。非结构化剪枝产生的稀疏矩阵,就像一盘散沙。NPU要处理它,得额外花时间去“找”哪些是有效数据。
我记得有一次,在某个NPU上跑非结构化剪枝后的模型。理论计算量减少了80%,实际推理速度反而慢了30%。为什么?因为硬件不支持非零值的随机索引。它只能按部就班地读数据,遇到零值还得跳过,效率极低。
避坑指南: 我曾经天真地以为,只要把权重置零就能加速。结果发现,没有硬件支持的稀疏加速,剪枝就是“纸面加速”。如果你用的NPU不支持稀疏矩阵运算,非结构化剪枝基本等于白干。
3.2 结构化剪枝:NPU的“亲儿子”
结构化剪枝,也叫粗粒度剪枝。它不剪单个权重,而是剪整个“结构”。比如:
- 通道剪枝:直接删掉整个卷积核(比如把64个通道剪成32个)
- 滤波器剪枝:删掉整个滤波器
- 层剪枝:删掉整个网络层(这个比较激进,慎用)
你想想看,删掉一个通道,意味着这个通道对应的所有权重都消失了。剩下的网络结构依然是规整的矩阵。NPU处理起来,跟没剪枝的模型一模一样,只是矩阵变小了。
# 结构化剪枝示例(通道剪枝)
# 假设我们有一个卷积层,输入通道64,输出通道64
# 我们想剪掉一半的输入通道
def channel_prune(conv_layer, keep_ratio=0.5):
# 计算每个通道的重要性(这里用L1范数)
importance = torch.norm(conv_layer.weight, p=1, dim=(0, 2, 3))
# 保留最重要的通道
num_keep = int(conv_layer.in_channels * keep_ratio)
_, indices = torch.topk(importance, num_keep)
# 重新构建卷积层
new_conv = nn.Conv2d(
in_channels=num_keep,
out_channels=conv_layer.out_channels,
kernel_size=conv_layer.kernel_size,
stride=conv_layer.stride,
padding=conv_layer.padding
)
# 复制保留的权重
new_conv.weight.data = conv_layer.weight.data[:, indices, :, :]
return new_conv
我个人习惯用结构化剪枝。原因很简单:立竿见影。剪完通道,模型大小直接减半,推理速度也接近翻倍。而且不需要硬件支持,任何NPU都能受益。
小技巧: 结构化剪枝的精度损失通常比非结构化大。我一般会配合微调(fine-tuning)来恢复精度。剪掉20%的通道,微调几个epoch,精度基本能回来。
3.3 细粒度 vs 粗粒度:一个维度上的不同视角
其实,细粒度和粗粒度,跟结构化、非结构化是交叉的概念。我画个表格你就明白了:
| 剪枝类型 | 粒度 | 剪枝单位 | 硬件友好度 | 压缩率潜力 |
|---|---|---|---|---|
| 非结构化剪枝 | 细粒度 | 单个权重 | 低(需专用硬件) | 高 |
| 结构化剪枝(通道级) | 粗粒度 | 整个通道 | 高(通用硬件) | 中 |
| 结构化剪枝(滤波器级) | 粗粒度 | 整个滤波器 | 高 | 中 |
| 向量级剪枝 | 中粒度 | 连续的一组权重 | 中(需部分支持) | 中高 |
嗯,这里要注意。向量级剪枝是个折中方案。它把权重分成一组一组的,比如每4个权重为一组,要么全保留,要么全剪掉。这样既保留了一定的灵活性,又让数据变得相对规整。有些NPU支持这种模式,比如华为的达芬奇架构。
3.4 实际项目中的选择策略
我在多个NPU芯片上做过剪枝,总结了一套选择策略:
- 先看硬件文档:搞清楚你的NPU支持什么。如果不支持稀疏加速,直接放弃非结构化剪枝。
- 优先结构化剪枝:对于大多数场景,通道剪枝是最稳妥的选择。我建议从剪掉30%的通道开始尝试。
- 混合使用:如果精度要求极高,可以先用结构化剪枝剪掉20%,再用非结构化剪枝剪掉50%。但要注意,非结构化部分可能无法加速。
- 考虑重训练:剪枝后一定要微调。我曾经试过不微调,精度掉了5个点,微调后只掉了0.5个点。
核心观点: 剪枝不是目的,加速才是。选择剪枝类型时,永远把“硬件能不能加速”放在第一位。否则你只是在做数学游戏,而不是工程优化。
最后说一句。剪枝分类体系看似复杂,其实就两个核心问题:剪什么?怎么剪? 非结构化剪得细但硬件不买账,结构化硬件喜欢但精度损失大。作为工程师,我们的任务就是在两者之间找到平衡点。
下一章,我会讲具体的剪枝算法——如何判断哪些权重该剪,哪些该留。到时候咱们再细聊。