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 基于权重大小的剪枝策略

这是最直观、最常用的剪枝策略。核心思想就一句话:权重绝对值越小,越不重要

为什么?因为权重值小,意味着它对输出的影响小。去掉它,输出变化不大。

具体实现步骤

  1. 训练一个完整的模型
  2. 计算所有权重的绝对值
  3. 按绝对值排序,设定剪枝比例(比如剪掉50%)
  4. 将最小的50%权重置零
  5. 微调恢复精度
剪枝比例 精度损失(典型值) 模型大小缩减 推理加速
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 迭代剪枝:边剪边练

一次性剪掉太多,模型受不了。更好的做法是:迭代剪枝

具体流程:

  1. 训练完整模型
  2. 剪掉一小部分(比如10%)
  3. 微调恢复精度
  4. 重复步骤2~3,直到达到目标剪枝比例

这样做的好处是:每次只剪一点点,模型有足够的时间去适应。最终精度损失更小。

我个人习惯用迭代剪枝做高比例剪枝(比如剪掉80%以上)。虽然训练时间长了点,但效果确实好。有一次,我用迭代剪枝把MobileNet剪掉了75%,精度只掉了1.2%,部署到手机上,推理速度提升了3倍。

小技巧:迭代剪枝时,每次剪枝后可以做一个“权重回退”操作——把被剪掉的权重恢复成上一次迭代的值,而不是从零开始。这样能加速收敛。

4.7 总结与建议

模型剪枝,说白了就是一场“取舍”的艺术。你要在模型大小、推理速度和精度之间找到平衡点。

我的建议

  • 做工程部署,优先选结构化剪枝,硬件友好,加速明显
  • 做学术研究或专用硬件,可以试试非结构化剪枝,精度保留更好
  • 剪枝比例从30%开始,逐步增加,不要贪心
  • 微调是必须的,学习率要小,迭代次数不用太多
  • 如果精度掉得厉害,试试迭代剪枝,效果往往更好

嗯,剪枝技术就讲到这里。下一章,我们会聊模型量化——另一个让模型变轻的利器。到时候你会发现,剪枝和量化搭配使用,效果更佳。