3、剪枝粒度:细粒度剪枝、向量剪枝、核内剪枝、通道剪枝
剪枝粒度,说白了就是你要「砍」多细的问题。
我刚开始做模型压缩时,总觉得剪枝就是一刀切,把不重要的权重归零就完事了。后来踩了不少坑才明白——剪枝的粒度选择,直接决定了你的模型能跑多快、能省多少显存。
今天咱们就把四种主流粒度掰开揉碎讲清楚。
3.1 细粒度剪枝:最基础的「零敲碎打」
细粒度剪枝,也叫非结构化剪枝。它的思路很简单:每个权重单独判断去留。
你想想看,一个卷积核里可能有 9 个权重,其中 3 个绝对值特别小,对输出几乎没贡献。那好,把它们设成 0 就行了。
核心特点:
- 剪枝单位:单个权重
- 稀疏度:可以做到很高(90% 以上)
- 硬件友好度:低(需要稀疏矩阵计算库支持)
我在项目中遇到过这种情况:用 PyTorch 做细粒度剪枝,剪掉 80% 的权重后,模型精度只掉了 0.5%。当时还挺得意,结果一部署到手机上——速度根本没提升!
为什么?因为普通硬件不支持稀疏矩阵加速。你虽然把权重设成了 0,但计算时还得一个个乘过去,白忙活。
# 细粒度剪枝示例(PyTorch)
import torch.nn.utils.prune as prune
# 对卷积层做细粒度剪枝,剪掉 30% 的权重
prune.l1_unstructured(conv_layer, name='weight', amount=0.3)
# 查看剪枝后的权重
print(conv_layer.weight_mask) # 0/1 掩码矩阵
我的建议:细粒度剪枝适合学术研究或对精度要求极高的场景。生产环境慎用,除非你有专门的稀疏计算库(比如 NVIDIA 的 cuSPARSE)。
3.2 向量剪枝:按「行」或「列」整组砍掉
向量剪枝比细粒度进了一步。它不再单个砍权重,而是以向量为单位整组砍掉。
什么叫向量?比如一个卷积核的某一行,或者某一列。你想想看,如果一整行权重都很小,那留着干嘛?直接整行干掉。
核心特点:
- 剪枝单位:行向量或列向量
- 稀疏度:中等(通常 50%-70%)
- 硬件友好度:中等(可以利用矩阵分块计算)
我记得有一次做语音模型压缩,细粒度剪枝死活压不下去,精度掉得厉害。后来换成向量剪枝,按输出通道的某几行整组砍掉,效果反而更好。
说白了,向量剪枝保留了矩阵的规整结构,硬件做矩阵乘法时不用跳来跳去,效率高不少。
# 向量剪枝思路(按行剪枝)
def vector_prune(weight, prune_ratio):
# 计算每行的 L2 范数
row_norms = torch.norm(weight, dim=1)
# 找到要剪掉的行
threshold = torch.quantile(row_norms, prune_ratio)
mask = row_norms > threshold
return weight[mask] # 保留重要行
注意:向量剪枝虽然比细粒度友好,但剪枝粒度还是偏细。如果你用 SIMD 指令集加速,向量长度最好对齐到 4 或 8 的倍数,否则会有性能浪费。
3.3 核内剪枝:砍掉卷积核内部的「小单元」
核内剪枝,也叫结构化剪枝的一种变体。它针对的是卷积核内部的某个子结构。
举个例子:一个 3x3 的卷积核,你可以砍掉中间的一行,或者砍掉某个 2x2 的块。但注意,不是整个核都砍掉,只是核内部的部分区域。
核心特点:
- 剪枝单位:卷积核内的子块(如 2x2、1x3 等)
- 稀疏度:较低(通常 20%-40%)
- 硬件友好度:较高(保持卷积核整体结构)
我曾经在 MobileNet 上试过核内剪枝。MobileNet 用的是深度可分离卷积,每个卷积核本来就小,再砍细粒度意义不大。但核内剪枝就不一样——我把 3x3 的核砍成 3x2 或 2x3,精度几乎没变,速度还快了 15%。
# 核内剪枝示例:砍掉卷积核的中间一行
def kernel_inside_prune(conv_weight):
# conv_weight shape: [out_channels, in_channels, k, k]
# 砍掉中间一行(k=3 时,砍掉 index=1 的行)
conv_weight[:, :, 1, :] = 0
return conv_weight
小技巧:核内剪枝特别适合那些卷积核尺寸较大的层(比如 5x5、7x7)。对于 1x1 卷积,核内剪枝就没啥意义了——总共就一个点,还剪啥?
3.4 通道剪枝:最「暴力」也最有效的方式
通道剪枝,是工业界用得最多的方法。它直接砍掉整个通道——也就是把某个卷积核的所有权重全部删掉。
你想想看,一个卷积层有 64 个输出通道,其中 10 个通道的权重范数特别小,对后续层几乎没影响。那好,直接把这 10 个通道连根拔起。
核心特点:
- 剪枝单位:整个通道(即整个卷积核)
- 稀疏度:较低(通常 20%-50%)
- 硬件友好度:最高(直接减少计算量,无需特殊库)
通道剪枝的好处是立竿见影。你剪掉 30% 的通道,推理速度就能快 30% 左右,而且不需要任何稀疏计算库。我在部署边缘设备时,首选就是通道剪枝。
# 通道剪枝示例:按 L1 范数排序,剪掉不重要通道
def channel_prune(model, prune_ratio):
# 计算每个通道的 L1 范数
conv_weight = model.conv1.weight.data
channel_norms = torch.sum(torch.abs(conv_weight), dim=(1, 2, 3))
# 排序并确定剪枝阈值
_, indices = torch.sort(channel_norms)
keep_channels = int(conv_weight.size(0) * (1 - prune_ratio))
# 保留重要通道
model.conv1.weight.data = conv_weight[indices[:keep_channels]]
# 注意:还要同步调整后续层的输入通道数
避坑指南:我曾经在 ResNet 上做通道剪枝,剪完发现精度崩了。后来排查半天,原来是 shortcut 连接的通道数没对齐。通道剪枝一定要处理好残差连接、拼接操作这些「依赖关系」,否则模型结构就乱了。
3.5 四种粒度怎么选?一张表说清楚
| 剪枝粒度 | 剪枝单位 | 稀疏度上限 | 硬件友好度 | 典型场景 |
|---|---|---|---|---|
| 细粒度 | 单个权重 | 90%+ | 低 | 学术研究、专用硬件 |
| 向量剪枝 | 行/列向量 | 70% | 中 | 语音模型、NLP 模型 |
| 核内剪枝 | 核内子块 | 40% | 较高 | 大核卷积、MobileNet |
| 通道剪枝 | 整个通道 | 50% | 最高 | 工业部署、边缘设备 |
我个人习惯是:先粗后细。先用通道剪枝砍掉明显不重要的通道,再用细粒度剪枝做微调。这样既能保证速度提升,又能把精度损失降到最低。
最后说一句:剪枝粒度没有绝对的好坏。你想想看,如果你的模型要部署到 FPGA 上,那细粒度剪枝就是噩梦。但如果你的目标是发一篇论文,那细粒度剪枝的高稀疏度就是亮点。选哪种,取决于你的「终点」在哪。