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 实际应用中的权衡

你想想看,剪枝率和稀疏度越高,模型压缩得越狠,但精度损失也越大。怎么找到那个“甜点”?

我分享一个我在项目里用的方法:

  1. 先做敏感性分析:对每一层单独做剪枝测试,看每层对剪枝的敏感程度。有些层剪掉50%精度都不掉,有些层剪掉10%就崩了。
  2. 分层设定剪枝率:敏感度低的层,剪枝率设高一点(比如50%);敏感度高的层,剪枝率设低一点(比如10%)。
  3. 迭代剪枝:不要一次剪到位。我习惯每次剪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项目来演示,从模型训练到剪枝再到部署,一步步走通。到时候见。