目录
论文解读
TensorFlow源码解读
论文解读
摘要:
大致说的是mobileNet是一种高效,轻量级,最新型的(streamlined)一种网络结构,使用深度分离卷积(depthwise separable convolutions),同时还使用两个超参数,width multiplier 和resolution multiplier来权衡模型速度上和精度上的平衡。
模型适合在计算性能有限的设备上运行,并且具有不错的效果。文中还附带了一些应用,比如目标检测,细粒度分割,人脸属性和大规模地理定位。
Prior work:
第一篇使用深度分离卷积的网络是xception。还有一些用于模型压缩的工作有SqueezeNet和FallentedNet。
用低比特保存权重;向量量化,霍夫曼编码,
还有一种shrinking的办法是知识蒸馏。
然而以上方法都不直接。因此mobileNet在使得模型变小的工作是极其重要的。
模型结构
文中这一部分先介绍了什么是深度分离卷积(depthwise separable convolution)
深度分离卷积分成两个部分,第一部分是depthwise convolution部分;第二部分是pointwise convolution。
先介绍depthwise convolution部分。请见下图
图画的不好,见谅。这里稍微做下解释。左边是一张图,有三个通道,分别是R,g,b,中间蓝色的是卷积核,且只是用一个卷积核,大小是3x3x3,分别代表的是[kernel_height,kernel_width,kernel_channels]。这个卷积核的通道数是和输入数据的通道数是一样的,因此通道维度上分离卷积的结果,自然通道数也是输入数据的通道数。可以我们知道层与层之间的通道数往往是不同的,而且一般都是先递增在递减。这就是第二部分pointwise convolution要做的事情。
请见下图:
这就是深度分离卷积的两个步骤。
我们再来看看深度分离卷积的计算量比传统卷积的计算量少了多少倍。
DK是卷积核的尺寸,整个网络都使用的3x3的卷积,所以计算量少了8-9倍。
两个超参数,width multiplier ,resolution multiplier
尽管mobileNet已经很小同时延长很低,但应用场合还是需要更小的模型和更快的速度,因此作者还提出了使用两个超参数来控制模型的大小和输入大小。其中width multiplier能控制模型大小,参数数目;resolution multiplier可以控制输入大小。
设a是width multiplier的值,b是resolution multiplier的值。在一次深度分离卷积的第一部分中,输入通道数目为M,输出通道数为N,那么加入了width mutlplier,输入通道数变成a*M,输出通道数变成了a*N。a介于0-1。文中设置了a有四个值,{0.25,0.5,0.75,1.0}。
从resolution multiplier名称中也能看出这个超参数是控制输入map的大小的。这里的输入map既指原图,也指内部的特征图。不信的话请见原文:
但我有一个疑惑,论文仅仅说的width multiplier参数可以减少通道数,但是却没说如何做到的,我查了几篇博客,也只是对这一部分的翻译。我还去看了源码,官方发布的slim 源码中,width multilier被固定的设置为了1.
将深度分离卷积视作2层,整个模型有28层。
(注:这个模型和tensorflow公布的slim源码有出入,我估计后面他们自己又修改了模型)
每一层后面都使用了batch norm 和relu。下图是一个深度分离卷积的完整过程
下面是不同的超参数对模型的计算数目和参数数目的对比,以及在imageNet上的准确率
TensorFlow-slim源码解读
tf目录tensorflow/research/slim/nets下有谷歌提供的官方mobidleNetV1的源码,虽然网络结构和论文上不一样,但是应该是谷歌又修改之后的结构,只会更好不会更差。有四个文件,mobilenet_v1.py,mobilenet_v1_eval.py,mobilenet_v1_train.py,mobilenet_v1_test.py。
其中mobilenet_v1.py记录了主结构, mobilenet_v1_train.py 用于训练,我们仅仅看下这两个文件代码。
mobilenet_v1.py
from collections import namedtuple
import functoolsimport tensorflow as tfslim = tf.contrib.slimConv = namedtuple('Conv', ['kernel', 'stride', 'depth'])
DepthSepConv = namedtuple('DepthSepConv', ['kernel', 'stride', 'depth'])# _CONV_DEFS specifies the MobileNet body #定义了网络结果,所用的方式比较新颖,用 #collections中的namedtuple先定义好存储每一层核大 #小,卷积步长,和输出通道数。这样构建网络的之后遍历 #下面的列表,代码格式就很清晰简洁了。
_CONV_DEFS = [Conv(kernel=[3, 3], stride=2, depth=32),DepthSepConv(kernel=[3, 3], stride=1, depth=64),DepthSepConv(kernel=[3, 3], stride=2, depth=128),DepthSepConv(kernel=[3, 3], stride=1, depth=128),DepthSepConv(kernel=[3, 3], stride=2, depth=256),DepthSepConv(kernel=[3, 3], stride=1, depth=256),DepthSepConv(kernel=[3, 3], stride=2, depth=512),DepthSepConv(kernel=[3, 3], stride=1, depth=512),DepthSepConv(kernel=[3, 3], stride=1, depth=512),DepthSepConv(kernel=[3, 3], stride=1, depth=512),DepthSepConv(kernel=[3, 3], stride=1, depth=512),DepthSepConv(kernel=[3, 3], stride=1, depth=512),DepthSepConv(kernel=[3, 3], stride=2, depth=1024),DepthSepConv(kernel=[3, 3], stride=1, depth=1024)
]
def mobilenet_v1_base(inputs,final_endpoint='Conv2d_13_pointwise',min_depth=8,depth_multiplier=1.0,conv_defs=None,output_stride=None,use_explicit_padding=False,scope=None):
mobilenet_v1_base :这个函数是inference部分,设计思路是用for循环遍历记录网络结构的_CONV_DEFS,判断每一个namedtuple的名称是conv还是depthconv。因为mobilenet的第一层是传统的卷积方式,所以用 if isinstance 来判断卷积种类(不得不佩服谷歌工程师的代码能力啊)。depthwise卷积函数使用slim.separable_conv2d完成的。这个函数的用法见我的另一篇博客 https://mp.csdn.net/postedit/86073166
不过呢,我们知道深度分离卷积是有两个步骤的,这个函数集成了depthwise 和pointwise conv,但源码只用了这个完成depthwise conv的操作。请见下面:
net = slim.separable_conv2d(net, None, conv_def.kernel,depth_multiplier=1,stride=layer_stride,rate=layer_rate,normalizer_fn=slim.batch_norm,scope=end_point)
第二个参数为None,第二个参数本意是最终输出的通道数,这里为None,那么这个函数执行的仅仅是depthwise部分。相当于这个函数的作用了slim.depthwise_conv2d等价了。
def mobilenet_v1(inputs,num_classes=1000,dropout_keep_prob=0.999,is_training=True,min_depth=8,depth_multiplier=1.0,conv_defs=None,prediction_fn=tf.contrib.layers.softmax,spatial_squeeze=True,reuse=None,scope='MobilenetV1',global_pool=False):
mobilenet_v1:这个函数是用来做图像分类的。整体的结果是先写variable_scope, slim.arg_scope, 之后送入inputs进入mobilenet_v1_base获得全局平均池化的网络输出,之后GAP(全局平均池化),然后用个1x1的卷积核代替FC层作为logits输出。
这一部分还是很好理解的。另外我们也能注意到有很多可能觉得奇怪的函数比如,
def _fixed_padding(inputs, kernel_size, rate=1)def _reduced_kernel_size_for_small_input(input_tensor, kernel_size)
这些都是谷歌考虑到用户送入的不是mobilenet规定的图像大小,做出的自适应操作。就是说即便我们送入尺寸不是224x224的,inference部分也不会出错。换言之,我们在预处理的过程中resize样本到224x224,这些函数是用不到的。
mobilenet_v1_train.py
整个工程是使用slim写的,封装度非常高,所以代码结果很容易看懂。
def train_model():"""Trains mobilenet_v1."""g, train_tensor = build_model()with g.as_default():slim.learning.train(train_tensor,FLAGS.checkpoint_dir,is_chief=(FLAGS.task == 0),master=FLAGS.master,log_every_n_steps=FLAGS.log_every_n_steps,graph=g,number_of_steps=FLAGS.number_of_steps,save_summaries_secs=FLAGS.save_summaries_secs,save_interval_secs=FLAGS.save_interval_secs,init_fn=get_checkpoint_init_fn(),global_step=tf.train.get_global_step())def main(unused_arg):train_model()
我们可以发现,main中执行的是train_model,train_model中第一个函数就是bulid_model,从函数名称你们也该猜得出,这是构建前向传播过程。所以我们来看看build_model。
def build_model():g = tf.Graph()with g.as_default(), tf.device(tf.train.replica_device_setter(FLAGS.ps_tasks)):inputs, labels = imagenet_input(is_training=True)with slim.arg_scope(mobilenet_v1.mobilenet_v1_arg_scope(is_training=True)): logits, _ = mobilenet_v1.mobilenet_v1( #前向传播结构inputs,is_training=True,depth_multiplier=FLAGS.depth_multiplier,num_classes=FLAGS.num_classes)tf.losses.softmax_cross_entropy(labels, logits) #计算损失if FLAGS.quantize:tf.contrib.quantize.create_training_graph(quant_delay=get_quant_delay())total_loss = tf.losses.get_total_loss(name='total_loss')# Configure the learning rate using an exponential decay.num_epochs_per_decay = 2.5imagenet_size = 1271167decay_steps = int(imagenet_size / FLAGS.batch_size * num_epochs_per_decay)learning_rate = tf.train.exponential_decay( #指数衰减学习率get_learning_rate(),tf.train.get_or_create_global_step(),decay_steps,_LEARNING_RATE_DECAY_FACTOR,staircase=True)opt = tf.train.GradientDescentOptimizer(learning_rate)train_tensor = slim.learning.create_train_op( #创建train_optotal_loss,optimizer=opt)slim.summaries.add_scalar_summary(total_loss, 'total_loss', 'losses')slim.summaries.add_scalar_summary(learning_rate, 'learning_rate', 'training')return g, train_tensor
主要过程都写在了注释里。不清楚的或者有错误的还请在下方留言。觉得好的点个赞。^_^