4. 剪枝分类(下):静态剪枝 vs 动态剪枝,训练后剪枝 vs 训练时剪枝
好,咱们接着聊剪枝的分类。上一节我们把结构化剪枝和非结构化剪枝讲透了,这一节要聊的这两对概念,其实更偏向「什么时候剪」和「怎么剪」的问题。
我个人习惯把剪枝策略分成两个维度来看:一个是剪枝的时机(训练前、训练中、训练后),另一个是剪枝的方式(静态还是动态)。这两个维度交叉组合,基本能覆盖业界主流的剪枝方案。
4.1 静态剪枝 vs 动态剪枝
先说说静态剪枝和动态剪枝。这俩的区别,说白了就是:剪完以后,模型结构还变不变?
静态剪枝:一次剪完,永不再改
静态剪枝,就是训练结束后,我们一次性把不重要的权重或通道删掉。删完之后,模型结构就固定了。以后每次推理,都用这个固定结构。
我在项目中遇到过这样一个场景:要把一个 50MB 的模型部署到嵌入式设备上。设备存储只有 30MB。怎么办?静态剪枝。我直接剪掉 40% 的通道,模型变成 28MB,结构固定,推理速度也快了 1.5 倍。嗯,效果还不错。
静态剪枝的优点很明显:
- 部署简单:剪完后的模型就是一个标准模型,不需要额外运行时支持
- 推理速度快:结构变小了,计算量自然减少
- 硬件友好:特别是结构化剪枝,对 GPU、NPU 都很友好
缺点呢?
- 不够灵活:剪错了就回不去了
- 需要重新训练:剪完后通常要微调,不然精度掉得厉害
核心要点:静态剪枝适合部署环境固定的场景。比如手机端、IoT 设备,模型一旦部署就不动了。
动态剪枝:运行时动态决定
动态剪枝就灵活多了。模型在推理时,会根据输入数据动态决定哪些权重或通道可以跳过。说白了,就是「看菜下饭」——输入简单,就多剪点;输入复杂,就少剪点。
我记得有一次做视频分析模型,输入画面大部分时间都是静止的背景,只有一小块区域在动。如果用静态剪枝,为了保精度,剪枝率不敢设太高。但用动态剪枝,遇到静止画面时,模型自动跳过大量计算,推理速度直接翻倍。
动态剪枝的实现方式主要有两种:
- 基于门控机制:训练一个门控网络,决定哪些通道激活
- 基于阈值判断:设定一个阈值,激活值低于阈值的通道直接跳过
注意:动态剪枝虽然灵活,但需要额外的运行时支持。你想想看,每次推理都要做一次「剪枝决策」,这个决策本身也是有计算成本的。如果决策成本太高,反而得不偿失。
动态剪枝的优缺点:
- 优点:自适应性强,对不同输入都能达到最优效率
- 缺点:实现复杂,硬件支持要求高,推理延迟不稳定
4.2 训练后剪枝 vs 训练时剪枝
这一对概念,关注的是剪枝发生在训练的哪个阶段。
训练后剪枝:先训练,再剪枝
这是最传统、最常用的方式。流程很简单:
- 先训练一个完整的大模型
- 然后对模型进行剪枝
- 最后微调恢复精度
我曾经在一个图像分类项目里用过这种方式。模型是 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)
小技巧:训练后剪枝时,我建议先做一次「剪枝敏感性分析」。就是逐层剪枝,看看每层对精度的影响有多大。敏感度高的层少剪点,敏感度低的层多剪点。这样剪出来的模型,精度损失最小。
训练时剪枝:边训练边剪枝
训练时剪枝,也叫「渐进式剪枝」。它的思路是:在训练过程中,逐步把不重要的权重剪掉。而不是等训练完了再一刀切。
为什么会有这种思路?你想想看,训练后剪枝有个问题:模型已经收敛到某个局部最优了,你突然剪掉一堆权重,模型得重新适应。而训练时剪枝,模型在训练过程中就慢慢适应了「被剪掉」的状态,最终收敛时,精度损失往往更小。
训练时剪枝的典型流程:
- 初始化一个完整模型
- 训练几个 epoch,让模型初步收敛
- 剪掉一小部分权重(比如 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 训练时:什么时候剪?训练后一刀切,训练时慢慢来。
实际项目中,我一般这样组合:
- 如果是做学术研究或探索性项目,我会用训练时 + 动态剪枝,灵活性最高
- 如果是做产品落地,我会用训练后 + 静态剪枝,稳定可靠
- 如果是做大模型,我会用训练时 + 静态剪枝,兼顾效率和效果
下一节,我们会聊剪枝的具体实现技术,包括如何计算权重重要性、如何选择剪枝率等。嗯,这些才是真正动手时最需要的东西。