一、本文介绍
本文记录的是基于MobileNet V3的YOLOv9目标检测轻量化改进方法研究。MobileNet V3
的模型结构是通过网络搜索得来的,其中的基础模块结合了MobileNet V1
的深度可分离卷积、MobileNet V2
的线性瓶颈和倒置残差结构以及MnasNet
中基于挤压和激励的轻量级注意力模块,使模型在性能、效率和灵活性方面都具有显著的优势。
模型 | 参数量 | 计算量 | 推理速度(bs=32) |
---|---|---|---|
YOLOv9-c | 50.69M | 236.6GFLOPs | 32.1ms |
Improved | 42.05M | 192.3GFLOPs | 28.1ms |
文章目录
二、MoblieNet V3设计原理
MobileNet V3
是基于一系列互补的搜索技术和新颖的架构设计而提出的新一代神经网络模型,其设计的原理和优势主要包括以下几个方面:
- 原理:
- 网络搜索:
- 平台感知的NAS(Platform - Aware NAS):用于搜索全局网络结构,通过优化每个网络块来实现。对于大型移动模型,复用了
MnasNet - A1
的结构,并在此基础上应用NetAdapt
和其他优化。对于小型移动模型,观察到原奖励设计未针对其优化,因此调整了权重因子w,重新进行架构搜索以找到初始种子模型。 - NetAdapt:用于逐层搜索过滤器的数量,是对平台感知的NAS的补充。它从平台感知的NAS找到的种子网络架构开始,通过生成新的提案并根据某些指标选择最佳提案,逐步微调单个层,直到达到目标延迟。在选择提案时,修改了算法以最小化延迟变化和准确率变化的比率。
- 平台感知的NAS(Platform - Aware NAS):用于搜索全局网络结构,通过优化每个网络块来实现。对于大型移动模型,复用了
- 网络改进:
- 重新设计昂贵层:对网络末尾和开头的一些昂贵层进行修改。对于末尾的层,将产生最终特征的层移动到最终平均池化之后,以降低延迟并保持高维特征,同时去除了之前瓶颈层中的投影和过滤层,进一步降低计算复杂度。对于初始的滤波器层,实验发现使用hard swish非线性函数并将滤波器数量减少到16时,能在保持准确率的同时减少延迟和计算量。
- 非线性函数:引入了名为
h-swish
的非线性函数,它是swish
非线性函数的改进版本,计算更快且更有利于量化。通过将sigmoid函数
替换为分段线性的hard版本(如h - swish [x] = x * ReLU6(x + 3) / 6),并在网络的后半部分使用h-swish
,减少了计算成本,同时在准确率上与原始版本没有明显差异。 - 大的挤压 - 激励(Large squeeze - and - excite):将挤压 - 激励瓶颈的大小固定为扩展层通道数的1 / 4,在增加少量参数的情况下提高了准确率,且没有明显的延迟成本。
- 高效的移动构建块:结合了
MobileNet V1
的深度可分离卷积、MobileNet V2
的线性瓶颈和倒置残差结构以及MnasNet
中基于挤压和激励的轻量级注意力模块,同时升级了这些层,使用修改后的swish非线性函数以提高效率。
- 网络搜索:
-
优势:
MobileNet V3
通过网络搜索和改进,结合了多种技术的优势,在性能、效率和灵活性方面都具有显著的优势,适用于移动设备上的各种计算机视觉任务。并且定义了MobileNetV3 - Large
和MobileNetV3 - Small
两个模型,分别针对高资源和低资源使用场景,可根据不同需求进行选择和应用。
论文:https://arxiv.org/abs/2211.12905
源码:https://github.com/huawei-noah/Efficient-AI-Backbones/tree/master/ghostnetv2_pytorch
三、GhostModuleV2模块的实现代码
GhostModuleV2模块
的实现代码如下:
class h_sigmoid(nn.Module):
def __init__(self, inplace=True):
super(h_sigmoid, self).__init__()
self.relu = nn.ReLU6(inplace=inplace)
def forward(self, x):
return self.relu(x + 3) / 6
class h_swish(nn.Module):
def __init__(self, inplace=True):
super(h_swish, self).__init__()
self.sigmoid = h_sigmoid(inplace=inplace)
def forward(self, x):
return x * self.sigmoid(x)
class SELayer(nn.Module):
def __init__(self, channel, reduction=4):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel),
h_sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x)
y = y.view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y
class conv_bn_hswish(nn.Module):
def __init__(self, c1, c2, stride):
super(conv_bn_hswish, self).__init__()
self.conv = nn.Conv2d(c1, c2, 3, stride, 1, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = h_swish()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def fuseforward(self, x):
return self.act(self.conv(x))
class MobileNet_Block(nn.Module):
def __init__(self, inp, oup, hidden_dim, kernel_size, stride, use_se, use_hs):
super(MobileNet_Block, self).__init__()
assert stride in [1, 2]
self.identity = stride == 1 and inp == oup
if inp == hidden_dim:
self.conv = nn.Sequential(
# dw
nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim,
bias=False),
nn.BatchNorm2d(hidden_dim),
h_swish() if use_hs else nn.ReLU(inplace=True),
# Squeeze-and-Excite
SELayer(hidden_dim) if use_se else nn.Sequential(),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
else:
self.conv = nn.Sequential(
# pw
nn.Conv2d(inp, hidden_dim, 1, 1, 0, bias=False),
nn.BatchNorm2d(hidden_dim),
h_swish() if use_hs else nn.ReLU(inplace=True),
# dw
nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride, (kernel_size - 1) // 2, groups=hidden_dim,
bias=False),
nn.BatchNorm2d(hidden_dim),
# Squeeze-and-Excite
SELayer(hidden_dim) if use_se else nn.Sequential(),
h_swish() if use_hs else nn.ReLU(inplace=True),
# pw-linear
nn.Conv2d(hidden_dim, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
y = self.conv(x)
if self.identity:
return x + y
else:
return y
四、添加步骤
4.1 修改common.py
此处需要修改的文件是models/common.py
common.py中定义了网络结构的通用模块
,我们想要加入新的模块就只需要将模块代码放到这个文件内即可。
此时需要将上方实现的代码添加到common.py
中。
注意❗:在4.2小节
中的yolo.py
文件中需要声明的模块名称为:conv_bn_hswish
和MobileNet_Block
。
4.2 修改yolo.py
此处需要修改的文件是models/yolo.py
yolo.py用于函数调用
,我们只需要将common.py
中定义的新的模块名添加到parse_model函数
下即可。
conv_bn_hswish
和MobileNet_Block
模块添加后如下:
五、yaml模型文件
5.1 模型改进⭐
在代码配置完成后,配置模型的YAML文件。
此处以models/detect/yolov9-c.yaml
为例,在同目录下创建一个用于自己数据集训练的模型文件yolov9-c-mobilenetv3 .yaml
。
将yolov9-c.yaml
中的内容复制到yolov9-c-mobilenetv3 .yaml
文件下,修改nc
数量等于自己数据中目标的数量。
📌 模型的修改方法是将骨干网络中的所有RepNCSPELAN4模块
替换成MobileNet_Block模块
,搭建MobileNetV3 - Small
模型,并将其替换YOLOv9
的骨干网络,相比原有的骨干网络,这可以大大减少模型的计算负担,提高推理速度,并且减小后的模型,使其在部署应用方面更加方便。
结构如下:
# YOLOv9
# parameters
nc: 1 # number of classes
depth_multiple: 1.0 # model depth multiple
width_multiple: 1.0 # layer channel multiple
#activation: nn.LeakyReLU(0.1)
#activation: nn.ReLU()
# anchors
anchors: 3
# YOLOv9 backbone
backbone:
[
[-1, 1, Silence, []],
# conv down
# [-1, 1, Conv, [64, 3, 2]], # 1-P1/2
# conv down
[-1, 1, conv_bn_hswish, [16, 2]], # 1-P1/2
# elan-1 block
[-1, 1, MobileNet_Block, [16, 16, 3, 2, 1, 0]], # 2-P2/4
# avg-conv down
[-1, 1, MobileNet_Block, [24, 72, 3, 2, 0, 0]], # 3-P3/8
# elan-2 block
[-1, 1, MobileNet_Block, [24, 88, 3, 1, 0, 0]], # 4
# avg-conv down
[-1, 1, MobileNet_Block, [40, 96, 5, 2, 1, 1]], # 5-P4/16
# elan-2 block
[-1, 1, MobileNet_Block, [40, 240, 5, 1, 1, 1]], # 6
[-1, 1, MobileNet_Block, [40, 240, 5, 1, 1, 1]], # 7
[-1, 1, MobileNet_Block, [48, 120, 5, 1, 1, 1]], # 8
[-1, 1, MobileNet_Block, [48, 144, 5, 1, 1, 1]], # 9
# avg-conv down
[-1, 1, MobileNet_Block, [96, 288, 5, 2, 1, 1]], # 10-P5/32
# elan-2 block
[-1, 1, MobileNet_Block, [96, 576, 5, 1, 1, 1]], # 11
[-1, 1, MobileNet_Block, [96, 576, 5, 1, 1, 1]], # 12
]
# YOLOv9 head
head:
[
# elan-spp block
[-1, 1, SPPELAN, [512, 256]], # 10
# up-concat merge
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 9], 1, Concat, [1]], # cat backbone P4
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 512, 256, 1]], # 13
# up-concat merge
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
# elan-2 block
[-1, 1, RepNCSPELAN4, [256, 256, 128, 1]], # 16 (P3/8-small)
# avg-conv-down merge
[-1, 1, ADown, [256]],
[[-1, 16], 1, Concat, [1]], # cat head P4
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 512, 256, 1]], # 19 (P4/16-medium)
# avg-conv-down merge
[-1, 1, ADown, [512]],
[[-1, 13], 1, Concat, [1]], # cat head P5
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 512, 256, 1]], # 22 (P5/32-large)
# multi-level reversible auxiliary branch
# routing
[4, 1, CBLinear, [[256]]], # 23
[9, 1, CBLinear, [[256, 512]]], # 24
[12, 1, CBLinear, [[256, 512, 512]]], # 25
# conv down
[0, 1, Conv, [64, 3, 2]], # 26-P1/2
# conv down
[-1, 1, Conv, [128, 3, 2]], # 27-P2/4
# elan-1 block
[-1, 1, RepNCSPELAN4, [256, 128, 64, 1]], # 28
# avg-conv down fuse
[-1, 1, ADown, [256]], # 29-P3/8
[[26, 27, 28, -1], 1, CBFuse, [[0, 0, 0]]], # 30
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 256, 128, 1]], # 31
# avg-conv down fuse
[-1, 1, ADown, [512]], # 32-P4/16
[[27, 28, -1], 1, CBFuse, [[1, 1]]], # 33
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 512, 256, 1]], # 34
# avg-conv down fuse
[-1, 1, ADown, [512]], # 35-P5/32
[[28, -1], 1, CBFuse, [[2]]], # 36
# elan-2 block
[-1, 1, RepNCSPELAN4, [512, 512, 256, 1]], # 37
# detection head
# detect
[[34, 37, 40, 19, 22, 25], 1, DualDDetect, [nc]], # DualDDetect(A3, A4, A5, P3, P4, P5)
]
六、成功运行结果
分别打印网络模型可以看到MobileNet_Block
已经加入到模型中,并可以进行训练了。
yolov9-c-mobilenetv3:
from n params module arguments
0 -1 1 0 models.common.Silence []
1 -1 1 464 models.common.conv_bn_hswish [3, 16, 2]
2 -1 1 612 models.common.MobileNet_Block [16, 16, 16, 3, 2, 1, 0]
3 -1 1 3864 models.common.MobileNet_Block [16, 24, 72, 3, 2, 0, 0]
4 -1 1 5416 models.common.MobileNet_Block [24, 24, 88, 3, 1, 0, 0]
5 -1 1 13736 models.common.MobileNet_Block [24, 40, 96, 5, 2, 1, 1]
6 -1 1 55340 models.common.MobileNet_Block [40, 40, 240, 5, 1, 1, 1]
7 -1 1 55340 models.common.MobileNet_Block [40, 40, 240, 5, 1, 1, 1]
8 -1 1 21486 models.common.MobileNet_Block [40, 48, 120, 5, 1, 1, 1]
9 -1 1 28644 models.common.MobileNet_Block [48, 48, 144, 5, 1, 1, 1]
10 -1 1 91848 models.common.MobileNet_Block [48, 96, 288, 5, 2, 1, 1]
11 -1 1 294096 models.common.MobileNet_Block [96, 96, 576, 5, 1, 1, 1]
12 -1 1 294096 models.common.MobileNet_Block [96, 96, 576, 5, 1, 1, 1]
13 -1 1 550400 models.common.SPPELAN [96, 512, 256]
14 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
15 [-1, 9] 1 0 models.common.Concat [1]
16 -1 1 2882048 models.common.RepNCSPELAN4 [560, 512, 512, 256, 1]
17 -1 1 0 torch.nn.modules.upsampling.Upsample [None, 2, 'nearest']
18 [-1, 4] 1 0 models.common.Concat [1]
19 -1 1 787712 models.common.RepNCSPELAN4 [536, 256, 256, 128, 1]
20 -1 1 164352 models.common.ADown [256, 256]
21 [-1, 16] 1 0 models.common.Concat [1]
22 -1 1 2988544 models.common.RepNCSPELAN4 [768, 512, 512, 256, 1]
23 -1 1 656384 models.common.ADown [512, 512]
24 [-1, 13] 1 0 models.common.Concat [1]
25 -1 1 3119616 models.common.RepNCSPELAN4 [1024, 512, 512, 256, 1]
26 4 1 6400 models.common.CBLinear [24, [256]]
27 9 1 37632 models.common.CBLinear [48, [256, 512]]
28 12 1 124160 models.common.CBLinear [96, [256, 512, 512]]
29 0 1 1856 models.common.Conv [3, 64, 3, 2]
30 -1 1 73984 models.common.Conv [64, 128, 3, 2]
31 -1 1 212864 models.common.RepNCSPELAN4 [128, 256, 128, 64, 1]
32 -1 1 164352 models.common.ADown [256, 256]
33 [26, 27, 28, -1] 1 0 models.common.CBFuse [[0, 0, 0]]
34 -1 1 847616 models.common.RepNCSPELAN4 [256, 512, 256, 128, 1]
35 -1 1 656384 models.common.ADown [512, 512]
36 [27, 28, -1] 1 0 models.common.CBFuse [[1, 1]]
37 -1 1 2857472 models.common.RepNCSPELAN4 [512, 512, 512, 256, 1]
38 -1 1 656384 models.common.ADown [512, 512]
39 [28, -1] 1 0 models.common.CBFuse [[2]]
40 -1 1 2857472 models.common.RepNCSPELAN4 [512, 512, 512, 256, 1]
41[34, 37, 40, 19, 22, 25] 1 21542822 models.yolo.DualDDetect [1, [512, 512, 512, 256, 512, 512]]
yolov9-c-mobilenetv3 summary: 902 layers, 42053396 parameters, 42053364 gradients, 192.3 GFLOPs