模型剪枝:结构化与非结构化剪枝原理与实践

大家好,我是你们这堂课的主讲人。今天咱们聊聊模型剪枝。

说实话,模型剪枝是我在实际项目中用得最多的压缩技术。为什么?因为它直观,效果好,而且落地起来相对可控。你想想看,一个神经网络里,很多参数其实都是「冗余」的——它们对最终结果贡献很小,甚至没有贡献。剪掉它们,模型照样跑得欢。

但剪枝不是乱剪。剪错了,模型精度直接崩掉。我刚开始做剪枝的时候,就吃过这个亏。嗯,今天我把这些经验都摊开来讲。

什么是模型剪枝?

模型剪枝,说白了就是「砍掉不重要的连接或神经元」。

一个训练好的模型,权重矩阵里有很多值接近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。

剪枝的完整流程

不管是结构化还是非结构化,剪枝的流程都差不多。我总结为三步:

  1. 训练一个基准模型——先让模型收敛到不错的精度
  2. 剪枝——根据某种准则(比如L1范数、梯度、信息熵)剪掉不重要的参数
  3. 微调——剪完之后,模型精度会掉。用少量epoch重新训练,恢复精度

这个流程可以迭代多次。每次剪一点,微调一下,再剪一点。这叫迭代式剪枝

我个人习惯用L1范数作为剪枝准则。为什么?简单有效。权重的绝对值之和越小,说明这个卷积核/神经元对输出的影响越小。剪掉它,损失最小。

当然,也有更高级的准则,比如基于梯度的、基于信息论的。但说实话,在大多数实际项目中,L1范数已经够用了。别为了炫技而过度设计。

两种剪枝的对比

对比维度 非结构化剪枝 结构化剪枝
剪枝粒度 单个权重 通道/卷积核/层
存储压缩 高(可达90%+稀疏度) 中等(取决于剪枝比例)
推理加速 低(需专用硬件) 高(通用硬件直接加速)
精度损失 较小(细粒度调整) 较大(粗粒度删除)
实现难度 简单 中等(需调整网络结构)
适用场景 存储受限、有稀疏计算硬件 推理加速、边缘设备部署

你看这个表就明白了。非结构化剪枝适合「存得下」,结构化剪枝适合「跑得快」。具体选哪个,看你的业务需求。

知识体系图:剪枝的核心逻辑

下面我用一张SVG图来总结剪枝的知识体系。这张图把剪枝的流程、分类、关键决策点都串起来了。

模型剪枝知识体系 模型剪枝 非结构化剪枝 结构化剪枝 剪单个权重 L1/L2准则 稀疏存储 剪整个通道 剪卷积核 剪整层 完整流程:训练基准模型 → 剪枝 → 微调恢复精度 → 迭代 关键决策:存储压缩选非结构化,推理加速选结构化

这张图把剪枝的核心逻辑讲清楚了。你从中间开始,往左是非结构化,往右是结构化。底部是完整流程,最下面是关键决策点。

我的建议:刚开始做剪枝,先从结构化剪枝入手。虽然实现起来稍微复杂一点,但效果立竿见影。非结构化剪枝虽然压缩率高,但如果没有稀疏计算硬件,加速效果很有限。别为了追求压缩率而牺牲实际部署效果。

实践中的注意事项

最后,分享几个我在实际项目中总结的经验:

  • 剪枝比例不是越高越好。我见过有人把模型剪到95%稀疏度,精度掉了20个点,微调也救不回来。一般来说,结构化剪枝控制在30%-50%比较安全。
  • 不同层对剪枝的敏感度不同。靠近输入的层,剪枝要谨慎。靠近输出的层,可以多剪一些。我习惯用逐层敏感度分析来确定每层的剪枝比例。
  • 微调的学习率要小。剪完之后,模型处于一个「受伤」的状态。用太大的学习率,容易让模型跑偏。我一般用原学习率的十分之一。
  • 别忘了做推理速度测试。剪完之后,别只看模型体积和精度。一定要在目标硬件上跑一下,看看实际推理时间。有时候剪枝后模型体积小了,但推理速度没变——那就是白干了。

好了,这一章的内容就到这里。剪枝是个实践性很强的技术,光看理论没用。我建议你找个开源模型,比如ResNet-18或者MobileNet,动手剪一剪。踩几个坑,你就真正理解了。

专注资料整理