3. 模型剪枝技术

模型剪枝,说白了就是给神经网络“减肥”。

我刚开始接触剪枝时,总觉得这玩意儿挺玄乎——把模型里的参数砍掉一部分,精度居然还能保住?后来踩过几次坑才明白,剪枝的本质是去掉那些“不重要”的连接或通道。就像你整理书架,把不常看的书收进储藏室,常用的书留在手边,阅读效率反而更高。

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

非结构化剪枝,也叫细粒度剪枝。它直接干掉单个权重参数,把那些接近零的数值直接置零。

举个例子,一个卷积核是 3x3 的矩阵:

原始权重:
[[0.1, 0.8, -0.02],
 [0.0, 0.5,  0.01],
 [0.9, 0.3,  0.001]]

剪枝后(阈值 0.05):
[[0.1, 0.8,  0.0],
 [0.0, 0.5,  0.0],
 [0.9, 0.3,  0.0]]

你看,绝对值小于 0.05 的权重都被干掉了。这样做的好处是压缩率很高,模型体积能大幅缩小。但问题也很明显——权重矩阵变得稀疏且不规则,普通硬件(比如 GPU)没法高效计算。你想想看,GPU 擅长的是密集矩阵乘法,突然冒出一堆零,计算单元就闲置了。

⚠️ 我曾经踩过的坑: 非结构化剪枝后,模型文件确实小了,但推理速度几乎没变。后来我才意识到,除非你用专门的稀疏计算库(比如 NVIDIA 的 cuSPARSE),否则在通用硬件上根本吃不到红利。

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

结构化剪枝就聪明多了。它不剪单个权重,而是直接干掉整个通道、整个卷积核,甚至整个层。

我个人习惯用通道剪枝。比如一个卷积层有 64 个输出通道,剪掉 16 个不重要的,剩下 48 个。这样一来,网络结构变窄了,但形状还是规整的,GPU 照样跑得飞起。

# 伪代码示例:按通道 L2 范数剪枝
def channel_prune(conv_weight, prune_ratio):
    # conv_weight shape: [out_channels, in_channels, k_h, k_w]
    l2_norms = torch.norm(conv_weight.view(conv_weight.size(0), -1), dim=1)
    threshold = torch.quantile(l2_norms, prune_ratio)
    mask = l2_norms > threshold
    return conv_weight[mask]  # 保留重要通道

嗯,这里要注意:结构化剪枝虽然硬件友好,但压缩率通常不如非结构化剪枝。你想想看,一刀切掉整个通道,损失的信息量肯定比只切几个权重多。所以实际项目中,我一般先用结构化剪枝砍掉 20%-30% 的通道,再用非结构化剪枝做精细调整。

3.3 剪枝策略:怎么判断哪些参数该砍?

剪枝的核心问题就一个:怎么定义“不重要”?

我常用的策略有三种,各有各的脾气。

3.3.1 L1/L2 范数剪枝

这是最直观的方法。计算每个权重或每个通道的 L1 或 L2 范数,范数越小,说明这个参数对输出的贡献越小,优先剪掉。

策略 计算公式 特点
L1 范数 ||w||₁ = Σ|wᵢ| 倾向于产生稀疏解,对异常值不敏感
L2 范数 ||w||₂ = √(Σwᵢ²) 对大权重更敏感,保留能量大的通道

我在项目中遇到过这种情况:用 L1 范数剪枝,剪完后模型精度掉得比预期快。后来发现是因为 L1 范数对所有权重一视同仁,而有些通道虽然范数小,但负责的是关键特征(比如边缘检测)。所以我现在更倾向于用 L2 范数,或者两者结合。

3.3.2 基于梯度的剪枝

范数只看权重本身的大小,但梯度能告诉我们权重对 loss 的影响程度。

具体做法是:在训练过程中,记录每个权重的梯度值。如果一个权重的值很小,而且梯度也很小,说明它既不重要,也不在优化方向上,可以放心剪掉。

# 基于梯度与权重的混合重要性评分
importance = abs(weight) * abs(gradient)

这个公式看着简单,但效果出奇的好。我习惯在训练的最后几个 epoch 开始收集梯度信息,然后做一次性剪枝。为什么?因为训练初期梯度波动大,容易误判。

💡 我的个人经验: 如果你资源有限,只做一次剪枝,我建议用“L2 范数 + 梯度”的混合策略。单独用范数容易剪掉“小而精”的参数,单独用梯度又容易受学习率影响。两者相乘,相当于给重要性加了双重保险。

3.4 剪枝后的微调与恢复

剪枝不是终点,微调才是关键。你想想看,模型被砍掉一部分参数后,剩下的参数需要重新适应新的网络结构。

我见过不少新手,剪完枝直接部署,结果精度暴跌 10 个点。然后跑来问我:“是不是剪枝算法有问题?”其实不是算法的问题,是没做微调。

3.4.1 标准微调流程

  1. 剪枝后重置学习率:我一般把学习率设为初始训练时的 1/10。比如原来用 0.01,微调时用 0.001。
  2. 冻结未剪枝层:如果只剪了部分层,其他层可以先冻结,只训练被剪的层。等被剪层恢复精度后,再解冻所有层做联合微调。
  3. 迭代剪枝:不要一次剪太多。我习惯每次剪 10%-15%,然后微调 5-10 个 epoch,再继续剪。这叫“渐进式剪枝”。

3.4.2 精度恢复的“黄金法则”

根据我的经验,剪枝后微调能否恢复精度,取决于三个因素:

  • 剪枝率:低于 30% 的剪枝,微调后基本能恢复原精度。超过 50%,精度损失就很难补回来了。
  • 模型冗余度:大模型(比如 ResNet-50)比小模型(比如 MobileNet)更能扛剪枝。因为大模型本身就有冗余。
  • 微调时长:我建议至少微调原训练周期的 20%。比如原模型训练了 100 个 epoch,微调至少 20 个 epoch。

🔑 核心要点总结:

  • 非结构化剪枝:压缩率高,但硬件不友好,适合存储优化
  • 结构化剪枝:硬件友好,适合推理加速,但压缩率有限
  • 剪枝策略:L2 范数 + 梯度混合策略最稳健
  • 微调是必须的,渐进式剪枝比一次性剪枝效果好得多

3.5 本章知识体系图

下面这张图帮你理清剪枝技术的整体脉络:

模型剪枝技术体系 非结构化剪枝 结构化剪枝 L1/L2 范数剪枝 基于梯度剪枝 混合策略:L2 范数 × 梯度 微调与精度恢复 推理加速 ✅ 剪枝类型 剪枝策略 优化方法

这张图把剪枝的完整流程串起来了。从选择剪枝类型,到确定剪枝策略,再到最后的微调恢复,每一步都有讲究。我个人觉得,最关键的还是微调这一步——剪枝只是手段,恢复精度才是目的。

📌 避坑指南: 我曾经在剪枝后直接做 fine-tune,结果发现精度不升反降。排查了半天,原来是学习率设得太大了,导致剩余参数被冲乱。后来我把学习率降到原来的 1/10,再配合 warmup,精度才慢慢爬回来。所以记住:剪枝后的微调,学习率一定要小,步子要稳。

好了,这一章的内容就到这里。剪枝技术说难不难,说简单也不简单,关键是多动手试。你可以在自己的模型上跑一遍,看看不同剪枝策略的效果差异,感受会更直观。