Classification基础实验系列三——MobileNet论文笔记与复现

14755769-68396a46402b281c.png

一、概述

  本文记录MobileNet论文的阅读笔记以及代码实现。其实本来是想直接看MobileNet v2的,结果v2的论文太难了看不大懂...所以先来看看v1吧🐶

  首先根据知乎大佬的说法,MobileNet并非2017年的创作,而是Google此前两年就有的工作,只是文章一直没发表而已。这也解释了为什么MobileNet看起来是一种非常复古的“直筒结构”,性价比不高。关于“性价比”,大佬是这么解释的:


  “后续一系列的ResNet, DenseNet等结构已经证明通过复用图像特征, 使用concat/eltwise+ 等操作进行融合, 能极大提升网络的性价比”


  其次MobileNet中的Depthwise Conv在训练过程中也有一些问题。这个等到写MobileNet v2笔记时再进一步分析。

1.1 论文主要内容

  本文主要分为三个方面的内容:

  1. 介绍Depthwise Separable Convolution(深度可分离卷积)以及其在计算量方面相对于传统卷积的优势;
  2. 提出了两个超参数Width MultiplierResolution Multiplier ,用于进一步压缩模型的复杂度,减小参数数量。
  3. 大量的实验结果用于对比Depthwise和普通卷积,以及比较不同Multiplier下模型的复杂度和准确率。

二、论文阅读

2.1 Depthwise Separable Convolution

  主要思想是对传统卷积做了一个分解——将传统卷积分成depthwise和pointwise两步来完成。其中depthwise就是对输入特征的每个通道单独使用一个filter,然后对得到的若干个输出通道再使用1x1conv进行组合,得到新特征,即pointwise Conv。第二步也体现了1x1 Conv跨通道整流的特点。

  论文中对此的解释是,标准卷积中,滤波和滤波之后特征的线性组合可以看做两个步骤,深度可分离卷积就是将这两个步骤分开进行。

  值得注意的一点是,这种卷积背后的假设是跨channel相关性和跨spatial相关性的解耦。而这种假设目前还无法证明,但是实际实验结果表明深度可分离卷积确实可以减小参数数量。参考上面知乎帖子大佬的回答:


近两年是深度可分离卷积(Depth-wise Separable Convolution)大红大紫的两年,甚至有跟ResNet的Skip-connection一样成为网络基本组件的趋势。Xception论文指出,这种卷积背后的假设是跨channel相关性和跨spatial相关性的解耦。应用深度可分离卷积的另一个优势即是参数量的节省(这一点其实也是解耦的结果,参数描述上享受了正交性的乘法增益)。然而,这一假设的成立与否还是存疑的,目前我们也没有足够的工具去描述和证明这一假设。


  下面的草图展示了depthwise和pointwise两个层及其计算量。


14755769-fddfd9072e09feeb.png
Fig. 1. 深度可分离卷积示意图

  由于标准卷积的计算量为,用上图的式子除以该式,结果为 (忽略D_G和D_F之间的差距)由于网络中用的卷积核大小D_k均为3x3,使用深度可分离卷积的参数量约为标准卷积的。

14755769-bbbaf0cc6ea8b070.png
Fig. 2. Depthwise和Pointwise视为两层,后面各接一组BN和ReLU

  除此之外,论文中还提到了一个实现细节,就是深度可分离卷积中大部分运算量都来自1x1 Conv,而1x1 Convs "do not require this reordering in memory and can be implemented directly with GEMM which is one of the most optimized numerical linear algebra algorithms.",这里的reordering就是经典的im2col,即caffe中实现卷积的方法。就是说如果空间维度大于1的卷积,为了使用高级优化方法GEMM,需要先用im2col进行矩阵重组。而1x1 Conv节省了这一步,故文章中说1x1 Conv是线性代数数值计算中的最优算法之一。详细了解im2col可以见这篇博客。注意caffe中是按行优先,而Matlab是按列优先。

14755769-5a05b0a91f61eb7a.png
Fig. 3. im2col与reordering

2.2 Width Multiplier与Resolution Multiplier

  • Width Multiplier
    对各层使用同一个缩放因子来减少滤波器个数,使输入通道数变为,输出通道数变为。通常在1, 0.75, 0.5, 0.25中选择。1为标准MobileNet,其他三个为精简的MobileNet。节省的计算量约为 。

  • Resolution Multiplier
    对网络输入尺寸使用一个缩放因子,进而使得每一层feature map尺寸都变成原来的倍。这个缩放因子实际上就是选择不同的输入尺寸,没有像Width Multiplier那样显式设置。该因子节省的计算量也约为。

2.3 实验分析

2.3.1 深度可分离卷积带来的性能下降可以接收
14755769-cb6f7154b7c9b892.png
Fig. 4. 将MobileNet中的卷积全部换成普通卷积,对比结果
2.3.2 Shallower or thinner?

  在对原始MobileNet进行进一步压缩时,一个问题是为什么我们要使用Width Multiplier,而不是简单地减少模型的层数?文章通过实验证明,使用缩减网络比减小网络层数性能要好。

14755769-de07ca3e4c7af3f7.png
Fig. 5. Narrow v.s. Shallow MobileNet

2.3.3 Width Multiplier可以设多大?

  这个应该是大家最关心的实验了。本实验证明在从1降到0.5的过程中,模型性能是平滑下降的;当从0.5降到0.25时,性能大幅下降。可见对模型性能影响较小,通常是一个值得尝试的选择。

14755769-3b4615c9a52f4c58.png
Fig. 6. α的影响

2.3.4 输入分辨率的影响?

  随着输入分辨率的降低,模型性能的下降趋势较平滑:


14755769-de7102249097eaee.png
Fig. 7. ρ的影响
2.3.5 其他实验

  本文还比较了MobileNet与其他网络的性能。比如1.0MobileNet-224要强于GoogLeNet,稍逊于VGG16,但是相比VGG16,模型大小缩小了32倍,计算量少了27倍!另外,0.5 MobileNet-160的性能就超过SqueezeNet和AlexNet了。

三、代码实现

3.1 Gluon中Conv2D

  Gluon中nn.Conv2D提供了便利的接口,即通过设置groups参数,可以指定卷积进行的方式。见下方的参数说明


14755769-e9e1b712e9730740.png
Fig. 8. Conv2D参数
  • 这个参数介绍信息量还是很大的。值得注意的有几个地方:
  1. 通常一种比较省事的做法是使用Conv2D时不指定input_channels,不过这样定义完网络之后,模型的初始化会被推迟进行,即当模型第一次call forward函数时才会根据输入图像尺寸推断input_channels进而初始化网络参数。这样可能会影响model.summary()等函数的使用。
  2. 看起来Conv2D也提供了扩张卷积的接口
  3. groups参数:控制输入特征图和输出特征图的连接方式。默认groups=1,即卷积核的深度和输入通道深度相同。如果取2,则效果相当于将输入特征按通道那一维分成两半,每一半与一个group进行卷积,输出的特征图concat得到一个特征图。因此,如果将groups参数设置为同输入通道数目一致,就是Depthwise Convolution。

  首先我做了一个小实验来对比普通卷积和groups>1的卷积,来验证该参数是否可用于简便地实现depthwise conv。实验虽然选取了较小的输入feature map和卷积核,但是输出结果还是比较冗长。有兴趣的可以直接看代码,后面MobileNet的搭建以及训练实验也在里面。这里直接放上结论:

  • 可以使用Gluon提供的groups接口实现Depthwise Conv。 只需要设置num_in_channels(in_channels)==num_out_channels(channels)==groups,当然,同普通卷积,in_channels参数也可以不显式地指定。不过这样模型初始化会延后到第一次model.forward之后进行。

3.2 实现MobileNet

  

from mxnet import nd
import numpy as np
from mxnet.gluon import nn
import mxnet as mx
from mxnet import gluon

class ConvBlock(nn.HybridBlock):
    def __init__(self, in_channels, channels, strides, padding, num_sync_bn_devices=-1, multiplier=1.0):
        super(ConvBlock, self).__init__()
        self.conv_block = nn.HybridSequential()
        with self.conv_block.name_scope():
            self.conv_block.add(nn.Conv2D(int(channels*multiplier), 3, strides, padding, 
                                          in_channels=in_channels, use_bias=False))
            if num_sync_bn_devices == -1:
                self.conv_block.add(nn.BatchNorm())
            else:
                self.conv_block.add(gluon.contrib.nn.SyncBatchNorm(num_devices=num_sync_bn_devices))
            self.conv_block.add(nn.Activation('relu'))
    def hybrid_forward(self, F, x):
        return self.conv_block(x)

class DepthwiseSeperable(nn.HybridBlock):
    def __init__(self, in_channels, channels, strides, num_sync_bn_devices=-1, multiplier=1.0, **kwags):
        # Weidth Multiplier
        in_channels = int(in_channels * multiplier)
        channels = int(channels * multiplier)
        super(DepthwiseSeperable, self).__init__(**kwags)
        self.depthwise = nn.HybridSequential()
        with self.depthwise.name_scope():
            self.depthwise.add(nn.Conv2D(in_channels, 3, strides, padding=1,groups=in_channels, 
                                         in_channels=in_channels, use_bias=False))
            if num_sync_bn_devices == -1:
                self.depthwise.add(nn.BatchNorm())
            else:
                self.depthwise.add(gluon.contrib.nn.SyncBatchNorm(num_devices=num_sync_bn_devices))
            self.depthwise.add(nn.Activation('relu'))
            
        self.pointwise = nn.HybridSequential()
        with self.pointwise.name_scope():
            self.pointwise.add(nn.Conv2D(channels, 1, in_channels=in_channels, use_bias=False))
            if num_sync_bn_devices == -1:
                self.pointwise.add(nn.BatchNorm())
            else:
                self.pointwise.add(gluon.contrib.nn.SyncBatchNorm(num_devices=num_sync_bn_devices))
            self.pointwise.add(nn.Activation('relu'))
       
    def hybrid_forward(self, F, x):
        return(self.pointwise(self.depthwise(x)))

class MobileNet(nn.HybridBlock):
    def __init__(self, num_classes, n_devices=2, multiplier=1.0, **kwags):
        super(MobileNet, self).__init__(**kwags)
        self.net = nn.HybridSequential()
        self.net.add(ConvBlock(3, 32, 2, 1, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(32, 64, 1, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(64, 128, 2, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(128, 128, 1, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(128, 256, 2, n_devices, multiplier))

        self.net.add(DepthwiseSeperable(256, 256, 1, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(256, 512, 2, n_devices, multiplier))
        for _ in range(5):
            self.net.add(DepthwiseSeperable(512, 512, 1, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(512, 1024, 2, n_devices, multiplier))
        self.net.add(DepthwiseSeperable(1024, 1024, 1, n_devices, multiplier))

        self.net.add(nn.GlobalAvgPool2D())
        self.net.add(nn.Dense(num_classes))

    def hybrid_forward(self, F, x):
        return self.net(x)
  • 可以用下面这段小代码检查下上面定义的模型:
net = MobileNet(num_classes=1000, n_devices=2, multiplier=1.0)
net.initialize(mx.init.Xavier())
test_input = nd.random_normal(shape=(1, 3, 224, 224))
net.summary(test_input)
  • 也可以使用mxnet.viz.plot_networks打印出模型的graph,不过需要先将模型转换为symbol:
net.hybridize()
output = net.forward(test_input)
# export之后同时生成了一个.json文件和.params文件
net.export('Gluon-MobileNet')
symnet = mx.symbol.load('Gluon-MobileNet-symbol.json')
mx.viz.plot_network(symnet, title='mobilnet_viz', shape={'data':(1, 3, 224, 224)})

图片太长这里就不放了。可以到notebook中去看。

3.3 Experiment on CIFAR10

超参数设置:

OPTIMIZER: 'nag'
BATCH_SIZE = 64  # per gpu
EPOCHS = 200
LR = 1e-1
WD = 5e-4
MOMENTUM = 0.9
lr_decay_dict = {40:0.1, 80:0.1, 120:0.1}

数据增强:

  • 训练数据:随机左右翻转;归一化到0-1;减去数据集均值,除以标准差;
  • 测试数据:归一化到0-1;减去数据集均值,除以标准差;

结果:


14755769-cb0058007f3ec876.png
Fig. 9. Training Curve

14755769-7576ddfa29602eeb.png
Fig. 10. Classification Report
  • 一个奇怪的现象是模型在第二次decay 0.1时的第二或第三个epoch开始(图中大概第80个epoch处)会迅速开始过拟合。看起来模型的训练受学习率衰减的影响非常大。

更多精彩内容