前言
MobileNet 系列 是 Andrew G. Howard(Google Inc.) 等人于 2017 年(其实是 2016 年先于 Xception 已经提出,但是直到 2017 年才挂到 arXiv 上)在 MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications 中提出的一种网络结构,这种网络结构的特点是模型小,计算速度快,适合部署到移动端或者嵌入式系统中。 之后原作者团队对模型进行了进一步改进,提出V2和V3版本,如GoogLeNet一样,也是一项系列文章。
MobileNetV1论文下载链接:
https://arxiv.org/pdf/1704.04861.pdf%EF%BC%89
MobileNetV2论文下载链接:
https://openaccess.thecvf.com/content_cvpr_2018/papers/Sandler_MobileNetV2_Inverted_Residuals_CVPR_2018_paper.pdf
MobileNetV3论文:
http://openaccess.thecvf.com/content_ICCV_2019/papers/Howard_Searching_for_MobileNetV3_ICCV_2019_paper.pdf
一、Motivation
卷积神经网络(CNN)已经普遍应用在计算机视觉领域,并且已经取得了不错的效果。但是,近几年来CNN在ImageNet竞赛的表现可以看到,为了追求分类准确度,模型深度越来越深,模型复杂度也越来越高,如深度残差网络(ResNet)其层数已经多达152层。然而,在某些真实的应用场景如移动或者嵌入式设备,如此大而复杂的模型是难以被应用的。
首先是模型过于庞大,面临着内存不足的问题,其次一些场景要求低延迟,或者说响应速度要快,想象一下自动驾驶汽车的行人检测系统如果速度很慢会发生什么可怕的事情。所以,研究小而高效的CNN模型在这些场景至关重要,至少目前是这样,尽管未来硬件也会越来越快。
目前的研究总结来看分为两个方向:
一是对训练好的复杂模型进行压缩得到小模型,也就是业界常说的模型量化;
二是直接设计小模型并进行训练。
不管如何,其目标在保持模型性能(accuracy)的前提下降低模型大小(parameters size),同时提升模型速度(speed, low latency)。
本文的主角MobileNet系列的网络属于后者,是Google最近提出的一种小巧而高效的CNN模型,其在accuracy和latency之间做了折中。下面对MobileNet做详细的介绍。
二、MobileNetV1
1.深度可分离卷积
MobileNetV1之所以轻量,与深度可分离卷积的关系密不可分。
如上图所示,模型推理中卷积操作占用了大部分的时间,因此MobileNetV1使用了深度可分离卷积对卷积操作做了进一步的优化,具体解释如下:
常规卷积操作
对于5x5x3的输入,如果想要得到3x3x4的feature map,那么卷积核的shape为3x3x3x4;如果padding=1,那么输出的feature map为5x5x4,如下图:
卷积层共4个Filter,每个Filter包含一个通道数为3(与输入信息通道相同),且尺寸为3×3的kernel。因此卷积层的参数数量可以用如下公式来计算(即:卷积核W x 卷积核H x 输入通道数 x 输出通道数):
N_std = 4 × 3 × 3 × 3 = 108
计算量可以用如下公式来计算(即:卷积核W x 卷积核H x (图片W-卷积核W+1) x (图片H-卷积核H+1) x 输入通道数 x 输出通道数):
C_std =3*3*(5-2)*(5-2)*3*4=972
深度可分离卷积 主要是两种卷积变体组合使用,分别为逐通道卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)。
逐通道卷积(Depthwise Convolution)
Depthwise Convolution的一个卷积核只有一个通道,输入信息的一个通道只被一个卷积核卷积,这个过程产生的feature map通道数和输入的通道数完全一样,如下图所示:
一张5×5像素、三通道彩色输入图片(shape为5×5×3),Depthwise Convolution每个卷积核只负责计算输入信息的某一个通道。卷积核的数量与输入信息的通道数相同。所以一个三通道的图像经过运算后一定是生成了3个Feature map。卷积核的shape即为:(卷积核W x 卷积核H x 输入通道数)。
此时,卷积部分的参数个数计算如下( 卷积核Wx卷积核Hx输入通道数),即:
N_depthwise = 3 × 3 × 3 = 27
计算量为( 卷积核W x 卷积核H x (图片W-卷积核W+1) x (图片H-卷积核H+1) x 输入通道数)即:
C_depthwise=3x3x(5-2)x(5-2)x3=243
Depthwise Convolution完成后的Feature map数量与输入层的通道数相同,无法在通道维度上扩展或压缩Feature map的数量。而且这种运算对输入层的每个通道独立进行卷积运算,没有有效的利用不同通道在相同空间位置上的features的相关性。简而言之,虽然减少了计算量,但是失去了通道维度上的信息交互。因此需要Pointwise Convolution来将这些Feature maps进行组合生成新的Feature maps。
逐点卷积(Pointwise Convolution)
Pointwise Convolution的运算与常规卷积运算非常相似,其实就是1X1的卷积。它的卷积核的尺寸为 1×1×M,M为上一层输出信息的通道数。所以这里Pointwise Convolution的每个卷积核会将上一步的feature maps在通道方向上进行加权组合,生成新的feature map。有几个卷积核就有几个新的feature maps输出。
由于采用的是1×1卷积的方式,此步中卷积涉及到的参数个数可以计算为(1 x 1 x 输入通道数 x 输出通道数):
N_pointwise = 1 × 1 × 3 × 4 = 12
计算量则为(1 x 1 特征层W x 特征层H x 输入通道数 x 输出通道数):
C_pointwise = 1 × 1 × 3 × 3 × 3 × 4 = 108
经过Pointwise Convolution之后,由四个卷积核输出了4张Feature map,与常规卷积的输出维度相同。
参数对比
回顾一下,常规卷积的参数个数为:
N_std = 4 × 3 × 3 × 3 = 108
Depthwise Separable Convolution的参数由两部分相加得到:
N_depthwise = 3 × 3 × 3 = 27
N_pointwise = 1 × 1 × 3 × 4 = 12
N_separable = N_depthwise + N_pointwise = 39
相同的输入,同样是得到4张Feature map,Separable Convolution的参数个数是常规卷积的约1/3。因此,在参数量相同的前提下,采用Depthwise Separable Convolution的神经网络层数可以做的更深。
计算量对比
回顾一下,常规卷积的计算量为:
C_std =33(5-2)*(5-2)34=972
Depthwise Separable Convolution的计算量由两部分相加得到:
C_depthwise=3x3x(5-2)x(5-2)x3=243
C_pointwise = 1 × 1 × 3 × 3 × 3 × 4 = 108
C_separable = C_depthwise + C_pointwise = 351
相同的输入,同样是得到4张Feature map,Separable Convolution的计算量也是常规卷积的约1/3。因此,在计算量相同的情况下,Depthwise Separable Convolution可以将神经网络层数可以做的更深。
2. MBconv
前面讲述了depthwise separable convolution,这是MobileNet的基本操作,但是在真正应用中会加入batchnorm,并使用ReLU激活函数,所以depthwise separable convolution的基本结构如下图右面所示, 左面是正常的conv:
整体网络就是通过不断堆叠MBconv组件组成的,另外值得提一句的是深度可分卷积并不是 MobileNet 首次提出的,仅仅是利用这一变换来达到减少参数量和计算量的目的。
3.Model Architecture
MobileNet的网络结构如表1所示。首先是一个3x3的标准卷积,然后后面就是堆积depthwise separable convolution,并且可以看到其中的部分depthwise convolution会通过strides=2进行down sampling。经过 卷积提取特征后再采用average pooling将feature变成1x1,根据预测类别大小加上全连接层,最后是一个softmax层。
如果单独计算depthwise convolution和pointwise convolution,整个网络有28层(这里Avg Pool和Softmax不计算在内)。我们还可以分析整个网络的参数和计算量分布,如下面第二张图所示。
可以看到整个计算量基本集中在1x1卷积上,如果你熟悉卷积底层实现的话,你应该知道卷积一般通过一种im2col方式实现,其需要内存重组,但是当卷积核为1x1时,其实就不需要这种操作了,底层可以有更快的实现。对于参数也主要集中在1x1卷积,除此之外还有就是全连接层占了一部分参数。
4. 小结
以上简单介绍了Google提出的移动端模型MobileNetV1模型,其核心是采用了可分解的depthwise separable convolution,其不仅可以降低模型计算复杂度,而且可以大大降低模型的参数量。另外,值得一提的是,文中将激活函数从Relu替换成Relu6。在真实的移动端应用场景,像MobileNet这样类似的网络将是持续研究的重点。
三、MobileNetV2
Andrew G. Howard 等于 2018 年在 MobileNet V1 的基础上又提出了改进版本 MobileNet V2。具体可以参考原始论文 MobileNetV2: Inverted Residuals and Linear Bottlenecks。
从标题我们就可以看出,V2 中主要用到了 Inverted Residuals 和 Linear Bottlnecks。
1. Inverted Residuals
上一篇我们看到 V1 的网络结构还是非常传统的直桶模型(没有旁路),但是 ResNet 在模型中引入旁路并取得了很好的效果,因此到了 V2 的时候,作者也想引入进来,这就有了我们要探讨的问题了。
首先我们看下ResNet BottleNeck Block。下图可以看到,采用 1x1 的卷积核先将 256 维度降到 64 维,经过 3x3 的卷积之后,然后又通过 1x1 的卷积核恢复到 256 维。
我们要把 ResNet BottleNeck Block 运用到 MobileNet 中来的话,如果我们还是采用相同的策略显然是有问题的,因为 MobileNet 中由于逐通道卷积,本来 feature 的维度就不多,如果还要先压缩的话,会使模型太小了,所以作者提出了 Inverted Residuals,即先扩展(6倍)后压缩,这样就不会使模型被压缩的太厉害。 下图对比了原始残差和反转残差结构:
2. Linear Bottlnecks
Linear Bottlnecks 听起来很高级,其实就是把上面的 Inverted Residuals block 中的 bottleneck 处的 ReLU 去掉。通过下面的图片对比就可以很容易看出,实际上就是去掉了最后一个 1x1 卷积后面的 ReLU。整体的网络模型就是由堆叠下图右图的Bottlenecks搭建成的。
那为什么要去掉呢?而且为什么是去掉最后一个1X1卷积后面的 ReLU 呢?因为在训练 MobileNet V1 的时候发现最后 Depthwise 部分的 kernel 训练容易失去作用,最终再经过ReLU出现输出为0的情况。作者发现是因为ReLU 会对 channel 数较低的张量造成较大的信息损耗,因此执行降维的卷积层后面不会接类似于ReLU这样的非线性激活层。说人话就是:1X1卷积降维操作本来就会丢失一部分信息,而加上 ReLU 之后那是雪上加霜,所以去掉 ReLU 缓一缓。
3. Model Architecture
完整的MobileNetV2的网络结构参数如下:
t代表反转残差中第一个1X1卷积升维的倍数;c代表通道数;n代表堆叠bottleneck的次数;s代表DWconv的幅度(1或2),不同的步幅对应了不同的模块,详见上图(d)MobilenetV2。
效果上,在 ImageNet 图像分类的任务中,相比 V1 参数量减少了,效果也更好了,详见下图:
4. 小结
MobileNetV2最大的贡献就是改进了通道数较少的网络运用残差连接的方式:设计了反转残差(Inverted Residuals)的结构。另外,提出了Linear Bottlnecks的模型设计技巧。
四、MobileNetV3
V3 保持了一年一更的节奏,Andrew G. Howard 等在 2019 年又提出了 MobileNet V3。文中提出了两个网络模型, MobileNetV3-Small 与 MobileNetV3-Large 分别对应对计算和存储要求低和高的版本。具体可以参考原始论文 Searching for MobileNetV3。
这回的标题(Searching for MobileNetV3)说的不是 V3 里面有什么,而是说的 V3 是怎么来的。Searching 说的是网络架构搜索技术(NAS),即 V3 是通过搜索和网络优化而来。
这里我们不详细讨论 NAS网络搜索技术,虽然这是论文的一大亮点。原因是这个技术不是一般人玩得起的…它相当于训练的不是模型参数,而是模型架构。说白了就是设计一个网络模型结构的集合,通过不同网络层的排列组合可以组合出许多许多的模型,再通过NAS搜索技术搜索出最佳的网络结构。这相当于大力出奇迹嘛,将调参工作交给NAS技术去做,实属一种降维打击。
当然,随之而来的缺点也很明显,这需要大量的计算资源才能完成,恐怕只有想GoogLe,Baidu这种可以买显卡当买白开水的公司才有财力去搞这些研究。而且,由于搜索过程中最关注网络的性能,因此最优的的网络结构可能长得五花八门,换一种说法就是层级结构的排列可能比较混乱。
这就导致了两个缺点:
一:网络的可解释性更差,没办法说为啥这么排列性能好,只能说实验得出…
二:这种不规律的模型层级排列也不利于模型的部署。因此经过NAS搜索后的模型一般需要人为的进行进一步调整,让它长的规矩一些。也是性能和部署之间的trade-off(折中)。
当然,NAS网络结构搜索技术还是很强的,一方面是通过设计不同的搜索空间,NAS可以设计不同的搜索目标 。这算是一个比较前沿的方向了。
1. 对 V2 最后几层的修改
作者实验发现 V2 网络最后一部分结构可以优化,如Figure5所示,原始的结构用 1x1 的卷积来调整 feature 的维度,从而提高预测的精度,但是这一部分也会造成一定的延时,为了减少延时,作者把 average pooling 提前,这样的话,这样就提前把 feature 的 size 减下来了(pooling 之后 feature size 从 7x7 降到了 1x1)。这样一来延时减小了,但是试验证明精度却几乎没有降低。
2. h-swish
这个得先说说 swish(也是 google 自家人搞出来的),说是这个激活函数好用,替换 ReLU 可以提高精度,但是这个激活函数(主要是 σ ( x ) \sigma(x)σ(x) 部分)在移动端设备上显得太耗资源,所以作者又提出了一个新的 h-swish 激活函数来取代 swish,效果跟 swish 差不多,但是计算量却大大减少。
具体的数学公式如下:
其中,说明一下ReLU6,卷积之后通常会接一个ReLU非线性激活,在Mobile v1里面使用ReLU6,ReLU6就是普通的ReLU但是限制最大输出值为6(对输出值做clip),这是为了在移动端设备float16的低精度的时候,也能有很好的数值分辨率,如果对ReLU的激活范围不加限制,输出范围为0到正无穷,如果激活值非常大,分布在一个很大的范围内,则低精度的float16无法很好地精确描述如此大范围的数值,带来精度损失。
3. squeeze-and-excite(SE)
Squeeze-and-Excitation Networks(SENet)是由自动驾驶公司Momenta在2017年公布的一种全新的图像识别结构,它通过对特征通道间的相关性进行建模,把重要的特征进行强化来提升准确率。这个结构是2017 ILSVR竞赛的冠军,top5的错误率达到了2.251%,比2016年的第一名还要低25%,可谓提升巨大。SE也一下成为基于通道的注意力机制。
与MobileNetV2相比,MobileNetV3 中增加了 SE 结构,并且将含有 SE 结构部分的 expand layer 的 channel 数减少为原来的 1/4 以减少延迟(但是,从计算时间上看,貌似只是减少了1/2),试验发现这样不仅提高了模型精度,而且整体上延迟也并没有增加。
4. Model Architecture
MobileNetV3的网络模型如下,其中Table1对应着MobileNetV3_Large版的网络结构参数;Table2对应着MobileNetV3_Small版;
值得注意的是:对比 V2 还可以发现, V3模型开始的 conv2d 部分的输出feature map数量减少为原来的一般了,试验发现延迟有所降低,精度没有下降。
至于效果,相比 V2 1.0 来说, V3-Small 和 V3-Large 在性能和精度上各有优势。但是在工程实际中,特别是在移动端上 V2 用的更为广泛,因为 V2 结构更简单,移植更方便,速度也更有优势。
5. 小结
(1)利用NAS网络搜索结构优化了网络架构
(2)使用h-swish激活函数
(3)加入SE模块
五、代码实现
这里给出V1模型搭建的python代码(基于pytorch实现)。
import time
import torch
import torch.nn as nn
import torch.backends.cudnn as cudnn
import torchvision.models as models
from torch.autograd import Variable
class MobileNet(nn.Module):
def __init__(self, n_class=1000):
super(MobileNet, self).__init__()
self.nclass = n_class
def conv_bn(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU(inplace=True)
)
def conv_dw(inp, oup, stride):
return nn.Sequential(
nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
nn.BatchNorm2d(inp),
nn.ReLU(inplace=True),
nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
nn.BatchNorm2d(oup),
nn.ReLU(inplace=True),
)
self.model = nn.Sequential(
conv_bn(3, 32, 2),
conv_dw(32, 64, 1),
conv_dw(64, 128, 2),
conv_dw(128, 128, 1),
conv_dw(128, 256, 2),
conv_dw(256, 256, 1),
conv_dw(256, 512, 2),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 512, 1),
conv_dw(512, 1024, 2),
conv_dw(1024, 1024, 1),
nn.AvgPool2d(7),
)
self.fc = nn.Linear(1024, self.nclass)
def forward(self, x):
x = self.model(x)
x = x.view(-1, 1024)
x = self.fc(x)
return