上周在部署YOLOv5到边缘设备时遇到一个典型问题:模型在PC端推理速度30ms,上板子直接飙到300ms。硬件资源吃紧,帧率上不去,客户现场等着验收。这种时候,优化模型复杂度就成了硬需求。今天要聊的模型剪枝,就是我们在这种场景下常用的“手术刀”——不是换模型,而是在原有结构上做减法。

一、剪枝的本质是什么?

很多人以为剪枝就是简单删掉一些权重,其实核心思想是移除对输出贡献小的参数,同时尽量保持模型精度。这就像修剪树枝——剪掉细枝末节,主干依然能生长。我们在部署时经常遇到这种情况:训练时追求高mAP,但部署时发现大量卷积核其实在“摸鱼”,输出接近零,这些就是优先裁剪的对象。

这里踩过一个大坑:早期尝试直接按权重绝对值剪枝,结果精度掉得厉害。后来才明白,单看权重值不靠谱,得结合通道激活值来评估重要性。比如某个卷积层的输出通道在整个验证集上激活值都很小,那这个通道大概率是冗余的。

二、实战:基于BN层Gamma值的通道剪枝

现在主流的剪枝方法里,基于BN层缩放因子Gamma的通道剪枝效果比较稳定。原理很简单:训练时BN层的Gamma参数会和卷积核权重一起优化,Gamma值的大小反映了对应通道的重要性。接近零的Gamma,对应的通道就可以考虑剪掉。

下面是我们项目里用过的一个简化版剪枝流程,基于PyTorch:

import torch
import torch.nn as nn

def collect_gamma(model):
    """收集所有BN层的Gamma参数"""
    gamma_list = []
    for m in model.modules():
        if isinstance(m, nn.BatchNorm2d):
            # 取绝对值,我们关心幅度大小
            gamma_list.append(m.weight.data.abs().clone())
    return gamma_list

def prune_model(model, prune_ratio=0.3):
    """
    按比例剪枝
    prune_ratio: 要剪掉的比例,比如0.3表示剪掉30%的通道
    """
    # 拿到所有Gamma值并拼接
    all_gamma = torch.cat([g.flatten() for g in collect_gamma(model)])
    # 确定阈值:按比例取分位点
    threshold = torch.quantile(all_gamma, prune_ratio)
    
    # 标记需要剪枝的通道
    masks = {}
    for name, m in model.named_modules():
        if isinstance(m, nn.BatchNorm2d):
            # Gamma小于阈值的通道标记为0(剪掉)
            mask = (m.weight.data.abs() > threshold).float()
            masks[name] = mask
            # 这里先不真剪,只是把对应Gamma和Beta置零
            m.weight.data *= mask
            m.bias.data *= mask
    
    return masks

# 注意:这只是标记和软剪枝,实际剪掉通道需要重建网络结构

关键点:这里只是把不重要的通道权重置零,并没有真正删除通道。真正的结构化剪枝需要重建网络,因为下一层的输入通道数也变了。别直接拿这个去导出模型,否则速度不会有提升——参数还在,只是很多是零。

三、剪枝后的微调必不可少

剪枝本质上是对网络的破坏性操作,一定会损失精度。所以剪枝后必须微调(fine-tune),让网络适应新的结构。微调时学习率要设小,通常用初始学习率的1/10到1/100,训练轮数也不用太多,一般5-10个epoch就能恢复大部分精度。

# 微调配置示例
optimizer = torch.optim.SGD(model.parameters(), 
                            lr=0.001,  # 初始训练可能是0.01
                            momentum=0.9)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, 
                                                       T_max=10)  # 短周期

微调阶段的数据增强可以减弱,更关注让模型适应剪枝后的结构。验证集监控要更频繁,一旦发现精度回升变慢,及时调整。

四、部署时的实际加速效果

剪枝的加速效果和部署框架强相关。TensorRT、OpenVINO等推理引擎对稀疏模型的支持程度不同。我们遇到过:在PyTorch里剪枝后模型大小减半,但用TensorRT推理速度没变化。一查文档才发现,TensorRT默认不支持稀疏计算,除非手动开启相关插件。

所以剪枝前一定要明确部署链路。如果后端是TensorRT,建议用其自带的剪枝工具(比如sparsity参数训练);如果是ONNX Runtime,可以尝试其稀疏推理功能。通用性最好的还是结构化剪枝(直接删除通道),因为输出的是更小的稠密模型,所有框架都支持。

五、几个血泪教训

  1. 不要一上来就剪太狠:从10%-20%开始,逐步增加。有一次我们贪心直接剪50%,精度崩了,微调也救不回来,只能重训。
  2. 卷积剪枝注意残差连接:残差块的两个卷积层要一起考虑,单独剪一个会破坏shortcut的维度匹配。
  3. 剪枝后模型体积不一定线性下降:剪掉30%参数,模型文件可能只小15%,因为模型结构描述等元数据还在。
  4. 敏感层放过:靠近输入和输出的层对精度影响大,尽量少剪或不剪。中间层尤其是深层的大卷积(比如3x3)冗余多,优先开刀。
  5. 剪枝不是银弹:如果硬件有专用加速单元(比如NPU),可能直接换更小的模型效果更好。剪枝适合“模型已经定了,不能换”的场景。

最后一点个人建议

模型剪枝更像是一门工程手艺,不是纯算法。效果好坏取决于你对模型结构和业务数据的理解。建议动手前先做层敏感度分析:逐层剪枝,看精度损失。形成自己的“剪枝策略表”,比如对YOLOv5,我们经验是backbone的深层可剪30%-40%,head部分不超过20%。

记住,剪枝的最终目标是部署加速。一切以实测速度为准,不要只看参数量的减少。在目标设备上跑通整个链路,测出真实帧率提升,这个优化才算闭环。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐