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 如何选择剪枝策略?

说了这么多,到底该用哪种?

我一般按这个顺序来选:

  1. 先看硬件:如果是通用 GPU,结构化剪枝优先;如果有稀疏计算单元,非结构化也可以。
  2. 再看精度要求:精度要求高,用细粒度剪枝,慢慢调阈值;精度要求不高,直接上结构化剪枝,简单粗暴。
  3. 最后看部署框架:有些框架(比如 TensorRT)对结构化剪枝支持很好,对非结构化剪枝基本不支持。提前查好文档,别像我一样踩坑。

嗯,剪枝这东西,理论不难,但实践起来全是细节。下一章我会讲具体的剪枝算法和实现流程,到时候咱们再深入聊。