论文:
MobileNetV3:Searching for MobileNetV3
博客:
MobileNetv3网络详解
MobileNetV3网络结构详解
MobileNetV3论文理解,以及tensorflow、pytorch相关代码
MobileNetV3论文翻译
MobileNetV3论文讲解
整体来说MobileNetV3
有两大创新点:
(1)互补搜索技术组合:
使用Platform-Aware Neural Architecture Search(NAS)来形成网络结构,并利用NetAdapt技术进一步筛选网络层结构。
(2)网络结构改进:
如下图所示是MobieNetV2
的Block:
1x1
卷积层来进行升维处理,在卷积后会跟有BN
和ReLU6
激活函数;3x3
大小DW
卷积,卷积后面依旧会跟有BN
和ReLU6
激活函数;1x1
卷积,起到降维作用,注意卷积后只跟了BN
结构,并没有使用ReLU6
激活函数;shotcut
,将我们输入特征矩阵与输出特征矩阵在相同维度的数值上进行相加操作。并且只有DW
卷积的步距为1,且input_channel==output_channel
,才有shotcut
连接。下图是MobieNetV3
的Block:
SE(Squeeze-and-Excitation) 模块类似于一个注意力模块,MobileNetV3
版本中SE模块加在了bottleneck结构的内部,在深度卷积后增加SE块,scale操作后再做逐点卷积,如下图所示。
MobileNetV3版本的SERadio系数为0.25。
对得到的特征矩阵,对每个channel进行池化处理,接下来通过两个全连接层得到输出的向量,其中第一个全连接层,它的全连接层节点数是等于输入特征矩阵channel乘以SERadio系数(0.25),第二个全连接层的channel是与我们特征矩阵的channel保持一致的。
经过平均池化+两个全连接层,输出的特征向量可以理解为是对SE之前的特征矩阵的每一个channel分析出了一个权重关系,它认为比较重要的channel会赋予一个更大的权重,对于不是那么重要的channel维度上对应一个比较小的权重。
之前在v2版本我们基本都是使用ReLU6
激活函数,ReLu6激活函数如下图所示,相当于限制最大值为6。
R e L U 6 ( x ) = m i n ( m a x ( x , 0 ) , 6 ) ReLU6(x)=min(max(x,0),6) ReLU6(x)=min(max(x,0),6)
现在比较常用的激活函数叫swish
激活函数:
s w i t h x = x σ ( x ) swithx=xσ(x) swithx=xσ(x)
其中 σ \sigma σ 的计算公式如下:
σ ( x ) = 1 1 + e − x σ(x)={\frac{1}{1+e^{−x}}} σ(x)=1+e−x1
使用switch
激活函数之后,确实能够提高网络的准确率,但是也存在2个问题:
由于存在这个问题,作者就提出了h-switch
激活函数,在讲h-switch
激活函数之前我们来讲一下h-sigmoid
激活函数,h-sigmoid
激活函数是在ReLU6
激活函数上进行修改的:
h − s i g m o i d = R e L U 6 ( x + 3 ) 6 h−sigmoid={\frac{ReLU6(x+3)}{6}} h−sigmoid=6ReLU6(x+3)
从图中可以看出h-sigmoid与sigmoid激活函数比较接近,所以在很多场景中会使用h-sigmoid激活函数去替代我们的sigmoid激活函数。因此h-switch中 σ \sigma σ 替换为h-sigmoid之后,函数的形式如下:
h − s w i t c h [ x ] = x R e L U 6 ( x + 3 ) 6 h−switch[x]=x{\frac{ReLU6(x+3)}{6}} h−switch[x]=x6ReLU6(x+3)
论文首先使用了神经网络搜索功能(NAS
)来构建全局的网络结构,随后利用了NetAdapt
算法来对每层的核数量进行优化,在尽量优化模型延时的同时保持精度,减小扩充层和每一层中瓶颈的大小。
(1)减少第一个卷积层的卷积个数(32 -> 16)
在v1,v2版本第一层卷积核个数都是32的,在v3版本中我们只使用了16个,作者说将卷积核Filter个数从32变为16之后,它的准确率是和32是一样的,既然准确率没有影响,使用更少的卷积核计算量就变得更小了。这里节省了大概2ms
的运算时间。
(2)精简 Last Stage
在使用NAS
搜索出来的网络结构的最后一部分,叫做Original last Stage
,它的网络结构如下:
该网络是主要是通过堆叠卷积而来的,作者在使用过程中发现这个Original Last Stage
是一个比较耗时的部分,作者就针对该结构进行了精简,提出了一个Efficient Last Stage
:
Efficient Last Stage
相比之前的Original Last Stage
,少了很多卷积层,作者发现更新网络后准确率是几乎没有变化的,但是节省了7ms
的执行时间。这7ms
占据了推理11%
的时间,因此使用Efficient Last Stage
之后,对我们速度提升还是比较明显的。
作者针对不同需求,通过NAS
得到两种结构,一个是MobilenetV3-Large
,结构如下图:
简单看下表中各参数的含义:
input
表示输入层特征矩阵的维度;
out
代表的输出特征矩阵的channel,我们说过在v3版本中第一个卷积核使用的是16
个卷积核;
SE
表示是否使用注意力机制,只要表格中标√
所对应的bneck
结构才会使用我们的注意力机制;
NL
代表的是激活函数,其中HS
代表的是hard switch
激活函数,RE
代表的是ReLU6
激活函数;
s
代表的DW
卷积的步距stride
;
NBN
表示这卷积不使用BN
结构,最后两个卷积相conv2d 1x1当于全连接的作用;
exp size
代表的是第一个升维的卷积,我们要将维度升到多少,exp size
多少,我们就用第一层1x1
卷积升到多少维;
第一个
bneck
结构,这里有一点比较特殊,它的exp size
和输入的特征矩阵channel是一样的,本来bneck
中第一个卷积起到升维
的作用,但这里并没有升维。所以在实现过程中,第一个bneck
结构是没有1x1
卷积层的,它是直接对我们特征矩阵进行DW
卷积处理。对于
shortcut
捷径分支,必须是DW
卷积的步距为1,且bneck
的input_channel=output_channel
才有shortcut
连接。
另一个是MobilenetV3-Small
,结构如下图:
参考:
【MobileNetV3】MobileNetV3网络结构详解
PyTorch深度可分离卷积与MobileNet-v3
# ------------------------------------------------------#
# 这个函数的目的是确保Channel个数能被8整除。
# 很多嵌入式设备做优化时都采用这个准则
# ------------------------------------------------------#
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
# int(v + divisor / 2) // divisor * divisor:四舍五入到8
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
h − s w i t c h [ x ] = x R e L U 6 ( x + 3 ) 6 h−switch[x]=x{\frac{ReLU6(x+3)}{6}} h−switch[x]=x6ReLU6(x+3)
class Hswish(nn.Module):
def __init__(self):
super(Hswish, self).__init__()
self.relu6=nn.ReLU6(inplace=True)
def forward(self, x):
return x*self.relu6(x+3)/6
首先对特征图进行平均池化(无论此时的特征图大小是多少,都池化为1x1的大小);
然后接两个全连接层(conv2d 1x1),第一个全连接层的激活函数为ReLU6,输出通道采用输入通道数的1/4,第二个全连接层的激活函数采用H-swish,输出通道采用设定的输出通道大小
class SE_Block(nn.Module): # Squeeze-and-Excitation block
def __init__(self, in_channels):
super(SE_Block, self).__init__()
# 保证通道数是 8 的倍数,原因是:适配于硬件优化加速
mid_channel = _make_divisible(in_channels // 4, 8)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # GAP
self.conv1 = nn.Conv2d(in_channels, mid_channel, kernel_size=1) # 1x1的卷积核充当FC
self.relu = nn.ReLU6()
self.conv2 = nn.Conv2d(mid_channel, in_channels, kernel_size=1) # 1x1的卷积核充当FC
self.hswish = Hswish()
def forward(self, x):
x = self.avgpool(x)
x = self.conv1(x)
x = self.relu(x)
x = self.conv2(x)
out = self.hswish(x)
return out
class SEInvertedBlock(nn.Module):
def __init__(self,in_channels, exp_size, out_channels,
kernel_size, stride, activate=1, use_se=1):
super(SEInvertedBlock, self).__init__()
self.use_se = use_se
self.stride = stride
self.NL = nn.ReLU6(inplace=True) if activate else Hswish()
# 1x1 升维
self.conv1=nn.Sequential(
nn.Conv2d(in_channels, exp_size, kernel_size=1, stride=1),
nn.BatchNorm2d(exp_size),
self.NL
)
# dw卷积
self.dwconv=nn.Sequential(
nn.Conv2d(in_channels=exp_size, out_channels=exp_size,
kernel_size=kernel_size, stride=stride,
padding=kernel_size//2, groups=exp_size),
nn.BatchNorm2d(exp_size),
self.NL
)
# 引入SE模块
if self.use_se:
self.SE=SE_Block(exp_size)
# 1x1 降维(不激活)
self.conv3=nn.Sequential(
nn.Conv2d(exp_size, out_channels,kernel_size=1, stride=1),
nn.BatchNorm2d(out_channels)
)
# 只有同时满足两个条件时,才使用短连接
self.use_res_connect = (self.stride == 1 and in_channels == out_channels)
def forward(self,x):
out=self.conv1(x) # 1x1卷积核升维
out=self.dwconv(out) # 3x3dw卷积
if self.use_se: # 是否引入SE模块,引入的话需要计算SE-Block分支的输出
SE_out=self.SE(out)
out = out * SE_out # 对位相乘,重新加权
out=self.conv3(out) # 1x1卷积核降维
if self.use_res_connect:# 是否需要残差连接(stride==1&&in==out时)
return x + out
else:
return out
input
表示输入层特征矩阵的维度;out
代表的输出特征矩阵的channel,我们说过在v3版本中第一个卷积核使用的是16
个卷积核;SE
表示是否使用注意力机制,只要表格中标√
所对应的bneck
结构才会使用我们的注意力机制;NL
代表的是激活函数,其中HS
代表的是hard switch
激活函数,RE
代表的是ReLU6
激活函数;s
代表的DW
卷积的步距stride
;NBN
表示这卷积不使用BN
结构,最后两个卷积相conv2d 1x1当于全连接的作用;exp size
代表的是第一个升维的卷积,我们要将维度升到多少,exp size
多少,我们就用第一层1x1
卷积升到多少维;class MobileNetV3(nn.Module):
def __init__(self, num_classes=1000, width_mult=1.0,
inverted_residual_setting=None, round_nearest=8):
"""
MobileNet V3 main class
Args:
num_classes (int): Number of classes
width_mult (float): Width multiplier - adjusts number of channels in each layer by this amount
inverted_residual_setting: Network structure
round_nearest (int): Round the number of channels in each layer to be a multiple of this number
Set to 1 to turn off rounding
"""
super(MobileNetV3, self).__init__()
# 保证通道数是 8 的倍数,原因是:适配于硬件优化加速
input_channel = _make_divisible(16 * width_mult, round_nearest)
last_channel = _make_divisible(960 * width_mult, round_nearest)
if inverted_residual_setting is None:
inverted_residual_setting = [
# kernel_size, exp_size, out, SE, NL, s
[3, 16, 16, 0, 0, 1], # 112x112x16 -> 112x112x16
[3, 64, 24, 0, 0, 2], # 112x112x16 -> 56x56x24
[3, 72, 24, 0, 0, 1], # 56x56x24 -> 56x56x24
[5, 72, 40, 1, 0, 2], # 56x56x24 -> 28x28x40
[5, 120, 40, 1, 0, 1], # 28x28x40 -> 28x28x40
[5, 120, 40, 1, 0, 1], # 28x28x40 -> 28x28x40
[3, 240, 80, 0, 1, 2], # 28x28x40 -> 14x14x80
[3, 200, 80, 0, 1, 1], # 14x14x80 -> 14x14x80
[3, 184, 80, 0, 1, 1], # 14x14x80 -> 14x14x80
[3, 184, 80, 0, 1, 1], # 14x14x80 -> 14x14x80
[3, 480, 112, 1, 1, 1], # 14x14x80 -> 14x14x112
[3, 672, 112, 1, 1, 1], # 14x14x112 -> 14x14x112
[5, 672, 160, 1, 1, 2], # 14x14x112 -> 7x7x160
[5, 960, 160, 1, 1, 1], # 7x7x160 -> 7x7x160
[5, 960, 160, 1, 1, 1] # 7x7x160 -> 7x7x160
]
# 检查传入的配置是否正确
if len(inverted_residual_setting) == 0 or len(inverted_residual_setting[0]) != 6:
raise ValueError("inverted_residual_setting should be non-empty "
"or a 6-element list, got {}".format(inverted_residual_setting))
# conv_first layer
# 224,224,3 -> 112,112,16
self.conv_first = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=input_channel,
kernel_size=3, stride=2, padding=1),
nn.BatchNorm2d(input_channel),
Hswish()
)
# 加入features列表
features = [self.conv_first]
# building inverted residual blocks
# kernel_size:代表卷积核大小
# exp_size:代表的是第一个1x1升维的卷积,我们要将维度升到多少;
# out:代表该层输出特征矩阵的channel;
# SE:表示是否使用注意力机制,只要表格中标`√`所对应的`bneck`结构才会使用我们的注意力机制;
# NL:代表的是激活函数,其中`HS`代表的是`hard switch`激活函数,`RE`代表的是`ReLU6`激活函数;
# s:代表的`DW`卷积的步距`stride`;
for kernel_size, exp_size, out, SE, NL, s in inverted_residual_setting:
# 各层的输出通道数
output_channel = _make_divisible(out * width_mult, round_nearest)
# SEInvertedBlock函数
features.append(SEInvertedBlock(in_channels=input_channel, exp_size=exp_size,
out_channels=out, kernel_size=kernel_size,
stride=s, activate=NL, use_se=SE))
# 这一层的输出通道数作为下一层的输入通道数
input_channel = output_channel
# conv_last layer
self.conv_last = nn.Sequential(
nn.Conv2d(in_channels=input_channel, out_channels=last_channel, kernel_size=1),
nn.BatchNorm2d(last_channel),
Hswish()
)
# 加入features列表
features.append(self.conv_last)
# *features表示位置信息,将特征层利用nn.Sequential打包成一个整体
self.features = nn.Sequential(*features)
# building classifier
# 自适应平均池化下采样层,输出矩阵高和宽均为1
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.classifier = nn.Sequential(
nn.Linear(in_features=last_channel, out_features=1280),
Hswish(),
nn.Linear(in_features=1280, out_features=num_classes)
# 以下方式会出错,1维不能用2d卷积?
# nn.Conv2d(in_channels=last_channel, out_channels=1280, kernel_size=1),
# Hswish(),
# nn.Conv2d(in_channels=1280, out_channels=num_classes, kernel_size=1),
)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1) # 展平处理
x = self.classifier(x)
return x
def test():
# ------------------------------------#
# 方法1 获取计算量与参数量
# ------------------------------------#
net = MobileNetV3()
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
summary(net, (3, 224, 224))
if __name__ == '__main__':
test()