模型剪枝:结构化与非结构化剪枝原理与实践
大家好,我是你们这堂课的主讲人。今天咱们聊聊模型剪枝。
说实话,模型剪枝是我在实际项目中用得最多的压缩技术。为什么?因为它直观,效果好,而且落地起来相对可控。你想想看,一个神经网络里,很多参数其实都是「冗余」的——它们对最终结果贡献很小,甚至没有贡献。剪掉它们,模型照样跑得欢。
但剪枝不是乱剪。剪错了,模型精度直接崩掉。我刚开始做剪枝的时候,就吃过这个亏。嗯,今天我把这些经验都摊开来讲。
什么是模型剪枝?
模型剪枝,说白了就是「砍掉不重要的连接或神经元」。
一个训练好的模型,权重矩阵里有很多值接近0。这些值对推理结果影响微乎其微。把它们置为0,或者直接删掉对应的连接,模型体积就变小了,推理速度也快了。
剪枝的核心问题就两个:
- 剪哪里?——哪些权重或神经元不重要
- 怎么剪?——剪完之后怎么恢复精度
我个人习惯把剪枝分为两大类:非结构化剪枝和结构化剪枝。这两者的区别,直接决定了你的模型能不能在硬件上真正加速。
非结构化剪枝:细粒度,但硬件不友好
非结构化剪枝,也叫细粒度剪枝。它剪的是单个权重。
做法很简单:设定一个阈值,把绝对值小于阈值的权重置为0。比如一个卷积核有9个权重,其中3个接近0,那就把它们变成0。
代码实现也很直接:
import torch
import torch.nn.utils.prune as prune
# 定义一个简单的线性层
model = torch.nn.Linear(10, 5)
# 对权重进行非结构化剪枝,剪掉50%的参数
prune.l1_unstructured(model, name='weight', amount=0.5)
# 查看剪枝后的权重
print(model.weight)
你看,就这么几行代码,50%的参数就被置为0了。
但问题来了——这些0值权重,在硬件上真的被跳过了吗?
答案是否定的。大多数硬件(CPU、GPU)在执行矩阵乘法时,并不会自动跳过0值。你只是把权重变成了0,但计算量一点没少。推理速度没变,模型体积倒是小了——因为可以用稀疏格式存储。
我在项目中遇到过这种情况:非结构化剪枝到90%稀疏度,模型文件小了5倍,但推理速度只快了10%。嗯,这就是典型的「存储省了,计算没省」。
核心结论:非结构化剪枝适合存储压缩,不适合推理加速(除非你有专门的稀疏计算硬件,比如NVIDIA的Ampere架构支持2:4稀疏)。
结构化剪枝:粗粒度,硬件友好
结构化剪枝,剪的是整个结构单元——比如一个卷积核、一个通道、甚至一整层。
举个例子:一个卷积层有64个卷积核,我们评估后发现其中8个对输出贡献很小。那就直接把这8个卷积核删掉。删完之后,这一层的输出通道数从64变成56,后续层的输入通道数也跟着变。
这样做的好处是:模型结构变了,但计算图还是规则的。硬件可以正常执行矩阵乘法,没有稀疏性带来的额外开销。推理速度直接提升。
代码示例(使用PyTorch的通道剪枝):
import torch
import torch.nn.utils.prune as prune
# 假设我们有一个卷积层
conv = torch.nn.Conv2d(3, 64, kernel_size=3)
# 对卷积核进行结构化剪枝,剪掉25%的通道
prune.ln_structured(conv, name='weight', amount=0.25, n=2, dim=0)
# 注意:这里只是把被剪的卷积核置为0,并没有真正删除通道
# 真正删除需要后续处理
这里有个坑:PyTorch的prune.ln_structured只是把权重置为0,并没有真正改变张量的形状。要真正删除通道,你需要自己写代码或者用专门的库(比如torch.nn.utils.prune的remove方法,或者用TensorRT的剪枝工具)。
我曾经踩过的坑:有一次做结构化剪枝,剪完通道后直接跑推理,发现速度没变。查了半天才发现,PyTorch只是把权重置为0,通道数还是64。真正的通道删除需要手动调整网络结构。从那以后,我每次剪枝都会检查一下张量的shape。
剪枝的完整流程
不管是结构化还是非结构化,剪枝的流程都差不多。我总结为三步:
- 训练一个基准模型——先让模型收敛到不错的精度
- 剪枝——根据某种准则(比如L1范数、梯度、信息熵)剪掉不重要的参数
- 微调——剪完之后,模型精度会掉。用少量epoch重新训练,恢复精度
这个流程可以迭代多次。每次剪一点,微调一下,再剪一点。这叫迭代式剪枝。
我个人习惯用L1范数作为剪枝准则。为什么?简单有效。权重的绝对值之和越小,说明这个卷积核/神经元对输出的影响越小。剪掉它,损失最小。
当然,也有更高级的准则,比如基于梯度的、基于信息论的。但说实话,在大多数实际项目中,L1范数已经够用了。别为了炫技而过度设计。
两种剪枝的对比
| 对比维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 通道/卷积核/层 |
| 存储压缩 | 高(可达90%+稀疏度) | 中等(取决于剪枝比例) |
| 推理加速 | 低(需专用硬件) | 高(通用硬件直接加速) |
| 精度损失 | 较小(细粒度调整) | 较大(粗粒度删除) |
| 实现难度 | 简单 | 中等(需调整网络结构) |
| 适用场景 | 存储受限、有稀疏计算硬件 | 推理加速、边缘设备部署 |
你看这个表就明白了。非结构化剪枝适合「存得下」,结构化剪枝适合「跑得快」。具体选哪个,看你的业务需求。
知识体系图:剪枝的核心逻辑
下面我用一张SVG图来总结剪枝的知识体系。这张图把剪枝的流程、分类、关键决策点都串起来了。
这张图把剪枝的核心逻辑讲清楚了。你从中间开始,往左是非结构化,往右是结构化。底部是完整流程,最下面是关键决策点。
我的建议:刚开始做剪枝,先从结构化剪枝入手。虽然实现起来稍微复杂一点,但效果立竿见影。非结构化剪枝虽然压缩率高,但如果没有稀疏计算硬件,加速效果很有限。别为了追求压缩率而牺牲实际部署效果。
实践中的注意事项
最后,分享几个我在实际项目中总结的经验:
- 剪枝比例不是越高越好。我见过有人把模型剪到95%稀疏度,精度掉了20个点,微调也救不回来。一般来说,结构化剪枝控制在30%-50%比较安全。
- 不同层对剪枝的敏感度不同。靠近输入的层,剪枝要谨慎。靠近输出的层,可以多剪一些。我习惯用逐层敏感度分析来确定每层的剪枝比例。
- 微调的学习率要小。剪完之后,模型处于一个「受伤」的状态。用太大的学习率,容易让模型跑偏。我一般用原学习率的十分之一。
- 别忘了做推理速度测试。剪完之后,别只看模型体积和精度。一定要在目标硬件上跑一下,看看实际推理时间。有时候剪枝后模型体积小了,但推理速度没变——那就是白干了。
好了,这一章的内容就到这里。剪枝是个实践性很强的技术,光看理论没用。我建议你找个开源模型,比如ResNet-18或者MobileNet,动手剪一剪。踩几个坑,你就真正理解了。