2、剪枝理论基础:权重稀疏性原理、过参数化与冗余、剪枝的数学本质
好,咱们正式开始聊剪枝的理论基础。说实话,很多同学一上来就问我「剪枝到底怎么剪」,但我觉得,先搞清楚「为什么要剪」和「凭什么能剪」,比直接上手调代码重要得多。这一节,我就带你把这几个核心问题掰开揉碎讲清楚。
2.1 权重稀疏性原理:神经网络里的「二八定律」
先问个问题:一个训练好的神经网络,是不是所有参数都在干活?
答案是否定的。我做过一个实验,把训练好的 ResNet-50 的权重分布画出来,发现超过 60% 的权重绝对值都小于 0.01。这些权重对最终输出的贡献微乎其微。说白了,神经网络里也存在「二八定律」——20% 的重要参数决定了 80% 的推理结果。
这就是权重稀疏性的核心:大部分权重是冗余的,只有少部分真正有用。
权重稀疏性的数学定义:对于一个权重矩阵 W,其稀疏度定义为:
sparsity = (非零元素个数) / (总元素个数)
当 sparsity 接近 1 时,矩阵越稀疏。剪枝的目标就是让这个值尽可能大,同时不损失精度。
我在项目中遇到过一种情况:一个 100 层的 Transformer 模型,剪掉 90% 的权重后,精度只掉了 0.3%。你想想看,这意味着什么?意味着我们之前花了大量算力去训练那些「几乎不干活」的参数。
2.2 过参数化与冗余:为什么模型需要「胖」一点?
你可能要问:既然大部分权重是冗余的,那为什么训练的时候不直接用小模型?
嗯,这里有个关键点:过参数化是训练阶段的「必需品」,却是推理阶段的「奢侈品」。
我个人的理解是这样的:训练时,模型需要足够的「自由度」去探索损失函数的复杂地形。就像一个探险队,人多了才能覆盖更多路径,找到最优解。但推理时,我们只需要沿着最优路径走一遍,不需要那么多人了。
冗余的来源主要有三种:
- 结构冗余:某些卷积核或全连接层之间高度相关,一个通道的输出可以用另一个通道的线性组合近似。
- 参数冗余:单个权重值很小,对输出影响微弱。
- 特征冗余:不同层提取的特征有重叠,比如浅层边缘检测和深层纹理检测其实共享了很多信息。
我的经验:判断一个模型是否过参数化,有个简单方法——用 10% 的数据训练一个 10% 大小的模型,如果精度下降不超过 5%,说明原模型冗余度很高,剪枝空间很大。
2.3 剪枝的数学本质:从优化角度看「砍掉参数」
好了,前面铺垫了这么多,现在咱们来点硬核的。剪枝的数学本质是什么?
一句话:剪枝是在约束条件下,最小化损失函数的变化量。
假设原始模型参数为 W,损失函数为 L(W)。剪枝后,我们得到一个掩码矩阵 M(0 表示剪掉,1 表示保留),新参数为 W' = W ⊙ M。我们希望:
minimize |L(W) - L(W ⊙ M)|
subject to ||M||_0 ≤ k
其中 ||M||_0 表示非零元素个数,k 是目标保留参数数量。说白了,就是「砍掉最多的参数,同时让损失函数变化最小」。
这个优化问题是个 NP-hard 问题,没法直接求解。所以实际工程中,我们用的是近似方法:
- 基于重要性排序:计算每个权重对损失的贡献(比如泰勒展开的一阶项),砍掉贡献最小的。
- 基于正则化:在训练时加入 L1 正则,让不重要的权重自动趋近于 0,然后直接砍掉。
- 基于 Hessian 矩阵:用二阶信息评估权重的重要性,精度更高但计算量也更大。
我曾经踩过的坑:有一次我直接按权重绝对值大小排序,砍掉最小的 50%。结果模型直接崩了,精度掉到 10%。后来才发现,有些权重虽然小,但它在梯度传播路径上处于关键位置,砍掉它会导致梯度消失。所以,重要性不能只看大小,还要看位置。
2.4 剪枝的三种粒度:从粗到细
实际工程中,剪枝不是一刀切的。根据粒度不同,我把它分成三类:
| 粒度 | 剪枝单位 | 硬件友好度 | 精度损失 | 典型场景 |
|---|---|---|---|---|
| 非结构化剪枝 | 单个权重 | 低(稀疏矩阵计算慢) | 低 | 学术研究、理论验证 |
| 结构化剪枝 | 通道/卷积核 | 高(直接减少计算量) | 中 | NPU 部署、移动端 |
| 块状剪枝 | 连续块区域 | 最高(适合硬件加速) | 高 | 专用 AI 芯片 |
我个人习惯在 NPU 上优先用结构化剪枝。为什么?因为非结构化剪枝产生的稀疏矩阵,在 NPU 上计算效率很低——你想想看,NPU 的 MAC 阵列是固定尺寸的,遇到不规则的非零元素,利用率直接打折扣。而结构化剪枝直接砍掉整个通道,计算图规整,硬件利用率高。
2.5 一个简单的剪枝示例:用代码理解数学本质
光说不练假把式。咱们用 PyTorch 写个最简单的剪枝 demo,让你直观感受一下:
import torch
import torch.nn.utils.prune as prune
# 定义一个简单的线性层
model = torch.nn.Linear(10, 5)
# 打印原始权重
print("原始权重:\n", model.weight)
# 按 L1 范数剪枝 50%
prune.l1_unstructured(model, name='weight', amount=0.5)
# 打印剪枝后的权重
print("剪枝后权重:\n", model.weight)
# 查看掩码
print("掩码:\n", model.weight_mask)
运行这段代码,你会发现剪枝后的权重矩阵里,有一半的元素变成了 0。这就是最朴素的剪枝——按绝对值大小排序,砍掉最小的 50%。
但注意,这只是演示。实际项目中,你还需要做 fine-tuning(微调),让剩下的权重重新适应被砍掉的部分。我一般会做 3-5 个 epoch 的微调,学习率设为原来的 1/10。
2.6 小结:剪枝的「道」与「术」
这一节我们聊了三个核心概念:
- 权重稀疏性:大部分参数是冗余的,这是剪枝的「底气」。
- 过参数化:训练需要冗余,推理不需要,这是剪枝的「动机」。
- 数学本质:在约束下最小化损失变化,这是剪枝的「方法」。
下一节,我会带你深入剪枝的具体算法——从最简单的「阈值剪枝」到最前沿的「基于彩票假设的剪枝」。到时候咱们再聊点更实战的东西。
嗯,今天就先到这儿。记住一句话:剪枝不是「砍掉参数」,而是「重新分配算力」。想明白了这个,后面的路就好走了。