4、模型剪枝技术:结构化剪枝与非结构化剪枝,基于权重大小的剪枝策略,剪枝后的模型微调与恢复
模型剪枝,说白了就是给神经网络“瘦身”。
我刚开始接触这个领域时,总觉得模型越大越准,参数越多越强。直到有一次,我要把一个百兆级的模型部署到嵌入式设备上——内存不够,延迟超标,根本跑不起来。那时候我才意识到,模型剪枝不是锦上添花,而是刚需。
4.1 剪枝的核心思想:去掉不重要的连接
神经网络里,并不是所有参数都同等重要。有些权重值接近零,对最终输出的贡献微乎其微。剪枝就是把这些“冗余”的参数去掉,让模型变轻、变快。
你想想看,一个模型可能有上千万个参数,但真正起作用的,可能只有20%~30%。剩下的,说白了就是噪声。去掉它们,模型精度几乎不受影响。
核心原则:剪枝的本质是“重要性评估”。谁不重要,就干掉谁。
4.2 非结构化剪枝:细粒度,但硬件不友好
非结构化剪枝,是对单个权重做判断。比如一个权重是0.001,远小于阈值,我就把它置为零。这种方式非常灵活,可以保留任意位置的参数。
具体做法:
- 设定一个阈值(比如0.01)
- 遍历所有权重,绝对值小于阈值的,直接置零
- 或者按比例剪枝:保留权重绝对值最大的前k%
import torch
import torch.nn.utils.prune as prune
# 定义一个简单的线性层
model = torch.nn.Linear(10, 5)
# 非结构化剪枝:按L1范数剪掉30%的权重
prune.l1_unstructured(model, name='weight', amount=0.3)
# 查看剪枝后的权重
print(model.weight)
# 你会发现很多权重变成了0
我在项目中遇到过一个问题:非结构化剪枝后,模型精度几乎没掉,但推理速度反而变慢了。为什么?因为权重矩阵变成了稀疏矩阵,而普通硬件(CPU/GPU)对稀疏矩阵的加速支持很差。说白了,零值占着位置,但计算时还得跳过它们,反而增加了开销。
避坑指南:非结构化剪枝适合学术研究或专用硬件(如稀疏加速器)。在通用硬件上,效果往往不如预期。
4.3 结构化剪枝:粗粒度,但硬件友好
结构化剪枝,不是剪单个权重,而是剪整个“结构”。比如剪掉一个卷积核、一个通道、甚至一整层。这样做的好处是:剪完后模型结构规整,可以直接利用现有的矩阵运算库加速。
常见策略:
- 通道剪枝:评估每个通道的重要性,去掉不重要的通道
- 卷积核剪枝:评估每个卷积核的L1范数,去掉范数小的核
- 层剪枝:评估整层对输出的贡献,去掉冗余层
# 结构化剪枝示例:按L2范数剪掉卷积层中30%的通道
# 注意:这里需要自定义实现,因为PyTorch内置的prune不支持结构化剪枝
def channel_prune(conv_layer, prune_ratio):
# 计算每个卷积核的L2范数
norms = torch.norm(conv_layer.weight.view(conv_layer.out_channels, -1), dim=1)
# 按范数排序,找到阈值
k = int(conv_layer.out_channels * prune_ratio)
threshold = torch.sort(norms)[0][k]
# 生成掩码:范数大于阈值的保留
mask = norms > threshold
# 只保留重要的通道
conv_layer.weight.data = conv_layer.weight.data[mask]
conv_layer.bias.data = conv_layer.bias.data[mask]
conv_layer.out_channels = mask.sum().item()
return conv_layer
我个人习惯用结构化剪枝做工程部署。虽然精度损失比非结构化稍大,但推理速度的提升是实打实的。我记得有一次,对一个ResNet-50做30%的通道剪枝,模型大小减少了40%,推理速度提升了1.5倍,精度只掉了0.3%。这个性价比,非常划算。
我的经验:结构化剪枝后,记得调整后续层的输入通道数。否则维度不匹配,模型直接报错。这个坑我踩过两次。
4.4 基于权重大小的剪枝策略
这是最直观、最常用的剪枝策略。核心思想就一句话:权重绝对值越小,越不重要。
为什么?因为权重值小,意味着它对输出的影响小。去掉它,输出变化不大。
具体实现步骤:
- 训练一个完整的模型
- 计算所有权重的绝对值
- 按绝对值排序,设定剪枝比例(比如剪掉50%)
- 将最小的50%权重置零
- 微调恢复精度
| 剪枝比例 | 精度损失(典型值) | 模型大小缩减 | 推理加速 |
|---|---|---|---|
| 10% | 几乎无损失 | 10% | 不明显 |
| 30% | < 0.5% | 30% | 10%~20% |
| 50% | 1%~3% | 50% | 30%~50% |
| 70% | 5%~10% | 70% | 50%~80% |
嗯,这里要注意:剪枝比例不是越高越好。我曾经试过把模型剪掉90%,结果精度直接崩了,从95%掉到60%,基本没法用。所以,剪枝比例要根据任务需求来定,一般建议从30%开始试。
4.5 剪枝后的模型微调与恢复
剪枝完,模型精度一定会掉。这时候就需要微调(Fine-tuning)来恢复。
微调的核心思路:
- 保留剪枝后的稀疏结构(被置零的权重不再更新)
- 用原始训练数据,以较小的学习率继续训练
- 通常只需要几个epoch就能恢复大部分精度
# 剪枝后微调示例
def finetune_pruned_model(model, train_loader, epochs=5, lr=1e-4):
optimizer = torch.optim.SGD(model.parameters(), lr=lr)
criterion = torch.nn.CrossEntropyLoss()
for epoch in range(epochs):
for inputs, labels in train_loader:
outputs = model(inputs)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
# 关键步骤:对被剪枝的权重,梯度置零,不更新
for name, param in model.named_parameters():
if hasattr(param, 'mask'):
param.grad.data *= param.mask
optimizer.step()
print(f'Epoch {epoch+1}, Loss: {loss.item():.4f}')
我在项目中遇到过一种情况:剪枝后微调,精度死活恢复不到原来的水平。后来发现,是因为剪枝比例太大,模型容量严重不足。这时候,要么降低剪枝比例,要么换一个更大的模型来剪。
重要提醒:微调时,学习率一定要小。我一般用原始学习率的1/10。学习率太大,模型容易震荡,甚至发散。
4.6 迭代剪枝:边剪边练
一次性剪掉太多,模型受不了。更好的做法是:迭代剪枝。
具体流程:
- 训练完整模型
- 剪掉一小部分(比如10%)
- 微调恢复精度
- 重复步骤2~3,直到达到目标剪枝比例
这样做的好处是:每次只剪一点点,模型有足够的时间去适应。最终精度损失更小。
我个人习惯用迭代剪枝做高比例剪枝(比如剪掉80%以上)。虽然训练时间长了点,但效果确实好。有一次,我用迭代剪枝把MobileNet剪掉了75%,精度只掉了1.2%,部署到手机上,推理速度提升了3倍。
小技巧:迭代剪枝时,每次剪枝后可以做一个“权重回退”操作——把被剪掉的权重恢复成上一次迭代的值,而不是从零开始。这样能加速收敛。
4.7 总结与建议
模型剪枝,说白了就是一场“取舍”的艺术。你要在模型大小、推理速度和精度之间找到平衡点。
我的建议:
- 做工程部署,优先选结构化剪枝,硬件友好,加速明显
- 做学术研究或专用硬件,可以试试非结构化剪枝,精度保留更好
- 剪枝比例从30%开始,逐步增加,不要贪心
- 微调是必须的,学习率要小,迭代次数不用太多
- 如果精度掉得厉害,试试迭代剪枝,效果往往更好
嗯,剪枝技术就讲到这里。下一章,我们会聊模型量化——另一个让模型变轻的利器。到时候你会发现,剪枝和量化搭配使用,效果更佳。