moblienetv1、moblienetv2、mobilenetv3发展历程
moblienetv1、moblienetv2、mobilenetv3发展历程
参考知乎大神R.JD,总结的非常到位:https://zhuanlan.zhihu.com/p/70703846utm_source=wechat_session&utm_medium=social&utm_oi=973704300736712704
添加了v1-v3的pytorch实现代码,结合起来更容易理解
MobileNet V1
MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications 论文地址:https://arxiv.org/abs/1704.04861 收录:CVPR2017
2017年4月,谷歌提出MobileNetV1,这一专注于在移动设备上的轻量级神经网络。一直都有一个争议,说MobileNetV1怎么和Xception的网络block结构一样,都大量用到了深度可分离?
其实这里有个小插曲: MobileNetV1在http://arxiv.org上的介绍是:
可以看到MobileNetV1是在2017年4月才提交的v1版本,但是
Xception是早在2016年10月v1版本就提出来了。那么,真的是MobileNet“抄袭”了Xception吗?其实并不是的,在Xception v1版本论文当中有这样的一句话:
而这个Andew Howard是谁呢?没错,就是MobileNetV1的作者。而在Xception v3论文版本中,一句话就变成了:
真相只有一个: 2016年6月,谷歌提出了MobileNetV1,由于各种原因当时没有挂上arxiv,一直到2017年4月才提交。好巧不巧,谷歌的另一团队,同时提出了Xception。所有才有了两个结构都是基于深度可分离卷积的相似争论。
好了,键盘党们可以擦擦键盘休息休息了。
废话说完,开始正文(好像每篇文章都有开头废话啊喂!!!)
其实介绍MobileNetV1(以下简称V1)只有一句话,MobileNetV1就是把VGG中的标准卷积层换成深度可分离卷积就可以了。
那么,这个深度可分离卷积是什么?
深度可分离卷积
深度可分离卷积(depthwise separable convolution),根据史料记载,可追溯到2012年的论文Simplifying ConvNets for Fast Learning,作者提出了可分离卷积的概念(下图(a)):
Laurent Sifre博士2013年在谷歌实习期间,将可分离卷积拓展到了深度(depth),并且在他的博士论文Rigid-motion scattering for image classification中有详细的描写,感兴趣的同学可以去看看论文
可分离卷积主要有两种类型:空间可分离卷积和深度可分离卷积。
空间可分离
顾名思义,空间可分离就是将一个大的卷积核变成两个小的卷积核,比如将一个3×3的核分成一个3×1和一个1×3的核:
由于空间可分离卷积不在MobileNet的范围内,就不说了。
深度可分离卷积
深度可分离卷积就是将普通卷积拆分成为一个深度卷积和一个逐点卷积。
我们先来看一下标准的卷积操作:
输入一个12×12×3的一个输入特征图,经过5×5×3的卷积核卷积得到一个8×8×1的输出特征图。如果此时我们有256个特征图,我们将会得到一个8×8×256的输出特征图。
以上就是标准卷积做干的活。那深度卷积和逐点卷积呢?
深度卷积
与标准卷积网络不一样的是,我们将卷积核拆分成为但单通道形式,在不改变输入特征图像的深度的情况下,对每一通道进行卷积操作,这样就得到了和输入特征图通道数一致的输出特征图。如上图:输入12×12×3的特征图,经过5×5×1×3的深度卷积之后,得到了8×8×3的输出特征图。输入个输出的维度是不变的3。这样就会有一个问题,通道数太少,特征图的维度太少,能获取到足够的有效信息吗?
逐点卷积
逐点卷积就是1×1卷积。主要作用就是对特征图进行升维和降维,如下图:
在深度卷积的过程中,我们得到了8×8×3的输出特征图,我们用256个1×1×3的卷积核对输入特征图进行卷积操作,输出的特征图和标准的卷积操作一样都是8×8×256了。
标准卷积与深度可分离卷积的过程对比如下:
为什么要深度可分离卷积?
这个问题很好回答,如果有一个方法能让你用更少的参数,更少的运算,但是能达到差的不是很多的结果,你会使用吗?
深度可分离卷积就是这样的一个方法。我们首先来计算一下标准卷积的参数量与计算量(只考虑MAdd):
标准卷积的参数量
卷积核的尺寸是Dk×Dk×M,一共有N个,所以标准卷积的参数量是:
标准卷积的计算量
卷积核的尺寸是Dk×Dk×M,一共有N个,每一个都要进行Dw×Dh次运算,所以标准卷积的计算量是:
标准卷积算完了,我们接下来计算深度可分离卷积的参数量和计算量:
深度可分离卷积的参数量
深度可分离卷积的参数量由深度卷积和逐点卷积两部分组成:
深度卷积的卷积核尺寸Dk×Dk×M;逐点卷积的卷积核尺寸为1×1×M,一共有N个,所以深度可分离卷积的参数量是:
深度可分离卷积的计算量
深度可分离卷积的计算量也是由深度卷积和逐点卷积两部分组成:
深度卷积的卷积核尺寸Dk×Dk×M,一共要做Dw×Dh次乘加运算;逐点卷积的卷积核尺寸为1×1×M,有N个,一共要做Dw×Dh次乘加运算,所以深度可分离卷积的计算量是:
总的来说:
可以参数数量和乘加操作的运算量均下降为原来的
我们通常所使用的是3×3的卷积核,也就是会下降到原来的九分之一到八分之一。
假设
输出为一个224×224×3的图像,VGG网络某层卷积输入的尺寸是112×112×64的特征图,卷积核为3×3×128,标准卷积的运算量是:
3×3×128×64×112×112 = 924844032
深度可分离卷积的运算量是:
3×3×64×112×112+128×64×112×112 = 109985792
这一层,MobileNetV1所采用的深度可分离卷积计算量与标准卷积计算量的比值为:
109985792 /924844032 = 0.1189
与我们所计算的九分之一到八分之一一致。
V1卷积层
上图左边是标准卷积层,右边是V1的卷积层,虚线处是不相同点。V1的卷积层,首先使用3×3的深度卷积提取特征,接着是一个BN层,随后是一个ReLU层,在之后就会逐点卷积,最后就是BN和ReLU了。这也很符合深度可分离卷积,将左边的标准卷积拆分成右边的一个深度卷积和一个逐点卷积。
等等,我们发现有什么东西混了进来???ReLU6是什么?
ReLU6
上图左边是普通的ReLU,对于大于0的值不进行处理,右边是ReLU6,当输入的值大于6的时候,返回6,relu6“具有一个边界”。作者认为ReLU6作为非线性激活函数,在低精度计算下具有更强的鲁棒性。(这里所说的“低精度”,我看到有人说不是指的float16,而是指的定点运算(fixed-point arithmetic))
现在就有一个问题,标准卷积核深度可分离卷积层到底对结果有什么样的影响呢?
上实验。
可以看到使用深度可分离卷积与标准卷积,参数和计算量能下降为后者的九分之一到八分之一左右。但是准确率只有下降极小的1%。
V1网络结构
MobileNet的网络结构如上图所示。首先是一个3x3的标准卷积,s2进行下采样。然后就是堆积深度可分离卷积,并且其中的部分深度卷积会利用s2进行下采样。然后采用平均池化层将feature变成1x1,根据预测类别大小加上全连接层,最后是一个softmax层。整个网络有28层,其中深度卷积层有13层。
V1实现代码:pytorch
import time import torch import torch.nn as nn import torchvision.models._utils as _utils import torchvision.models as models import torch.nn.functional as F from torch.autograd import Variable def conv_bn(inp, oup, stride = 1, leaky = 0): return nn.Sequential( nn.Conv2d(inp, oup, 3, stride, 1, bias=False), nn.BatchNorm2d(oup), nn.LeakyReLU(negative_slope=leaky, inplace=True) ) def conv_bn_no_relu(inp, oup, stride): return nn.Sequential( nn.Conv2d(inp, oup, 3, stride, 1, bias=False), nn.BatchNorm2d(oup), ) def conv_bn1X1(inp, oup, stride, leaky=0): return nn.Sequential( nn.Conv2d(inp, oup, 1, stride, padding=0, bias=False), nn.BatchNorm2d(oup), nn.LeakyReLU(negative_slope=leaky, inplace=True) ) def conv_dw(inp, oup, stride, leaky=0.1): return nn.Sequential( nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False), nn.BatchNorm2d(inp), nn.LeakyReLU(negative_slope= leaky,inplace=True), nn.Conv2d(inp, oup, 1, 1, 0, bias=False), nn.BatchNorm2d(oup), nn.LeakyReLU(negative_slope= leaky,inplace=True), ) class SSH(nn.Module): def __init__(self, in_channel, out_channel): super(SSH, self).__init__() assert out_channel % 4 == 0 leaky = 0 if (out_channel <= 64): leaky = 0.1 self.conv3X3 = conv_bn_no_relu(in_channel, out_channel//2, stride=1) self.conv5X5_1 = conv_bn(in_channel, out_channel//4, stride=1, leaky = leaky) self.conv5X5_2 = conv_bn_no_relu(out_channel//4, out_channel//4, stride=1) self.conv7X7_2 = conv_bn(out_channel//4, out_channel//4, stride=1, leaky = leaky) self.conv7x7_3 = conv_bn_no_relu(out_channel//4, out_channel//4, stride=1) def forward(self, input): conv3X3 = self.conv3X3(input) conv5X5_1 = self.conv5X5_1(input) conv5X5 = self.conv5X5_2(conv5X5_1) conv7X7_2 = self.conv7X7_2(conv5X5_1) conv7X7 = self.conv7x7_3(conv7X7_2) out = torch.cat([conv3X3, conv5X5, conv7X7], dim=1) out = F.relu(out) return out class FPN(nn.Module): def __init__(self,in_channels_list,out_channels): super(FPN,self).__init__() leaky = 0 if (out_channels <= 64): leaky = 0.1 self.output1 = conv_bn1X1(in_channels_list[0], out_channels, stride = 1, leaky = leaky) self.output2 = conv_bn1X1(in_channels_list[1], out_channels, stride = 1, leaky = leaky) self.output3 = conv_bn1X1(in_channels_list[2], out_channels, stride = 1, leaky = leaky) self.merge1 = conv_bn(out_channels, out_channels, leaky = leaky) self.merge2 = conv_bn(out_channels, out_channels, leaky = leaky) def forward(self, input): # names = list(input.keys()) input = list(input.values()) output1 = self.output1(input[0]) output2 = self.output2(input[1]) output3 = self.output3(input[2]) up3 = F.interpolate(output3, size=[output2.size(2), output2.size(3)], mode="nearest") output2 = output2 + up3 output2 = self.merge2(output2) up2 = F.interpolate(output2, size=[output1.size(2), output1.size(3)], mode="nearest") output1 = output1 + up2 output1 = self.merge1(output1) out = [output1, output2, output3] return out class MobileNetV1(nn.Module): def __init__(self): super(MobileNetV1, self).__init__() self.stage1 = nn.Sequential( conv_bn(3, 8, 2, leaky = 0.1), # 3 conv_dw(8, 16, 1), # 7 conv_dw(16, 32, 2), # 11 conv_dw(32, 32, 1), # 19 conv_dw(32, 64, 2), # 27 conv_dw(64, 64, 1), # 43 ) self.stage2 = nn.Sequential( conv_dw(64, 128, 2), # 43 + 16 = 59 conv_dw(128, 128, 1), # 59 + 32 = 91 conv_dw(128, 128, 1), # 91 + 32 = 123 conv_dw(128, 128, 1), # 123 + 32 = 155 conv_dw(128, 128, 1), # 155 + 32 = 187 conv_dw(128, 128, 1), # 187 + 32 = 219 ) self.stage3 = nn.Sequential( conv_dw(128, 256, 2), # 219 +3 2 = 241 conv_dw(256, 256, 1), # 241 + 64 = 301 ) self.avg = nn.AdaptiveAvgPool2d((1,1)) self.fc = nn.Linear(256, 1000) def forward(self, x): x = self.stage1(x) x = self.stage2(x) x = self.stage3(x) x = self.avg(x) # x = self.model(x) x = x.view(-1, 256) x = self.fc(x) return x
V1实验结果
V1论文中还有一部分对V1网络再进行调整,在此就不赘述了,感兴趣的同学可以去看看原论文。
V1的效果到底好不好,作者将V1与大型网络GoogleNet和VGG16进行了比较:
可以发现,作为轻量级网络的V1在计算量小于GoogleNet,参数量差不多是在一个数量级的基础上,在分类效果上比GoogleNet还要好,这就是要得益于深度可分离卷积了。VGG16的计算量参数量比V1大了30倍,但是结果也仅仅只高了1%不到。
目标检测,在COCO数据集上的结果:
对了,作者还在论文中分析整个了网络的参数和计算量分布,如下图所示。可以看到整个计算量基本集中在1x1卷积上。对于参数也主要集中在1x1卷积,除此之外还有就是全连接层占了一部分参数。
MobileNet V2
MobileNetV2: Inverted Residuals and Linear Bottlenecks 论文地址:https://arxiv.org/abs/1704.04861 收录:CVPR2018
MobileNetV1(以下简称:V1)过后,我们就要讨论讨论MobileNetV2(以下简称:V2)了。为了能更好地讨论V2,我们首先再回顾一下V1:
回顾MobileNet V1
V1核心思想是采用 深度可分离卷积 操作。在相同的权值参数数量的情况下,相较标准卷积操作,可以减少数倍的计算量,从而达到提升网络运算速度的目的。
V1的block如下图所示:
首先利用3×3的深度可分离卷积提取特征,然后利用1×1的卷积来扩张通道。用这样的block堆叠起来的MobileNetV1既能较少不小的参数量、计算量,提高网络运算速度,又能的得到一个接近于标准卷积的还不错的结果,看起来是很美好的。
但是!
有人在实际使用的时候, 发现深度卷积部分的卷积核比较容易训废掉:训完之后发现深度卷积训出来的卷积核有不少是空的:
这是为什么?
作者认为这是ReLU这个浓眉大眼的激活函数的锅。(没想到你个浓眉大眼的ReLU激活函数也叛变革命了???)
ReLU做了些啥?
V2的论文中,作者也有这样的一个解释。(论文中的实在不是很好懂,我就根据一些解读结合我的想法简单说说吧。有说的不正确的地方,还请各位大佬指出,感谢!)
这是将低维流形的ReLU变换embedded到高维空间中的的例子。
我们在这里抛弃掉流形这个概念,通俗理解一下。
假设在2维空间有一组由m个点组成的螺旋线Xm数据(如input),利用随机矩阵T映射到n维空间上并进行ReLU运算,即:
其中,Xm被随机矩阵T映射到了n维空间:
再利用随机矩阵T的逆矩阵T-1,将y映射回2维空间当中:
全过程如下表示:
换句话说,就是对一个n维空间中的一个“东西”做ReLU运算,然后(利用T的逆矩阵T-1恢复)对比ReLU之后的结果与Input的结果相差有多大。
可以看到:
当n = 2,3时,与Input相比有很大一部分的信息已经丢失了。而当n = 15到30,还是有相当多的地方被保留了下来。
也就是说,对低维度做ReLU运算,很容易造成信息的丢失。而在高维度进行ReLU运算的话,信息的丢失则会很少。
这就解释了为什么深度卷积的卷积核有不少是空。发现了问题,我们就能更好地解决问题。针对这个问题,可以这样解决:既然是ReLU导致的信息损耗,将ReLU替换成线性激活函数。
Linear bottleneck
我们当然不能把所有的激活层都换成线性的啊,所以我们就悄咪咪的把最后的那个ReLU6换成Linear。(至于为什么换最后一个ReLU6而不换第一个和第二个ReLU6,看到后面就知道了。)
Separable with linear bottleneck
作者将这个部分称之为linear bottleneck。对,就是论文名中的那个linear bottleneck。
Expansion layer
现在还有个问题是,深度卷积本身没有改变通道的能力,来的是多少通道输出就是多少通道。如果来的通道很少的话,DW深度卷积只能在低维度上工作,这样效果并不会很好,所以我们要“扩张”通道。既然我们已经知道PW逐点卷积也就是1×1卷积可以用来升维和降维,那就可以在DW深度卷积之前使用PW卷积进行升维(升维倍数为t,t=6),再在一个更高维的空间中进行卷积操作来提取特征:
也就是说,不管输入通道数是多少,经过第一个PW逐点卷积升维之后,深度卷积都是在相对的更高6倍维度上进行工作。
Inverted residuals(核心)
回顾V1的网络结构,我们发现V1很像是一个直筒型的VGG网络。我们想像Resnet一样复用我们的特征,所以我们引入了shortcut结构,这样V2的block就是如下图形式:
对比一下V1和V2:
可以发现,都采用了 1×1 -> 3 ×3 -> 1 × 1 的模式,以及都使用Shortcut结构。但是不同点呢:
- ResNet 先降维 (0.25倍)、卷积、再升维。
- MobileNetV2 则是 先升维 (6倍)、卷积、再降维。
刚好V2的block刚好与Resnet的block相反,作者将其命名为Inverted residuals。就是论文名中的Inverted residuals。
V2的block
至此,V2的最大的创新点就结束了,我们再总结一下V2的block:
我们将V1和V2的block进行一下对比:
左边是v1的block,没有Shortcut并且带最后的ReLU6。
右边是v2的加入了1×1升维,引入Shortcut并且去掉了最后的ReLU,改为Linear。步长为1时,先进行1×1卷积升维,再进行深度卷积提取特征,再通过Linear的逐点卷积降维。将input与output相加,形成残差结构。步长为2时,因为input与output的尺寸不符,因此不添加shortcut结构,其余均一致。
V2的网络结构
28×28×32那一层的步长为2的话,输出应该是14×14,应该是一处错误。按照作者论文里的说法,自己修改了一下:
V2实验结果
Image Classification
图像分类的实验,主要是在以上的网络上进行的,ShuffleNet是V1的版本使用了分组卷积和shuffling, 也使用了类似的残差结构(c)中的(b)。
结果如下:
详细对比如下:
Object Detection SSDLite v2
目标检测方面,作者首先提出了SSDLite。就是对SSD结构做了修改,将SSD的预测层中所有标准卷积替换为深度可分离卷积。作者说这样参数量和计算成本大大降低,计算更高效。SSD与SSDLite对比:
应用在物体检测任务上,V1与常用检测网络的对比:
可以看到,基于MobileNetV2的SSDLite在COCO数据集上超过了YOLOv2,并且大小小10倍速度快20倍。
Semantic Segmentation v2
分割效果如下:
V1 VS V2
可以看到,虽然V2的层数比V1的要多很多,但是FLOPs,参数以及CPU耗时都是比V1要好的。
V1V2在google Pixel 1手机上在Image Classification任务的对比:
MobileNetV2 模型在整体速度范围内可以更快实现相同的准确性。
目标检测和语义分割的结果:
综上,MobileNetV2 提供了一个非常高效的面向移动设备的模型,可以用作许多视觉识别任务的基础。
但是!
在我实际应用V1V2时,V1的效果都要稍微好一点。上一张gluonCV的结果图,和我的实现也差不多:
不知道为什么。
V2实现代码,针对分割做了layer的分层
"""MobileNetV2""" import torch.nn as nn from light.nn import _ConvBNReLU, InvertedResidual __all__ = ['MobileNetV2', 'get_mobilenet_v2', 'mobilenet_v2_1_0', 'mobilenet_v2_0_75', 'mobilenet_v2_0_5', 'mobilenet_v2_0_25'] class MobileNetV2(nn.Module): def __init__(self, num_classes=1000, width_mult=1.0, dilated=False, norm_layer=nn.BatchNorm2d, **kwargs): super(MobileNetV2, self).__init__() layer1_setting = [ # t, c, n, s [1, 16, 1, 1]] layer2_setting = [ [6, 24, 2, 2]] layer3_setting = [ [6, 32, 3, 2]] layer4_setting = [ [6, 64, 4, 2], [6, 96, 3, 1]] layer5_setting = [ [6, 160, 3, 2], [6, 320, 1, 1]] # building first layer self.in_channels = int(32 * width_mult) if width_mult > 1.0 else 32 last_channels = int(1280 * width_mult) if width_mult > 1.0 else 1280 self.conv1 = _ConvBNReLU(3, self.in_channels, 3, 2, 1, relu6=True, norm_layer=norm_layer) # building inverted residual blocks self.layer1 = self._make_layer(InvertedResidual, layer1_setting, width_mult, norm_layer=norm_layer) self.layer2 = self._make_layer(InvertedResidual, layer2_setting, width_mult, norm_layer=norm_layer) self.layer3 = self._make_layer(InvertedResidual, layer3_setting, width_mult, norm_layer=norm_layer) if dilated: self.layer4 = self._make_layer(InvertedResidual, layer4_setting, width_mult, dilation=2, norm_layer=norm_layer) self.layer5 = self._make_layer(InvertedResidual, layer5_setting, width_mult, dilation=2, norm_layer=norm_layer) else: self.layer4 = self._make_layer(InvertedResidual, layer4_setting, width_mult, norm_layer=norm_layer) self.layer5 = self._make_layer(InvertedResidual, layer5_setting, width_mult, norm_layer=norm_layer) # building last several layers self.classifier = nn.Sequential( _ConvBNReLU(self.in_channels, last_channels, 1, relu6=True, norm_layer=norm_layer), nn.AdaptiveAvgPool2d(1), nn.Dropout2d(0.2), nn.Conv2d(last_channels, num_classes, 1)) self._init_weight() # def _make_layer(self, block, block_setting, width_mult, dilation=1, norm_layer=nn.BatchNorm2d): # layers = list() # for t, c, n, s in block_setting: # out_channels = int(c * width_mult) # for i in range(n): # stride = s if (i == 0 and dilation == 1) else 1 # layers.append(block(self.in_channels, out_channels, stride, t, dilation, norm_layer=norm_layer)) # self.in_channels = out_channels # return nn.Sequential(*layers) def _make_layer(self, block, block_setting, width_mult, dilation=1, norm_layer=nn.BatchNorm2d): layers = list() for t, c, n, s in block_setting: out_channels = int(c * width_mult) stride = s if (dilation == 1) else 1 layers.append(block(self.in_channels, out_channels, stride, t, dilation, norm_layer=norm_layer)) self.in_channels = out_channels for i in range(n - 1): layers.append(block(self.in_channels, out_channels, 1, t, 1, norm_layer=norm_layer)) self.in_channels = out_channels return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.layer4(x) x = self.layer5(x) x = self.classifier(x) x = x.view(x.size(0), -1) return x def _init_weight(self): # weight initialization for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out') if m.bias is not None: nn.init.zeros_(m.bias) elif isinstance(m, nn.BatchNorm2d): nn.init.ones_(m.weight) nn.init.zeros_(m.bias) elif isinstance(m, nn.Linear): nn.init.normal_(m.weight, 0, 0.01) if m.bias is not None: nn.init.zeros_(m.bias) def get_mobilenet_v2(width_mult=1.0, pretrained=False, root='~/.torch/models', **kwargs): model = MobileNetV2(width_mult=width_mult, **kwargs) if pretrained: raise ValueError("Not support pretrained") return model def mobilenet_v2_1_0(**kwargs): return get_mobilenet_v2(1.0, **kwargs) def mobilenet_v2_0_75(**kwargs): return get_mobilenet_v2(0.75, **kwargs) def mobilenet_v2_0_5(**kwargs): return get_mobilenet_v2(0.5, **kwargs) def mobilenet_v2_0_25(**kwargs): return get_mobilenet_v2(0.25, **kwargs) if __name__ == '__main__': model = MobileNetV2()
MobileNet V3
V1,V2都看完了,现在就来到了MobileNetV3(以下简称V3)。
Searching for MobileNetV3 论文地址:https://arxiv.org/pdf/1905.02244.pdf
MobileNetV3,是谷歌在2019年3月21日提出的网络架构。首先,引入眼帘的是这篇文章的标题,“searching”一词就把V3的论文的核心观点展示了出来——用神经结构搜索(NAS)来完成V3。虽然本人没有接触过NAS,但是我已经闻到了金钱的味道。
"抱歉,有钱真的可以为..."
由于真的没有接触过NAS,所以V3就讲讲其他的,除NAS之外的东西吧。
先上结果:
可以看到,在同一大小的计算量下,V3在ImageNet上的结果都是最好的。
我们先来看看V3做了什么?
MobileNetV3的相关技术
- 0.网络的架构基于NAS实现的MnasNet(效果比MobileNetV2好)
- 1.引入MobileNetV1的深度可分离卷积
- 2.引入MobileNetV2的具有线性瓶颈的倒残差结构
- 3.引入基于squeeze and excitation结构的轻量级注意力模型(SE)
- 4.使用了一种新的激活函数h-swish(x)
- 5.网络结构搜索中,结合两种技术:资源受限的NAS(platform-aware NAS)与NetAdapt
- 6.修改了MobileNetV2网络端部最后阶段
第0点,关于MnasNet也是基于NAS的,也不是很了解。大家感兴趣的话,可以参考曲晓峰老师的这个回答如何评价 Google 最新的模型 MnasNet? - 曲晓峰的回答 - 知乎,写的很棒!所以我们只要认为MnasNet是一个比MobileNet精度和实时性更高的模型就行了。
第1,2点在前面的MobileNetV1和V2上有讨论,在这里就不赘述了。
第3点引入SE模块,主要为了利用结合特征通道的关系来加强网络的学习能力。先不仔细讨论,之后在【深度回顾经典网络】系列的时候再详细讨论吧,感兴趣的同学,可以看看这一篇文章。
激活函数h-swish
swish
h-swish是基于swish的改进,swish最早是在谷歌大脑2017的论文Searching for Activation functions所提出(又是Searching for!!!)。
swish论文的作者认为,Swish具备无上界有下界、平滑、非单调的特性。并且Swish在深层模型上的效果优于ReLU。仅仅使用Swish单元替换ReLU就能把MobileNet,NASNetA在 ImageNet上的top-1分类准确率提高0.9%,Inception-ResNet-v的分类准确率提高0.6%。
V3也利用swish当作为ReLU的替代时,它可以显著提高神经网络的精度。但是呢,作者认为这种非线性激活函数虽然提高了精度,但在嵌入式环境中,是有不少的成本的。原因就是在移动设备上计算sigmoid函数是非常明智的选择。所以提出了h-swish。
h-swish
可以用一个近似函数来逼急这个swish,让swish变得硬(hard)。作者选择的是基于ReLU6,作者认为几乎所有的软件和硬件框架上都可以使用ReLU6的优化实现。其次,它能在特定模式下消除了由于近似sigmoid的不同实现而带来的潜在的数值精度损失。
下图是Sigmoid和swish的hard、soft形式:
我们可以简单的认为,hard形式是soft形式的低精度化。作者认为swish的表现和其他非线性相比,能够将过滤器的数量减少到16个的同时保持与使用ReLU或swish的32个过滤器相同的精度,这节省了3毫秒的时间和1000万MAdds的计算量。
并且同时,作者认为随着网络的深入,应用非线性激活函数的成本会降低,能够更好的减少参数量。作者发现swish的大多数好处都是通过在更深的层中使用它们实现的。因此,在V3的架构中,只在模型的后半部分使用h-swish(HS)。
网络结构搜索NAS
由于不熟,就简单写一点吧。
主要结合两种技术:资源受限的NAS(platform-aware NAS)与NetAdapt。
资源受限的NAS,用于在计算和参数量受限的前提下搜索网络来优化各个块(block),所以称之为模块级搜索(Block-wise Search) 。
NetAdapt,用于对各个模块确定之后网络层的微调每一层的卷积核数量,所以称之为层级搜索(Layer-wise Search)。
一旦通过体系结构搜索找到模型,我们就会发现最后一些层以及一些早期层计算代价比较高昂。于是作者决定对这些架构进行一些修改,以减少这些慢层(slow layers)的延迟,同时保持准确性。这些修改显然超出了当前搜索的范围。
对V2最后阶段的修改
作者认为,当前模型是基于V2模型中的倒残差结构和相应的变体(如下图)。使用1×1卷积来构建最后层,这样可以便于拓展到更高维的特征空间。这样做的好处是,在预测时,有更多更丰富的特征来满足预测,但是同时也引入了额外的计算成本与延时。
所以,需要改进的地方就是要保留高维特征的前提下减小延时。首先,还是将1×1层放在到最终平均池之后。这样的话最后一组特征现在不是7x7(下图V2结构红框),而是以1x1计算(下图V3结构黄框)。
这样的好处是,在计算和延迟方面,特征的计算几乎是免费的。最终,重新设计完的结构如下:
在不会造成精度损失的同时,减少10ms耗时,提速15%,减小了30m的MAdd操作。
V3的block
综合以上,V3的block结构如下所示:
与V2的block相比较:
MobileNetV3的网络结构
MobileNetV3定义了两个模型: MobileNetV3-Large和MobileNetV3-Small。V3-Large是针对高资源情况下的使用,相应的,V3-small就是针对低资源情况下的使用。两者都是基于之前的简单讨论的NAS。
MobileNetV3-Large
MobileNetV3-Small
v3实现代码,pytorch版本:
用于构建fpn结构,返回不同block的特征图
# -*- coding: utf-8 -*- # @Time : 2019/5/23 15:22 # @Author : zhoujun import torch import torch.nn as nn import torch.nn.functional as F from torch.nn import init class hswish(nn.Module): def forward(self, x): out = x * F.relu6(x + 3, inplace=True) / 6 return out class hsigmoid(nn.Module): def forward(self, x): out = F.relu6(x + 3, inplace=True) / 6 return out class SeModule(nn.Module): def __init__(self, in_size, reduction=4): super(SeModule, self).__init__() self.avg_pool = nn.AdaptiveAvgPool2d(1) self.se = nn.Sequential( nn.Conv2d(in_size, in_size // reduction, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(in_size // reduction), nn.ReLU(inplace=True), nn.Conv2d(in_size // reduction, in_size, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(in_size), hsigmoid() ) def forward(self, x): return x * self.se(x) class Block(nn.Module): '''expand + depthwise + pointwise''' def __init__(self, kernel_size, in_size, expand_size, out_size, nolinear, semodule, stride): super(Block, self).__init__() self.stride = stride self.se = semodule self.conv1 = nn.Conv2d(in_size, expand_size, kernel_size=1, stride=1, padding=0, bias=False) self.bn1 = nn.BatchNorm2d(expand_size) self.nolinear1 = nolinear self.conv2 = nn.Conv2d(expand_size, expand_size, kernel_size=kernel_size, stride=stride, padding=kernel_size // 2, groups=expand_size, bias=False) self.bn2 = nn.BatchNorm2d(expand_size) self.nolinear2 = nolinear self.conv3 = nn.Conv2d(expand_size, out_size, kernel_size=1, stride=1, padding=0, bias=False) self.bn3 = nn.BatchNorm2d(out_size) self.shortcut = nn.Sequential() if stride == 1 and in_size != out_size: self.shortcut = nn.Sequential( nn.Conv2d(in_size, out_size, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(out_size), ) def forward(self, x): out = self.nolinear1(self.bn1(self.conv1(x))) out = self.nolinear2(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) if self.se != None: out = self.se(out) out = out + self.shortcut(x) if self.stride == 1 else out return out class MobileNetV3_Large(nn.Module): def __init__(self, pretrained): super(MobileNetV3_Large, self).__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(16) self.hs1 = hswish() self.layer1 = nn.Sequential( Block(3, 16, 16, 16, nn.ReLU(inplace=True), None, 1), Block(3, 16, 64, 24, nn.ReLU(inplace=True), None, 2), Block(3, 24, 72, 24, nn.ReLU(inplace=True), None, 1), ) self.layer2 = nn.Sequential( Block(5, 24, 72, 40, nn.ReLU(inplace=True), SeModule(40), 2), Block(5, 40, 120, 40, nn.ReLU(inplace=True), SeModule(40), 1), Block(5, 40, 120, 40, nn.ReLU(inplace=True), SeModule(40), 1), ) self.layer3 = nn.Sequential( Block(3, 40, 240, 80, hswish(), None, 2), Block(3, 80, 200, 80, hswish(), None, 1), Block(3, 80, 184, 80, hswish(), None, 1), Block(3, 80, 184, 80, hswish(), None, 1), Block(3, 80, 480, 112, hswish(), SeModule(112), 1), Block(3, 112, 672, 112, hswish(), SeModule(112), 1), Block(5, 112, 672, 160, hswish(), SeModule(160), 1), ) self.layer4 = nn.Sequential( Block(5, 160, 672, 160, hswish(), SeModule(160), 2), Block(5, 160, 960, 160, hswish(), SeModule(160), 1), ) self.conv2 = nn.Conv2d(160, 960, kernel_size=1, stride=1, padding=0, bias=False) self.bn2 = nn.BatchNorm2d(960) self.hs2 = hswish() self.init_params() def init_params(self): for m in self.modules(): if isinstance(m, nn.Conv2d): init.kaiming_normal_(m.weight, mode='fan_out') if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): init.constant_(m.weight, 1) init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): init.normal_(m.weight, std=0.001) if m.bias is not None: init.constant_(m.bias, 0) def forward(self, x): c1 = self.hs1(self.bn1(self.conv1(x))) c2 = self.layer1(c1) c3 = self.layer2(c2) c4 = self.layer3(c3) c5 = self.layer4(c4) # c5 = self.hs2(self.bn2(self.conv2(c5))) return c1, c2, c3, c4, c5 class MobileNetV3_Small(nn.Module): def __init__(self, pretrained): super(MobileNetV3_Small, self).__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=2, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(16) self.hs1 = hswish() self.layer1 = Block(3, 16, 16, 16, nn.ReLU(inplace=True), SeModule(16), 2) self.layer2 = nn.Sequential( Block(3, 16, 72, 24, nn.ReLU(inplace=True), None, 2), Block(3, 24, 88, 24, nn.ReLU(inplace=True), None, 1), ) self.layer3 = nn.Sequential( Block(5, 24, 96, 40, hswish(), SeModule(40), 2), Block(5, 40, 240, 40, hswish(), SeModule(40), 1), Block(5, 40, 240, 40, hswish(), SeModule(40), 1), Block(5, 40, 120, 48, hswish(), SeModule(48), 1), Block(5, 48, 144, 48, hswish(), SeModule(48), 1), ) self.layer4 = nn.Sequential( Block(5, 48, 288, 96, hswish(), SeModule(96), 2), Block(5, 96, 576, 96, hswish(), SeModule(96), 1), Block(5, 96, 576, 96, hswish(), SeModule(96), 1), ) self.conv2 = nn.Conv2d(96, 576, kernel_size=1, stride=1, padding=0, bias=False) self.bn2 = nn.BatchNorm2d(576) self.hs2 = hswish() self.init_params() def init_params(self): for m in self.modules(): if isinstance(m, nn.Conv2d): init.kaiming_normal_(m.weight, mode='fan_out') if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): init.constant_(m.weight, 1) init.constant_(m.bias, 0) elif isinstance(m, nn.Linear): init.normal_(m.weight, std=0.001) if m.bias is not None: init.constant_(m.bias, 0) def forward(self, x): c1 = self.hs1(self.bn1(self.conv1(x))) c2 = self.layer1(c1) c3 = self.layer2(c2) c4 = self.layer3(c3) c5 = self.layer4(c4) # c5 = self.hs2(self.bn2(self.conv2(c5))) return c1, c2, c3, c4, c5 if __name__ == '__main__': import time device = torch.device('cpu') net = MobileNetV3_Large(pretrained=False) net.eval() x = torch.zeros(1, 3, 608, 800).to(device) start = time.time() y = net(x) print(time.time() - start) for u in y: print(u.shape) torch.save(net.state_dict(), f'MobileNetV3_Large111.pth')
就像之前所说的:只有在更深层次使用h-swish才能得到比较大的好处。所以在上面的网络模型中,不论大小,作者只在模型的后半部分使用h-swish。
用谷歌pixel 1/2/3来对大小V3进行测试的结果。
v3实验结果
Image Classification
Detection V3
Semantic Segmentation V3
感觉实验结果没什么好说的。
对了,有一点值得说一下,训练V3用的是4x4 TPU Pod,batch size 409...(留下了贫穷的泪水)
为什么MobileNet会这么快?
在写这篇文章的时候看到了一篇文章Why MobileNet and Its Variants (e.g. ShuffleNet) Are Fast?,这也让我有了一样的一个问题,这篇文章主要是从结构方面进行了讨论,从深度可分离卷积到组卷积的参数计算量等,因为之前的文章都有写过,在这里就不赘述了,感兴趣的同学可以翻阅下之前的文章。
在这里换一个的角度。我们直接从用时多少的角度去讨论下这个问题。
下图来自Caffe作者贾扬清的博士论文:
该图是AlexNet网络中不同层的GPU和CPU的时间消耗,我们可以清晰的看到,不管是在GPU还是在CPU运行,最重要的“耗时杀手”就是conv,卷积层。也就是说,想要提高网络的运行速度,就得到提高卷积层的计算效率。
我们以MobileNetV1为主,看看MobileNet的资源分布情况:
可以看到,MobileNet的95%的计算都花费在了1×1的卷积上,那1×1卷积有什么好处吗?
我们都知道,卷积操作就是如下图所示的乘加运算:
在计算机操作时,需要将其存入内存当中再操作(按照“行先序”):
这样一来,特征图y11,y12,y21,y22的计算如下所示:
按照卷积计算,实线标注出卷积计算中的访存过程(对应数据相乘),我们可以看到这一过程是非常散乱和混乱的。直接用卷积的计算方式是比较愚蠢的。
这时候就要用到im2col操作。
im2col
一句话来介绍im2col操作的话,就是通过牺牲空间的手段(约扩增K×K倍),将特征图转换成庞大的矩阵来进行卷积计算。
其实思路非常简单:
把每一次循环所需要的数据都排列成列向量,然后逐一堆叠起来形成矩阵(按通道顺序在列方向上拼接矩阵)。
比如Ci×Wi×Hi大小的输入特征图,K×K大小的卷积核,输出大小为Co×Wo×Ho,
输入特征图将按需求被转换成(K∗K)×(Ci∗Wo∗Ho)的矩阵,卷积核将被转换成Co×(K∗K)的矩阵,
然后调用GEMM(矩阵乘矩阵)库加速两矩阵相乘也就完成了卷积计算。由于按照计算需求排布了数据顺序,每次计算过程中总是能够依次访问特征图数据,极大地提高了计算卷积的速度。(不光有GEMM,还有FFt(快速傅氏变换))
换一种表示方法能更好地理解,图片来自High Performance Convolutional Neural Networks for Document Processing:
这样可以更清楚的看到卷积的定义进行卷积操作(上图上半部分),内存访问会非常不规律,以至于性能会非常糟糕。而Im2col()以一种内存访问规则的方式排列数据,虽然Im2col操作增加了很多数据冗余,但使用Gemm的性能优势超过了这个数据冗余的劣势。
所以标准卷积运算大概就是这样的一个过程:
那我们现在回到1×1的卷积上来,有点特殊。按照我们之前所说的,1×1的卷积的原始储存结构和进行im2col的结构如下图所示:
可以看到矩阵是完全相同的。标准卷积运算和1×1卷积运算对比如下图:
也就是说,1x1卷积不需要im2col的过程,所以底层可以有更快的实现,拿起来就能直接算,大大节省了数据重排列的时间和空间。
当然,这些也不是那么绝对的,因为毕竟MobileNet速度快不快,与CONV1x1运算的优化程度密切相关。如果使用了定制化的硬件(比如用FPGA直接实现3x3的卷积运算单元),那么im2col就失去了意义而且反而增加了开销。
回到之前的MobileNet的资源分布,95%的1×1卷积和优化的网络结构就是MobileNet能如此快的原因了。
Reference
MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
MobileNetV2: Inverted Residuals and Linear Bottlenecks
Xception: Deep Learning with Depthwise Separable Convolutions
Simplifying ConvNets for Fast Learning
a-basic-introduction-to-separable-convolution
https://mp.weixin.qq.com/s/O2Bhn66cWCN_87P52jj8hQ
http://machinethink.net/blog/mobilenet-v2/
如何评价 Google 最新的模型 MnasNet? - 曲晓峰的回答 - 知乎
Learning Semantic Image Representations at a Large Scale 贾扬清博士论文
在 Caffe 中如何计算卷积? - 贾扬清的回答 - 知乎
High Performance Convolutional Neural Networks for Document Processing