一、概述
本文记录MobileNet论文的阅读笔记以及代码实现。其实本来是想直接看MobileNet v2的,结果v2的论文太难了看不大懂...所以先来看看v1吧🐶
首先根据知乎大佬的说法,MobileNet并非2017年的创作,而是Google此前两年就有的工作,只是文章一直没发表而已。这也解释了为什么MobileNet看起来是一种非常复古的“直筒结构”,性价比不高。关于“性价比”,大佬是这么解释的:
“后续一系列的ResNet, DenseNet等结构已经证明通过复用图像特征, 使用concat/eltwise+ 等操作进行融合, 能极大提升网络的性价比”
其次MobileNet中的Depthwise Conv在训练过程中也有一些问题。这个等到写MobileNet v2笔记时再进一步分析。
1.1 论文主要内容
本文主要分为三个方面的内容:
- 介绍Depthwise Separable Convolution(深度可分离卷积)以及其在计算量方面相对于传统卷积的优势;
- 提出了两个超参数
Width Multiplier
和Resolution Multiplier
,用于进一步压缩模型的复杂度,减小参数数量。 - 大量的实验结果用于对比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两个层及其计算量。
由于标准卷积的计算量为,用上图的式子除以该式,结果为 (忽略D_G和D_F之间的差距)由于网络中用的卷积核大小D_k均为3x3,使用深度可分离卷积的参数量约为标准卷积的。
除此之外,论文中还提到了一个实现细节,就是深度可分离卷积中大部分运算量都来自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是按列优先。
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 深度可分离卷积带来的性能下降可以接收
2.3.2 Shallower or thinner?
在对原始MobileNet进行进一步压缩时,一个问题是为什么我们要使用Width Multiplier,而不是简单地减少模型的层数?文章通过实验证明,使用缩减网络比减小网络层数性能要好。
2.3.3 Width Multiplier可以设多大?
这个应该是大家最关心的实验了。本实验证明在从1降到0.5的过程中,模型性能是平滑下降的;当从0.5降到0.25时,性能大幅下降。可见对模型性能影响较小,通常是一个值得尝试的选择。
2.3.4 输入分辨率的影响?
随着输入分辨率的降低,模型性能的下降趋势较平滑:
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参数,可以指定卷积进行的方式。见下方的参数说明
- 这个参数介绍信息量还是很大的。值得注意的有几个地方:
- 通常一种比较省事的做法是使用Conv2D时不指定input_channels,不过这样定义完网络之后,模型的初始化会被推迟进行,即当模型第一次call forward函数时才会根据输入图像尺寸推断input_channels进而初始化网络参数。这样可能会影响model.summary()等函数的使用。
- 看起来Conv2D也提供了扩张卷积的接口
-
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;减去数据集均值,除以标准差;
结果:
- 一个奇怪的现象是模型在第二次decay 0.1时的第二或第三个epoch开始(图中大概第80个epoch处)会迅速开始过拟合。看起来模型的训练受学习率衰减的影响非常大。