3、模型剪枝基础概念:什么是剪枝?结构化剪枝与非结构化剪枝的区别,剪枝率与稀疏度
好,咱们正式开始聊剪枝。说实话,我第一次接触这个概念的时候,脑子里蹦出来的画面是园艺师傅拿着大剪刀咔嚓咔嚓修剪树枝。嗯,其实道理差不多——把模型里那些“不干活”或者“干得少”的参数给剪掉,让模型瘦身。
3.1 什么是剪枝?
剪枝,说白了就是从训练好的神经网络里,移除掉一部分权重或者神经元。你想想看,一个模型动辄几百万甚至上千万个参数,里面其实有很多是冗余的。我做过一个实验,把某个卷积层里30%的小权重直接清零,模型精度几乎没掉。这说明什么?说明模型“吃得太饱了”。
剪枝的核心思想很简单:保留重要的连接,砍掉不重要的。那怎么判断重不重要?最常见的方法就是看权重绝对值的大小。权重越小,对最终输出的贡献就越小,剪掉它影响也最小。
剪枝的本质: 在模型大小和精度之间找一个平衡点。不是单纯地删参数,而是有选择地删。
我在项目里遇到过一种情况:一个语音识别模型,剪掉40%的参数后,在STM32上推理速度提升了将近一倍,而准确率只下降了0.3%。这种收益,说实话,非常可观。
3.2 结构化剪枝 vs 非结构化剪枝
这两个概念,我建议你放在一起对比着理解。它们最大的区别在于:剪完之后的模型结构是否规整。
3.2.1 非结构化剪枝
非结构化剪枝,就是对单个权重下手。比如一个卷积核是3x3的,里面有9个权重。非结构化剪枝可能只把其中3个小的权重置为零,剩下的6个还留着。
这样做的好处是:剪枝粒度细,精度损失小。你可以精确地控制每个权重去留。
但坏处也很明显:剪完后的权重矩阵是稀疏的,里面到处是零。普通的硬件和库(比如STM32上的CMSIS-NN)没法直接加速这种稀疏矩阵。你得用专门的稀疏矩阵运算库,或者自己写代码跳过零值。说实话,在嵌入式平台上搞这个,挺折腾的。
// 非结构化剪枝后的权重示例(3x3卷积核)
// 0 表示被剪掉的权重
float kernel[3][3] = {
{0.12, 0.00, -0.05},
{0.00, 0.88, 0.00},
{0.33, 0.00, -0.21}
};
我曾经踩过的坑: 在STM32F4上尝试非结构化剪枝模型,结果发现推理速度反而变慢了。因为每次计算都要判断权重是否为零,分支预测失败率很高。后来我改用结构化剪枝,速度才真正提上来。
3.2.2 结构化剪枝
结构化剪枝,是对整个结构单元下手。比如直接剪掉整个卷积核(channel pruning),或者剪掉一整层神经元(neuron pruning)。
这样做的好处是:剪完后的模型结构依然规整。权重矩阵还是密集的,没有零散的空洞。普通的矩阵乘法库可以直接加速,不需要特殊处理。
坏处是:剪枝粒度粗,精度损失相对大一些。你一次剪掉一整个卷积核,影响肯定比剪掉几个权重要大。
// 结构化剪枝后的权重示例(剪掉第2个卷积核)
// 整个kernel[1]被移除,矩阵尺寸从3x3x3变为3x3x2
float kernel[2][3][3] = {
{ {0.12, 0.34, -0.05}, {0.21, 0.88, -0.11}, {0.33, 0.45, -0.21} },
// 第2个卷积核被剪掉
{ {0.56, -0.23, 0.78}, {0.09, 0.67, -0.34}, {0.44, -0.12, 0.55} }
};
| 对比维度 | 非结构化剪枝 | 结构化剪枝 |
|---|---|---|
| 剪枝粒度 | 单个权重 | 卷积核/通道/层 |
| 精度损失 | 较小 | 相对较大 |
| 硬件加速 | 困难,需稀疏计算库 | 容易,直接使用标准库 |
| 模型规整性 | 稀疏矩阵,不规整 | 密集矩阵,规整 |
| 嵌入式适用性 | 较差 | 较好 |
我的建议: 在STM32这类MCU上做部署,优先考虑结构化剪枝。虽然精度损失大一点,但后续的优化空间大得多。非结构化剪枝更适合在GPU上用cuSPARSE这类库来加速。
3.3 剪枝率与稀疏度
这两个指标,我刚开始也经常搞混。咱们捋一捋。
3.3.1 剪枝率(Pruning Ratio)
剪枝率,指的是被剪掉的参数占总参数的比例。公式很简单:
剪枝率 = 被剪掉的参数数量 / 总参数数量 × 100%
举个例子:一个卷积层有1000个权重,你剪掉了300个,那剪枝率就是30%。
剪枝率越高,模型越小,但精度下降的风险也越大。我一般习惯从10%开始试,每次增加10%,直到精度掉到不可接受为止。
3.3.2 稀疏度(Sparsity)
稀疏度,指的是权重矩阵中零值所占的比例。注意,这里的零值包括原本就是零的,也包括被剪枝置为零的。
稀疏度 = 零值参数数量 / 总参数数量 × 100%
在剪枝的场景下,剪枝率和稀疏度通常是相等的——因为你剪掉的参数都被置为零了。但严格来说,稀疏度是一个更通用的概念,它描述的是矩阵中零元素的密度。
关键区别: 剪枝率是主动操作的度量(你剪了多少),稀疏度是最终状态的度量(矩阵里有多少零)。在剪枝完成后,这两个值通常一致。
3.3.3 实际应用中的权衡
你想想看,剪枝率和稀疏度越高,模型压缩得越狠,但精度损失也越大。怎么找到那个“甜点”?
我分享一个我在项目里用的方法:
- 先做敏感性分析:对每一层单独做剪枝测试,看每层对剪枝的敏感程度。有些层剪掉50%精度都不掉,有些层剪掉10%就崩了。
- 分层设定剪枝率:敏感度低的层,剪枝率设高一点(比如50%);敏感度高的层,剪枝率设低一点(比如10%)。
- 迭代剪枝:不要一次剪到位。我习惯每次剪5%-10%,然后做微调(fine-tuning),再剪下一轮。
// 分层剪枝率设定示例
// 假设有5个卷积层
float pruning_rates[5] = {
0.3, // layer1: 敏感度低,剪30%
0.1, // layer2: 敏感度高,只剪10%
0.4, // layer3: 敏感度低,剪40%
0.2, // layer4: 敏感度中等,剪20%
0.5 // layer5: 敏感度很低,剪50%
};
注意: 剪枝率和稀疏度不是越高越好。我曾经为了追求极致压缩,把剪枝率推到80%,结果模型精度直接掉到和随机猜测差不多。后来老老实实降到50%,配合微调,才把精度拉回来。
3.4 小结
嗯,这一章的内容就这些。总结一下:
- 剪枝就是砍掉不重要的参数,让模型变小变快。
- 非结构化剪枝粒度细但加速难,结构化剪枝粒度粗但加速容易。在STM32上,我推荐结构化剪枝。
- 剪枝率是你主动剪了多少,稀疏度是最终矩阵里有多少零。两者在剪枝场景下通常相等。
- 实际项目中,分层剪枝 + 迭代微调是更稳妥的做法。
下一章,咱们会聊具体的剪枝算法和实现步骤。到时候我会拿一个实际的STM32项目来演示,从模型训练到剪枝再到部署,一步步走通。到时候见。