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芯片上做过剪枝,总结了一套选择策略:

  1. 先看硬件文档:搞清楚你的NPU支持什么。如果不支持稀疏加速,直接放弃非结构化剪枝。
  2. 优先结构化剪枝:对于大多数场景,通道剪枝是最稳妥的选择。我建议从剪掉30%的通道开始尝试。
  3. 混合使用:如果精度要求极高,可以先用结构化剪枝剪掉20%,再用非结构化剪枝剪掉50%。但要注意,非结构化部分可能无法加速。
  4. 考虑重训练:剪枝后一定要微调。我曾经试过不微调,精度掉了5个点,微调后只掉了0.5个点。

核心观点: 剪枝不是目的,加速才是。选择剪枝类型时,永远把“硬件能不能加速”放在第一位。否则你只是在做数学游戏,而不是工程优化。

最后说一句。剪枝分类体系看似复杂,其实就两个核心问题:剪什么?怎么剪? 非结构化剪得细但硬件不买账,结构化硬件喜欢但精度损失大。作为工程师,我们的任务就是在两者之间找到平衡点。

下一章,我会讲具体的剪枝算法——如何判断哪些权重该剪,哪些该留。到时候咱们再细聊。