第2章:模型轻量化基础——参数量与计算量(FLOPs)概念、模型剪枝(Pruning)原理、通道剪枝与结构化剪枝实战

做端侧部署,第一个要面对的问题就是:模型太大,跑不动。

我刚开始接触YOLO部署时,拿着一个YOLOv5s模型就往开发板上塞。结果呢?推理一帧要800多毫秒,卡得跟幻灯片似的。后来我才明白——不搞轻量化,端侧部署就是纸上谈兵。

这一章,咱们把模型轻量化的两个核心指标讲透,再上手剪枝。你想想看,一个模型几百万参数,真正有用的可能不到一半。剪掉冗余,速度就上来了。

2.1 参数量与FLOPs——衡量模型的两个硬指标

先说参数量。说白了,就是模型里所有可学习参数的总数。卷积层的权重、偏置,全连接层的矩阵,都算。

计算公式其实不复杂:

参数量 = (输入通道 × 输出通道 × 卷积核高 × 卷积核宽) + 输出通道(偏置)

举个例子,一个3×3卷积,输入64通道,输出128通道:

参数量 = 64 × 128 × 3 × 3 + 128 = 73,856

嗯,七万多个参数。一个YOLOv5s模型,参数量大概在700万左右。这个数字直接决定了模型占多大存储空间。700万个float32参数,算下来就是:

7,000,000 × 4字节 ≈ 28 MB

28MB,听起来不大?放到只有几十MB Flash的MCU上,你就知道什么叫捉襟见肘了。

再说FLOPs——浮点运算次数。它衡量的是模型推理一次需要做多少次乘加运算。这个指标直接跟推理速度挂钩。

卷积层的FLOPs计算公式:

FLOPs = (2 × 输入通道 × 卷积核高 × 卷积核宽 × 输出通道) × 输出特征图高 × 输出特征图宽

注意那个2,代表一次乘法和一次加法。我见过不少新手只算乘法,漏了加法,结果FLOPs少算一半。

关键区别:参数量决定模型能塞进多大的存储空间,FLOPs决定模型能跑多快。两个指标都要看,缺一不可。

我在项目中遇到过一件事:一个模型参数量剪掉了一半,但推理速度只提升了20%。为什么?因为FLOPs没降下来。剪枝剪在了参数量大但计算量小的层上,白费功夫。

2.2 模型剪枝原理——为什么要剪?剪什么?

模型剪枝,说白了就是把不重要的连接或通道砍掉。神经网络训练完后,很多权重值接近0,这些权重对最终结果贡献极小。砍掉它们,模型照样能工作。

剪枝的核心逻辑就三步:

  1. 评估重要性:判断哪些参数是冗余的
  2. 剪掉冗余:把不重要的参数置零或直接移除
  3. 微调恢复:剪完后重新训练,让模型找回精度

为什么要微调?我刚开始做剪枝时,剪完直接部署,结果mAP从0.85掉到0.3。后来才明白——剪枝相当于给模型做了个手术,术后需要康复训练。

剪枝按粒度分,主要有两种:

剪枝类型 粒度 优点 缺点
非结构化剪枝 单个权重 压缩率高,精度损失小 硬件加速难,稀疏矩阵效率低
结构化剪枝 通道/卷积核 直接减少计算量,硬件友好 压缩率相对低,精度损失大

避坑指南:我曾经在项目里只做非结构化剪枝,剪完模型文件小了,但推理速度一点没变。因为NPU不支持稀疏矩阵加速。端侧部署,结构化剪枝才是王道。

2.3 通道剪枝实战——以YOLOv5s为例

通道剪枝,是结构化剪枝里最常用的一种。它直接剪掉卷积层的整个通道,效果立竿见影。

怎么做?核心思路是:用BN层的缩放因子γ来判断通道重要性。

BN层的计算公式里有个γ参数,每个通道对应一个γ。训练时,γ值越大,说明这个通道对后续层贡献越大。γ接近0的通道,基本就是冗余的。

具体流程:

  1. 稀疏化训练:在损失函数里加一个L1正则项,让γ尽量往0靠
  2. 设定剪枝率:比如剪掉50%的通道
  3. 剪掉低γ通道:把γ值排序,砍掉最小的那一批
  4. 微调:用原始数据重新训练几个epoch

代码实现的核心部分:

# 稀疏化训练时,在损失函数中加入γ的L1正则
def sparsity_loss(model, s=0.0001):
    reg_loss = 0
    for module in model.modules():
        if isinstance(module, nn.BatchNorm2d):
            reg_loss += torch.norm(module.weight, 1)  # L1范数
    return s * reg_loss

# 剪枝:根据γ阈值移除通道
def prune_channels(model, prune_ratio=0.5):
    gamma_list = []
    for module in model.modules():
        if isinstance(module, nn.BatchNorm2d):
            gamma_list.extend(module.weight.data.cpu().numpy())
    
    # 计算阈值
    threshold = np.percentile(gamma_list, prune_ratio * 100)
    
    # 对每个BN层,保留γ大于阈值的通道
    for module in model.modules():
        if isinstance(module, nn.BatchNorm2d):
            mask = module.weight.data.abs() > threshold
            # 实际剪枝操作需要重建卷积层
            # 这里省略了具体重建逻辑
    return model

实战经验:剪枝率不是越高越好。我试过剪掉70%的通道,精度直接崩了。建议从30%开始试,每次增加10%,观察精度变化。YOLOv5s一般剪到40%-50%还能保持95%以上的原始精度。

2.4 结构化剪枝的完整流程

通道剪枝只是结构化剪枝的一种。完整的结构化剪枝还包括卷积核剪枝和层剪枝。

我画了一张流程图,帮你理清关系:

结构化剪枝流程 原始训练模型 稀疏化训练(加L1正则) 通道剪枝 按γ值排序裁剪 卷积核剪枝 按L1范数排序裁剪 层剪枝 直接移除冗余层 微调恢复精度 轻量化部署模型 三种剪枝方式可以组合使用,但建议先做通道剪枝,效果最明显

三种剪枝方式各有适用场景:

  • 通道剪枝:最常用,直接减少特征图数量,计算量下降明显
  • 卷积核剪枝:针对单个卷积核,粒度更细,但实现复杂
  • 层剪枝:直接移除整个层,适合冗余度高的深层网络

我个人的习惯是:先通道剪枝,再层剪枝。通道剪枝能砍掉大部分冗余,层剪枝再处理那些确实没用的残差块。卷积核剪枝我一般不用,因为对硬件加速不友好。

2.5 剪枝后的效果评估

剪枝不是剪完就完事了。你得验证两个东西:精度和速度。

精度验证很简单,跑一遍验证集,看mAP。速度验证就得真刀真枪上板子测了。我见过有人只在PC上测FLOPs,觉得降了50%就万事大吉。结果上板子一跑,速度只快了20%。为什么?因为FLOPs是理论值,实际推理还受内存带宽、算子优化程度影响。

我的建议:剪枝后一定要在目标硬件上做benchmark。PC上的FLOPs降低比例,跟实际加速比例,往往不是线性关系。

给你一个参考数据,我之前在RK3588上做的YOLOv5s剪枝测试:

剪枝率 参数量 FLOPs mAP@0.5 推理速度(ms)
0%(原始) 7.2M 16.5G 0.837 45
30% 5.0M 11.6G 0.831 33
50% 3.6M 8.3G 0.812 25
70% 2.2M 5.0G 0.745 18

看到没?剪到50%时,mAP只掉了2.5个点,但速度提升了将近一倍。这就是剪枝的价值。

曾经踩过的坑:我试过剪完枝后不做微调,直接部署。结果检测框全飘了。记住,剪枝后的微调不是可选项,是必选项。至少微调50个epoch,学习率用原始训练的1/10。

好了,这一章的内容就到这。参数量和FLOPs这两个概念,你得刻在脑子里。剪枝的原理和实战,我建议你拿YOLOv5s跑一遍,剪个30%试试手。只有亲手做过,才能真正理解。