2、模型剪枝基础:剪枝的定义、分类与核心思想
好,咱们正式开始聊剪枝。说实话,我第一次接触模型剪枝是在一个边缘端的人脸识别项目上。模型跑在树莓派上,帧率只有可怜的3帧,CPU风扇呼呼转,跟开拖拉机似的。当时我就在想,这模型里到底有多少参数是真正有用的?
后来我明白了,剪枝就是干这个的——把模型里那些「摸鱼」的参数给裁掉,让模型轻装上阵。
2.1 剪枝的定义:到底在剪什么?
模型剪枝,说白了就是从训练好的大模型中,移除那些对最终结果影响不大的参数或结构。你想想看,一个上亿参数的模型,里面很多权重值其实非常小,接近零。这些参数对推理结果的贡献微乎其微,但计算它们却要消耗实实在在的算力。
我个人的理解是:剪枝就像给一棵大树修剪枝叶。主干和粗枝保留,那些细枝末节砍掉,树照样活得好好的,甚至长得更壮实。
核心定义:模型剪枝 = 在保持模型精度可接受的前提下,通过移除冗余参数来减小模型体积、降低计算量。
这里有个关键点——「保持精度可接受」。不是剪完精度一点不掉,而是掉在可容忍范围内。我在项目中一般设定精度下降不超过1%就算成功。
2.2 剪枝的分类:结构化 vs 非结构化
剪枝的分类,我习惯从剪枝粒度来划分。这是最实用的分法,直接决定了你后续能不能拿到硬件加速收益。
2.2.1 非结构化剪枝
非结构化剪枝,也叫细粒度剪枝。它剪的是单个权重。比如一个卷积核有9个权重,我把其中3个接近零的权重直接置为零。
优点很明显:
- 剪枝率可以很高,甚至能剪掉90%的参数
- 精度损失相对较小
缺点也很致命:
- 权重矩阵变得稀疏,零值分布不规则
- 普通硬件(CPU/GPU)很难加速这种稀疏计算
- 需要专门的稀疏计算库(如NVIDIA的cuSPARSE)
我的经验:非结构化剪枝更适合学术研究。我在实际部署中很少用,因为边缘端的芯片(比如瑞芯微、地平线)基本不支持这种不规则稀疏。你剪完了,推理速度反而可能变慢,因为多了很多判断零值的开销。
2.2.2 结构化剪枝
结构化剪枝,剪的是整个结构单元。比如:
- 剪掉整个卷积核(channel pruning)
- 剪掉整层(layer pruning)
- 剪掉整个注意力头(head pruning)
这才是工业界的主流做法。为什么?因为剪完后模型结构还是规整的,可以直接跑在现有的推理框架上,不需要任何特殊支持。
| 对比维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 卷积核/通道/层 |
| 压缩率 | 高(可达90%+) | 中等(通常30%-50%) |
| 硬件加速 | 困难,需专用库 | 直接支持,无需改动 |
| 精度损失 | 较小 | 相对较大 |
| 部署难度 | 高 | 低 |
注意:结构化剪枝虽然部署友好,但剪枝率过高时精度会断崖式下跌。我曾经在一个分类模型上尝试剪掉60%的通道,结果准确率直接从95%掉到了82%,完全不可用。后来调整到40%,精度只掉了0.3%。所以,剪枝率不是越高越好,得找到那个平衡点。
2.3 剪枝的核心思想:重要性评估
剪枝的核心思想其实就一句话:找到不重要的参数,然后干掉它们。
但问题来了——怎么判断一个参数「不重要」?
我总结了几种主流方法:
2.3.1 基于权重大小
这是最直观的方法。权重绝对值越小,说明它对输出的影响越小。直接按权重绝对值排序,剪掉最小的那批。
# 伪代码示例
import numpy as np
def magnitude_prune(weights, prune_ratio):
# 计算权重的绝对值
abs_weights = np.abs(weights)
# 找到剪枝阈值
threshold = np.percentile(abs_weights, prune_ratio * 100)
# 生成掩码
mask = abs_weights > threshold
return weights * mask
这个方法简单粗暴,但有个坑:不同层的权重分布差异很大。第一层的权重可能普遍偏大,最后一层普遍偏小。如果全局统一剪枝,最后一层可能被剪光。我一般会分层剪枝,每层单独设定阈值。
2.3.2 基于梯度
权重小不代表不重要。有些权重虽然小,但梯度很大,说明它对损失函数很敏感。这时候应该保留它。
常用的指标是:权重 × 梯度(一阶泰勒展开近似)。这个值越大,说明参数越重要。
避坑指南:我曾经在一个Transformer模型上只用权重大小剪枝,结果剪完后模型直接崩了。后来改用梯度加权的方法,同样的剪枝率,精度只掉了0.5%。所以,对于注意力层这种梯度变化剧烈的结构,建议用梯度信息辅助判断。
2.3.3 基于结构化重要性
对于结构化剪枝,我们需要评估整个通道或卷积核的重要性。常用方法:
- BN层缩放因子:BatchNorm层的γ值越小,说明这个通道越不重要。直接按γ值排序剪枝。
- 通道激活值:统计每个通道在验证集上的平均激活值,激活值低的通道可以剪掉。
- 通道相关性:计算通道之间的相关性,冗余度高的通道可以合并或剪掉。
我个人最常用的是BN层缩放因子法。原因很简单——不需要额外计算,BN层的γ值本来就是训练好的,直接拿来用就行。
2.4 剪枝的完整流程
一个标准的剪枝流程通常包含三步:
- 预训练:先训练一个完整的大模型,作为剪枝的起点
- 剪枝:根据重要性评估,移除不重要的参数
- 微调:对剪枝后的模型进行少量训练,恢复精度
这里有个常见的误区——很多人觉得剪完枝直接就能用。其实不行。剪枝后的模型精度一定会掉,必须微调才能恢复。微调一般只需要原始训练量的10%-20%的迭代次数就够了。
关键结论:剪枝不是一锤子买卖。我建议采用「迭代剪枝」策略——每次剪掉一小部分(比如10%),然后微调,再剪,再微调。这样比一次性剪掉50%再微调的效果好得多。
2.5 小结
嗯,这一章的内容差不多就这些。总结一下:
- 剪枝就是去掉模型里不重要的参数
- 非结构化剪枝精度高但部署难,结构化剪枝部署友好但精度损失大
- 核心思想是重要性评估,方法有基于权重、基于梯度、基于结构化指标
- 剪枝后一定要微调,推荐迭代剪枝策略
下一章我会带大家手写一个简单的剪枝工具,从零开始剪一个CNN模型。到时候咱们直接上代码,看看剪枝到底是怎么操作的。