对MODNet 主干网络 MobileNetV2的剪枝探索

目录

1 引言

1.1 MODNet 原理

1.2 MODNet 模型分析

2 MobileNetV2 剪枝

2.1 剪枝过程

2.2 剪枝结果

2.2.1 网络结构

2.2.2 推理时延

2.3 实验结论

3 模型嵌入

3.1 模型保存与加载

法一:保存整个模型

法二:仅保存模型的参数

小试牛刀

小结

3.2 修改 MobileNetV2 中的 block

阶段一

阶段二

总结

3.2 MobileNetv2 参数嵌入 v1

3.2.1 遇到问题

3.2.2 解决方案

3.2.3 探索过程

阶段一

阶段二

阶段三

3.2.4 结论

3.3 MobileNetV2 参数嵌入 v2

3.3.1 开展思路✨

3.3.2 探索过程

阶段一

ERROR1

ERROR 2

阶段二

阶段三

模拟推理

ERROR 1

ERROR 2

阶段四

ERROR 1

ERROR 2

实验结果


1 引言

1.1 MODNet 原理

基于卷积神经网络擅长处理单一任务的特性,MODNet 将人像抠图分解成了三个相关的子任务:语义估计(Semantic Estimation)、细节预测(Detail Prediction)、语义-细节融合(Semantic-Detail Fusion),分别对应着模型结构中 Low-Resolution Branch(LR-Branch)、High-Resolution Branch(HR-Branch)、Fusion Branch(F-Branch) 三个分支。其中,LR-Branch 用于人像整体轮廓的预测,HR-Branch 用于前景人像与背景过渡区域的细节预测, Fusion Branch 负责将前两部分预测得到的轮廓与边缘细节融合。语义估计部分是独立的分支,依赖于细节预测、语义-细节融合两个分支。

MODNet论文地址:https://arxiv.org/pdf/2011.11961.pdf

1.2 MODNet 模型分析

作为轻量化模型,MODNet 参数量仅6.45MB,但作为底层视觉任务,MODNet输入、输出分辨率一致,计算复杂度较高。笔者利用 NNI 分析了 MODNet 三个分支的参数量与计算量:

MODNet分支参数量(MB)计算量(GFLOPs)
LR-Branch6.165.03
HR-Branch0.2410.58
F-Branch0.052.71

从计算量来看,MODNet 三个分支 LR-Branch、HR-Branch、F-Branch 分别占整个网络的27.7%、58.2%、14.1%。HR-Branch 作为高分辨率分支,相比其它两个分支的计算量较高,是剪枝重点关注的部分。

在实际剪枝时,由于 MODNet 内部包含了三个分支,直接剪枝较为复杂,因此对结构进行拆分。

接下来,是笔者对主干网络 MobileNetV2 部分的探索实验。

2 MobileNetV2 剪枝

2.1 剪枝过程

确定待剪枝的对象:

  • Cov2d layer
  • BatchNorm2d
  • Linear

✨剪枝策略:基于L1范数结构化剪枝

model = MobileNetV2(3)

config_list = [{'sparsity': 0.8, 'op_types': ['Conv2d']}]
pruner = L1NormPruner(model, config_list)
_, masks = pruner.compress()
pruner._unwrap_model()
ModelSpeedup(model, torch.rand(1, 3, 512, 512), masks).speedup_model()

2.2 剪枝结果

参数量:3.5M → 309.76k

复杂度:1.67 GMac → 41.54 MMac

从参数量和计算复杂度来看,对MobileNetV2的剪枝可以明显压缩模型,下面具体来看网络结构、推理时延与精度。

2.2.1 网络结构

分别展示三组,A组、B组、C组分别代表MobileNetV2第0~15层、第16~32层、第33~52层。

A组剪枝前:

A组剪枝后:

B组剪枝前:

B组剪枝后:

C组剪枝前:

C组剪枝后:

可以发现,不论是靠近模型输入端,还是输出端,卷积层都有大幅度的裁剪

2.2.2 推理时延

使用PyTorch框架,分别在GPU与CPU上测试,结果如下。

GPU上

剪枝前剪枝后
10.93390.8370
20.91440.7980
30.95300.8759

CPU:

剪枝前剪枝后
10.35290.0609
20.39060.0549
30.36000.0520

第一次观察剪枝前、后的推理时延时,发现差距并不大。正在此时,笔者联想到前一秒的数据类型不一的报错:由于 dummy_input 转到了 CUDA 上,而 model 还是 CPU上 的 model,导致无法 run。于是笔者把 model 转到了cuda上。也正是在这个时候,笔者想到了推理硬件的问题如果数据量本身就不大的时候,在 CUDA 上运行速度不一定比 CPU 快。数据如果要利用 GPU 计算,就需要从内存转移到显存上,数据传输具有很大开销,对于小规模数据来说,传输时间已经超过了 CPU 直接计算的时间。因此,规模小无法体现出 GPU 的优势,而大规模神经网络采用 GPU 加速意义较大,推理同样如此。

通过对MobileNetV2剪枝,我们可以发现:剪枝前、后的模型在 CPU 上的推理状况已经有了明显的差距,推理时延的降低达到了预期!!!

仔细看推理,dummy_input 初始化时涉及了batch,上述实验都是基于batch=1的情况。因此,接下来对剪枝前、后的模型,在不同的batch、在不同的硬件上推理进行对比。

CPU:

batch剪枝前剪枝后
10.300.07
20.640.07
41.140.18
82.220.25
164.330.39
3211.760.78
6466.451.49

GPU:

batch剪枝前剪枝后
10.810.77
20.900.79
40.950.76
8xxx0.83
16xxx0.91
32xxx0.97
64xxxxxx

2.3 实验结论

回首LeNet,不论是pth格式的PyTorch模拟推理还是实际推理,以及 ONNX、OpenVINO 推理,剪枝前、后的模型在 CPU 上的推理速度差异不明显就可以解释了,同时也验证了先前的猜想LeNet 本身属于小规模网络,结构简单,推理速度已经很快了。因此,不论如何压缩模型,在batch较小的时候都无法有效降低推理时延。


另外,笔者总结了下列结论🎉:

  • 当网络规模并非很大时,在CPU上推理比GPU更适合;(规模的阈值没有明确界限,需要尝试)

  • 当 batch 逐渐增大时,剪枝后的模型推理速度占有绝对优势,且大模型优于小模型;

  • 若要进行实时推理,保证较低的推理时延,batch 可以考虑设置为1;

  • 相较于小模型,大模型剪枝在实际推理中更有意义;(LeNet剪枝前后batch为1时延不变,而mobilenetv2差异较大)

  • 不仅仅是模型大小,数据本身也会影响推理速度;(推理小数据负担较小,所以在做模型压缩时,是否可以考虑前处理,例如图像压缩)

3 模型嵌入

3.1 模型保存与加载

PyTorch 模型的保存与读取方式有两种,接下来还是以 LeNet 为例,结构定义如下:

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 3)
        self.conv2 = nn.Conv2d(6, 16, 3)
        self.fc1 = nn.Linear(16 * 5 * 5, 120) 
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, int(x.nelement() / x.shape[0]))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

法一:保存整个模型

model = LeNet()
torch.save(model,PATH)

# 直接加载,得到网络结构:
model = torch.load(PATH)

💥注意:参数可以通过 model.named_parameters() 获取。

法二:仅保存模型的参数

torch.save(model.state_dict(),PATH)

# 直接加载,得到权重参数:
model = torch.load(PATH)

若要将参数加载到网络中,需要获取网络结构,也就是初始化model:

model = LeNet()
model.load_state_dict(torch.load('model2.pth'))
print(model)

💥注意:参数和网络结构应当相匹配,否则参数无法传入。

这里做个示范:将第二个全连接层的输出通道以及第三个全连接层的输入通道都改为94,继续load,则:

因此,在对网络结构裁剪后,利用 state_dict 读取参数前需要相应修改网络结构,可以参考裁剪后的模型通道数以及卷积核的变化情况。

小试🐂🔪

以 LeNet 为例,由于之前都是直接保存整个结构,因此没有涉及到结构的修改问题,接下来只保存模型参数。

剪枝后的网络结构为:

加载剪枝后的模型:

model = LeNet()
path = 'new_model.pth'
model.load_state_dict(torch.load(path))
print(model)

所以,如果直接加载则会产生参数与结构不匹配的错误,因此修改如下:

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 2, 3)
        self.conv2 = nn.Conv2d(2, 4, 3)
        self.fc1 = nn.Linear(4 * 5 * 5, 120) 
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, int(x.nelement() / x.shape[0]))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

小结

模型保存时只保存参数,相对保存整个模型而言,不需要在加载时与结构的定义在同一脚本中;此外仅保存模型的参数占用较少的内存,且节省时间,所以一般情况下只保存参数的做法更常用。

3.2 修改 MobileNetV2 中的 block

阶段一

修改前、后对比:

# 修改前
    interverted_residual_setting = [
            # t, c, n, s
            [1, 16, 1, 1],
            [expansion, 24, 2, 2],
            [expansion, 32, 3, 2],
            [expansion, 64, 4, 2],
            [expansion, 96, 3, 1],
            [expansion, 160, 3, 2],
            [expansion, 320, 1, 1],
        ]
    
# 修改后:
        interverted_residual_setting = [
            # t, c, n, s
            [1, 4, 1, 1],
            [expansion, 8, 2, 2],
            [expansion, 18, 3, 2],
            [expansion, 39, 4, 2],
            [expansion, 45, 3, 1],
            [expansion, 77, 3, 2],
            [expansion, 64, 1, 1],
        ]

然而,由于裁剪后通道数没有保持8的整数倍,加载失败也在预料之中。

🎉解决方案:

  • 利用_make_divisible对NNI剪枝源码进行修改;❌
  • 修改裁剪比例✅

初步探索:

①将根据稀疏比例设定位0.2进行裁剪,保留80%的成分,但又由于浮点数问题的取整问题也无法保证是8的倍数。

②再次观察通道数,只要将第二个残差块的out_channels(24)保留,剩下的裁剪50%即可保证。但通过实验发现,NNI的API在根据预先设定的稀疏度裁剪时存在误差,在对于MobileNetv2这样的倒置残差块结构来说,无法达到理想的效果。

③通过查看源码,发现了剪枝时的mode选项:当设置为“dependency_aware”时,剪枝器将强制具有依赖关系的卷积层剪枝相同的通道。因此,一方面可以保证准确的稀疏度;另一方面,加速模块可以更好地从剪枝模型中获得速度优势。


剪枝配置如下:

config_list = [{'sparsity': 0.5,
                'op_types': ['Conv2d']},
               {'exclude': True,
                'op_names': ['features.0.0','features.2.conv.6']
                }]

这里加入了first layer的conv,如果不排除,也会导致channel不是8的倍数。

这一次达到了预期效果,但加在模型参数时依然提示do not match。

这里思考了很久,再一看模型,发现实际上笔者一直在关注channel是否为8的倍数,而忽略了interverted_residual_block中的扩展因子,每次扩展都会将低维乘6。因此,block内部的维度扩展在剪枝过后不再满足要求,如下图:

阶段二

🎉解决方案:

  • 修改block层的expansion系数;
  • 排除每一个block“低维--->高维”时的第一层进行剪枝;

首先,将低维扩展后的第一个高维conv进行保留测试,剪枝后的shape如下:

按照这样的思路,将其余block的第一个扩展层进行排除,配置如下:

config_list = [{'sparsity': 0.5,
                'op_types': ['Conv2d']},
               {'exclude': True,
                'op_names': ['features.0.0', 'features.2.conv.6', 'features.2.conv.0', 'features.3.conv.0',
                             'features.4.conv.0', 'features.5.conv.0', 'features.6.conv.0', 'features.7.conv.0',
                             'features.8.conv.0', 'features.9.conv.0', 'features.10.conv.0', 'features.11.conv.0',
                             'features.12.conv.0', 'features.13.conv.0', 'features.14.conv.0', 'features.15.conv.0',
                             'features.16.conv.0', 'features.17.conv.0']
                }]

整体上达到了理想效果,仅有一个block层不符合expansion,随之修改对应block层的expansion以及each block setting。

由于该层会重复执行两次,在将expansion修改为3以后,第二次执行就不再符合条件。这里根据通道变化情况针对执行次数单独控制,如下:

        interverted_residual_setting = [
            # t, c, n, s
            [1, 8, 1, 1],
            [6, 24, 2, 2],  # 1. 8-->24  2. 24-->24,expansion=3(modify)
            [6, 16, 3, 2],  # 1. 24-->16(modify)  2. 16-->96
            [expansion, 32, 4, 2],
            [expansion, 48, 3, 1],
            [expansion, 80, 3, 2],
            [expansion, 160, 1, 1],
        ]

成功加载:

mobilenet = MobileNetV2(3)
mobilenet.load_state_dict(torch.load('modified_mobilenet_3.pth'))
print(mobilenet)

接下来,针对MODNet预训练模型中的MobileNetV2部分进行裁剪。

第一次剪枝下来有个别层的通道有误,同时,个别通道没有满足预设的expansion,比如:

小结

  1. 考虑到计算机处理器单元架构的问题,通道数保持8的倍数能够较快计算,因此,剪枝也应当遵循这个原则;

  2. 考虑到每一个block内部的扩展因子expansion,因此,排除这些层中的第一个连接层以满足倍数关系;

  3. 对仍不满足要求的层特殊处理,对相同blcok不同执行次数时的expansion控制;

3.2 MobileNetv2 参数嵌入 v1

3.2.1 遇到问题

将MobileNetV2结构嵌入 MODNet 并不困难,难点在于基于MODNet预训练模型参数裁剪后的嵌入、进而推理。

在将裁剪后的MobileNetV2模型结构直接嵌入MODNet,开始训练时,提示通道数不匹配的error,原因在于MobileNetV2只是MODNet中的一部分。

因此,针对这样的问题,笔者考虑了下列四种方案。

3.2.2 解决方案

方案一:保留 MobileNetV2 模型的 input 以及 output channel,重新裁剪;

方案二:修改 MobileNetV2 前、后模型的关联通道数;

方案三:将裁剪后的预训练模型参数加载到修改后的 MobileNetV2,将其余参数正常加载到其余三个子模块,最后合并;

方案四:直接对 MODNet 进行裁剪;

3.2.3 探索过程

阶段一

易由于已经对 MODNet 预训练参数中的 MobileNetV2 参数部分进行了裁剪,同时也修改了 MobileNetV2 结构,在单独对这一子模块进行测试时可以成功嵌入。基于这样的情况,首先采用方案三。

参数合并具体作法:通过对象访问类的成员,即lr_branch、hr_branch、f_branch,在获取其结构后,从MODNet预训练参数中load对应部分的参数,再结合已有的backbone,分别替换原来MODNet中的四个子模块。

替换 backbone 部分:

mobilenet2 = my_mobilenetv2_v2.MobileNetV2(3)
mobilenet2.load_state_dict(torch.load('my_mobilenetv2_2.pth'), strict=False)

modnet.MODNet().backbone = mobilenet2
print(modnet.MODNet().backbone)

实际上,该方法是无效的。通过比对替换前后的MODNet中的backbone参数,可以发现并非理想的数值。

阶段二

因此,笔者开始寻找有效的参数替换方法,参考这位博主。

接下来,在LeNet上进行实验。

按照博客上的代码跑下来出现了一个bug,即缺乏 state_dict 属性,原因在于只保存了模型参数,而非整个模型。只有完整的模型才拥有状态字典。如下:

由于本次剪枝是针对MODNet预训练模型参数,因此,这里按照博客的思路重新调整语句,如下:

model0 = LeNet()
model1 = LeNet()

# 同一结构,不同参数
path0 = 'new_model.pth'
path1 = 'new_model1.pth'

params0 = torch.load(path0)
params1 = torch.load(path1)

new_param = {}
for name, params in model0.named_parameters():
    new_param[name] = params0[name] + params1[name]

torch.save(new_param, 'new.pth')
model0.load_state_dict(torch.load('new.pth'))
print(list(model0.named_parameters()))

params0:

params1:

new_params:(+)

结果是将两个同一结构、不同参数的模型,进行求和运算,得到新模型,或者说,实现了参数修改。

💥注意:模型结构不变,只有参数在变化。


基于此,回到MODNet,继续实现。

此时又意识到了一个问题:MODNet中的子结构发生了变化,也就是说,不仅仅是参数的替换。

这里有一种方案:

1、根据剪枝后的情况修改网络结构,得到新结构,并保存随机参数;

2、将新结构中不含backbone部分的参数用预训练模型中的参数替换,backbone部分使用裁剪后的参数;

3、保存,得到新结构、新参数

在实验过程中遇到了一个error:加载预训练模型以后,每一次打印的参数不一致。

相关代码如下:

modnet = modnet.MODNet(backbone_pretrained=False)
pretrained_ckpt = torch.load('modnet_photographic_portrait_matting.ckpt')
modnet.load_state_dict(pretrained_ckpt, strict=False)

print(list(modnet.parameters()))

这与笔者当时的认知产生了冲突,通过在LeNet上进行实验,证明了原来的想法是正确的:如果只是初始化对象,此时获得的模型参数是随机的;但当加载保存的参数后,填入网络结构的参数已经被固定了下来。

虽然并未找到相同的问题,但有一个相似的问题值得关注:这里。

其中谈论的问题本质在于,采用了多块GPU并行计算:

model = torch.nn.DataParallel(model)

回到MODNet,在作者提供的trainer中,也出现了并行计算的语句。于是,按照这样的方式修改读取语句,如下:

modnet = modnet.MODNet(backbone_pretrained=False)
modnet = torch.nn.DataParallel(modnet)

pretrained_ckpt = torch.load('modnet_photographic_portrait_matting.ckpt')
modnet.load_state_dict(pretrained_ckpt, strict=False)

print(list(modnet.parameters()))

此时,打印出来的参数便和预期的一致了。

不过,在利用对象访问的形式获取模型中的子模块参数时,依旧是参数不一致问题。

mobilenet = modnet.MODNet(backbone_pretrained=False).mobilenet

猜想其中的原因在于参数与结构没有绝对匹配,即这里的mobilenet结构只是ckpt参数的一部分,虽然没有出现size mismatch的bug,但还是无法满足需求。

因此,该方案也就宣布了失败!同时,这也表明最初基于MODNet预训练模型参数的裁剪是错误的!

阶段三

采用方案四,即直接对MODNet模型裁剪。

通过打印输出可发现,MODNet总共有72个卷积层,2个全连接层。由于两个全连接层权重以及输入尺寸较大,因此占有较多的参数,同时,与第二个卷积层相连接的卷积层也有着巨大的参数量。如下:

而这一卷积层的参数量将近整个MODNet的1/2,因此,对该层的裁剪不容忽视。

首先排除剪枝时无法被NNI支持的算子:

算子排除有问题,偶然间看到了新版NNI,安装以后对官方代码进行测试:

虽然NNI2.8 的确可以成功 run 带有 expand_as 的算子,但从问题本身来看,该算子没有进行处理,即只是官方在源码中对该算子进行了排除,并非如 aten_relu 算子般检测到以后使用 torch.relu() 进行操作。

3.2.4 结论

  • 虽然 MobileNetV2 只是 MODNet 的一部分,在裁剪时也是针对这部分进行了操作,但在修改 MobileNetV2 结构以后,从整个 MODNet 结构来看,与 MobileNetV2 相邻的结构也相应产生了变化,也正表明:刚开始的方案三,针对MobileNetV2以及其余子模块分别处理的不合理性,而方案一和二也就没有了必要。

  • 参数修改的有效办法并非简单进行结构赋值,而是通过 state_dict 根据键值对替换;

  • 在利用多块 GPU 并行计算时,需要利用 DataParallel,在保存参数时需要使用 model.module.state_dict(),而非像往常一样的 model.state_dict()。虽然后者不会报错,但需要通过指定 load 时 strict 为 false,才可以将参数名匹配。当然,如果采用前者保存,一方面不需要在 load 时指定 strict,同时也能保证加载到模型的参数名保持一致。

  • aten:expand_as 算子在新版的 NNI 源码中被排除;

  • 对 MODNet 子模块剪枝较容易,但难以将参数嵌入;

3.3 MobileNetV2 参数嵌入 v2

获取网络块,在将 MODNet 参数加载到结构后,通过对象访问获取 MobileNetv2,参数可以保持一致。

model = modnet.MODNet(backbone_pretrained=False)
pretrained_ckpt = 'modnet_photographic_portrait_matting.ckpt'
model.load_state_dict({k.replace('module.', ''): v for k, v in torch.load(pretrained_ckpt).items()})
model = model.backbone  # 获取主干网络:mobilenet V2

3.3.1 开展思路✨

  1. 按照先前的 config 设定进行剪枝,并对 MobileNetV2 网络进行修改;

  2. 将修改后的 MobileNet V2 嵌入 MODNet,得到新的 MODNet,保存一份带有随机初始化参数的模型;

  3. 按照上述获取 MobileNet V2 参数的方式,获取其余三个子模块的参数,保存;

  4. 读取2中保存的模型,用3得到的权重参数进行替换;

  5. 保存新模型;

3.3.2 探索过程

阶段一
ERROR1

分析原因:网络结构中backbone外面包裹着model,而模型参数保存时缺少,因此参数不匹配。

解决方案:可以通过指定strict = False,当然从根源上来看,修改模型保存时的指令(类似多卡训练保存!)

# 将 torch.save(model.state_dict(), PATH)替换为下列语句:

torch.save(model.model.state_dict(), PATH)

ERROR 2

分析原因:classifier层的参数缺失,因此保存参数时也就缺少了这一部分的权重参数。

实际上,先前剪枝同样遇到了这样的问题,但在加载时通过指定strict = False,对齐了参数名以及结构。

解决方案:暂时先以同样的方式对齐,解决了该问题。

与此同时,证明了这一结论:如果模型仅保存了部分参数,当加载到网络中时,部分参数可以正常填入,而剩余的部分会随机初始化。

进一步观察网络结构可以发现,MobileNet V2 中包含 classifier 层,但由于该层用于分类任务,因此在 MODNet 使用时将其去除,这也就成了剪枝后 classifier 参数无法对应填入的原因。

解决方案:修改MobileNet V2网络结构,去除 classifier 的定义。

实验结果:

在去除分类层以后,参数与结构一一对应,因此也就不存在上述假设的问题了。

至此,MobileNet V2 的剪枝、结构修改都已完成。

阶段二

先将裁剪后的 MobileNet V2 的参数替换至 MODNet MobileNet V2 的随机初始化参数:

import torch
from src.models.modnet import MODNet

model = MODNet(backbone_pretrained=False)
model_params = torch.load("pruned_backbone_state.pth")

new_params = {}

for name, params in model.named_parameters():  # 随机初始化参数,将整个MODNet中的层都打印了出来
    if 'backbone' in name:  # 将裁剪后的参数成功填入
        new_name = name.replace('backbone.model.', '')
        new_params[name] = model_params[new_name]
    else:
        new_params[name] = params
        
# print(list(model.named_parameters()))
torch.save(new_params, 'new_substitution_model.pth')

替换前后对比如下:

替换前MODNet头部:

MobileNet V2头部:

替换以后MODNet头部:

此时,新模型除了MobileNet V2以外,其他参数都是随机的。

接下来,利用新结构加载该模型参数。

这里的bug让笔者觉得奇怪,在原先查看权值参数时,每一个layer仅仅只有weight与bias,但这里却出现了其他属性。

检查MODNet所有的参数名并检查替换后的state_dict:

model = MODNet(backbone_pretrained=False)
d = {name: param for name, param in model.named_parameters()}
print(d.keys())

print(list(new_params.keys()))

通过比对两者参数,可以发现:数量与参数名都一致。

有一个值得考虑的地方:在参数替换时利用了 named_parameters() 获取模型参数,因此检验两者固然相同。但是,named_parameters() 本身是否真的获取到了模型所有参数呢?

查阅资料可知:named_parameters() 对于获取BN layer来说,只有两个可学习参数alpha、belta,而running_mean以及running_var,作为两个不可学习的统计量只有在model.state_dict()的时候输出。其作用在于作为训练过程对batch的数据统计

基于此,由于这些参数对实际推理不起作用,直接用规定strict进行加载。

阶段三
模拟推理
import torch
from src.models.modnet import MODNet
from time import time

model = MODNet(backbone_pretrained=False)
model.load_state_dict(torch.load('new_substitution_model.pth'), strict=False)
print(list(model.named_parameters()))

# inference
dummy_input = torch.randn([1, 3, 512, 512]).to('cpu')
t1 = time()
a = model(dummy_input)
t2 = time()
print("Infer time: ", t2 - t1)
ERROR 1

分析原因:在对mobilenet v2裁剪后,其最后一层的output channel从1280-->640,无法去下一个模块的input channel 匹配。

解决方案:排除mobilenet v2最后一层,保留1280的channels,重新剪枝。


ERROR 2

下列是笔者debug解决bug的过程,或许只是对笔者有意义罢了😅,感兴趣的伙伴可以看一下~~

debug寻找到产生bug的语句,定位到HRBranch模块:

观察中间变量可以发现,enc2x的shape与bug显示的input一致:

于是,进入HR Branch,定位到enc2x的定义:

可以看到两个channels定义,向上索引找到MODNet中HR Branch的初始化地方:

可知hr_channels为32,enc_channels用同样的方法,索引到backbone中:

至此,便可明确enc_2x定义时的通道数数值:

这也正好对应上了bug中的weight尺寸:[32, 16, 1, 1]。

由于目前尚未明确input如何从上一层的输出:[1,1,32,32] ---> [1,16,256,256](模块交界处)

因此可以修改权重shap: 16 ---> 8.

至此,该问题成功解决!


这里的enc_channels和mobilenet v2中的interverted_residual channels相关。

参考剪枝前模型的enc_channels设置修改:32--->16,96--->48

32--->16:产生了上述类似的错误,因此暂时保持不变。

96--->48:成功load!


然而,成功load并未加载裁剪后的参数,起初是为了分解问题。

目前,结构本身的推理已经能够成功执行!

阶段四

加载参数,填入模型。

ERROR 1

将channel从96--->48后导致ckpt中的参数shape无法与model中的shape匹配,所以,先恢复到96。


ERROR 2

结合上述的两个问题,如果将8--->16,那么模型本身子模块间的匹配就不再满足。这是一个矛盾点!

因此,在已有的正确MODNet结构的基础上,重新填入裁剪后的参数,保存模型,成功加载!


但channel从96--->48是一个遗憾,这里再次尝试设置48。

实际上,这个问题和ERROR6异曲同工(参数shape > 结构中的shape),成功解决!

实验结果

①参数量

对比channels从96--->48的MODNet参数量对比:

通道数为96通道数为48剪枝前
参数量4.93M 3.36M6.45M

②推理时延

剪枝前剪枝后
10.8340.665
20.8750.690
30.8220.680

写在最后

本文在分析了 MODNet 结构特性后,先针对主干网络MobileNetV2部分进行剪枝,基于L2范数方法,在剪枝完成后进行参数替换。尽管遇到了许多坎坷,但都一一克服,最后成功完成了剪枝,并将其嵌入MODNet!

接下来,笔者将分享对MODNet其他模块的剪枝、以及MODNet剪枝的探索过程!

希望本文的一些历程可以为小伙伴们带来启发~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/339414.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

服务端实现微信小游戏登录

1 微信小程序用户登录及其流程 小程序可以通过微信官方提供的登录能力,便能方便的获取微信提供的用户身份标识,达到建立用户体系的作用。 官方文档提供了登录流程时序图,如下: 从上述的登录流程时序图中我们发现,这里总共涉及到三个概念。 第一个是小程序,小程序即我们…

C#,入门教程(30)——扎好程序的笼子,错误处理 try catch

上一篇: C#,入门教程(29)——修饰词静态(static)的用法详解https://blog.csdn.net/beijinghorn/article/details/124683349 程序员语录:凡程序必有错,凡有错未必改! 程序出错的原因千千万&…

Rockchip linux USB 驱动开发

Linux USB 驱动架构 Linux USB 协议栈是一个分层的架构,如下图 5-1 所示,左边是 USB Device 驱动,右边是 USB Host 驱动,最底层是 Rockchip 系列芯片不同 USB 控制器和 PHY 的驱动。 Linux USB 驱动架构 USB PHY 驱动开发 USB 2…

LeetCode 77. 组合

77. 组合 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 示例 1: 输入:n 4, k 2 输出: [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ] 示例 2: 输入:…

Kafka(八)使用Kafka构建数据管道

目录 1 使用场景2 构建数据管道时需要考虑的问题2.1 及时性2.2 可靠性高可用可靠性数据传递 2.3 高吞吐量2.4 数据格式2.5 转换ETLELT 2.6 安全性2.7 故障处理2.8 耦合性和灵活性临时数据管道元数据丢失末端处理 3 使用Connect API3.1 Connect的数据处理流程sourcesinkconnecto…

csrf漏洞之DedeCMS靶场漏洞(超详细)

1.Csrf漏洞: 第一步,查找插入点,我选择了网站栏目管理的先秦两汉作为插入点 第二步修改栏目名称 第三步用burp拦截包 第四步生成php脚本代码 第五步点击submit 第六步提交显示修改成功 第二个csrf 步骤与上述类似 红色边框里面的数字会随着…

第91讲:MySQL主从复制集群主库与从库状态信息的含义

文章目录 1.主从复制集群正常状态信息2.从库状态信息中重要参数的含义 1.主从复制集群正常状态信息 通过以下命令查看主库的状态信息。 mysql> show processlist;在主库中查询当前数据库中的进程,看到Master has sent all binlog to slave; waiting for more u…

Rocky Linux 8.9 安装图解

风险告知 本人及本篇博文不为任何人及任何行为的任何风险承担责任,图解仅供参考,请悉知!本次安装图解是在一个全新的演示环境下进行的,演示环境中没有任何有价值的数据,但这并不代表摆在你面前的环境也是如此。生产环境…

2023 IoTDB Summit:北京城建智控科技股份有限公司高级研发主管刘喆《IoTDB在城市轨道交通综合监控系统中的应用》...

12 月 3 日,2023 IoTDB 用户大会在北京成功举行,收获强烈反响。本次峰会汇集了超 20 位大咖嘉宾带来工业互联网行业、技术、应用方向的精彩议题,多位学术泰斗、企业代表、开发者,深度分享了工业物联网时序数据库 IoTDB 的技术创新…

Laykefu客服系统 任意文件上传漏洞复现

0x01 产品简介 Laykefu 是一款基于workerman+gatawayworker+thinkphp5搭建的全功能webim客服系统,旨在帮助企业有效管理和提供优质的客户服务。 0x02 漏洞概述 Laykefu客服系统/admin/users/upavatar.html接口处存在文件上传漏洞,而且当请求中Cookie中的”user_name“不为…

SpringBoot+Email发送邮件

引言 邮件通知是现代应用中常见的一种通信方式,特别是在需要及时反馈、告警或重要事件通知的场景下。Spring Boot提供了简单而强大的邮件发送功能,使得实现邮件通知变得轻而易举。本文将研究如何在Spring Boot中使用JavaMailSender实现邮件发送&#xf…

geemap学习笔记052:影像多项式增强

前言 下面介绍的主要内容是应用Image.polynomial() 对图像进行多项式增强。 1 导入库并显示地图 import ee import geemap ee.Initialize() Map geemap.Map(center[40, -100], zoom4)2 多项式增强 # 使用函数 -0.2 2.4x - 1.2x^2 对 MODIS 影像进行非线性对比度增强。# L…

Oracle Linux 7.9 安装图解

风险告知 本人及本篇博文不为任何人及任何行为的任何风险承担责任,图解仅供参考,请悉知!本次安装图解是在一个全新的演示环境下进行的,演示环境中没有任何有价值的数据,但这并不代表摆在你面前的环境也是如此。生产环境…

画眉(京东科技设计稿转代码平台)介绍

前言 随着金融App业务的不断发展,为了满足不同场景下的用户体验及丰富的业务诉求,业务产品层面最直接体现就是大量新功能的上线及老业务的升级,随之也给研发带来了巨大的压力,所以研发效率的提升就是当前亟需解决的问题&#xff…

赛氪公开课|AI发展的影响与对策

欢迎参赛者们参加中国计算机应用技术大赛,由中国计算机学会主办的中国计算机应用技术大赛于2023年10月至2024年7月举办。为了增强参赛者对计算机领域专业知识与技能的兴趣,提升参赛者解决问题、逻辑思维、自主创新、团队协作等能力。赛氪网特别邀请了人工…

【驾照C1】-海淀驾校-科目三(聂各庄东路)-点位

写在前面 本文记录一下在海淀驾校(北京)考C1驾照时,科目三(聂各庄东路)的点位。科目三的点位以及记忆点比较多,不梳理出来套路真容易忘记。如果你和我的考点不一样,你也可以按照文中的这种思路梳…

事件循环机制

初认识: javaScript事件循环进制是一个用于处理异步事件的回调函数的机制。是JavaScript实现异步编程的核心。 用于管理任务队列和调用栈,以及在适当的时候执行回调函数。可以监听消息队列中的事件,并根据事件处理的优先级顺序来依次执行相…

渐开线齿轮计算软件开发Python

从0开始开发计算软件,欢迎大家加入 源代码仓库

深入剖析:Kafka流数据处理引擎的核心面试问题解析75问(5.7万字参考答案)

Kafka 是一款开源的分布式流处理平台,被广泛应用于构建实时数据管道、日志聚合、事件驱动的架构等场景。本文将深入探究 Kafka 的基本原理、特点以及其在实际应用中的价值和作用。 Kafka 的基本原理是建立在发布-订阅模式之上的。生产者将消息发布到主题&#xff08…

三天吃透Java集合面试八股文

内容摘自我的学习网站:topjavaer.cn 常见的集合有哪些? Java集合类主要由两个接口Collection和Map派生出来的,Collection有三个子接口:List、Set、Queue。 Java集合框架图如下: List代表了有序可重复集合&#xff0c…