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 上,那细粒度剪枝就是噩梦。但如果你的目标是发一篇论文,那细粒度剪枝的高稀疏度就是亮点。选哪种,取决于你的「终点」在哪。