MobileNet v1 and MobileNet v2

tags: MobileNet

前言

MobileNet(这里叫做MobileNet v1,简称v1)中使用的Depthwise Separable Convolution是模型压缩的一个最为经典的策略,它是通过将跨通道的3×33\times3卷积换成单通道的3×33\times3卷积+跨通道的1×11\times1卷积来达到此目的的。

MobileNet v2 是在v1的Depthwise Separable的基础上引入了残差结构。并发现了ReLU的在通道数较少的Feature Map上有非常严重信息损失问题,由此引入了Linear Bottlenecks和Inverted Residual。

首先在这篇文章中我们会详细介绍两个版本的MobileNet,然后我们会介绍如何使用Keras实现这两个算法。

1. MobileNet v1

1.1 回顾:传统卷积的参数量和计算量

传统的卷积网络是跨通道的,对于一个通道数为MM的输入Feature Map,我们要得到通道数为NN的输出Feature Map。普通卷积会使用NN个不同的DK×DK×MD_K \times D_K \times M以滑窗的形式遍历输入Feature Map,因此对于一个尺寸为DK×DKD_K\times D_K的卷积的参数个数为DK×DK×M×ND_K \times D_K \times M \times N。一个普通的卷积可以表示为:

Gk,l,n=i,j,mKi,j,m,nKk+i1,l+j1,mG_{k,l,n} = \sum_{i,j,m} \mathbf{K}_{i,j,m,n} \cdot \mathbf{K}_{k+i-1, l+j-1, m}

它的一层网络的计算代价约为:

DK×DK×M×N×DW×DHD_K \times D_K \times M \times N \times D_W \times D_H

其中(DW,DH)(D_W, D_H)为Feature Map的尺寸。普通卷积如图1所示。

v1中介绍的Depthwise Separable Convolution就是解决了传统卷积的参数数量和计算代价过于高昂的问题。Depthwise Separable Convolution分成Depthwise Convolution和Pointwise Convolution。

1.2 Depthwise卷积

其中Depthwise卷积是指不跨通道的卷积,也就是说Feature Map的每个通道有一个独立的卷积核,并且这个卷积核作用且仅作用在这个通道之上,如图2所示。

从图2和图1的对比中我们可以看出,因为放弃了卷积时的跨通道。Depthwise卷积的参数数量为DK×DK×MD_K \times D_K \times M。Depthwise Convolution的数学表达式为:

G^k,l,m=i,jK^i,j,nFk+i1,l+j1,m\hat{G}_{k,l,m} = \sum_{i,j} \hat{K}_{i,j,n} \cdot F_{k+i-1, l+j-1, m}

它的计算代价也是传统卷积的1N\frac{1}{N}为:

DK×DK×M×DW×DHD_K \times D_K \times M \times D_W \times D_H

在Keras中,我们可以使用DepthwiseConv2D实现Depthwise卷积操作,它有几个重要的参数:

  • kernel_size:卷积核的尺寸,一般设为3。

  • strides:卷积的步长

  • padding:是否加边

  • activation:激活函数

由于Depthwise卷积的每个通道Feature Map产生且仅产生一个与之对应的Feature Map,也就是说输出层的Feature Map的channel数量等于输入层的Feature map的数量。因此DepthwiseConv2D不需要控制输出层的Feature Map的数量,因此并没有filters这个参数。

1.3 Pointwise卷积

Depthwise卷积的操作虽然非常高效,但是它仅相当于对当前的Feature Map的一个通道施加了一个过滤器,并不会合并若干个特征从而生成新的特征,而且由于在Depthwise卷积中输出Feature Map的通道数等于输入Feature Map的通道数,因此它并没有升维或者降维的功能。

为了解决这些问题,v1中引入了Pointwise卷积用于特征合并以及升维或者降维。很自然的我们可以想到使用1×11\times1卷积来完成这个功能。Pointwise的参数数量为M×NM\times N,计算量为:

M×N×DW×DHM\times N \times D_W \times D_H

Pointwise的可视化如图3:

1.4 Depthwise Separable卷积

合并1.2中的Depthwise卷积和1.3中的Pointwise卷积便是v1中介绍的Depthwise Separable卷积。它的一组操作(一次Depthwise卷积加一次Pointwise卷积)的参数数量为:DK×DK×M+M×ND_K \times D_K \times M + M\times N是普通卷积的

DK×DK×M+M×NDK×DK×M×N=1N+1DK2\frac{D_K \times D_K \times M + M\times N}{D_K \times D_K \times M \times N} = \frac{1}{N} + \frac{1}{D_K^2}

计算量为:

DK×DK×M×DW×DH+M×N×DW×DHD_K \times D_K \times M \times D_W \times D_H + M\times N \times D_W \times D_H

和普通卷积的比值为:

DK×DK×M×DW×DH+M×N×DW×DHDK×DK×M×N×DW×DH=1N+1DK2\frac{D_K \times D_K \times M \times D_W \times D_H + M\times N \times D_W \times D_H }{D_K \times D_K \times M \times N \times D_W \times D_H} = \frac{1}{N} + \frac{1}{D_K^2}

对于一个3×33\times3的卷积而言,v1的参数量和计算代价均为普通卷积的18\frac{1}{8}左右。

1.5 Mobile v1的Keras实现及实验结果分析

通过上面的分析,我们知道一个普通卷积的一组卷积操作可以拆分成了个Depthwise卷积核一个Pointwise卷积,由此而形成MobileNet v1的结构。在这个实验中我们首先会搭建一个普通卷积,然后再将其改造成v1,并在MNIST上给出实验结果,代码和实验结果见链接CPUGPU

首先我们搭建的传统卷积的结构如下面代码片段:

def Simple_NaiveConvNet(input_shape, k):
    inputs = Input(shape=input_shape)
    x = Conv2D(filters=32, kernel_size=(3,3), strides=(2,2), padding='same', activation='relu', name='n_conv_1')(inputs)
    x = Conv2D(filters=64, kernel_size=(3,3),padding='same', activation='relu', name='n_conv_2')(x)
    x = Conv2D(filters=128, kernel_size=(3,3),padding='same', activation='relu', name='n_conv_3')(x)
    x = Conv2D(filters=128, kernel_size=(3,3), strides=(2,2),padding='same', activation='relu', name='n_conv_4')(x)
    x = GlobalAveragePooling2D(name='n_gap')(x)
    x = BatchNormalization(name='n_bn_1')(x)
    x = Dense(128, activation='relu', name='n_fc1')(x)
    x = BatchNormalization(name='n_bc_2')(x)
    x = Dense(k, activation='softmax', name='n_output')(x)
    model = Model(inputs, x)
    return model

通过将3×33\times3Conv2D()换成3×33\times3DepthwiseConv2D加上1×11\times1Conv2D()(第一层保留传统卷积),我们将其改造成了MobileNet v1。

def Simple_MobileNetV1(input_shape, k):
    inputs = Input(shape=input_shape)
    x = Conv2D(filters=32, kernel_size=(3,3), strides=(2,2), padding='same', activation='relu', name='m_conv_1')(inputs)
    x = DepthwiseConv2D(kernel_size=(3,3),padding='same', activation='relu', name = 'm_dc_2')(x)
    x = Conv2D(filters=64, kernel_size=(1,1),padding='same', activation='relu', name = 'm_pc_2')(x)
    x = DepthwiseConv2D(kernel_size=(3,3), padding='same', activation='relu', name = 'm_dc_3')(x)
    x = Conv2D(filters=128, kernel_size=(1,1),padding='same', activation='relu', name = 'm_pc_3')(x)
    x = DepthwiseConv2D(kernel_size=(3,3), strides=(2,2),padding='same', activation='relu', name = 'm_dc_4')(x)
    x = Conv2D(filters=128, kernel_size=(1,1),padding='same', activation='relu', name = 'm_pc_4')(x)
    x = GlobalAveragePooling2D(name='m_gap')(x)
    x = BatchNormalization(name='m_bn_1')(x)
    x = Dense(128, activation='relu', name='m_fc1')(x)
    x = BatchNormalization(name='m_bc_2')(x)
    x = Dense(k, activation='softmax', name='m_output')(x)
    model = Model(inputs, x)
    return model

通过Summary()函数我们可以得到每个网络的每层的参数数量,见图4,左侧是普通卷积,右侧是MobileNet v1。

普通卷积的参数总量为259,082,去除未改造的部分剩余的参数数量为239,936。v1的参数总量为48,330去掉未改造的部分剩余参数29,184个。两个的比值为239,93629,1848.22\frac{239,936}{29,184} \approx 8.22,符合我们之前的推算。

接着我们在MNIST上跑一下实验,我们在CPU(Intel i7)和GPU(Nvidia 1080Ti)两个环境下运行一下,得到的收敛曲线如图5。在都训练10个epoch的情况下,我们发现MobileNet v1的结果要略差于传统卷积,这点完全可以理解,毕竟MobileNet v1的参数更少。

对比单个Epcoh的训练时间,我们发现了一个奇怪的现象,在CPU上,v1的训练时间约70秒,传统卷积训练时间为140秒,这和我们的直觉是相同的。但是在GPU环境下,传统卷积和v1的训练时间分别为40秒和50秒,v1在GPU上反而更慢了,这是什么原因呢?

问题在于cudnn对传统卷积的并行支持比较完善,而在cudnn7之前的版本并不支持depthwise卷积,现在虽然支持了,其并行性并没有做优化,依旧采用循环的形式遍历每个通道,因此在GPU环境下MobileNet v1反而要慢于传统卷积。所以说,是开源工具慢,并不是MobileNet v1的算法慢

最后,论文中给出了两个超参数α\alphaρ\rho分别用于控制每层的Feature Map的数量以及输入图像的尺寸,由于并没有涉及很多特有知识,这里不过多介绍。

2. MobileNet v2 详解

在MobileNet v2中,作者将v1中加入了残差网络,同时分析了v1的几个缺点并针对性的做了改进。v2的改进策略非常简单,但是在编写论文时,缺点分析的时候涉及了流行学习等内容,将优化过程弄得非常难懂。我们在这里简单总结一下v2中给出的问题分析,希望能对论文的阅读有所帮助,对v2的motivation感兴趣的同学推荐阅读论文。

当我们单独去看Feature Map的每个通道的像素的值的时候,其实这些值代表的特征可以映射到一个低维子空间的一个流形区域上。在进行完卷积操作之后往往会接一层激活函数来增加特征的非线性性,一个最常见的激活函数便是ReLU。根据我们在残差网络中介绍的数据处理不等式(DPI),ReLU一定会带来信息损耗,而且这种损耗是没有办法恢复的,ReLU的信息损耗是当通道数非常少的时候更为明显。为什么这么说呢?我们看图6中这个例子,其输入是一个表示流形数据的矩阵,和卷机操作类似,他会经过nn个ReLU的操作得到nn个通道的Feature Map,然后我们试图通过这nn个Feature Map还原输入数据,还原的越像说明信息损耗的越少。从图6中我们可以看出,当nn的值比较小时,ReLU的信息损耗非常严重,当时当nn的值比较大的时候,输入流形就能还原的很好了。

根据对上面提到的信息损耗问题分析,我们可以有两种解决方案:

  1. 既然是ReLU导致的信息损耗,那么我们就将ReLU替换成线性激活函数;

  2. 如果比较多的通道数能减少信息损耗,那么我们就使用更多的通道。

2.1 Linear Bottlenecks

我们当然不能把ReLU全部换成线性激活函数,不然网络将会退化为单层神经网络,一个折中方案是在输出Feature Map的通道数较少的时候也就是bottleneck部分使用线性激活函数,其它时候使用ReLU。代码片段如下:

def _bottleneck(inputs, nb_filters, t):
    x = Conv2D(filters=nb_filters * t, kernel_size=(1,1), padding='same')(inputs)
    x = Activation(relu6)(x)
    x = DepthwiseConv2D(kernel_size=(3,3), padding='same')(x)
    x = Activation(relu6)(x)
    x = Conv2D(filters=nb_filters, kernel_size=(1,1), padding='same')(x)
    # do not use activation function
    if not K.get_variable_shape(inputs)[3] == nb_filters:
        inputs = Conv2D(filters=nb_filters, kernel_size=(1,1), padding='same')(inputs)
    outputs = add([x, inputs])
    return outputs

这里使用了MobileNet中介绍的ReLU6激活函数,它是对ReLU在6上的截断,数学形式为:

ReLU(6)=min(max(0,x),6)ReLU(6) = min(max(0,x),6)

图7便是结合了残差网络和线性激活函数的MobileNet v2的一个block,最右侧是v1。

2.2 Inverted Residual

当激活函数使用ReLU时,我们可以通过增加通道数来减少信息的损耗,使用参数tt来控制,该层的通道数是输入Feature Map的tt倍。传统的残差块的tt一般取小于1的小数,常见的取值为0.1,而在v2中这个值一般是介于5105-10之间的数,在作者的实验中,t=6t=6。考虑到残差网络和v2的tt的不同取值范围,他们分别形成了锥子形(两头小中间大)和沙漏形(两头大中间小)的结构,如图8所示,其中斜线Feature Map表示使用的是线性激活函数。这也就是为什么这种形式的卷积block被叫做Interved Residual block,因为他把short-cut转移到了bottleneck层。

2.3 MobileNet v2

综上我们可以得到MobileNet v2的一个block的详细参数,如图9所示,其中ss代表步长:

MobileNet v2的实现可以通过堆叠bottleneck的形式实现,如下面代码片段

def MobileNetV2_relu(input_shape, k):
    inputs = Input(shape = input_shape)
    x = Conv2D(filters=32, kernel_size=(3,3), padding='same')(inputs)
    x = _bottleneck_relu(x, 8, 6)
    x = MaxPooling2D((2,2))(x)
    x = _bottleneck_relu(x, 16, 6)
    x = _bottleneck_relu(x, 16, 6)
    x = MaxPooling2D((2,2))(x)
    x = _bottleneck_relu(x, 32, 6)
    x = GlobalAveragePooling2D()(x)
    x = Dense(128, activation='relu')(x)
    outputs = Dense(k, activation='softmax')(x)
    model = Model(inputs, outputs)
    return model

3. 总结

在这篇文章中,我们介绍了两个版本的MobileNet,它们和传统卷积的对比如图10。

如图(b)所示,MobileNet v1最主要的贡献是使用了Depthwise Separable Convolution,它又可以拆分成Depthwise卷积和Pointwise卷积。MobileNet v2主要是将残差网络和Depthwise Separable卷积进行了结合。通过分析单通道的流形特征对残差块进行了改进,包括对中间层的扩展(d)以及bottleneck层的线性激活(c)。Depthwise Separable Convolution的分离式设计直接将模型压缩了8倍左右,但是精度并没有损失非常严重,这一点还是非常震撼的。

Depthwise Separable卷积的设计非常精彩但遗憾的是目前cudnn对其的支持并不好,导致在使用GPU训练网络过程中我们无法从算法中获益,但是使用串行CPU并没有这个问题,这也就给了MobileNet很大的市场空间,尤其是在嵌入式平台。

最后,不得不承认v2的论文的一系列证明非常精彩,虽然没有这些证明我们也能明白v2的工作原理,但是这些证明过程还是非常值得仔细品鉴的,尤其是对于从事科研方向的工作人员。

最后更新于