MobileNet v2

MobileNet v2 是 v1 的改进,随后 shufflenetv1 又对 mobilenetv1 进行了改进,并且结合 resnet 构建思想采用了 botttleneck 结构,相比之下,mobilenetv1 设计思想就显得比较陈旧了(没有 block 的概念,也没有残差连接),故这些问题都在 MobileNet v2 中进行了改进,效果提升非常明显。主要创新点是:

  • 提出线性瓶颈层 Linear Bottlenecks,也就是去掉了 1x1 降维输出层后面的非线性激活层,目的是为了保证模型的表达能力

  • 提出反转残差块 Inverted Residual block,该结构和传统 residual block 中维度先缩减再扩增正好相反,因此 shotcut 也就变成了连接的是维度缩减后的 feature map

简单来说,MobileNet v2 是基于 mobilenetv1 的通道可分离卷积优势,然后再加入了残差模块功能,最后通过理论分析和实验证明,对残差模块进行了一定程度的修改,使其在移动端发挥最大性能。

1 算法设计

1.1 算法核心

mobilenetV1 主要是引入了 depthwise separable convolution 代替传统的卷积操作,实现了 spatial 和 channel 之间的解耦,达到模型加速的目的,但是整个模型非常类似 vgg,思想比较陈旧,最主流的做法应该是参考 resnet 设计思想,引入 block 和残差设计。

而且作者通过实验发现 mobilenetV1 结构存在比较大的缺陷。在实际使用的时候, 发现 Depthwise 部分的 kernel 比较容易训废掉即训完之后发现 depthwise 训出来的 kernel 有不少是空的即大部分输出为 0,这个问题在定点化低精度训练的时候会进一步放大。针对这个问题,本论文进行了深入分析。

假设经过激活层后的张量被称为兴趣流形(manifold of interest),shape 为 HxWxD。根据前人研究,兴趣流形可能仅分布在激活空间的一个低维子空间里,或者说兴趣流形是可以用一个低维度空间来表达的,简单来说是说神经网络某层特征,其主要信息可以用一个低维度特征来表征。一般来说我们都会采用类似 resnet 中的 bottlenck 层来进行主要流形特征提取,在具体实现上,通常使用 1x1 卷积将张量降维,但由于 ReLU 的存在,这种降维实际上会损失较多的信息,特别是在通道本身就必须小的时候。

如上图所示,利用 MxN 的矩阵 T 将张量(2D,即 dim=2)变换到 M(M 可以任意设置)维的空间中,通过 ReLU 后(y=ReLU(Bx)),再用此矩阵 T 的逆恢复原来的张量。可以看到,当 M 较小时,恢复后的张量坍缩严重,M 较大(30)时则恢复较好。这意味着,在较低维度的张量表示(兴趣流形)上进行 ReLU 等线性变换会有很大的信息损耗。显然,当把原始输入维度增加到 15 或 30 后再作为 ReLU 的输入,输出恢复到原始维度后基本不会丢失太多的输入信息;相比之下如果原始输入维度只增加到 2 或 3 后再作为 ReLU 的输入,输出恢复到原始维度后信息丢失较多。因此在 MobileNet V2 中,执行降维的卷积层后面不会接类似 ReLU 这样的非线性激活层,也就是所提的 linear bottleneck。总结如下:

  • 对于 ReLU 层输出的非零值而言,ReLU 层起到的就是一个线性变换的作用

  • ReLU 层可以保留输入兴趣流形的信息,但是只有当输入兴趣流形是输入空间的一个低维子空间时才有效。

如果上述不好理解的话,那么通俗理解就是:当采用 1x1 逐点卷积进行降维,如果原始降维前输入通道本身就比较小,那么经过 Relu 层后会损失很多信息,导致不断堆叠层后有效信息越来越小,到最后分类性能肯定会下降,就会表现出很多 kernel 出现全 0 的现象,特别是逐深度卷积+1x1 逐点卷积时候现象更加明显,因为逐深度卷积表达能力不如标准卷积。

1.2 Linear Bottlenecks

基于上述分析,为了保证信息不会丢失太多,在 Bottlenecks 层的降维后不在接 relu,图示如下:

蓝色块表示特征图,浅色块表示下一个 block 的开始,红色块表示卷积或者 Relu 操作,含有斜线的块表示不包括非线性层

  • (a)为标准卷积

  • (b)为逐深度可分离卷积

  • (c)为在(b)后面加入“bottlenck layer”,即在后面接了一个不含有 relu 层的 1x1 卷积进行通道扩张,防止信息丢失过多

  • 考虑到 block 是互相堆积的,调整一下视角,将“bottlenck layer”看成 block 的输入,那么这种结构也等价于(d),block 中开始的 1x1 卷积层称为“expansion layer”,它的通道大小和输入 bottleneck 层的通道大小之比,称为扩展比(expansion ratio)。扩展层之后是 depthwise 卷积,然后采用 1x1 卷积得到 block 的输出特征,这个卷积后面没有非线性激活

1.3 Inverted residuals

(a)为标准的残差块,而(b)为所提出的反转残差块,为啥叫做反转?原始是残差块一般先采用 bottleneck layer(1x1 卷积)进行降维,最后在采用 bottleneck layer 进行扩展,而反转的做法是先采用 bottleneck layer(1x1 卷积)进行升维,最后在采用 bottleneck layer 进行降维,是相当于残差块的反向操作,其中斜线块表示在该特征图上面没有采用 relu 激活函数,通过实验表明反转残差块可以在实现上减少内存的使用,并且精度更少。

请注意:上图(b)应该是绘制错误,论文里面说过在 1x1 降维层不采用 relu 激活函数,故实际上应该是 1x1conv 升维度(有 relu)+3x3d conv+1x1conv 降维(没有 relu),但是上面图明显绘制错误了。通过下面图示也可以看出来:

当 stride=2 的时候没有采用残差设计,代码如下:

class InvertedResidual(nn.Module):
    def __init__(self,
                 in_channels,
                 out_channels,
                 stride,
                 expand_ratio,
                 conv_cfg=None,
                 norm_cfg=dict(type='BN'),
                 act_cfg=dict(type='ReLU6')):
        super(InvertedResidual, self).__init__()
        self.stride = stride
        # 扩张后通道维度
        hidden_dim = int(round(in_channels * expand_ratio))
        layers = []
        if expand_ratio != 1:
            # 1x1卷积先扩展维度,有relu6
            layers.append(
                ConvModule(
                    in_channels=in_channels,
                    out_channels=hidden_dim,
                    kernel_size=1,
                    conv_cfg=conv_cfg,
                    norm_cfg=norm_cfg,
                    act_cfg=act_cfg))
        layers.extend([
            #3x3 dw卷积,有relu6
            ConvModule(
                in_channels=hidden_dim,
                out_channels=hidden_dim,
                kernel_size=3,
                stride=stride,
                padding=1,
                groups=hidden_dim,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=act_cfg),
            # 1x1 pw卷积,没有relu
            ConvModule(
                in_channels=hidden_dim,
                out_channels=out_channels,
                kernel_size=1,
                conv_cfg=conv_cfg,
                norm_cfg=norm_cfg,
                act_cfg=None)
        ])
        self.conv = nn.Sequential(*layers)

    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)

总结一下,本文主要是提出了 linear bottleneck,和 inverted residual 两个改进,其中针对 1x1 点卷积后接 relu 会丢失大量信息,而提出了 linear bottleneck,在该改进基础上配合残差块的设计思想,最终提出了 inverted residual block。

图片来源:MobileNet version 2,该博客认为反转残差块的设计思想可以将 expansion layer 可以看做解压器,类似 unzip,将特征恢复到高维空间,而 depthwise layer 可以看成过滤器,筛选比较重要的特征,最后 projection layer 将其压缩到低维空间。

1.4 网络设计

基于 inverted residual block 就可以构建整个 MobileNet v2 了。

t 是扩张系数,c 是输出通道数,n 是重复次数,s 是 stride。可以看出这个基础网络都没有 pool 操作,而是采用 stride 代替。

实际上 v2 也设计了宽度因子和分辨率因子两个超参数控制网络的参数量,默认是默认宽度因子是 1.0,输入大小是 224x224,分辨率因子影响的是特征图空间大小,而宽度因子影响的是特征图 channel 大小。输入大小可以从 96 到 224,而宽度因子可以从 0.35 到 1.4。值得注意的一点是当宽度因子小于 1 时,不对最后一个卷积层的 channel 进行调整以保证性能,即维持 1280。

2 实验结果

在 imagenet 图像分类任务上面进行训练,结果如上,在同样参数大小下,MobileNet v2 比 MobileNetv1 和 ShuffleNet 要好,而且速度也更快一些。

上表可以看出 MobileNet V2 + SSDLite 达到了极高的性能,且速度非常块。

3 总结

MobileNet v2 主要是对 v1 进行针对性改进,针对 dwconv+pwconv 后经过 relu 激活层会丢失大量信息,导致 dw 很多 kernel 都失活,提出了线性瓶颈层,主要是 pw 卷积降维时候不采用 relu,接着在该基础上结合残差设计思想提出了反转残差块,通过堆叠反转残差块实现了在移动端上又快又好的模型。多个任务 benchmark 平台结果表明比 mobilenetv1 和 shufflenetv1 更加优异。

4 参考文献

Last updated