【模型剪枝2】不同剪枝方法实现对 yolov5n 剪枝测试及对比

目录

一、背景

二、剪枝

1. Network Slimming

1.0 代码准备

1.1 稀疏化训练

1.2 剪枝

1.3 微调

1.4 测试总结

2. Torch Pruning(TP)

2.1 MagnitudePruner

2.1.1 剪枝

2.1.2 retrain

2.1.3 测试总结

2.2 SlimmingPruner

2.2.1 定义重要性评估

2.2.2 定义剪枝器

2.2.3 初始化

2.2.4 稀疏化训练

2.2.5 剪枝

2.2.6 微调

2.2.7 测试总结

三、总结

参考资料


一、背景

        书接上回,本文记录一下上文中介绍的三种剪枝方法的使用,以剪枝 yolov5n6 为例,对比一下剪枝前后模型体积和准确率变化,以及不同剪枝方法之间的对比。

二、剪枝

        [1] 官方没有提供代码,放到后面使用 Torch Pruning 工具实现,[2] 的官方代码是对 VGG 和 ResNet 进行的剪枝,有需要的朋友可以参考,这里使用的是 midasklr 大佬提供的 yolov5 实现[5],也是使用比较多的。

1. Network Slimming

        主要剪枝流程如下,作为比较,可以先训练一个原始 yolo 模型作为 baseline,之后再进行稀疏化训练。

剪枝流程图

原始 yolov5n6 模型精度测试结果

1.0 代码准备

        首先本文测试的模型是有 4 个检测尺度分支的 yolov5n6,所以需要先对源码进行修改已适配模型结构。

(1)在 models/yolo.py 的 Line538 后添加一行

from_to_map[named_m_base + ".m.3"] = fromlayer[f[3]]

(2)prune.py 从 Line432 开始的 pruned_yaml["backbone"] 和 pruned_yaml["head"] 改成 yolov5n6 的 backbone 和 head

(3)Line554 的 "model.24" 改成 “model.33”

1.1 稀疏化训练

        train_sparity.py 提供了稀疏化训练代码,这里的稀疏率 s 没有像官方一样使用定值,而是根据训练轮次增加逐渐减小,通常 s 越大稀疏化程度越高。

python train_sparity.py --st --sr 0.0005

训练完成后可以进入 http://localhost:6006/ 查看 BN 层权重分布直方图的变化情况

tensorboard --logdir=./runs/train
稀疏困难
稀疏过快
平稳变化

        s 如果设置过小,BN 层很难稀疏化,可剪枝空间很小;如果设置过大,BN 层会过于稀疏化,难以学习到有效权重,造成精度损失和过度剪枝。所以需要根据具体任务和训练结果不断调整 s,直到 BN 层的权重在训练过程中平稳变化至 0 点。

除了监控 BN 层,还可以通过 mAP 的变化情况判断模型的训练效果。

稀疏模型的精度测试

1.2 剪枝

稀疏训练完成后对得到的稀疏模型进行剪枝

python prune.py --percent 0.5 --weights /path/to/your/sparse_model

可以看到根据计算得到的 \gamma 阈值对各层进行了剪枝。

        当剪枝之后剩余通道数为 1 时,剪枝过程可能会报错。上图中 model.8.cv3.bn 剪枝后剩余 1 个通道,然后就会报错。

这表明对该层来说 \gamma 阈值偏大了,所以需要针对该层调整 \gamma,确保剪枝后剩余通道数>1,具体需要修改 utils/prune_utils.py 的 obtain_bn_mask(),每次剪完之后判断剩余通道数,如果余 1,则按1% 的步长降低 \gamma,这样能保证剪枝通道尽可能多,循环剪枝判断,直至剩余通道数>1。

此时再剪枝就没有问题了,可以看到 model.8.cv3.bn 层剩余通道数变2了。

剪枝 50% 后的精度测试

可以看到跟剪枝前的稀疏模型相比没有精度损失,说明还有剪枝空间,剪枝率可以再调大一些。

剪枝 70% 后的模型和精度测试:

可以看到准确率明显下降,需要微调来恢复精度。 

1.3 微调
python finetune.py --weights pruned_model.pt

微调 60 个 epochs 之后准确率恢复到可接受程度,相比剪枝前会有轻微下降。

1.4 测试总结

        上述剪枝流程还是比较清晰的,实际使用时很难说一次剪枝成功,根据效果多调参数,设置合适的剪枝比例基本不会有太大问题。下面对比总结一下剪枝前后模型准确率、体积、推理速度等方面的差异。

model

mAP

@.5

mAP

@.5:.95

Speed / CPU

 (ms/image)

Speed / GPU

(ms/image)

Params

(M)

FLOPs

(G)

Size

(MB)

baseline0.9950.92136.311.73.094.313.2@32FP

sparse

training

0.995   0.89737.612.03.094.313.2
pruned 50%0.9950.89728.311.61.283.05.5
pruned 70%0.6940.55

25.6 (↑29.4%)

10.4 (↑11.1%)0.672.23.0
finetune0.9940.87724.9 (↑31.4%)11.0 (↑6.0%)0.672.23.0

        从推理速度上看,剪枝后的模型在 GPU 上的加速效果不明显,在 CPU 上能加速 30% 左右,模型体积有明显降低。对于 GPU 加速不明显的问题,有人说是因为剪枝后的通道数不满足 2^n,不利于 GPU 的并行计算。

有小伙伴提出控制剪枝时的通道数为 2^n,实测下来提升效果不明显。

2. Torch Pruning(TP)

        下面介绍使用剪枝库 TP 实现两种 [1-2] 不同的剪枝器。建议安装最新版本,新旧版本的代码结构差别比较大。TP 的基本使用可以参考官方文档,里面说的很清楚。

2.1 MagnitudePruner

MagnitudePruner 不需要稀疏化训练,直接对训练后的模型剪枝即可。

2.1.1 剪枝

        TP 内部实现了基于权值重要性的剪枝算法,我们只需要调用相关接口生成一个自己的剪枝器就可以使用了。

def l1norm_filter_pruner(model, fake_input, iterative_steps, ratio, device='cuda:0'):
    for p in model.parameters():
        p.requires_grad_(True)

    # 定义重要性标准,p=2表示使用L2范数计算重要性分数
    imp = tp.importance.MagnitudeImportance(p=2)

    ignored_layer_outputs = []    # 忽略剪枝的输出通道
    ignored_layer_inputs = []     # 忽略剪枝的输入通道
    ignored_module = []
    # 自定义忽略剪枝的层,如果某些层对精度影响较大可以不剪,如果全剪则跳过本部分
    for k, m in model.named_modules():
        k_sp = k.split('.')
        if len(k_sp) <= 1: continue
        module_idx = int(k_sp[1])
        # 忽略对backbone的剪枝
        if module_idx < 12 and f"model.{module_idx}" not in ignored_module:
            ignored_module.append(f"model.{module_idx}")
            ignored_layer_outputs.append(m)
        # 忽略对Detect输出层的剪枝
        if isinstance(m, (Detect, )):
            ignored_layer_outputs.append(m)
        # 忽略对Detect输入层的剪枝
        if k in ['model.23.cv3', 'model.26.cv3', 'model.29.cv3', 'model.32.cv3']:
            ignored_layer_inputs.append(m)
    ignored_layers = ignored_layer_inputs + ignored_layer_outputs
    
    # 定义剪枝器
    pruner = tp.pruner.MagnitudePruner(
        model,
        fake_input,    # 样例输入,用于自动生成依赖图,shape: [bs, ch, h, w]
        importance=imp,    # 重要性标准
        iterative_steps=iterative_steps,    # 剪枝迭代次数(=1:一次剪枝;>1:迭代剪枝,每次剪枝比例=ratio/iterative_steps)
        pruning_ratio=ratio,    # 剪枝比例
        ignored_layers=ignored_layer_outputs,    #忽略层
    )
    return model, pruner

        注意剪枝比例是对于所有需要剪枝的层来说的,比如模型一共 20 层,需要剪枝 15 层,剪枝比例 70%,则实际剪枝 15\times 0.7\approx 11 层。

        有了剪枝器之后就可以剪枝了,这里使用了两种方式剪枝,一种是直接对训练完成的权重文件进行剪枝,另一种是 prune-retrain 迭代式剪枝。

一次剪枝

def prune_model(pruner, model, inputs, iters):
    # 原始模型的计算量和参数量
    base_macs, base_params = tp.utils.count_ops_and_params(model, example_inputs=inputs)
    # 开始剪枝
    for _ in range(iters):
        pruner.step()
    
    # 剪枝后的计算量和参数量
    macs, params = tp.utils.count_ops_and_params(model, example_inputs=inputs)
    print(model)
    print("Before Pruning: MACs=%.2f G, #Params=%.2f M" % (base_macs/1e9, base_params/1e6))
    print("After Pruning: MACs=%.2f G, #Params=%.2f M" % (macs/1e9, params/1e6))
    return model


if __name__ == '__main__':
    weights = 'runs/train/exp/weights/last.pt'
    model = attempt_load(weights, map_location='cuda:0')
    fake_input = torch.randn((1, 3, 640, 640), device='cuda:0')

    model, pruner = l1norm_filter_pruner(model, 
                                         fake_input,
                                         1, 
                                         0.5, 
                                         'cuda:0')
    pruned_model = prune_model(pruner, model, fake_input, 1)

测试剪枝后准确率,直接归零

剪枝率 50%

        实测不同剪枝比例,仅剪枝 20% 准确率依然是 0,剪枝 1% 时 mAP 也有明显下降,感觉这种剪枝方式不好用。

剪枝率 1%
2.1.2 retrain

        [1]中提供了两种 retrain 策略恢复精度,一次剪枝+retrain 和 剪枝+retrain 交替。

(1)One Shot

        对于一次剪枝+retrain,重新加载剪枝模型后从头开始训练,实测训练 120 个 epochs 模型基本收敛,精度不再有明显提升,最终恢复结果如下:

retrain 60 epochs
retrain 120 epochs

(2)迭代剪枝(Iter)

        逐层剪枝和 retrain 交替进行,进行下一次剪枝之前先 retrain。之前 TP 作者有说过还不支持该方式,我也看了一下自该回答之后更新过的版本,也没有看到支持该方式的提示,所以目前来说应该还不支持。(如果说的不对请大佬指正)

2.1.3 测试总结

        目前测试来看基于权值重要性的剪枝方式效果不如基于 BN 层的剪枝,前者剪枝比例稍大一些对于精度损失就很明显,而且 retrain 基本恢复不到原始精度,但是模型压缩比和 CPU 加速比更明显,具体对比测试结果如下:

Model

(prune method)

mAP

@.5

mAP

@.5:.95

Speed / CPU

 (ms/image)

Speed / GPU

(ms/image)

Params

(M)

FLOPs

(G)

Size

(MB)

baseline0.9950.92136.311.73.094.313.2@32FP
MagPru+50%00//0.781.23.4
SlimPru+50%0.9950.89728.311.61.283.05.5
SlimPru+70%0.6940.55

25.6 (↑29.4%)

10.4 (↑11.1%)0.672.23.0

MagPru+50%+retrain

(OneShot/120epochs) 

0.9950.86

20.6

(↑43.2%)

10.9

(↑6.8%)

0.781.23.4

SlimPru+70%+finetune

(60 epochs)

0.9940.87724.9 (↑31.4%)11.0 (↑6.0%)0.672.23.0
2.2 SlimmingPruner

        基于 TP 实现 Slimming 剪枝比较简单,[6] 中原作者也给了比较清晰的例子,定义好重要性评估和剪枝器,将 [5] 中的稀疏化训练代码替换一下就可以,后面的剪枝和微调操作不变。

2.2.1 定义重要性评估
class MySlimmingImportance(tp.importance.Importance):
    def __call__(self, group, **kwargs):
        group_imp = []
        for dep, idxs in group:
            layer = dep.target.module
            if isinstance(layer, nn.BatchNorm2d):
                local_imp = torch.abs(layer.weight.data)
                group_imp.append(local_imp)
        if not len(group_imp):
            return None
        group_imp = torch.stack(group_imp, dim=0).mean(dim=0)
        return group_imp
2.2.2 定义剪枝器
class MySlimmingPruner(tp.pruner.BasePruner):
    def regularize(self, model, sr):
        for m in model.modules():
            if isinstance(m, nn.BatchNorm2d):
                m.weight.grad.data.add_(sr * torch.sign(m.weight.data))     # L1正则
2.2.3 初始化

初始化重要性评估和剪枝器

imp = MySlimmingImportance()

ignored_layer_outputs = []
ignored_layer_inputs = []
for k, m in model.named_modules():
    if isinstance(m, Detect):
        ignored_layer_outputs.append(m)
    if k in ['model.23.cv3', 'model.26.cv3', 'model.29.cv3', 'model.32.cv3']:
        ignored_layer_inputs.append(m)
ignored_layers = ignored_layer_inputs + ignored_layer_outputs

iterative_steps = 1
pruner = MySlimmingPruner(
    model,
    fake_input,
    global_pruning=False,
    importance=imp,
    iterative_steps=iterative_steps,
    pruning_ratio=0.7,
    ignored_layers=ignored_layers,    # 忽略对Detect层的剪枝
)
2.2.4 稀疏化训练

        在反向传播和梯度更新之间插入一行代码即可。tp.Pruner.MetaPruner 提供了一个 regularize 接口,用于稀疏训练。

loss.backward()
srtmp = opt.sr*(1 - 0.9*epoch/epochs)
slpruner.regularize(model, srtmp)    # 稀疏化训练
optimizer.step()

        训练过程没有区别,训练完之后使用 TensorBoard 查看 BN 层权重变化,可以看到权重随着训练慢慢变得稀疏

2.2.5 剪枝

2.2.6 微调

微调 60 个 epochs 后基本恢复,测试精度上与直接使用 Network Slimming 没啥区别。

2.2.7 测试总结

        Network Slimming 的两种实现方式在最终的剪枝效果上没有啥区别,主要还是使用方式上的不同。

Model

mAP

@.5

mAP

@.5:.95

Speed / CPU

 (ms/image)

Speed / GPU

(ms/image)

Params

(M)

FLOPs

(G)

Size

(MB)

base0.9950.92136.311.73.094.313.2@32FP

Slimming

(prune 70%)

0.6940.55

25.6 (↑29.4%)

10.4 (↑11.1%)0.672.23.0

Slimming+ft

(60 epochs)

0.9940.87724.9 (↑31.4%)

11.0

(↑6.0%)

0.672.23.0

TP-Slimming

(prune 70%)

00////2.9

TP-Slimming+ft

(60 epochs)

0.9950.877

25.4

(↑30%)

11.1

(↑5.1%)

0.672.22.9

三、总结

        本文是基于之前学习的几篇剪枝论文,参考网上代码做的本地实现,并对比测试了不同剪枝方法之间的效果差异。目前的工作只能算是对模型剪枝的入门,剪枝的模型也是使用比较多的yolov5,实现难度上相对小一些。后面有时间也会慢慢尝试对其他更多模型的剪枝,学习更多的剪枝方法。

        现在使用的剪枝和量化操作都是独立进行的,而且剪枝对模型体积的压缩效果更明显,可以试着把两者结合起来,对剪枝后的模型做量化,进一步压缩模型体积的同时借助 NPU 加速模型推理。此外,除了模型剪枝和模型量化,模型蒸馏也是模型优化的一种方法,后面有时间也会去学习。

参考资料

[1] [1608.08710] Pruning Filters for Efficient ConvNets

[2] ICCV 2017 Open Access Repository

[3] yolov5s模型剪枝详细过程(v6.0)_yolov5剪枝-CSDN博客

[4] YOLOv5-6.1剪枝实战_yolov5通道剪枝-CSDN博客 

[5] https://github.com/midasklr/yolov5prune/tree/v6.0

[6] https://github.com/YINYIPENG-EN/Pruning_for_YOLOV5_pytorch 

[7] Torch-Pruning | 轻松实现结构化剪枝算法 - 知乎 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值