前言
在计算机视觉领域,高效且准确的卷积神经网络一直是研究的重点。随着移动设备和边缘计算的快速发展,对轻量级神经网络的需求日益增长。MobileNet系列作为轻量级网络的代表,不断迭代以满足在有限计算资源下实现高效图像分类等任务的需求。MobileNetV3作为该系列的最新版本,于2019年由Google研究院在ICCV上发表,它在性能上相较于前作有了显著提升,提出了large和small两个版本以适应不同场景。本文将深入探讨MobileNetV3的网络背景、创新点、存在的问题、网络结构,并给出详细的代码实现,帮助读者全面了解这一优秀的轻量级卷积神经网络。
MobileNetV3
一、网络背景
- 发表信息:2019 年 Google 研究院在韩国首尔举行的 ICCV(国际计算机视觉大会)上发表。
- 版本情况:提出了 large 和 small 两个版本,区别在于网络结构不同。
- 性能优势
- MobileNetV3 Large 在 ImageNet 分类任务上,较 MobileNetV2,TOP1 准确率提高约 3.2%,时间减少 20%。
- 与具有同等延迟的 MobileNetV2 模型相比,MobileNetV3 Small 的准确率高 6.6%。
- TOP1 含义:得到的识别结果中可能性最高的那个预测值与真实值的准确率。
- 论文下载:Searching for MobileNetV3.pdf
二、网络的创新
2.1 Block创新
对Block的结构进行了创新,具体操作如下:
- 在Block中加入SE模块(通道注意力机制)。
- 更新了激活函数。
2.1.1 加入SE(Squeeze-and-Excitation (挤压激励”或“压缩激励)
2.1.1.1 MobileNetV2倒残差结构
- 升维阶段:通过一个1x1卷积层进行升维处理,卷积后依次连接批量归一化(BN)层和ReLU6激活函数。
- 深度可分离卷积阶段:使用3x3大小的深度可分离(DW)卷积,卷积后同样跟有BN层和ReLU6激活函数。
- 降维阶段:最后一个1x1卷积层起到降维作用,卷积后仅跟有BN结构,未使用ReLU6激活函数。
- 捷径连接(shortcut):当步长(stride)为1且输入特征矩阵与输出特征矩阵相同时,采用shortcut结构。
2.1.1.2 MobileNetV3倒残差结构
在MobileNetV3的某些层中(非全部),在进行最后一个1x1卷积之前,会加入一个SE(Squeeze-and-Excitation,挤压激励或压缩激励)模块注意力机制。
2.1.1.3 SE模块详细介绍
2.1.1.3.1 原理与作用
SE模块是一种通道注意力机制,旨在对特征矩阵的通道进行特征注意。其核心作用是针对得到的特征矩阵,为每个通道分析出一个权重关系,将更重要的通道赋予更大的权重值,不那么重要的通道赋予较小的权重值。
2.1.1.3.2 具体操作步骤
- 池化处理:对特征矩阵的每一个通道进行池化(通常为平均池化)操作。特征矩阵有多少个通道,池化后得到的一维向量就有多少个元素。
- 第一次全连接层:将池化后的一维向量输入到第一个全连接层,该全连接层的节点个数为特征矩阵通道数的1/4,激活函数为ReLU。
- 第二次全连接层:将第一次全连接层的输出输入到第二个全连接层,其节点个数与特征矩阵的通道数相同,激活函数为h - sigmoid。
- 权重相乘:将第二次全连接层输出的向量与原始特征矩阵的每一个元素对应相乘,完成SE模块的功能。
2.1.1.4 示例说明
以一个简单的例子进行说明,假设Conv1是DW卷积后得到的特征矩阵,共有4个通道。对每个通道进行平均池化操作得到一个向量,经过第一次全连接层(节点个数为1,即4个通道的1/4,激活函数为ReLU)和第二次全连接层(节点个数为4,激活函数为h - sigmoid)后,得到一个长度为4的向量。将该向量的元素与Conv1的每个元素对应相乘,如Conv1左上角元素0.2与第一个通道的系数0.5相乘,得到Conv2左上角元素0.1,依此类推。
2.1.2 新的激活函数
在新的 Block 结构中更新了激活函数,用“NL”表示非线性激活函数。因不同层使用不同激活函数,具体每层所用激活函数会在网络结构中指出。
- MobileNetV2:几乎都使用 Relu6 激活函数。
- 部分网络:使用 swish x 激活函数,其公式为
s w i s h x = x ⋅ σ ( x ) swish\ x = x \cdot \sigma(x) swish x=x⋅σ(x)
其中 σ ( x ) \sigma(x) σ(x) 是 sigmoid 激活函数,公式为
s i g m o i d = 1 1 + e − x \mathrm{sigmoid}=\frac{1}{1 + e^{-x}} sigmoid=1+e−x1
该激活函数可提高准确性,但计算和求导复杂。 - MobileNetV3:提出 h - swish[x] 激活函数
公式为
h − s w i s h [ x ] = x ⋅ R e L U 6 ( x + 3 ) 6 \mathrm{h}-\mathrm{swish}[x]=x\cdot\frac{\mathrm{ReLU}6(x + 3)}{6} h−swish[x]=x⋅6ReLU6(x+3)
它是 x 乘以 h - sigmoid 激活函数。从图像看,h - swish[x] 与 swish x 激活函数很相似,且公式更简单,无幂运算,计算速度更快。不过,并非整个模型都使用 h - swish,模型的前一半层使用常规 ReLU。
2.2 使用 NAS 搜索参数
2.2.1 NAS 概述
- 定义:NAS(神经网络架构搜索)是一种自动化设计神经网络结构的技术,核心是通过算法自动优化网络层的类型、参数、连接方式等。
- 目标:自动确定网络的层数、每层类型(如卷积层、池化层)、输入输出通道数、激活函数等参数。
- 实现方式:利用随机搜索、遗传算法、强化学习等算法,在预设的搜索空间中尝试不同结构,通过评估训练/验证性能(如准确率、延迟)筛选最优架构。
2.2.2 NetAdapt 网络适配
- MobileNet v3 使用 NetAdapt 算法对各层的结构进行精调。该过程是在 NAS 搜索得到的网络基础上,对网络的某一层生成一些新的待选结构。
- 筛选步骤:先测试新的结构是否能降低延迟,在此基础上对能降低延迟的结构再测试其对应的模型准确率,最后选择最优的结构。
2.2.3 NAS 与 NetAdapt 的静态搜索本质
- NAS 和 NetAdapt 均通过预先运行算法确定最优网络架构(如层结构、通道数等),再部署到不同平台,属于静态搜索。与之相对,动态搜索指程序运行时实时优化架构。
参考资料:使用 NAS 搜索参数原理部分转载自:进入网站深入学习原理
2.3 重新设计耗时层
Paper中提出减少网络计算耗时的相关内容
- 减少第一个卷积层的卷积核个数:MobileNetV3把第一层卷积的卷积核个数从32个减至16个。调整后准确率不变,但可节省2ms时间。
- 精简Last Stage
- Original Last Stage:由NAS搜索得出,流程为“卷积->卷积->卷积->卷积->池化->卷积输出”,实际使用中该结构较耗时。
- Efficient Last Stage:对Original Last Stage进行精简,在第一个卷积后直接池化,再通过两个卷积层输出。相比原结构节省7ms时间,占整个推理过程的11%。
三、网路存在的问题
在实际使用中,可能存在 MobileNetV3 准确率不如 MobileNetV2 的情况。MobileNetV3 由 Google 研发,该网络更聚焦于在 Android 设备上的表现,在其他设备上运行或许无法实现最佳效果。
四、网络的结构
-
模型说明:paper的Table 1给出了网络结构的图,此处展示的是MobileNetV3 - Large模型,虚拟仿真流程使用该模型,small模型需自行学习。
-
表格参数含义:
- Input:输入当前层的特征矩阵。
- Operator:操作。其中“bneck”为倒残差结构,后面的“3x3”或者“5x5”表示DW卷积的卷积核大小。最后两层有“NBN”参数,“NBN”代表这两层无batchNorm,意味着其它层的Conv2D以及DWConv2D每一层之后都需要跟batchNorm。
- exp size:第一个升维的卷积所升到的维度。
- out:输出矩阵的channel。
- SE:表示这一层bneck是否使用注意力机制,打对号代表使用。
- NL:激活函数的类别,“HS”是h - swish[x]激活函数,“RE”是Relu激活函数。
- s:步距。在bneck结构中,“s”指的是DW卷积的步距,其它层步距依然为1。
-
数据集及类别数:使用的数据集为ImageNet,类别数k为1000。
五、代码
import torch
import torch.nn as nn
# SE模块(Squeeze-and-Excitation模块)
# 该模块用于自适应地调整通道特征的重要性
class SE(nn.Module):
def __init__(self, in_channels):
super(SE, self).__init__()
# 全局平均池化层,将每个通道的特征图压缩成一个值
# 这样可以捕获每个通道的全局信息
self.avg_pool = nn.AdaptiveAvgPool2d(1)
# 第一个全连接层,使用1x1卷积实现
# 将输入通道数压缩为原来的1/4
self.fc1_conv = nn.Conv2d(in_channels=in_channels, out_channels=in_channels // 4, kernel_size=1, bias=False)
# 第一个批量归一化层,用于加速模型收敛和提高稳定性
self.fc1_bn = nn.BatchNorm2d(in_channels // 4)
# 第一个ReLU激活函数,引入非线性
self.fc1_relu = nn.ReLU(inplace=True)
# 第二个全连接层,使用1x1卷积实现
# 将通道数恢复到输入通道数
self.fc2_conv = nn.Conv2d(in_channels=in_channels // 4, out_channels=in_channels, kernel_size=1, bias=False)
# 第二个批量归一化层
self.fc2_bn = nn.BatchNorm2d(in_channels)
# 第二个Hardsigmoid激活函数,输出每个通道的权重
self.fc2_hardsigmoid = nn.Hardsigmoid()
def forward(self, x):
# 通过全局平均池化得到每个通道的全局信息
weight = self.avg_pool(x)
# 通过第一个全连接层、批量归一化层和ReLU激活函数
weight = self.fc1_relu(self.fc1_bn(self.fc1_conv(weight)))
# 通过第二个全连接层、批量归一化层和Hardsigmoid激活函数得到每个通道的权重
weight = self.fc2_hardsigmoid(self.fc2_bn(self.fc2_conv(weight)))
# 将权重与输入特征图逐通道相乘,调整每个通道的重要性
return weight * x
# 瓶颈模块(Bottleneck模块)
# 该模块是MobileNetV3中的基本构建块
class Bottleneck(nn.Module):
def __init__(self, in_channels, kernel_size, exp_size, out, se, nl, s):
super(Bottleneck, self).__init__()
# 扩展卷积层,使用1x1卷积将输入通道数扩展到exp_size
self.exp_conv = nn.Conv2d(in_channels=in_channels, out_channels=exp_size, kernel_size=1)
# 扩展卷积后的批量归一化层
self.exp_bn = nn.BatchNorm2d(exp_size)
# 扩展卷积后的激活函数
# 如果nl为None,则使用恒等映射(即不进行激活)
self.exp_act = nl() if nl is not None else nn.Identity()
# 深度可分离卷积层,对每个通道进行独立卷积
# 可以减少参数数量和计算量
self.depth_wise_conv = nn.Conv2d(
in_channels=exp_size, out_channels=exp_size, kernel_size=kernel_size,
stride=s, padding=kernel_size // 2, groups=exp_size, bias=False
)
# 深度可分离卷积后的批量归一化层
self.depth_wise_bn = nn.BatchNorm2d(exp_size)
# 深度可分离卷积后的激活函数
self.depth_wise_act = nl() if nl is not None else nn.Identity()
# SE模块,如果se为True,则添加SE模块
self.se = SE(in_channels=exp_size) if se else None
# 逐点卷积层,使用1x1卷积将通道数调整为out
self.point_wise_conv = nn.Conv2d(in_channels=exp_size, out_channels=out, kernel_size=1, bias=False)
# 逐点卷积后的批量归一化层
self.point_wise_bn = nn.BatchNorm2d(out)
# 判断是否使用残差连接
# 当步长为1且输入通道数等于输出通道数时,使用残差连接
self.is_res = s == 1 and in_channels == out
def forward(self, x):
# 保存输入特征图,用于残差连接
identity = x
# 通过扩展卷积层、批量归一化层和激活函数
x = self.exp_act(self.exp_bn(self.exp_conv(x)))
# 通过深度可分离卷积层、批量归一化层和激活函数
x = self.depth_wise_act(self.depth_wise_bn(self.depth_wise_conv(x)))
# 如果有SE模块,则通过SE模块调整通道重要性
if self.se is not None:
x = self.se(x)
# 通过逐点卷积层和批量归一化层
x = self.point_wise_bn(self.point_wise_conv(x))
# 如果满足残差连接条件,则将输入特征图与输出特征图相加
if self.is_res:
x = x + identity
return x
# MobileNetV3 Large模型
class MobileNetV3_Large(nn.Module):
def __init__(self, num_classes=1000):
super(MobileNetV3_Large, self).__init__()
# 初始卷积层,将输入的3通道图像转换为16通道特征图
# 步长为2,进行下采样
self.c1_conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=2, padding=1, bias=False)
# 初始卷积后的批量归一化层
self.c1_bn = nn.BatchNorm2d(16)
# 初始卷积后的Hardswish激活函数
self.c1_act = nn.Hardswish()
# Bottleneck Blocks (根据MobileNetV3 Large结构调整参数)
# 第一个瓶颈模块,无SE模块,步长为1,不进行下采样
self.bneck1 = Bottleneck(in_channels=16, kernel_size=3, exp_size=16, out=16, se=False, nl=nn.ReLU, s=1)
# 第二个瓶颈模块,无SE模块,步长为2,进行下采样
self.bneck2 = Bottleneck(in_channels=16, kernel_size=3, exp_size=64, out=24, se=False, nl=nn.ReLU, s=2)
# 第三个瓶颈模块,无SE模块,步长为1,不进行下采样
self.bneck3 = Bottleneck(in_channels=24, kernel_size=3, exp_size=72, out=24, se=False, nl=nn.ReLU, s=1)
# 第四个瓶颈模块,有SE模块,步长为2,进行下采样
self.bneck4 = Bottleneck(in_channels=24, kernel_size=5, exp_size=72, out=40, se=True, nl=nn.ReLU, s=2)
# 第五个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck5 = Bottleneck(in_channels=40, kernel_size=5, exp_size=120, out=40, se=True, nl=nn.ReLU, s=1)
# 第六个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck6 = Bottleneck(in_channels=40, kernel_size=5, exp_size=120, out=40, se=True, nl=nn.ReLU, s=1)
# 第七个瓶颈模块,无SE模块,步长为2,进行下采样
self.bneck7 = Bottleneck(in_channels=40, kernel_size=3, exp_size=240, out=80, se=False, nl=nn.Hardswish, s=2)
# 第八个瓶颈模块,无SE模块,步长为1,不进行下采样
self.bneck8 = Bottleneck(in_channels=80, kernel_size=3, exp_size=200, out=80, se=False, nl=nn.Hardswish, s=1)
# 第九个瓶颈模块,无SE模块,步长为1,不进行下采样
self.bneck9 = Bottleneck(in_channels=80, kernel_size=3, exp_size=184, out=80, se=False, nl=nn.Hardswish, s=1)
# 第十个瓶颈模块,无SE模块,步长为1,不进行下采样
self.bneck10 = Bottleneck(in_channels=80, kernel_size=3, exp_size=184, out=80, se=False, nl=nn.Hardswish, s=1)
# 第十一个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck11 = Bottleneck(in_channels=80, kernel_size=3, exp_size=480, out=112, se=True, nl=nn.Hardswish, s=1)
# 第十二个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck12 = Bottleneck(in_channels=112, kernel_size=3, exp_size=672, out=112, se=True, nl=nn.Hardswish, s=1)
# 第十三个瓶颈模块,有SE模块,步长为2,进行下采样
self.bneck13 = Bottleneck(in_channels=112, kernel_size=5, exp_size=672, out=160, se=True, nl=nn.Hardswish, s=2)
# 第十四个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck14 = Bottleneck(in_channels=160, kernel_size=5, exp_size=960, out=160, se=True, nl=nn.Hardswish, s=1)
# 第十五个瓶颈模块,有SE模块,步长为1,不进行下采样
self.bneck15 = Bottleneck(in_channels=160, kernel_size=5, exp_size=960, out=160, se=True, nl=nn.Hardswish, s=1)
# 最后一个卷积层,将通道数扩展到960
self.c2_conv = nn.Conv2d(in_channels=160, out_channels=960, kernel_size=1, bias=False)
# 最后一个卷积后的批量归一化层
self.c2_bn = nn.BatchNorm2d(960)
# 最后一个卷积后的Hardswish激活函数
self.c2_act = nn.Hardswish()
# 全局平均池化层,将特征图压缩为1x1
self.avg_pool = nn.AdaptiveAvgPool2d(1)
# 分类器的第一个卷积层,将通道数扩展到1280
self.classifier_conv1 = nn.Conv2d(in_channels=960, out_channels=1280, kernel_size=1, bias=False)
# 分类器的第一个Hardswish激活函数
self.classifier_act1 = nn.Hardswish()
# 分类器的第二个卷积层,将通道数调整为num_classes
self.classifier_conv2 = nn.Conv2d(in_channels=1280, out_channels=num_classes, kernel_size=1)
def forward(self, x):
# 通过初始卷积层、批量归一化层和激活函数
x = self.c1_act(self.c1_bn(self.c1_conv(x)))
# 按顺序通过所有瓶颈块
x = self.bneck1(x)
x = self.bneck2(x)
x = self.bneck3(x)
x = self.bneck4(x)
x = self.bneck5(x)
x = self.bneck6(x)
x = self.bneck7(x)
x = self.bneck8(x)
x = self.bneck9(x)
x = self.bneck10(x)
x = self.bneck11(x)
x = self.bneck12(x)
x = self.bneck13(x)
x = self.bneck14(x)
x = self.bneck15(x)
# 通过最后一个卷积层、批量归一化层和激活函数
x = self.c2_act(self.c2_bn(self.c2_conv(x)))
# 通过全局平均池化层
x = self.avg_pool(x)
# 分类器
# 通过分类器的第一个卷积层和激活函数
x = self.classifier_act1(self.classifier_conv1(x))
# 通过分类器的第二个卷积层
x = self.classifier_conv2(x)
# 将输出展平为一维向量
x = x.flatten(start_dim=1)
return x
if __name__ == '__main__':
# 实例化MobileNetV3 Large模型,分类类别数为1000
model = MobileNetV3_Large(num_classes=1000)
# 生成随机输入数据,包含5个样本,每个样本是3通道224x224的图像
x = torch.randn(5, 3, 224, 224)
# 打印模型输出的形状,预期输出为[5, 1000]
print(model(x).shape)
总结
本文全面介绍了MobileNetV3这一轻量级卷积神经网络。首先阐述了其网络背景,包括发表信息、版本情况和性能优势,展示了它在ImageNet分类任务上相较于MobileNetV2的显著提升。接着详细分析了其创新点,涵盖Block创新(加入SE模块和更新激活函数)、使用NAS搜索参数(包括NetAdapt网络适配)以及重新设计耗时层等方面,这些创新使得网络在准确性和计算效率上取得了更好的平衡。同时也指出了MobileNetV3在实际使用中可能存在的问题,即准确率在某些情况下不如MobileNetV2,且在非Android设备上可能无法达到最佳效果。然后介绍了网络结构,结合表格参数详细说明了各层的功能和特点。最后给出了完整的代码实现,方便读者进行实践和应用。