3、模型剪枝基础:剪枝的分类、粒度与流程
各位同学,欢迎来到模型剪枝的第一课。
说实话,模型剪枝这个概念,我第一次接触是在做BERT推理加速的时候。当时模型太大,线上延迟动不动就几百毫秒,老板天天催。后来我发现,其实模型里有很多参数是「冗余」的——就像你衣柜里那些一年没穿过的衣服,留着占地方,扔了也不心疼。
剪枝,就是干这个的。
3.1 剪枝的两大分类:结构化 vs 非结构化
先搞清楚一个核心问题:剪枝到底剪什么?
我个人习惯把剪枝分成两类,说白了就是「粗剪」和「细剪」的区别。
3.1.1 非结构化剪枝
非结构化剪枝,也叫细粒度剪枝。它直接干掉单个权重。比如一个权重矩阵里,绝对值小于某个阈值的参数,直接置零。
我在项目中遇到过这种情况:非结构化剪枝可以轻松干掉 90% 的参数,模型精度几乎不掉。听起来很爽对吧?但坑在后面——
嗯,这里要注意:非结构化剪枝更适合学术研究,或者你有定制硬件的情况。工业落地,我建议谨慎。
3.1.2 结构化剪枝
结构化剪枝,是整块整块地砍。比如砍掉一个神经元、一个注意力头、甚至一整层。
为什么我喜欢结构化剪枝?因为它剪完以后,模型结构还是规整的。权重矩阵还是稠密的,不需要特殊硬件支持。你想想看,直接删掉一行或一列,矩阵乘法照样跑,多省心。
我曾经在 Transformer 的 FFN 层做结构化剪枝,把中间维度从 2048 砍到 1024,推理速度直接翻倍。而且精度只掉了 0.3%。这种收益,老板看了都笑。
| 对比维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 神经元/头/层 |
| 模型结构 | 稀疏不规则 | 稠密规整 |
| 硬件加速 | 需要特殊支持 | 通用硬件即可 |
| 压缩率 | 高(可达 90%+) | 中等(通常 30%-50%) |
| 工业落地 | 较难 | 推荐 |
3.2 剪枝粒度:从权重到层,逐级深入
剪枝粒度,说白了就是「你打算多细地剪」。我一般把它分成四个层级,从细到粗:
3.2.1 权重级剪枝
这就是上面说的非结构化剪枝。每个权重独立判断去留。优点是灵活,缺点是加速难。
3.2.2 神经元级剪枝
砍掉整个神经元。在 Transformer 里,就是砍掉 FFN 层的一个隐藏单元。我建议用 L1 范数来评估神经元的重要性——范数小的,说明它对输出贡献小,优先砍掉。
核心思路: 每个神经元对应一个输出向量。如果这个向量的 L1 范数接近 0,那它基本就是个「哑巴神经元」,留着也没用。
3.2.3 注意力头级剪枝
这是 Transformer 特有的。多头注意力机制里,有些头其实是冗余的。我记得有一篇论文说,BERT 里砍掉 30% 的头,效果几乎不变。
怎么判断哪个头该砍?我常用的方法是:看这个头的注意力权重分布。如果它学出来的注意力模式跟随机差不多,那它就是「摸鱼头」,砍掉。
3.2.4 层级剪枝
最狠的剪枝——直接删掉一整层 Transformer Block。这招一般用在深层模型上,比如 24 层的 BERT Large,砍掉 2-3 层,速度提升明显。
但层级剪枝风险也大。我曾经试过砍掉 4 层,结果模型直接崩了,精度掉到没法看。所以我的建议是:层级剪枝要配合知识蒸馏,或者逐层微调,别一上来就大刀阔斧。
3.3 剪枝的标准流程:三步走
不管哪种剪枝,流程都差不多。我总结了一个三步法,简单粗暴:
- 训练一个完整的大模型 —— 先让它充分收敛,学到足够的知识。
- 评估重要性,剪掉冗余部分 —— 用各种指标(L1 范数、梯度、Hessian 矩阵等)判断哪些参数不重要,然后剪掉。
- 微调恢复精度 —— 剪完以后模型精度肯定会掉,需要用小学习率再训练一段时间,把损失补回来。
这里有个细节:第二步和第三步可以反复迭代。也就是「剪一点 → 微调 → 再剪一点 → 再微调」。这种渐进式剪枝,效果往往比一次性剪到位要好。
我的小技巧: 微调时,学习率设成原来的 1/10 甚至 1/100。因为剪枝后的模型已经接近最优解了,学习率太大容易跑偏。
最后,给大家一个简单的剪枝代码框架,方便理解:
import torch
import torch.nn.utils.prune as prune
# 假设 model 是一个训练好的 Transformer
model = YourTransformerModel()
# 1. 对某个线性层做 L1 非结构化剪枝
prune.l1_unstructured(
model.encoder.layers[0].ffn.linear1,
name='weight',
amount=0.3 # 剪掉 30% 的权重
)
# 2. 剪枝后的权重变成了 mask 形式
# 原权重被保存在 weight_orig,mask 保存在 weight_mask
print(model.encoder.layers[0].ffn.linear1.weight_mask)
# 3. 微调前,记得先移除剪枝的 reparameterization
# 这样权重才会真正变成稀疏矩阵
prune.remove(model.encoder.layers[0].ffn.linear1, 'weight')
# 4. 然后开始微调...
嗯,代码很简单,但实际项目中坑不少。比如 PyTorch 的 prune 模块默认是永久性剪枝,如果你不调用 remove,权重会一直带着 mask,影响后续训练。我曾经因为这个 bug 排查了一整天,最后发现是 mask 没移除。
好了,这一章就到这里。下一章我们聊聊「如何评估参数的重要性」——这可是剪枝的核心技术,也是面试常考题。