3. 模型剪枝实战:基于L1范数的权重剪枝,基于通道重要性的通道剪枝,剪枝后的微调策略
模型剪枝,说白了就是给神经网络“瘦身”。
我刚开始做部署的时候,总觉得模型越大越准。后来发现,很多参数其实都是冗余的。你想想看,一个上千万参数的模型,真正起作用的可能只有一半。剪枝就是把这些“赘肉”去掉,让模型跑得更快,体积更小。
核心思想:剪枝不是随机删除,而是基于某种重要性指标,优先移除对最终结果影响最小的部分。
3.1 基于L1范数的权重剪枝
L1范数剪枝,是我个人最常用的入门方法。为什么?因为它简单、直观、效果好。
L1范数就是权重的绝对值之和。一个权重的L1范数越小,说明它对输出的贡献越弱。把这些小权重剪掉,模型精度损失最小。
我在项目中遇到过这样一个场景:一个3层全连接网络,原始大小50MB。用L1剪枝后,直接砍到15MB,精度只掉了0.3%。嗯,这买卖划算。
具体怎么做?看代码:
import torch
import torch.nn.utils.prune as prune
# 定义一个简单的线性层
model = torch.nn.Linear(10, 5)
# 基于L1范数剪枝,剪掉20%的权重
prune.l1_unstructured(model, name='weight', amount=0.2)
# 查看剪枝后的权重
print(model.weight)
# 你会发现很多权重变成了0
小技巧:剪枝比例不要一开始就设太高。我习惯从10%开始,逐步增加到50%。每次剪完都跑一遍验证集,看看精度变化。
L1剪枝的优点是实现简单,缺点也很明显——它是非结构化的。剪完之后,权重矩阵变得稀疏,很多硬件加速器并不支持稀疏矩阵运算。说白了,模型虽然小了,但实际推理速度可能没提升多少。
避坑指南:我曾经在某个项目里,把L1剪枝比例设到70%,结果模型直接崩了。后来发现,有些层对剪枝特别敏感,比如第一层和最后一层。建议对敏感层降低剪枝比例,或者干脆不剪。
3.2 基于通道重要性的通道剪枝
通道剪枝就不一样了。它直接砍掉整个通道,而不是单个权重。这样剪完之后,模型结构还是规整的,硬件加速器照样能跑。
通道重要性的评估方法有很多,我常用的是基于BN层缩放因子的方法。BN层里有个γ参数,它的大小直接反映了这个通道的重要性。γ越小,说明这个通道越不重要。
为什么?因为BN层的输出是 γ * (x - μ) / σ + β。如果γ接近0,那这个通道的输出基本就是常数β,对后续层几乎没有影响。砍掉它,模型照样工作。
实现起来也不复杂:
import torch
import torch.nn as nn
class PrunableConv2d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size):
super().__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size)
self.bn = nn.BatchNorm2d(out_channels)
def forward(self, x):
return self.bn(self.conv(x))
# 训练完成后,获取所有BN层的γ参数
gamma_list = []
for module in model.modules():
if isinstance(module, nn.BatchNorm2d):
gamma_list.append(module.weight.data.abs())
# 设定剪枝阈值,比如剪掉50%的通道
threshold = torch.quantile(torch.cat(gamma_list), 0.5)
# 对每个BN层,保留γ大于阈值的通道
for module in model.modules():
if isinstance(module, nn.BatchNorm2d):
mask = module.weight.data.abs() > threshold
# 这里需要根据mask重新构建网络结构
# 具体实现略复杂,建议使用torch-pruning等工具库
关键点:通道剪枝后,网络结构变了。你需要重新构建一个更窄的网络,然后把保留的权重拷贝过去。这一步不能省,否则模型结构对不上。
我记得有一次做图像分类模型压缩,用通道剪枝把ResNet-50从25MB剪到12MB,精度只掉了1.2%。而且推理速度提升了将近一倍。嗯,这才是真正的“瘦身”。
3.3 剪枝后的微调策略
剪完枝,模型精度肯定会掉。这时候就需要微调了。
微调不是简单地重新训练。我总结了一套策略:
| 阶段 | 学习率 | 训练轮数 | 说明 |
|---|---|---|---|
| 热身阶段 | 1e-5 ~ 1e-4 | 5 ~ 10 | 用小学习率让模型适应新结构 |
| 恢复阶段 | 1e-4 ~ 1e-3 | 20 ~ 50 | 正常训练,恢复精度 |
| 收敛阶段 | 1e-5 | 10 ~ 20 | 用小学习率微调,防止过拟合 |
个人经验:微调时,我习惯把剪枝后的模型先冻结前几层,只训练后面几层。等后面几层稳定了,再解冻所有层一起训练。这样能避免梯度震荡。
还有一个坑要注意:剪枝后的模型,BN层的统计量(μ和σ)需要重新计算。因为通道变了,原来的统计量已经不准确了。我建议在微调前,先用一小批数据跑一遍前向传播,更新BN层的统计量。
我曾经踩过的坑:有一次剪完枝直接微调,结果精度死活上不去。折腾了两天才发现,原来是BN层的统计量没更新。从那以后,我每次剪枝后都会加一步“BN校准”。
微调的总轮数不用太多。一般来说,30到50轮就够了。剪枝后的模型已经学到了大部分知识,微调只是帮它“找回状态”。
最后说一句:剪枝和微调是相辅相成的。剪得太多,微调也救不回来。剪得太少,又达不到压缩效果。我建议从20%的剪枝比例开始,逐步增加,找到那个“临界点”。
这张图把整个流程串起来了。从原始模型开始,你可以选择L1权重剪枝或者通道剪枝,然后进入微调阶段,最后得到部署模型。我个人建议:如果硬件支持稀疏计算,用L1剪枝;否则,用通道剪枝更稳妥。