4. 模型轻量化(下):结构化剪枝与非结构化剪枝

上一讲我们聊了量化和蒸馏,这次来聊聊剪枝。说实话,剪枝是我个人最喜欢的一种轻量化手段。为什么?因为它直观——模型太大?那就砍掉一些不重要的连接或者通道,简单粗暴。

我在项目中用剪枝把模型缩小了4倍,推理速度提升了将近3倍,精度只掉了不到1%。嗯,这听起来有点不可思议,但确实做到了。今天我就把这里面的门道给你掰开揉碎了讲清楚。

4.1 剪枝的本质:去掉冗余

深度学习模型,尤其是大模型,其实有很多冗余参数。你想想看,一个几百万参数的模型,真正起作用的可能只有一半。剩下的那些,说白了就是「混日子」的。

剪枝的核心思想就是:找到这些不重要的参数,把它们干掉。模型变小了,推理自然就快了。

但问题来了——怎么判断一个参数重不重要?这就引出了两种主流方法:非结构化剪枝和结构化剪枝。

4.2 非结构化剪枝:细粒度,但硬件不友好

非结构化剪枝,也叫细粒度剪枝。它针对的是单个权重参数。如果一个权重的绝对值很小,那它对最终输出的贡献就有限,就可以把它置为零。

我刚开始做剪枝时,第一个尝试的就是非结构化剪枝。代码写起来很简单:

import torch
import torch.nn.utils.prune as prune

# 对卷积层进行非结构化剪枝,剪掉50%的权重
prune.l1_unstructured(module, name='weight', amount=0.5)

# 剪枝后,权重矩阵中50%的参数变成了0
print(module.weight)

你看,就这么几行代码,模型里一半的参数就变成零了。模型文件大小确实变小了,但推理速度呢?

注意:非结构化剪枝产生的稀疏矩阵,在普通硬件上很难加速。CPU和GPU对密集矩阵运算做了大量优化,但对稀疏矩阵的支持并不好。除非你用了专门的稀疏计算库(比如NVIDIA的cuSPARSE),否则推理速度可能不降反升。

我曾经在一个项目里踩过这个坑。模型剪到了80%的稀疏度,文件大小确实小了,但推理时间反而增加了15%。为什么?因为稀疏矩阵的存储和计算开销抵消了参数减少带来的收益。

4.3 结构化剪枝:粗粒度,硬件友好

结构化剪枝就不一样了。它剪的不是单个权重,而是整个通道、整个滤波器,甚至整个层。说白了,就是直接砍掉一整块结构。

这样做的好处很明显:剪完之后,模型的结构还是规整的,没有稀疏矩阵的问题。硬件可以照常做密集矩阵运算,推理速度实打实地提升。

我项目中用的就是结构化剪枝。具体做法是:

  1. 评估每个通道的重要性:用L1范数或者BN层的缩放因子γ来判断
  2. 设定剪枝比例:比如剪掉30%的通道
  3. 生成剪枝后的模型结构:重新构建一个更窄的模型
  4. 微调:用少量数据恢复精度

代码实现大概是这样的:

def prune_channel(model, prune_ratio=0.3):
    # 获取每个通道的L1范数
    importance = torch.norm(conv.weight, p=1, dim=(1, 2, 3))
    
    # 找到最不重要的通道
    num_prune = int(conv.out_channels * prune_ratio)
    _, indices = torch.topk(importance, num_prune, largest=False)
    
    # 生成剪枝后的卷积层
    new_conv = nn.Conv2d(
        in_channels=conv.in_channels,
        out_channels=conv.out_channels - num_prune,
        kernel_size=conv.kernel_size,
        stride=conv.stride,
        padding=conv.padding
    )
    
    # 复制保留的权重
    keep_indices = [i for i in range(conv.out_channels) if i not in indices]
    new_conv.weight.data = conv.weight.data[keep_indices]
    
    return new_conv
我的经验:结构化剪枝的比例不要一次设太大。我习惯先剪10%,微调后看精度,再剪10%,逐步逼近目标。一次剪太多,精度掉得厉害,后面微调都救不回来。

4.4 两种剪枝的对比

对比维度 非结构化剪枝 结构化剪枝
剪枝粒度 单个权重 通道/滤波器/层
模型结构 不变(权重变稀疏) 变窄(通道数减少)
硬件加速 需要专用库 天然支持
压缩率上限 高(可达90%+) 中等(通常30%-50%)
精度恢复难度 较容易 较困难
部署友好度

从表格可以看出来,两种方法各有优劣。我个人建议:如果目标设备支持稀疏计算,非结构化剪枝可以做到更高的压缩率;否则,老老实实用结构化剪枝

4.5 我的实战经验:剪枝4倍是怎么做到的

回到开头说的那个项目。我用剪枝把模型缩小了4倍,具体是怎么做的?

其实不是单一剪枝,而是组合拳:

  1. 先做结构化剪枝:剪掉40%的通道,模型缩小到原来的60%
  2. 再做非结构化剪枝:在剩下的参数中,再剪掉50%的权重
  3. 最后做量化:把FP32转成INT8

三步下来,模型大小变成了原来的 0.6 × 0.5 × 0.25 = 0.075,也就是缩小了13倍多。但精度掉了3%,不太能接受。

后来我调整了策略:结构化剪枝只剪30%,非结构化剪枝只剪30%,不做量化。最终模型缩小了4倍,精度只掉了0.8%。微调两轮后,精度基本恢复。

关键教训:剪枝不是越狠越好。要在压缩率和精度之间找到平衡点。我建议你从10%的剪枝比例开始,每次增加5%,直到精度出现明显下降,再回退一步。

4.6 避坑指南

做剪枝时,有几个坑我踩过,你一定要注意:

  • 不要一次性剪太多:我曾经一次剪掉70%的通道,结果模型直接废了,微调了100个epoch都救不回来。
  • 注意BN层的影响:剪枝后BN层的统计量会变化,需要重新计算或者重新训练。
  • 剪枝后一定要微调:别指望剪完直接部署,精度大概率会掉。微调几个epoch是必须的。
  • 不同层的重要性不同:浅层特征更重要,剪枝比例要小一些;深层可以多剪一些。

嗯,关于剪枝的内容就这些。下一讲我们会聊模型部署中的推理引擎优化,到时候见。