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权重剪枝或者通道剪枝,然后进入微调阶段,最后得到部署模型。我个人建议:如果硬件支持稀疏计算,用L1剪枝;否则,用通道剪枝更稳妥。

专注资料整理