模型剪枝基础:什么是剪枝?

各位同学,咱们今天聊点实在的。模型剪枝,说白了就是给神经网络“减肥”。

你想想看,一个训练好的大模型,里面成千上万的参数,真正起作用的可能连一半都不到。剩下的那些,要么是冗余的,要么是噪声。我刚开始做模型部署那会儿,第一次把训练好的模型往嵌入式板子上烧,结果直接内存溢出——板子当场罢工。从那以后,我就明白了一个道理:模型不是越大越好,能跑起来的模型才是好模型。

什么是剪枝?

剪枝,就是从训练好的模型中,移除那些不重要的参数或结构。它的核心思想很简单:去掉那些对最终输出影响微乎其微的部分,保留关键信息。

举个例子。你训练了一个识别猫狗的分类器,模型有100万个参数。剪枝之后,可能只剩下30万个参数,但准确率只下降了0.5%。这30万个参数,就是模型的“骨架”。

核心目标:在尽量不损失精度的前提下,大幅减少模型的计算量和存储空间。

结构化剪枝 vs 非结构化剪枝

这里有个关键区分,也是我面试新人时必问的问题。剪枝分为两大类:结构化剪枝和非结构化剪枝。

非结构化剪枝

非结构化剪枝,是对单个权重或神经元进行裁剪。它不关心参数在矩阵中的位置,只关心这个参数“值不值得留”。

具体做法是:设定一个阈值,低于这个阈值的权重直接置零。结果就是,权重矩阵变得稀疏——里面有很多0。

# 非结构化剪枝示例(伪代码)
import numpy as np

weights = np.array([0.5, -0.02, 0.8, 0.001, -0.6, 0.0003])
threshold = 0.01

# 低于阈值的权重置零
pruned_weights = np.where(np.abs(weights) < threshold, 0, weights)
print(pruned_weights)  # 输出: [0.5, 0, 0.8, 0, -0.6, 0]

你看,第2个和第4个权重被干掉了。但问题是,这些0是散落在矩阵各处的。硬件加速器(比如NPU、GPU)对稀疏矩阵的支持很差,跑起来反而更慢。我踩过这个坑——剪枝后模型小了,但推理时间反而长了,因为CPU要花大量时间处理那些“空洞”。

注意:非结构化剪枝虽然压缩率高,但需要专门的稀疏计算库(如NVIDIA的cuSPARSE)才能发挥加速效果。在普通嵌入式设备上,效果往往不理想。

结构化剪枝

结构化剪枝,是整块整块地砍。比如砍掉整个卷积核、整个通道、甚至整个层。它保持矩阵的规整结构,不产生稀疏性。

我个人的习惯是,优先考虑结构化剪枝。为什么?因为剪完之后,模型还是“完整”的——权重矩阵依然是密集的、规整的。硬件跑起来非常顺畅。

# 结构化剪枝示例(按通道剪枝)
# 假设卷积层有64个输出通道,我们只保留最重要的32个

# 计算每个通道的重要性(比如L1范数)
channel_importance = np.sum(np.abs(conv_weights), axis=(0, 1, 2))

# 保留最重要的32个通道
keep_indices = np.argsort(channel_importance)[-32:]
pruned_weights = conv_weights[:, :, :, keep_indices]
对比维度 非结构化剪枝 结构化剪枝
裁剪粒度 单个权重 通道/卷积核/层
矩阵形态 稀疏矩阵(大量0) 密集矩阵(规整)
硬件加速 困难,需特殊支持 友好,通用硬件即可
压缩率 高(可达90%+) 中等(通常50%-70%)
精度恢复 较容易(微调即可) 需要更多微调

剪枝的数学原理

剪枝不是拍脑袋决定的,背后有数学支撑。核心问题就一个:如何判断一个参数是否重要?

常用的方法有三种:

1. 基于权重大小

最简单粗暴的方法。假设权重绝对值越小,对输出的影响越小。这个假设在大多数情况下成立。

数学表达:

重要性(w) = |w|

如果 |w| < 阈值,则剪掉

嗯,这里要注意:这个方法虽然简单,但有个致命缺陷——它没有考虑权重的“上下文”。一个很小的权重,如果它连接的是非常重要的特征,剪掉它可能损失很大。

2. 基于梯度

梯度反映了损失函数对权重的敏感度。梯度大的权重,稍微动一下损失就剧烈变化,说明它很重要。

数学表达:

重要性(w) = |w * ∂L/∂w|

其中 L 是损失函数,∂L/∂w 是梯度

这个公式把权重大小和梯度信息结合起来了。我在项目中常用这个方法,效果比纯权重大小好不少。

3. 基于泰勒展开

这个方法更高级一些。它用一阶泰勒展开来近似:如果去掉某个权重,损失函数会变化多少。

数学表达:

ΔL ≈ ∂L/∂w * w

重要性(w) = |∂L/∂w * w|

你看,这个公式和基于梯度的公式一模一样。但推导过程不同——泰勒展开给了它更坚实的理论基础。

实战建议:我个人习惯先用基于权重大小的方法做一轮粗剪,再用基于梯度的方法做一轮精剪。粗剪可以砍掉50%的参数,精剪再砍掉20%,总共压缩70%左右,精度损失控制在1%以内。

剪枝的整体流程

下面这张图,是我自己总结的剪枝标准流程。每次做项目,我都按这个步骤来,基本没出过岔子。

模型剪枝标准流程 训练完整模型 计算重要性 执行剪枝 微调恢复 迭代优化,直到精度达标 关键点:剪枝后必须微调,否则精度会断崖式下跌 微调技巧: • 学习率设为原训练时的1/10 • 只训练3-5个epoch,防止过拟合

这个流程我用了好几年,从最初的ResNet到现在的Transformer,基本都适用。唯一要调整的是“计算重要性”这一步——不同模型结构,重要性评估方法略有差异。

避坑指南

最后,分享几个我踩过的坑:

  • 一次性剪太多:我曾经试图一次性剪掉80%的参数,结果模型直接崩了,准确率掉到随机水平。正确做法是:每次剪10%-20%,然后微调,再剪下一轮。
  • 忽略BN层:剪枝后,Batch Normalization层的统计量(均值和方差)会失效。必须重新计算或微调。我有一回忘了这茬,模型在验证集上表现正常,一到线上就崩——因为线上数据分布和训练集不一样。
  • 剪枝后不微调:这个最要命。剪枝相当于给模型做了“手术”,术后必须“康复训练”(微调)。不微调的话,精度损失可能高达10%以上。

总结一句话:剪枝不是玄学,是工程。选对方法、控制节奏、做好微调,你就能在嵌入式设备上跑起原本跑不动的大模型。

专注资料整理