4、模型剪枝技术(上):剪枝的分类
模型剪枝,说白了就是给神经网络“瘦身”。
你想想看,一个训练好的大模型,里面其实有很多参数是冗余的。就像我们写文章,有些词删掉也不影响理解。剪枝要做的,就是把这些“废话”去掉,让模型跑得更快、占得更少。
我个人习惯把剪枝分成两大类来理解:结构化剪枝和非结构化剪枝。这两者的区别,直接决定了你的模型能不能在硬件上真正提速。
4.1 结构化剪枝 vs 非结构化剪枝
非结构化剪枝,也叫细粒度剪枝。它剪的是单个的权重参数。
举个例子,一个卷积核是 3x3 的矩阵,里面有 9 个权重值。非结构化剪枝会把其中几个接近 0 的值直接置为 0。剩下的权重还是散乱地分布在各个位置。
这样做的好处是:压缩率可以很高。我见过有人把 90% 的参数都剪掉,精度损失还不到 1%。
但问题也很明显——硬件不买账。
为什么?因为 GPU 和 NPU 做矩阵乘法时,喜欢规规整整的数据。你突然在中间插一堆 0,它还得专门跳过这些 0,反而更慢。除非你有专门的稀疏计算硬件,否则非结构化剪枝的加速效果很有限。
核心区别一句话:
- 非结构化剪枝:剪单个权重,压缩率高,但硬件加速难
- 结构化剪枝:剪整个通道/卷积核,压缩率低,但硬件友好
结构化剪枝就不一样了。它直接剪掉整个卷积核、整个通道,甚至整个层。
比如一个卷积层有 64 个输出通道,结构化剪枝可能直接砍掉 16 个通道。剩下的 48 个通道还是规整的矩阵。这样一来,硬件计算时矩阵规模变小了,实实在在的加速。
我在项目中遇到过这样一个场景:用非结构化剪枝把模型压缩了 10 倍,结果在嵌入式设备上跑,速度一点没变。后来换成结构化剪枝,只压缩了 3 倍,但推理速度直接快了 2 倍。嗯,这就是血泪教训。
我的建议:
如果你用的是专用 AI 芯片(比如华为昇腾、寒武纪),优先考虑结构化剪枝。如果只是做学术研究或者模型存储压缩,非结构化剪枝更灵活。
4.2 细粒度剪枝与向量剪枝
细粒度剪枝,就是上面说的非结构化剪枝。它把每个权重单独看待,小于阈值的就干掉。
但这里有个坑:阈值怎么设?
设得太小,剪不掉多少;设得太大,精度崩了。我一般会用 百分比阈值,比如保留权重绝对值最大的 30%,剩下的全剪掉。这样能保证剪枝后的模型还有足够的“骨干”参数。
代码实现其实很简单:
import torch
import torch.nn.utils.prune as prune
# 对卷积层进行非结构化剪枝
model = torch.load('my_model.pth')
prune.l1_unstructured(
model.conv1,
name='weight',
amount=0.7 # 剪掉70%的权重
)
你看,PyTorch 自带的剪枝工具,一行代码就能搞定。但要注意,amount=0.7 表示剪掉 70%,不是保留 70%。
向量剪枝呢?它介于细粒度和结构化之间。
向量剪枝不是剪单个权重,而是剪一整行或一整列。比如一个全连接层的权重矩阵是 1000x1000,向量剪枝可能直接删掉某几行。
这样做的好处是:计算时可以利用向量化指令。CPU 和 GPU 都有 SIMD(单指令多数据)指令,处理连续的一行数据比处理散乱的单个数据快得多。
| 剪枝类型 | 剪枝粒度 | 硬件友好度 | 压缩率 |
|---|---|---|---|
| 细粒度剪枝 | 单个权重 | 低 | 高 |
| 向量剪枝 | 整行/整列 | 中 | 中 |
| 结构化剪枝 | 整个通道 | 高 | 低 |
注意:
向量剪枝在嵌入式设备上效果不错,但前提是你的推理框架支持稀疏矩阵的向量化运算。我曾经在某个国产芯片上试过,结果它根本不认这种稀疏格式,白忙活一场。
4.3 权重剪枝与激活剪枝
前面讲的都是权重剪枝——剪掉模型参数中不重要的权重。这是最主流的方法。
但还有一种思路:激活剪枝。
激活剪枝剪的不是权重,而是中间层的输出特征图。比如某个卷积层输出了 64 张特征图,其中有些特征图全是 0 或者接近 0,那就可以直接跳过这些特征图的计算。
为什么会有全是 0 的特征图?因为 ReLU 激活函数会把负数变成 0。如果某个卷积核学到的特征在当前输入下不激活,那它输出的特征图就是全 0 的。
我个人觉得,激活剪枝更适合动态场景。因为权重剪枝是静态的——剪完就固定了。而激活剪枝是动态的——每次输入不同,被剪掉的特征图也不同。
举个例子:
# 伪代码:激活剪枝的思路
def forward_with_activation_pruning(x):
# 计算所有通道的激活值
out = conv(x)
# 计算每个通道的L1范数
channel_norms = torch.norm(out, p=1, dim=[0,2,3])
# 保留范数最大的前50%的通道
mask = channel_norms > torch.median(channel_norms)
# 只计算被保留的通道
return out[:, mask, :, :]
这段代码每次推理时,都会动态决定哪些通道要保留。但要注意,动态判断本身也有开销。如果特征图很小,判断开销可能比计算本身还大。
避坑指南:
我曾经在一个语音识别模型上试激活剪枝,结果发现每次都要计算通道范数,反而拖慢了速度。后来改成每隔 N 帧才做一次剪枝判断,效果才好起来。所以,激活剪枝更适合计算密集型的层,比如大卷积核的卷积层。
4.4 如何选择剪枝策略?
说了这么多,到底该用哪种?
我一般按这个顺序来选:
- 先看硬件:如果是通用 GPU,结构化剪枝优先;如果有稀疏计算单元,非结构化也可以。
- 再看精度要求:精度要求高,用细粒度剪枝,慢慢调阈值;精度要求不高,直接上结构化剪枝,简单粗暴。
- 最后看部署框架:有些框架(比如 TensorRT)对结构化剪枝支持很好,对非结构化剪枝基本不支持。提前查好文档,别像我一样踩坑。
嗯,剪枝这东西,理论不难,但实践起来全是细节。下一章我会讲具体的剪枝算法和实现流程,到时候咱们再深入聊。