MobileNet系列—mobileNetV1

最近在利用SSD检测物体时,由于实际项目要求,需要对模型进行轻量化,所以考虑利用轻量网络替换原本的骨架VGG16,查找一些资料后最终采用了google开源的mobileNetV2。这里对学习mobileNet系列的过程做一些总结

mobileNetV1是由google在2017年发布的一个轻量级深度神经网络,其主要特点是采用深度可分离卷积替换了普通卷积,2018年提出的mobileNetV2在V1的基础上引入了线性瓶颈 (Linear Bottleneck)和倒残差 (Inverted Residual)来提高网络的表征能力。

1.mobileNetV1

        mobileNet V1是一种体积较小、计算量较少、适用于移动设备的卷积神经网络。mobileNet V1的主要创新点是用深度可分离卷积(depthwise separable convolution)代替普通的卷积,并使用宽度乘数(width multiply)减少参数量,不过减少参数的数量和操作的同时也会使特征丢失导致精度下降。
       
        原文地址: 
https://arxiv.org/abs/1704.04861

1.1 普通卷积和深度可分离卷积

        标准的卷积过程如图1,卷积核做卷积运算时得同时考虑对应图像区域中的所有通道(channel),而深度可分离卷积对不同的通道采用不同的卷积核进行卷积,如图2所示它将普通卷积分解成了深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution)两个过程,这样可以将通道(channel)相关性和空间(spatial)相关性解耦。原文中给出的深度可分离卷积后面都接了一个BN和ReLU层。                                             图1 普通卷积                                                    图2 深度可分离卷积                                                                               

1.1.1 标准卷积核

        设输入特征维度为D
F*D
F*M,M为通道数。标准卷积核的参数为D
K*D
K*M*N,DK为卷积核大小,M为输入的通道数, N为输出的通道数。卷积后输出维度为:D
F*D
F*N。卷积过程中每个卷积核对图像区域进行D
F*D
F次扫描,每次扫描的深度为M(channel),每个通道需要D
K*D
K次加权求和运算, 所以
理论计算量(floating point operatios FLOPs)为:N*D
F*D
F*M*D
k*D
K。  

1.1.2 深度可分离卷积

  • 深度卷积:设输入特征维度为D F*D F*M,M为通道数。卷积核的参数为D K*D K*1*M。输出深度卷积后的特征维度为:D F*D F*M。卷积时每个通道只对应一个卷积核(扫描深度为1),所以 FLOPs为:M*D F*D F*D K*D K
  • 逐点卷积:输入为深度卷积后的特征,维度为D F*D F*M。卷积核参数为1*1*M*N。输出维度为D F*D F*N。卷积过程中对每个特征做1*1的标准卷积, FLOPs为:N*D F*D F*M。

1.1.3 深度可分离卷积的优势

  • 参数量:关系到模型大小,通常参数用float32表示,所以模型大小一般时参数量的4倍。标准卷积的参数量为D k*D k*M*N(M为输入通道数, N为输出通道数),深度卷积的参数量为D K*D K*N,逐点卷积的参数量为1*1*M*N,所以深度可分离卷积相对于标准卷积的参数量为(D K*D K*N + M*N)/ D K*D K*M*N = 1/M + 1/D K*D K
  • 计算量: 可以用来衡量算法/模型的复杂度, 通常只考虑乘加操作(Multi-Adds)的数量,而且只考虑 CONV 和 FC 等参数层的计算量,忽略 BN 和PReLU 等等。一般情况,CONV 和 FC 层也会忽略仅纯加操作的计算量,如 bias 偏置加和 shotcut 残差加等。标准卷积的计算量为:N*D F*D F*M*D K*D K,深度可分离卷积的计算量为M*D F*D F*D K*D K+N*D F*D F*M。所以深度可分离卷积的计算量相比于标准卷积为(M*D F*D F*D K*D K+N*D F*D F*M)/ N*D F*D F*M*D K*D K = 1/N + 1/D K*D K。     
  • 区域和通道分离: 深度可分离卷积将以往普通卷积操作同时考虑通道和区域改变(卷积先只考虑区域,然后再考虑通道),实现了通道和区域的分离。

      
举个例子:       假设输入特征的维度为224*224*3,卷积核大小为3*3,输出通道数为2,设置pad=1,stride=1,则如下图(图a)所示,标准卷积的输出维度为224*224*2。参数量为3*3*3*2=54,计算量为2*224*224*3*3*3=2709504。      在
深度卷积过程中(图b),输入为224*224*3,卷积核参数为3*3*1*3,每个通道做3*3的卷积,收集了每个通道的空间特征(Depthwise特征),输出特征维度为224*224*3。      接着进入
逐点卷积(图c),卷积核参数为1*1*3*2,对Depthwise特征做2个1*1的普通卷积,这样相当于收集了每个点的特征,输出维度为224*224*2。      
深度可分离卷积的参数量为3*3*2 + 3*2 = 24,相比于标准卷积缩减了2.25倍, 计算量为3*224*224*3*3 + 2*224*224*3 = 1655808,相比于标准卷积缩减了1.6倍。                            图a 标准卷积过程                                                                                     图b 深度卷积                                                                                              图c 逐点卷积

1.2 mobileNetV1网络结构

        mobileNetV1的网络结构如Table 1.前面的卷积层中除了第一层为标准卷积层外,其他都是深度可分离卷积(Conv dw + Conv/s1),卷积后接了一个7*7的平均池化层,之后通过全连接层,最后利用Softmax激活函数将全连接层输出归一化到0-1的一个概率值,根据概率值的高低可以得到图像的分类情况。
 

1.3 超参数

  • 宽度因子α(Width Multiplier)

        对于深度可分离卷积层,输入的通道数M乘上一个宽度因子α变为αM,输出通道数变为αN,其中α区间为(0,1],此时深度可分离卷积的参数量为:D
K*D
K*αN + αM*αN = α*α(1/α *D
K*D
K*N + M*N),计算量变为αM*D
F*D
F*D
K*D
K+αN*D
F*D
F*αM = α*α  (1/α*M*D
F*D
F*D
K*D
K+N*D
F*D
F*M),所以参数量和计算量差不多都变为原来的α*α倍。

  • 分辨率因子ρ (Resolution Multiplier)

        ρ改变输入层的分辨率,所以深度可分离卷积的参数量不变,但计算量为M*ρD
F*ρD
F*D
K*D
K+N*ρD
F*ρD
F*M = ρ*ρ(M*D
F*D
F*D
K*D
K+N*D
F*D
F*M),即计算量变为原来的ρ*ρ倍。

1.4 mobileNetV1 实现(基于框架keras / pytorch)

 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import  torch import  torch.nn as nn   def  conv3x3(in_planes, out_planes, stride = 1 , padding = 1 ):      return  nn.Conv2d(in_planes, out_planes, kernel_size = 3 , stride = stride, padding = padding, bias = False )   # why no bias: 如果卷积层之后是BN层,那么可以不用偏置参数,可以节省内存 def  conv1x1(in_planes, out_planes):      return  nn.Conv2d(in_planes, out_planes, kernel_size = 1 , stride = 1 , bias = False   class  DPBlock(nn.Module):      ‘‘‘          Depthwise convolution and Pointwise convolution.      ‘‘‘      def  __init__( self , in_planes, out_planes, stride = 1 ):          super (DPBlock,  self ).__init__()   # 调用基类__init__函数初始化          self .conv1  =  conv3x3(in_planes, out_planes, stride)          self .bn1  =  nn.BatchNorm2d(in_planes)          self .relu  =  nn.ReLU(inplace = True )          self .conv2  =  conv1x1(in_planes, out_planes)          self .bn2  =  nn.BatchNorm2d(out_planes)                def  forward( self , x):          out  =  self .conv1(x)          out  =  self .bn1(out)          out  =  self .relu(out)          out  =  self .conv2(out)          out  =  self .bn2(out)          out  =  self .relu(out)                    return  out           class  mobileNetV1Net(nn.Module):      def  __init__( self , block, num_class = 1000 ):          super (mobileNetV1Net,  self ).__init__()                    self .model  =  nn.Sequential(              conv3x3( 3 32 2 ),              nn.BatchNorm2d( 32 ),              nn.ReLU(inplace = True )              block( 32 64 1 ),              block( 64 128 2 ),              block( 128 128 1 ),              block( 128 256 2 ),              block( 256 256 1 ),              block( 256 512 2 ),              block( 512 512 1 ),              block( 512 512 1 ),              block( 512 512 1 ),              block( 512 512 1 ),              block( 512 512 1 ),              block( 512 1024 2 ),              block( 1024 1024 2 ),              nn.AvgPool2d( 7 )          )          self .fc  =  nn.Linear( 1024 , num_class)                def  forward( self , x):          =  self .model(x)          =  x.view( - 1 1024 )   # reshape          out  =  self .fc(x)                    return  out                mobileNetV1  =  mobileNetV1Net(DPBlock)

 

1.5 参考链接