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

一、概述

  本文记录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 Multiplier αResolution 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两个层及其计算量。


Fig. 1. 深度可分离卷积示意图

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

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是按列优先。

Fig. 3. im2col与reordering

2.2 Width Multiplier与Resolution Multiplier

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

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

2.3 实验分析

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

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

Fig. 5. Narrow v.s. Shallow MobileNet

2.3.3 Width Multiplier可以设多大?

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

Fig. 6. α的影响

2.3.4 输入分辨率的影响?

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


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参数,可以指定卷积进行的方式。见下方的参数说明


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;减去数据集均值,除以标准差;

结果:


Fig. 9. Training Curve

Fig. 10. Classification Report
  • 一个奇怪的现象是模型在第二次decay 0.1时的第二或第三个epoch开始(图中大概第80个epoch处)会迅速开始过拟合。看起来模型的训练受学习率衰减的影响非常大。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容