MobileV3网络结构理解+网络结构代码实现+水果图像分类和安全帽目标检测(YoloV5)效果展示.
1、博文说明以及论文链接
小编最近要进行开题了,我所做的研究需要用到MobileNet系列的神经网络,为了便于我在开题中多写一些内容以及方便我毕业答辩的时候进行知识回顾,所以趁这次机会,再好好看看MobileNet的论文,并进行总结。该篇博文是介绍MobileNetV3。
要了解MobileNetV1请看这篇博文:MobileV1网络结构理解+网络结构代码实现+图像分类和目标检测(YoloV5)效果展示.
要了解MobileNetV2请看这篇博文:MobileV2网络结构理解+网络结构代码实现+水果图像分类和安全帽目标检测(YoloV5)效果展示.
MobileNetV3的论文链接:Searching for MobileNetV3
2、MobileNetV3
2.1 简介
MobileNetV3是mobileNet系列的第三个版本,也是最后一个版本。V3版本保留了V1版本的深度可分离卷积的思想以及V2版本的反向残差结构,然后改变了其中的一些激活函数,以便让网络在移动设备和嵌入式设备上跑的更快,另外还加入了SE通道注意力机制,保证网络的准确性。
其实论文读下来,个人感觉MobileNet的研究人员变懒了,因为V3版本的网络结构一开始不是自己设计的,而是借用NetAdapt进行搭建的,然后将其中一些比较耗时的层以及结构进行修改而来。然后就是加入SE注意力机制和更换激活函数。当然,这里只是我的个人感觉。
所以只针对V3,其实就只需要了解SE注意力机制和两个激活函数就可以了。
SE模块(Squeeze-and-Excitation):是一种用于增强神经网络对输入特征的关注度的机制,旨在提高网络的性能。主要包含两个步骤:Squeeze(压缩)和Excitation(激发)。
H-Swish激活函数(Hard-Swish):Hard Swish是对Swish激活函数的一种修改,以在计算上更加轻量级,适用于移动设备和嵌入式设备的推理。使用h-swish代替Relu6。
H-sigmoid激活函数(Hard-Sigmoid):是一种硬性截断的Sigmoid函数,通常用于深度神经网络中,特别是在轻量级模型的设计中。H-sigmoid可以看作是对标准Sigmoid函数的一种简化形式,H-sigmoid的计算形式更加简单,因为它使用ReLU6的硬性截断,将输入x限制在 [-3, 3] 的范围内。这使得它在计算上更加轻量级,适合于移动设备和嵌入式设备的推理阶段。
2.2 SE模块(Squeeze-and-Excitation)介绍
SE注意力机制是主要针对通道而言的,在特征通道上加入注意力机制。这中注意力机制一共有两步操作,即压缩(Squeeze)和激发(Excitation)。这里的压缩操作时将特征图进行全局池化,得出一个实数,这个实数是这一个通道的特征图而来的,所以可以认为它有该特征图的全局感受野,加入说特征图是C个通道,那么压缩之后就变成了1x1xC的一个一维向量。激活操作其实就是给这个一维向量上乘上一个权重。
SE注意力机制在V3的运用如下图所示,变得更简单了,没有乘上权重这一操作。V3中SE注意力机制压缩后经过了两个全连接的操作,第一个全连接层按照论文的说法,使用Relu激活函数将向量缩短到原来的1/4,第二个全连接层使用H-sigmoid激活函数将其扩展为原来的长度。然后与通道数相同的特征图进行相乘。
2.3 H-Swish激活函数(Hard-Swish)介绍
按照论文里面的说法,H-Swish激活函数更加适用于移动设备使用,能减少内存访问次数,降低延时。
2.4 H-sigmoid激活函数(Hard-Sigmoid)介绍
H-Swish激活函数创造出来了,所以干脆sigmoid函数也进行改写,同样也是为了更加符合硬件的运算逻辑。
3、网络结构代码实现
3.1 H-Swish激活函数
直接按照上面的公式敲出代码就可以了。
# 定义hswish激活函数
class hswish(nn.Module):
def __init__(self, inplace=True):
super(hswish, self).__init__()
self.inplace = inplace
def forward(self, x):
f = nn.functional.relu6(x + 3., inplace=self.inplace) / 6.
return x * f
3.2 H-sigmoid激活函数
直接按照上面的公式敲出代码就可以了。
# 定义hsigmoid激活函数
class hsigmoid(nn.Module):
def __init__(self, inplace=True):
super(hsigmoid, self).__init__()
self.inplace = inplace
def forward(self, x):
f = nn.functional.relu6(x + 3., inplace=self.inplace) / 6.
return f
3.3 SE模块
SE模块第一步是全局池化;第二步是通过1x1卷积把通道变为原来的1/4,这一步中使用的时Relu激活函数;第三步是通过1x1卷积把通道还原成原来的通道个数,这一步使用的是H-sigmoid激活函数;第四步就是与我们的特征图做乘法。
# 定义Squeeze-and-Excitation模块
# 定义Squeeze-and-Excitation模块
class SqueezeExcitation(nn.Module):
def __init__(self, in_channel, out_channel, reduction=4):
super(SqueezeExcitation, self).__init__()
# 计算第一个全连接层的输出通道数
squeeze_c = in_channel // reduction
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Conv2d(in_channel, squeeze_c, kernel_size=1, stride=1)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(squeeze_c, out_channel, kernel_size=1, stride=1)
self.sigmoid = hsigmoid()
def forward(self, x):
out = self.pool(x)
out = self.fc1(out)
out = self.relu(out)
out = self.fc2(out)
out = self.sigmoid(out)
return out
3.4 定义我们的使用H-Swish激活函数卷积块
# 定义卷积块
class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, groups=1):
super(ConvBlock, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, groups=groups, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.act = hswish()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
3.5 定义我们新的反向残差模块
# 定义反向残差块
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, use_se=True):
super(ResidualBlock, self).__init__()
self.conv1 = ConvBlock(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=kernel_size//2)
self.conv2 = ConvBlock(in_channels=out_channels, out_channels=out_channels, kernel_size=kernel_size, stride=1, padding=kernel_size//2)
# 定义是否使用SE模块
self.use_se = use_se
if use_se:
self.se = SqueezeExcitation(out_channels, out_channels)
# 定义捷径分支
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
if self.use_se:
out = out * self.se(out)
out += self.shortcut(x)
out = nn.functional.relu(out, inplace=True)
return out
3.6 定义MobileNetV3Small网络结构
论文中的网络结构图如下:
# 定义MobileNetV3Small模型
class MobileNetV3Small(nn.Module):
def __init__(self, num_classes):
super(MobileNetV3Small, self).__init__()
self.conv1 = ConvBlock(3, 16, 3, 2, 1) # 1/2
self.bottlenecks = nn.Sequential(
ResidualBlock(16, 16, 3, 2, False), # 1/4
ResidualBlock(16, 72, 3, 2, False), # 1/8
ResidualBlock(72, 72, 3, 1, False),
ResidualBlock(72, 72, 3, 1, True),
ResidualBlock(72, 96, 3, 2, True), # 1/16
ResidualBlock(96, 96, 3, 1, True),
ResidualBlock(96, 96, 3, 1, True),
ResidualBlock(96, 240, 5, 2, True), # 1/32
ResidualBlock(240, 240, 5, 1, True),
ResidualBlock(240, 240, 5, 1, True),
ResidualBlock(240, 480, 5, 1, True),
ResidualBlock(480, 480, 5, 1, True),
ResidualBlock(480, 480, 5, 1, True),
)
self.conv2 = ConvBlock(480, 576, 1, 1, 0, groups=2)
self.conv3 = nn.Conv2d(576, 1024, kernel_size=1, stride=1, padding=0, bias=False)
self.bn = nn.BatchNorm2d(1024)
self.act = hswish()
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(1024, num_classes)
def forward(self, x):
out = self.conv1(x)
out = self.bottlenecks(out)
out = self.conv2(out)
out = self.conv3(out)
out = self.bn(out)
out = self.act(out)
out = self.pool(out)
out = nn.Flatten()
out = self.fc(out)
return out
3.7 model.py文件完整代码以及运行效果
import torch
import torch.nn as nn
from torch.autograd import Variable
from torch.utils.data import DataLoader # 添加的导入语句
from torchvision.transforms import transforms # 添加的导入语句
from torchsummary import summary
# 定义hswish激活函数
class hswish(nn.Module):
def __init__(self, inplace=True):
super(hswish, self).__init__()
self.inplace = inplace
def forward(self, x):
f = nn.functional.relu6(x + 3., inplace=self.inplace) / 6.
return x * f
# 定义hsigmoid激活函数
class hsigmoid(nn.Module):
def __init__(self, inplace=True):
super(hsigmoid, self).__init__()
self.inplace = inplace
def forward(self, x):
f = nn.functional.relu6(x + 3., inplace=self.inplace) / 6.
return f
# 定义Squeeze-and-Excitation模块
class SqueezeExcitation(nn.Module):
def __init__(self, in_channel, out_channel, reduction=4):
super(SqueezeExcitation, self).__init__()
# 计算第一个全连接层的输出通道数
squeeze_c = in_channel // reduction
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc1 = nn.Conv2d(in_channel, squeeze_c, kernel_size=1, stride=1)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(squeeze_c, out_channel, kernel_size=1, stride=1)
self.sigmoid = hsigmoid()
def forward(self, x):
out = self.pool(x)
out = self.fc1(out)
out = self.relu(out)
out = self.fc2(out)
out = self.sigmoid(out)
return out
# 定义卷积块
class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding, groups=1):
super(ConvBlock, self).__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, groups=groups, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.act = hswish()
def forward(self, x):
return self.act(self.bn(self.conv(x)))
# 定义反向残差块
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, use_se=True):
super(ResidualBlock, self).__init__()
self.conv1 = ConvBlock(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=kernel_size//2)
self.conv2 = ConvBlock(in_channels=out_channels, out_channels=out_channels, kernel_size=kernel_size, stride=1, padding=kernel_size//2)
# 定义是否使用SE模块
self.use_se = use_se
if use_se:
self.se = SqueezeExcitation(out_channels, out_channels)
# 定义捷径分支
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = self.conv1(x)
out = self.conv2(out)
if self.use_se:
out = out * self.se(out)
out += self.shortcut(x)
out = nn.functional.relu(out, inplace=True)
return out
# 定义MobileNetV3Small模型
class MobileNetV3Small(nn.Module):
def __init__(self, num_classes):
super(MobileNetV3Small, self).__init__()
self.conv1 = ConvBlock(3, 16, 3, 2, 1) # 1/2
self.bottlenecks = nn.Sequential(
ResidualBlock(16, 16, 3, 2, False), # 1/4
ResidualBlock(16, 72, 3, 2, False), # 1/8
ResidualBlock(72, 72, 3, 1, False),
ResidualBlock(72, 72, 3, 1, True),
ResidualBlock(72, 96, 3, 2, True), # 1/16
ResidualBlock(96, 96, 3, 1, True),
ResidualBlock(96, 96, 3, 1, True),
ResidualBlock(96, 240, 5, 2, True), # 1/32
ResidualBlock(240, 240, 5, 1, True),
ResidualBlock(240, 240, 5, 1, True),
ResidualBlock(240, 480, 5, 1, True),
ResidualBlock(480, 480, 5, 1, True),
ResidualBlock(480, 480, 5, 1, True),
)
self.conv2 = ConvBlock(480, 576, 1, 1, 0, groups=2)
self.conv3 = nn.Conv2d(576, 1024, kernel_size=1, stride=1, padding=0, bias=False)
self.bn = nn.BatchNorm2d(1024)
self.act = hswish()
self.pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(1024, num_classes)
def forward(self, x):
out = self.conv1(x)
out = self.bottlenecks(out)
out = self.conv2(out)
out = self.conv3(out)
out = self.bn(out)
out = self.act(out)
out = self.pool(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out
if __name__ == "__main__":
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MobileNetV3Small(5).to(device)
print(summary(model, (3, 224, 224)))
与MobileNetV1相比,参数多了接近100万。与MobileNetV2相比,参数多了接近200万。
4、MobileNetV3水果分类效果展示
一共跑了50轮,验证集准确率最高在89.8%,比MobileNetV1的89.5%高出了0.3个百分点,比MobileNetV1的89.5%高出了0.3个百分点,比MobileNetV2的90.5%低出了0.7个百分点。emmm,我也不知道怎么解释了。
5、基于MobileNetV3的Yolo安全帽目标检测
将mobileNetV3替换为yolo5的网络骨干后,对“炮哥带你学”博客中的安全帽数据集进行训练,预训练参数选择的是yolov5s。如果想要数据集可以看我另外一篇博文:复现炮哥带你学—Yolo5训练安全帽(vscode + pytorch)报错总结,数据库链接+权重文件链接
跑出来的效果如下:可以看到,我只训练了30轮,人和安全帽的准确率大概在89%,MAP最高时0.83,比MobileNetV2的效果要好上许多。但是参数文件的大小要大一倍,MobileNetV2的参数文件大小是由4.9MB。
推理时间为156ms,并没有比MobileNetV2要快。
6、总结
写这篇博文的原因其实是小编要开题汇报了,可能需要用到mobileNet系列的网络结构,为了以后方便写什么东西水水字数,决定写博文记录一下,顺便也看看自己到底对mobileNet掌握多少。本人也是一名深度学习的小白,如果文章中有上面很明显的错误,请留言共同学习。同时希望这篇博文能帮助到你。