剪枝分类:结构化剪枝 vs 非结构化剪枝,权重剪枝 vs 神经元剪枝
好,咱们今天来聊聊剪枝的分类。说实话,我刚入行那会儿,看到各种剪枝名词,脑子是懵的。什么结构化、非结构化,权重剪枝、神经元剪枝……感觉像绕口令。其实你把它拆开看,就两条主线:剪枝的粒度和剪枝的对象。
我个人习惯,先搞清楚「剪什么」,再想「怎么剪」。这样思路就顺了。
一、按粒度分:结构化剪枝 vs 非结构化剪枝
这个分类,说白了就是看你是「整块整块地砍」,还是「零敲碎打地剔」。
1. 非结构化剪枝(Unstructured Pruning)
非结构化剪枝,也叫细粒度剪枝。它针对的是单个权重参数。你觉得哪个权重不重要,直接把它置零。
举个例子,一个卷积核是 3x3 的,里面有 9 个权重。非结构化剪枝可能只把其中 3 个小的权重变成 0,剩下的 6 个还留着。你看,这就像在筛子里戳了几个洞,结构还在,但参数稀疏了。
核心特点:
- 剪枝粒度:单个权重
- 稀疏模式:不规则、随机分布
- 硬件友好度:低(需要特殊硬件或库支持稀疏计算)
- 压缩率:高(理论上可以剪掉 90% 以上)
我在项目中遇到过一个问题:用非结构化剪枝把模型压到了 10% 的密度,结果在普通 GPU 上跑,速度反而变慢了。为什么会这样?因为 GPU 是为密集矩阵乘法优化的,你搞一堆零,它还得一个个跳过,反而浪费了带宽。嗯,这里要注意,非结构化剪枝的收益,很大程度上取决于你的推理框架是否支持稀疏加速。
我的建议:如果你用的是 PyTorch 或 TensorFlow 的标准推理,非结构化剪枝更适合做模型体积压缩(比如从 100MB 压到 20MB),但不一定能提速。想提速?得配合专门的稀疏推理库,比如 NVIDIA 的 cuSPARSE 或者 Intel 的 MKL-DNN。
2. 结构化剪枝(Structured Pruning)
结构化剪枝就粗暴多了。它不跟你玩单个权重,而是直接砍掉整个通道、整个卷积核,甚至整个层。
你想想看,一个卷积层有 64 个卷积核,每个核是 3x3x3 的。结构化剪枝可能直接判定其中 16 个核是「废物」,连根拔起。这样一来,输出通道数就从 64 变成了 48。整个网络结构都变了。
核心特点:
- 剪枝粒度:通道、卷积核、层
- 稀疏模式:规则、整齐
- 硬件友好度:高(直接减少计算量,无需特殊支持)
- 压缩率:相对较低(通常 20%-50%)
我曾经在部署一个目标检测模型到手机端时,就用了结构化剪枝。因为手机芯片(比如高通、联发科)对不规则稀疏计算基本不支持。你搞非结构化剪枝,它根本不认。但结构化剪枝砍掉通道后,模型的计算图直接变小了,推理速度肉眼可见地提升。嗯,这才是真香。
避坑指南:结构化剪枝虽然硬件友好,但精度损失往往比非结构化大。我曾经试过一口气砍掉 50% 的通道,结果模型直接崩了,准确率掉了 15 个点。后来我改成渐进式剪枝,每次砍 10%,然后微调恢复,才稳住。所以,别贪心,慢慢来。
二、按对象分:权重剪枝 vs 神经元剪枝
这个分类,是从「剪枝的目标」来区分的。一个是剪参数,一个是剪激活值。
1. 权重剪枝(Weight Pruning)
权重剪枝,就是直接对模型的参数矩阵下手。它判断某个权重值太小(比如接近 0),就认为它对最终输出贡献不大,直接置零。
这其实是最常见的剪枝方式。非结构化剪枝几乎都是权重剪枝。结构化剪枝里,如果砍的是单个卷积核的权重,也算权重剪枝。
我记得有一次,我在做 BERT 模型的压缩。BERT 的参数量动辄上亿,全量部署根本不可能。我用了权重剪枝,把注意力层和全连接层中绝对值小于阈值的权重全部置零。结果模型体积从 400MB 降到了 120MB,精度只掉了 0.3%。效果相当不错。
# 一个简单的权重剪枝示例(PyTorch 风格)
import torch
import torch.nn.utils.prune as prune
# 假设 model 是一个训练好的模型
# 对 conv1 层的权重进行 L1 非结构化剪枝,剪掉 20%
prune.l1_unstructured(module=model.conv1, name='weight', amount=0.2)
# 剪枝后,权重矩阵中 20% 的元素变成了 0
print(model.conv1.weight)
个人经验:权重剪枝时,阈值的选择很关键。我一般会先看权重的分布直方图。如果大部分权重集中在 0 附近,说明模型本身就有冗余,可以大胆剪。如果分布比较均匀,就得小心了,剪多了精度会掉得很快。
2. 神经元剪枝(Neuron Pruning)
神经元剪枝,也叫单元剪枝。它针对的是神经元(或者说激活值)。一个神经元对应一个输出通道或一个隐藏单元。如果某个神经元的输出总是接近 0,或者对后续层的影响很小,那就可以把它整个删掉。
你想想看,神经元剪枝本质上就是结构化剪枝的一种。因为它一删就是整个通道或整个节点。所以,神经元剪枝通常会导致网络结构的变化。
举个例子,一个全连接层有 1024 个神经元。经过训练后,发现其中 200 个神经元的激活值几乎全是 0。那这 200 个神经元就是「死神经元」,留着纯属浪费计算资源。直接删掉,下一层的输入维度就从 1024 变成 824。
对比总结:
| 维度 | 权重剪枝 | 神经元剪枝 |
|---|---|---|
| 剪枝对象 | 单个权重参数 | 整个神经元/通道 |
| 粒度 | 细粒度 | 粗粒度 |
| 结构变化 | 不改变网络结构 | 改变网络结构 |
| 硬件加速 | 需特殊支持 | 天然支持 |
| 典型场景 | 模型体积压缩 | 推理速度优化 |
我在实际项目中,经常把两者结合起来用。先用神经元剪枝砍掉不重要的通道,把模型结构瘦身。然后再用权重剪枝,对剩下的参数做精细化的稀疏化。这样既能保证硬件友好,又能进一步压缩体积。说白了,就是「先宏观后微观」。
注意:神经元剪枝后,一定要做微调(fine-tuning)。因为网络结构变了,后面的层需要重新适应新的输入分布。我曾经偷懒没做微调,结果模型精度掉了 8 个点,后来老老实实跑了 10 个 epoch 才恢复。嗯,这个坑我替你们踩过了。
三、我的选择建议
说了这么多,到底该用哪种?我个人的经验是:
- 如果你追求极致压缩率(比如模型体积从 1GB 压到 100MB),选非结构化权重剪枝。但要做好推理框架适配的准备。
- 如果你追求推理速度(比如部署到手机或边缘设备),选结构化神经元剪枝。直接砍通道,效果立竿见影。
- 如果你两者都要(既要体积小又要速度快),那就先结构化剪枝,再非结构化剪枝。但要注意,两次剪枝之间一定要做微调,否则精度会崩。
好了,关于剪枝的分类,咱们就聊到这儿。下一节我会具体讲剪枝的评判标准——怎么判断一个权重或神经元该不该剪。到时候咱们再细聊。