4. 模型剪枝技术:结构化剪枝与非结构化剪枝、通道剪枝、权重剪枝实战
模型剪枝,说白了就是给神经网络“瘦身”。一个训练好的大模型,里面有很多参数其实贡献不大,砍掉它们,模型照样能干活,但体积更小、跑得更快。
我刚开始做模型部署时,总觉得剪枝很玄乎。后来在项目里被逼着把一个大模型塞进嵌入式设备,才真正体会到——剪枝不是玄学,是实打实的工程手段。今天咱们就把结构化剪枝、非结构化剪枝、通道剪枝、权重剪枝这几种主流方法,掰开揉碎了讲清楚。
4.1 剪枝的核心思想:为什么要“砍”参数?
一个训练好的神经网络,参数分布往往很稀疏。很多权重值接近零,对最终输出的贡献微乎其微。剪枝就是把这些“冗余”参数去掉,换来更小的模型体积和更快的推理速度。
剪枝的收益很直接:
- 体积减小:存储占用降低,适合嵌入式设备
- 计算加速:减少乘加运算次数,降低延迟
- 功耗降低:计算量少了,芯片自然更省电
关键指标:剪枝率 = 剪掉的参数数量 / 原始参数总数。剪枝率越高,压缩比越大,但精度损失风险也越高。我一般建议从 30% 开始试,逐步往上加。
4.2 非结构化剪枝:细粒度,但硬件不友好
非结构化剪枝,也叫细粒度剪枝。它针对单个权重做判断——如果某个权重的值小于阈值,就把它置为零。
举个例子:一个 3×3 的卷积核,里面有 9 个权重值。剪枝后可能变成这样:
原始权重: [0.12, -0.05, 0.98, 0.01, -0.03, 0.45, 0.00, 0.67, -0.22]
剪枝后: [0.12, 0.00, 0.98, 0.00, 0.00, 0.45, 0.00, 0.67, 0.00]
你看,值小于 0.1 的权重都被清零了。这种方法的优点是精度损失小,因为只砍掉最不重要的参数。但缺点也很明显——稀疏矩阵。
为什么说它硬件不友好?因为现代 CPU/GPU/NPU 都是为密集矩阵运算优化的。稀疏矩阵里大量零值,计算单元还得跳着读数据,访存效率极低。我在项目里试过,非结构化剪枝到 50% 以上,推理速度反而变慢了——因为稀疏计算的开销超过了计算减少的收益。
我曾经踩过的坑:非结构化剪枝后的模型,用普通框架推理几乎没加速效果。必须搭配专门的稀疏计算库(如 NVIDIA 的 cuSPARSE)或定制硬件才能发挥优势。如果你用的是通用芯片,建议谨慎选择。
4.3 结构化剪枝:粗粒度,硬件友好
结构化剪枝,砍的不是单个权重,而是整个“结构单元”——比如一个卷积核、一个通道、甚至一整层。这样剪完后,模型仍然是密集矩阵,硬件可以直接跑,不需要特殊支持。
常见的结构化剪枝粒度:
| 剪枝粒度 | 砍掉的对象 | 硬件友好度 | 精度损失 |
|---|---|---|---|
| 权重剪枝 | 单个权重 | 低 | 低 |
| 卷积核剪枝 | 整个卷积核 | 中 | 中 |
| 通道剪枝 | 整个通道 | 高 | 较高 |
| 层剪枝 | 整层网络 | 最高 | 高 |
我个人习惯优先用通道剪枝。为什么?因为通道剪枝直接减少特征图数量,后续所有层的计算量都会跟着降,收益最大。而且剪完后模型结构不变,硬件直接跑,不需要改代码。
4.4 通道剪枝实战:以卷积层为例
通道剪枝的核心是:评估每个通道的重要性,砍掉不重要的通道。评估方法有很多,我常用的是 L1 范数——一个通道对应的卷积核权重值之和越小,说明这个通道越不重要。
具体步骤:
- 对每个卷积核,计算其所有权重的 L1 范数
- 按 L1 范数从小到大排序
- 设定剪枝率,砍掉排名靠后的通道
- 微调模型,恢复精度
代码实现(PyTorch 风格):
def channel_prune(conv_layer, prune_ratio):
# 计算每个卷积核的 L1 范数
l1_norm = torch.sum(torch.abs(conv_layer.weight.data), dim=(1,2,3))
# 按 L1 范数排序,找到要保留的通道数
num_channels = conv_layer.weight.shape[0]
num_keep = int(num_channels * (1 - prune_ratio))
# 保留 L1 范数最大的 num_keep 个通道
_, indices = torch.topk(l1_norm, num_keep)
# 生成新的卷积层
new_conv = nn.Conv2d(
in_channels=conv_layer.in_channels,
out_channels=num_keep,
kernel_size=conv_layer.kernel_size,
stride=conv_layer.stride,
padding=conv_layer.padding
)
new_conv.weight.data = conv_layer.weight.data[indices]
new_conv.bias.data = conv_layer.bias.data[indices] if conv_layer.bias is not None else None
return new_conv, indices
小技巧:剪枝后一定要微调。我见过有人剪完直接部署,精度掉了 5 个点。微调 3-5 个 epoch,精度基本能回到原来的 98% 以上。微调时学习率要调低,一般是原学习率的 1/10。
4.5 权重剪枝实战:细粒度与粗粒度的折中
权重剪枝介于非结构化与结构化之间。它砍的是单个权重,但通过某种约束让稀疏模式变得“规则”,从而更容易被硬件加速。
举个例子,N:M 剪枝:每 M 个连续权重中,只保留 N 个非零值。比如 2:4 剪枝,每 4 个权重里保留 2 个最大的,其余置零。这种模式在 NVIDIA Ampere 架构的 GPU 上有专门的硬件支持,可以直接加速。
实现思路:
def n_m_prune(weight, n=2, m=4):
# 将权重按 m 个一组分组
shape = weight.shape
weight_flat = weight.view(-1)
num_groups = weight_flat.shape[0] // m
for g in range(num_groups):
start = g * m
end = start + m
group = weight_flat[start:end]
# 找到值最大的 n 个权重
_, indices = torch.topk(torch.abs(group), n)
mask = torch.zeros_like(group)
mask[indices] = 1
# 保留选中的权重,其余置零
weight_flat[start:end] = group * mask
return weight_flat.view(shape)
N:M 剪枝的好处是:稀疏模式固定,硬件可以预知数据布局,访存效率高。我在部署 Transformer 模型时常用 2:4 剪枝,推理速度能提升 1.5-2 倍,精度损失控制在 1% 以内。
4.6 剪枝策略对比与选型建议
不同剪枝方法适合不同场景。我整理了一张对比表,方便你选型:
| 方法 | 压缩率 | 硬件加速 | 精度保留 | 适用场景 |
|---|---|---|---|---|
| 非结构化剪枝 | 高 | 差 | 好 | 学术研究、定制硬件 |
| 通道剪枝 | 中 | 好 | 中 | 嵌入式部署、移动端 |
| N:M 权重剪枝 | 中高 | 好 | 较好 | GPU 加速、Transformer |
| 层剪枝 | 最高 | 最好 | 差 | 极简模型、资源极度受限 |
我的建议是:优先考虑通道剪枝。它平衡了压缩率和硬件友好度,是工业界最常用的方法。如果精度损失太大,再考虑 N:M 权重剪枝作为补充。
4.7 剪枝流程与知识体系
下面这张图总结了剪枝的完整流程和各类方法的关系:
4.8 避坑指南与实战建议
做了这么多年剪枝,我总结了几条血泪教训:
- 不要一次性剪太多:我建议每次剪 10%-20%,然后微调评估。一次性剪 50% 以上,精度可能崩盘,微调都救不回来。
- 注意 BN 层:通道剪枝后,BN 层的参数也要跟着调整。我见过有人忘了更新 BN 的 running_mean 和 running_var,推理结果全乱套。
- 评估指标要选对:除了精度,还要关注推理延迟和功耗。有时候剪枝后精度没降,但延迟反而高了——因为稀疏计算没优化好。
- 剪枝不是终点:剪完后的模型,建议再做一次量化。剪枝+量化组合拳,压缩效果翻倍。这个我们后面章节会细讲。
我的个人习惯:先做通道剪枝到 30%,微调后评估。如果精度损失小于 1%,继续加剪到 50%。如果损失超过 2%,就退回到 30%,改用 N:M 权重剪枝做补充。这样既能保证精度,又能最大化压缩率。