4. 模型剪枝技术:结构化剪枝与非结构化剪枝、通道剪枝、权重剪枝实战

模型剪枝,说白了就是给神经网络“瘦身”。一个训练好的大模型,里面有很多参数其实贡献不大,砍掉它们,模型照样能干活,但体积更小、跑得更快。

我刚开始做模型部署时,总觉得剪枝很玄乎。后来在项目里被逼着把一个大模型塞进嵌入式设备,才真正体会到——剪枝不是玄学,是实打实的工程手段。今天咱们就把结构化剪枝、非结构化剪枝、通道剪枝、权重剪枝这几种主流方法,掰开揉碎了讲清楚。

4.1 剪枝的核心思想:为什么要“砍”参数?

一个训练好的神经网络,参数分布往往很稀疏。很多权重值接近零,对最终输出的贡献微乎其微。剪枝就是把这些“冗余”参数去掉,换来更小的模型体积和更快的推理速度。

剪枝的收益很直接:

  • 体积减小:存储占用降低,适合嵌入式设备
  • 计算加速:减少乘加运算次数,降低延迟
  • 功耗降低:计算量少了,芯片自然更省电

关键指标:剪枝率 = 剪掉的参数数量 / 原始参数总数。剪枝率越高,压缩比越大,但精度损失风险也越高。我一般建议从 30% 开始试,逐步往上加。

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

非结构化剪枝,也叫细粒度剪枝。它针对单个权重做判断——如果某个权重的值小于阈值,就把它置为零。

举个例子:一个 3×3 的卷积核,里面有 9 个权重值。剪枝后可能变成这样:

原始权重: [0.12, -0.05, 0.98, 0.01, -0.03, 0.45, 0.00, 0.67, -0.22]
剪枝后:   [0.12,  0.00, 0.98, 0.00,  0.00, 0.45, 0.00, 0.67,  0.00]

你看,值小于 0.1 的权重都被清零了。这种方法的优点是精度损失小,因为只砍掉最不重要的参数。但缺点也很明显——稀疏矩阵。

为什么说它硬件不友好?因为现代 CPU/GPU/NPU 都是为密集矩阵运算优化的。稀疏矩阵里大量零值,计算单元还得跳着读数据,访存效率极低。我在项目里试过,非结构化剪枝到 50% 以上,推理速度反而变慢了——因为稀疏计算的开销超过了计算减少的收益。

我曾经踩过的坑:非结构化剪枝后的模型,用普通框架推理几乎没加速效果。必须搭配专门的稀疏计算库(如 NVIDIA 的 cuSPARSE)或定制硬件才能发挥优势。如果你用的是通用芯片,建议谨慎选择。

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

结构化剪枝,砍的不是单个权重,而是整个“结构单元”——比如一个卷积核、一个通道、甚至一整层。这样剪完后,模型仍然是密集矩阵,硬件可以直接跑,不需要特殊支持。

常见的结构化剪枝粒度:

剪枝粒度 砍掉的对象 硬件友好度 精度损失
权重剪枝 单个权重 低 低
卷积核剪枝 整个卷积核 中 中
通道剪枝 整个通道 高 较高
层剪枝 整层网络 最高 高

我个人习惯优先用通道剪枝。为什么?因为通道剪枝直接减少特征图数量,后续所有层的计算量都会跟着降,收益最大。而且剪完后模型结构不变,硬件直接跑,不需要改代码。

4.4 通道剪枝实战:以卷积层为例

通道剪枝的核心是:评估每个通道的重要性,砍掉不重要的通道。评估方法有很多,我常用的是 L1 范数——一个通道对应的卷积核权重值之和越小,说明这个通道越不重要。

具体步骤:

  1. 对每个卷积核,计算其所有权重的 L1 范数
  2. 按 L1 范数从小到大排序
  3. 设定剪枝率,砍掉排名靠后的通道
  4. 微调模型,恢复精度

代码实现(PyTorch 风格):

def channel_prune(conv_layer, prune_ratio):
    # 计算每个卷积核的 L1 范数
    l1_norm = torch.sum(torch.abs(conv_layer.weight.data), dim=(1,2,3))
    
    # 按 L1 范数排序,找到要保留的通道数
    num_channels = conv_layer.weight.shape[0]
    num_keep = int(num_channels * (1 - prune_ratio))
    
    # 保留 L1 范数最大的 num_keep 个通道
    _, indices = torch.topk(l1_norm, num_keep)
    
    # 生成新的卷积层
    new_conv = nn.Conv2d(
        in_channels=conv_layer.in_channels,
        out_channels=num_keep,
        kernel_size=conv_layer.kernel_size,
        stride=conv_layer.stride,
        padding=conv_layer.padding
    )
    new_conv.weight.data = conv_layer.weight.data[indices]
    new_conv.bias.data = conv_layer.bias.data[indices] if conv_layer.bias is not None else None
    
    return new_conv, indices

小技巧:剪枝后一定要微调。我见过有人剪完直接部署,精度掉了 5 个点。微调 3-5 个 epoch,精度基本能回到原来的 98% 以上。微调时学习率要调低,一般是原学习率的 1/10。

4.5 权重剪枝实战:细粒度与粗粒度的折中

权重剪枝介于非结构化与结构化之间。它砍的是单个权重,但通过某种约束让稀疏模式变得“规则”,从而更容易被硬件加速。

举个例子,N:M 剪枝:每 M 个连续权重中,只保留 N 个非零值。比如 2:4 剪枝,每 4 个权重里保留 2 个最大的,其余置零。这种模式在 NVIDIA Ampere 架构的 GPU 上有专门的硬件支持,可以直接加速。

实现思路:

def n_m_prune(weight, n=2, m=4):
    # 将权重按 m 个一组分组
    shape = weight.shape
    weight_flat = weight.view(-1)
    num_groups = weight_flat.shape[0] // m
    
    for g in range(num_groups):
        start = g * m
        end = start + m
        group = weight_flat[start:end]
        
        # 找到值最大的 n 个权重
        _, indices = torch.topk(torch.abs(group), n)
        mask = torch.zeros_like(group)
        mask[indices] = 1
        
        # 保留选中的权重,其余置零
        weight_flat[start:end] = group * mask
    
    return weight_flat.view(shape)

N:M 剪枝的好处是:稀疏模式固定,硬件可以预知数据布局,访存效率高。我在部署 Transformer 模型时常用 2:4 剪枝,推理速度能提升 1.5-2 倍,精度损失控制在 1% 以内。

4.6 剪枝策略对比与选型建议

不同剪枝方法适合不同场景。我整理了一张对比表,方便你选型:

方法 压缩率 硬件加速 精度保留 适用场景
非结构化剪枝 高 差 好 学术研究、定制硬件
通道剪枝 中 好 中 嵌入式部署、移动端
N:M 权重剪枝 中高 好 较好 GPU 加速、Transformer
层剪枝 最高 最好 差 极简模型、资源极度受限

我的建议是:优先考虑通道剪枝。它平衡了压缩率和硬件友好度,是工业界最常用的方法。如果精度损失太大,再考虑 N:M 权重剪枝作为补充。

4.7 剪枝流程与知识体系

下面这张图总结了剪枝的完整流程和各类方法的关系:

模型剪枝知识体系与流程 预训练模型 剪枝方法选择 非结构化剪枝 细粒度,稀疏矩阵 通道剪枝 结构化,硬件友好 N:M 权重剪枝 规则稀疏,GPU加速 微调恢复精度 部署到目标硬件 核心原则:剪枝 → 微调 → 评估,循环迭代直到精度与性能达到平衡 通道剪枝是工业首选,N:M 剪枝适合 GPU 场景,非结构化剪枝需硬件配合

4.8 避坑指南与实战建议

做了这么多年剪枝,我总结了几条血泪教训:

  • 不要一次性剪太多:我建议每次剪 10%-20%,然后微调评估。一次性剪 50% 以上,精度可能崩盘,微调都救不回来。
  • 注意 BN 层:通道剪枝后,BN 层的参数也要跟着调整。我见过有人忘了更新 BN 的 running_mean 和 running_var,推理结果全乱套。
  • 评估指标要选对:除了精度,还要关注推理延迟和功耗。有时候剪枝后精度没降,但延迟反而高了——因为稀疏计算没优化好。
  • 剪枝不是终点:剪完后的模型,建议再做一次量化。剪枝+量化组合拳,压缩效果翻倍。这个我们后面章节会细讲。

我的个人习惯:先做通道剪枝到 30%,微调后评估。如果精度损失小于 1%,继续加剪到 50%。如果损失超过 2%,就退回到 30%,改用 N:M 权重剪枝做补充。这样既能保证精度,又能最大化压缩率。