4. 剪枝分类(下):静态剪枝 vs 动态剪枝,训练后剪枝 vs 训练时剪枝

好,咱们接着聊剪枝的分类。上一节我们把结构化剪枝和非结构化剪枝讲透了,这一节要聊的这两对概念,其实更偏向「什么时候剪」和「怎么剪」的问题。

我个人习惯把剪枝策略分成两个维度来看:一个是剪枝的时机(训练前、训练中、训练后),另一个是剪枝的方式(静态还是动态)。这两个维度交叉组合,基本能覆盖业界主流的剪枝方案。

4.1 静态剪枝 vs 动态剪枝

先说说静态剪枝和动态剪枝。这俩的区别,说白了就是:剪完以后,模型结构还变不变?

静态剪枝:一次剪完,永不再改

静态剪枝,就是训练结束后,我们一次性把不重要的权重或通道删掉。删完之后,模型结构就固定了。以后每次推理,都用这个固定结构。

我在项目中遇到过这样一个场景:要把一个 50MB 的模型部署到嵌入式设备上。设备存储只有 30MB。怎么办?静态剪枝。我直接剪掉 40% 的通道,模型变成 28MB,结构固定,推理速度也快了 1.5 倍。嗯,效果还不错。

静态剪枝的优点很明显:

  • 部署简单:剪完后的模型就是一个标准模型,不需要额外运行时支持
  • 推理速度快:结构变小了,计算量自然减少
  • 硬件友好:特别是结构化剪枝,对 GPU、NPU 都很友好

缺点呢?

  • 不够灵活:剪错了就回不去了
  • 需要重新训练:剪完后通常要微调,不然精度掉得厉害

核心要点:静态剪枝适合部署环境固定的场景。比如手机端、IoT 设备,模型一旦部署就不动了。

动态剪枝:运行时动态决定

动态剪枝就灵活多了。模型在推理时,会根据输入数据动态决定哪些权重或通道可以跳过。说白了,就是「看菜下饭」——输入简单,就多剪点;输入复杂,就少剪点。

我记得有一次做视频分析模型,输入画面大部分时间都是静止的背景,只有一小块区域在动。如果用静态剪枝,为了保精度,剪枝率不敢设太高。但用动态剪枝,遇到静止画面时,模型自动跳过大量计算,推理速度直接翻倍。

动态剪枝的实现方式主要有两种:

  1. 基于门控机制:训练一个门控网络,决定哪些通道激活
  2. 基于阈值判断:设定一个阈值,激活值低于阈值的通道直接跳过

注意:动态剪枝虽然灵活,但需要额外的运行时支持。你想想看,每次推理都要做一次「剪枝决策」,这个决策本身也是有计算成本的。如果决策成本太高,反而得不偿失。

动态剪枝的优缺点:

  • 优点:自适应性强,对不同输入都能达到最优效率
  • 缺点:实现复杂,硬件支持要求高,推理延迟不稳定

4.2 训练后剪枝 vs 训练时剪枝

这一对概念,关注的是剪枝发生在训练的哪个阶段。

训练后剪枝:先训练,再剪枝

这是最传统、最常用的方式。流程很简单:

  1. 先训练一个完整的大模型
  2. 然后对模型进行剪枝
  3. 最后微调恢复精度

我曾经在一个图像分类项目里用过这种方式。模型是 ResNet-50,先训到 76% 的 Top-1 精度,然后剪掉 30% 的通道,精度掉到 73%。再微调 10 个 epoch,精度回到 75.5%。嗯,基本可以接受。

训练后剪枝的代码实现其实很简单:

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

# 加载预训练模型
model = torch.load('resnet50_pretrained.pth')

# 对卷积层进行 L1 非结构化剪枝
prune.l1_unstructured(
    model.conv1, 
    name='weight', 
    amount=0.3  # 剪掉 30% 的权重
)

# 剪枝后需要微调
for epoch in range(10):
    train_one_epoch(model, train_loader)
    validate(model, val_loader)

小技巧:训练后剪枝时,我建议先做一次「剪枝敏感性分析」。就是逐层剪枝,看看每层对精度的影响有多大。敏感度高的层少剪点,敏感度低的层多剪点。这样剪出来的模型,精度损失最小。

训练时剪枝:边训练边剪枝

训练时剪枝,也叫「渐进式剪枝」。它的思路是:在训练过程中,逐步把不重要的权重剪掉。而不是等训练完了再一刀切。

为什么会有这种思路?你想想看,训练后剪枝有个问题:模型已经收敛到某个局部最优了,你突然剪掉一堆权重,模型得重新适应。而训练时剪枝,模型在训练过程中就慢慢适应了「被剪掉」的状态,最终收敛时,精度损失往往更小。

训练时剪枝的典型流程:

  1. 初始化一个完整模型
  2. 训练几个 epoch,让模型初步收敛
  3. 剪掉一小部分权重(比如 5%)
  4. 继续训练,让模型适应
  5. 重复步骤 3-4,直到达到目标剪枝率

代码示例:

def progressive_pruning(model, train_loader, target_sparsity=0.5):
    current_sparsity = 0.0
    step_size = 0.05  # 每次剪 5%
    
    while current_sparsity < target_sparsity:
        # 训练几个 epoch
        for epoch in range(5):
            train_one_epoch(model, train_loader)
        
        # 剪掉 5% 的权重
        prune.l1_unstructured(model.conv1, name='weight', amount=step_size)
        current_sparsity += step_size
        
        print(f'当前稀疏度: {current_sparsity:.2f}')

关键区别:训练后剪枝是「先胖再瘦」,训练时剪枝是「边吃边减」。我个人更推荐训练时剪枝,特别是对于大模型。因为大模型微调成本高,训练时剪枝能让模型在训练过程中就学会「在稀疏状态下工作」。

4.3 如何选择?一张表说清楚

说了这么多,到底怎么选?我整理了一张表,方便你对照:

场景 推荐方案 原因
小模型快速部署 训练后 + 静态剪枝 简单直接,不需要额外训练成本
大模型(百亿参数以上) 训练时 + 静态剪枝 训练成本高,边训边剪更高效
动态输入场景(视频、语音) 训练后 + 动态剪枝 需要自适应不同输入复杂度
硬件资源极度受限 训练时 + 结构化静态剪枝 对硬件最友好,推理效率最高

避坑指南:我曾经在一个项目里,为了追求极致压缩率,同时用了动态剪枝和训练时剪枝。结果模型训练了 3 天才收敛,而且推理时动态决策的开销太大,实际加速效果还不如静态剪枝。所以,不要盲目堆叠剪枝策略,适合的才是最好的。

4.4 小结

这一节我们聊了两对概念:

  • 静态 vs 动态:剪完后结构变不变?静态固定,动态自适应。
  • 训练后 vs 训练时:什么时候剪?训练后一刀切,训练时慢慢来。

实际项目中,我一般这样组合:

  • 如果是做学术研究或探索性项目,我会用训练时 + 动态剪枝,灵活性最高
  • 如果是做产品落地,我会用训练后 + 静态剪枝,稳定可靠
  • 如果是做大模型,我会用训练时 + 静态剪枝,兼顾效率和效果

下一节,我们会聊剪枝的具体实现技术,包括如何计算权重重要性、如何选择剪枝率等。嗯,这些才是真正动手时最需要的东西。