您的位置:首页 > 理论基础 > 计算机网络

轻量级CNN网络之MobileNetv2

2018-12-10 18:00 1176 查看
版权声明:本文为博主原创文章,更多精彩文章请关注公众号【Jeemy110】 https://blog.csdn.net/l7H9JA4/article/details/84949300


  作者:叶   虎       
编辑:田   旭       



前 言



MobileNet网络是Google提出主要应用在移动端的轻量级CNN网络,之前的文章(CNN模型之MobileNet)已经对MobileNetv1版本进行了详细的介绍。目前,Google已经公开了MobileNetv2,它相比v1更高效。MobileNetv2依然使用v1中的depthwise separable convolution,区别在于v2又引入了残差结构和bottleneck层,这种新的结构称为Bottleneck residual block。



01

Depthwise Separable Convolution


 Depthwise Separable Convolution是MobileNetv1所采用的核心结构,如图 1所示,它包含一个depthwise卷积和1x1卷积(或者称为pointwise卷积),这种结构将空间相关性和通道相关性分离(见Xception文章),相比传统的卷积,它可以减少约 计算复杂度,当卷积核 时,大约比原来计算花费少8~9倍,但是却在准确度方面损失很少。MobileNetv2作为v1的升级版,也主要采用Depthwise Separable Convolution结构。


图1 Depthwise Separable Convolution结构

02

Linear Bottlenecks


对于CNN网络某一层 的特征图,其维度大小为 ,我们感兴趣的是它的核心信息,这里称为“主要流形”(manifold of interest)。而“主要流形”是可以用一个低维度空间来表达的。这实际上是说神经网络某层特征,其主要信息可以用一个低维度特征来表征。据此,我们可以在网络引入“bottlenck layer”来表征某一个深层特征的“主要流形”,作者发现这个“bottlenck layer”最好采用线性激活,而不是采用ReLU,因为在“bottlenck layer”采用非线性激活会损失很多信息,毕竟特征已经在低维度空间(paper中有理论分析和实验证明)。在Depthwise Separable Convolution引入“bottlenck layer”后,结构如图2所示,其中浅色代表的是下一个block的开始,并且注意方块代表的是特征图,而红色映射才是卷积或者ReLU操作。图2-a是常规的卷积,而图2-b是Depthwise Separable Convolution,这在MobileNetv1中大量使用。当在Depthwise Separable Convolution后面加入“bottlenck layer”就变成了图2-c,但是考虑到block是互相堆积的,调整一下视角,如果将“bottlenck layer”看成block的输入,那么这种结构也等价于图2-d。对于图2-d,block中开始的1x1卷积层称为“expansion layer”,它的通道大小(inner size)和输入bottleneck层的通道大小之比,称为扩展比(expansion ratio)。扩展层之后是depthwise卷积,然后采用1x1卷积得到block的输出特征,这个卷积后面没有非线性激活。对于图2-d这种结构,block的输入和输出特征是bottleneck特征,所以这个block称为bottleneck block,至于为什么要采用图2-d的视角,而不是图2-c,后面会有说明。

图2 加入linear bottlenecks的Depthwise Separable Convolution


03

Inverted Residuals


当我们在上面的block中加入短路连接,它就变成了残差结构,但是这个shortcut connection是在bottlenecks之间。这与常规的残差block是相反的,因为residual block一般先采用bottleneck layer进行降维,最后进行扩展。两者的差异如图3所示,可以看到两者的bottleneck的位置恰恰相反,paper里面称这种相反的残差block为inverted residual block。前面也说了,大家可能会疑惑为什么要采用这种相反的结构呢,paper说这种结果可以在实现上减少内存的使用,这点paper里面有详细的说明,另外一点是这种相反的结构从实验结果上也更好一点。

图3 inverted residual block和residual block之间的对比

04

网络结构


前面我们讲了MobileNetv2相比v1的两个主要改进:linear bottleneck和inverted residual。改进后的新结构称为bottleneck residual block,表1给出了这个block的内部结构,其中t是扩展比。
表1 Bottleneck residual block的内部构成
Bottleneck residual block其实包含3层卷积操作,图4更直观的给出了block的内部结构。首先是一个1x1的卷积,称为expansion layer,它将输入的通道维度从k变换为tk,在实际中t取大于1的值最有效,paper里面的默认值t=6。然后是3x3的depthwise conv,每个通道上是单独卷积的。最后是一个projection layer,它将得到block的输入特征,一般情形下是降低通道大小的,所以是bottleneck layer,如前面所述,bottleneck后面没有采用非线性激活。这种结构一个有意思的特征是它将网络的容量(capacity)和表达力(expressiveness)分离开来。其中bottleneck layer负责的是输入和输出之间的转换,这可以看成capacity,而中间的expansion layer充当的是非线性变换,可以看成是expressiveness。

图4 Bottleneck residual block示意图
这篇博客(MobileNet version 2)对这种结构给出了一个有趣的解释,如5所示。其中expansion layer可以看做解压器,类似unzip,将特征恢复到高维空间,而 depthwise layer可以看成过滤器,筛选比较重要的特征,最后projection layer将其压缩到低维空间。

图5 Bottleneck residual block的另一种解释
MobileNetv2与其它网络的对比如图6所示,值得注意的对于stride=2的block没有采用残差结构。

图6 MobileNetv2与其它网络的结构对比
将block堆积起来,就形成最终的MobileNetv2网络,各个block设计如表2所示,其中t是扩展比,c是block的输出特征的channel大小,n是block的重复次数,s是stride,注意只有对于重复的block只有开始的s才是2。另外与MobileNetv1类似,v2也设计了width multiplier和输入大小两个超参数控制网络的参数量,表2中默认的是width multiplier=1.0,输入大小是224x224。输入大小影响的是特征图空间大小,而width multiplier影响的是特征图channel大小。输入大小可以从96到224,而width multiplier可以从0.35到1.4。值得注意的一点是当width multiplier小于1时,不对最后一个卷积层的channel进行调整以保证性能,即维持1280。
表2 MobileNetv2的网络结构
MobileNetv2在ImageNet上分类效果与其它网络对比如表3所示,可以看到在同样参数大小下,MobileNetv2比MobileNetv1和ShuffleNet要好,而且速度也更快一些。另外MobileNetv2还可以应用在语义分割(DeepLab)和目标检测(SSD)中,也得到了较好的结果。
表3 MobileNetv2与其它网络在ImageNet上的对比

05

TensorFlow上的实现


Google已经开源了MobileNetv2的代码,其基于TensorFlow/slim框架实现,slim是一个较好的CNN轻量级框架。这里给出简化版本的实现。对于MobileNetv2的实际实现中,会对channel进行一些限制,如保证channel的最小值以及是8的倍数,所以会存在如下的辅助函数:


def _make_divisible(v, divisor, min_value=None):
   """make `v` is divided exactly by `divisor`, but keep the min_value"""
   if min_value is None:
       min_value = divisor
   new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
   # Make sure that round down does not go down by more than 10%.
   if new_v < 0.9 * v:
       new_v += divisor
   return new_v


@slim.add_arg_scope
def _depth_multiplier_func(params,
                          multiplier,
                          divisible_by=8,
                          min_depth=8):
   """get the new channles"""
   if 'num_outputs' not in params:
       return
   d = params['num_outputs']
   params['num_outputs'] = _make_divisible(d * multiplier, divisible_by,
                                                  min_depth)


这些对于通过width multiplier改变channel大小做了一定的限制。然后我们实现网络最核心的结构Bottleneck residual block:

@slim.add_arg_scope
def expanded_conv(x,
                 num_outputs,
                 expansion=6,
                 stride=1,
                 rate=1,
                 normalizer_fn=slim.batch_norm,
                 project_activation_fn=tf.identity,
                 padding="SAME",
                 scope=None):
   """The expand conv op in MobileNetv2
       1x1 conv -> depthwise 3x3 conv -> 1x1 linear conv
   """
   with tf.variable_scope(scope, default_name="expanded_conv") as s, \
      tf.name_scope(s.original_name_scope):
       prev_depth = x.get_shape().as_list()[3]
       # the filters of expanded conv
       inner_size = prev_depth * expansion
       net = x
       # only inner_size > prev_depth, use expanded conv
       if inner_size > prev_depth:
           net = slim.conv2d(net, inner_size, 1, normalizer_fn=normalizer_fn,
                             scope="expand")
       # depthwise conv
       net = slim.separable_conv2d(net, num_outputs=None, kernel_size=3,
                                   depth_multiplier=1, stride=stride,
                                   rate=rate, normalizer_fn=normalizer_fn,
                                   padding=padding, scope="depthwise")
       # projection
       net = slim.conv2d(net, num_outputs, 1, normalizer_fn=normalizer_fn,
                         activation_fn=project_activation_fn, scope="project")

       # residual connection
       if stride == 1 and net.get_shape().as_list()[-1] == prev_depth:
           net += x

       return net


这里的实现相比官方实现做了一定的简化,官方代码中包含很多灵活的参数,但是在实际中却没有采用,所以我选择了paper中的默认配置。注意这里的rate参数主要是为了配合语义分割模型,因为在DeepLab模型会采用空洞卷积来平衡特征图空间大小。
在定义网络之前,我们使用一个简单的Op类,对网络结构进行配置:

_Op = namedtuple("Op", ['op', 'params', 'multiplier_func'])

def op(op_func, **params):
   return _Op(op=op_func, params=params,
              multiplier_func=_depth_multiplier_func)


CONV_DEF = [op(slim.conv2d, num_outputs=32, stride=2, kernel_size=3),
           op(expanded_conv, num_outputs=16, expansion=1),
           op(expanded_conv, num_outputs=24, stride=2),
           op(expanded_conv, num_outputs=24, stride=1),
           op(expanded_conv, num_outputs=32, stride=2),
           op(expanded_conv, num_outputs=32, stride=1),
           op(expanded_conv, num_outputs=32, stride=1),
           op(expanded_conv, num_outputs=64, stride=2),
           op(expanded_conv, num_outputs=64, stride=1),
           op(expanded_conv, num_outputs=64, stride=1),
           op(expanded_conv, num_outputs=64, stride=1),
           op(expanded_conv, num_outputs=96, stride=1),
           op(expanded_conv, num_outputs=96, stride=1),
           op(expanded_conv, num_outputs=96, stride=1),
           op(expanded_conv, num_outputs=160, stride=2),
           op(expanded_conv, num_outputs=160, stride=1),
           op(expanded_conv, num_outputs=160, stride=1),
           op(expanded_conv, num_outputs=320, stride=1),
           op(slim.conv2d, num_outputs=1280, stride=1, kernel_size=1),
           ]


然后定义整个网络:
def mobilenetv2(x,
               num_classes=1001,
               depth_multiplier=1.0,
               scope='MobilenetV2',
               finegrain_classification_mode=False,
               min_depth=8,
               divisible_by=8,
               output_stride=None,
               ):
   """Mobilenet v2
   Args:
       x: The input tensor
       num_classes: number of classes
       depth_multiplier: The multiplier applied to scale number of
           channels in each layer. Note: this is called depth multiplier in the
           paper but the name is kept for consistency with slim's model builder.
       scope: Scope of the operator
       finegrain_classification_mode: When set to True, the model
           will keep the last layer large even for small multipliers.
           The paper suggests that it improves performance for ImageNet-type of problems.
       min_depth: If provided, will ensure that all layers will have that
         many channels after application of depth multiplier.
      divisible_by: If provided will ensure that all layers # channels
         will be divisible by this number.
   """
   conv_defs = CONV_DEF

   # keep the last conv layer very larger channel
   if finegrain_classification_mode:
       conv_defs = copy.deepcopy(conv_defs)
       if depth_multiplier < 1:
           conv_defs[-1].params['num_outputs'] /= depth_multiplier

   depth_args = {}
   # NB: do not set depth_args unless they are provided to avoid overriding
   # whatever default depth_multiplier might have thanks to arg_scope.
   if min_depth is not None:
       depth_args['min_depth'] = min_depth
   if divisible_by is not None:
       depth_args['divisible_by'] = divisible_by

   with slim.arg_scope([_depth_multiplier_func], **depth_args):
       with tf.variable_scope(scope, default_name='Mobilenet'):
           # The current_stride variable keeps track of the output stride of the
           # activations, i.e., the running product of convolution strides up to the
           # current network layer. This allows us to invoke atrous convolution
           # whenever applying the next convolution would result in the activations
           # having output stride larger than the target output_stride.
           current_stride = 1

           # The atrous convolution rate parameter.
           rate = 1

           net = x
           # Insert default parameters before the base scope which includes
           # any custom overrides set in mobilenet.
           end_points = {}
           scopes = {}
           for i, opdef in enumerate(conv_defs):
               params = dict(opdef.params)
               opdef.multiplier_func(params, depth_multiplier)
               stride = params.get('stride', 1)
               if output_stride is not None and current_stride == output_stride:
                   # If we have reached the target output_stride, then we need to employ
                   # atrous convolution with stride=1 and multiply the atrous rate by the
                   # current unit's stride for use in subsequent layers.
                   layer_stride = 1
                   layer_rate = rate
                   rate *= stride
               else:
                   layer_stride = stride
                   layer_rate = 1
                   current_stride *= stride
               # Update params.
               params['stride'] = layer_stride
               # Only insert rate to params if rate > 1.
               if layer_rate > 1:
                   params['rate'] = layer_rate

               try:
                   net = opdef.op(net, **params)
               except Exception:
                   raise ValueError('Failed to create op %i: %r params: %r' % (i, opdef, params))

           with tf.variable_scope('Logits'):
               net = global_pool(net)
               end_points['global_pool'] = net
               if not num_classes:
                   return net, end_points
               net = slim.dropout(net, scope='Dropout')
               # 1 x 1 x num_classes
               # Note: legacy scope name.
               logits = slim.conv2d(
                   net,
                   num_classes, [1, 1],
                   activation_fn=None,
                   normalizer_fn=None,
                   biases_initializer=tf.zeros_initializer(),
                   scope='Conv2d_1c_1x1')

               logits = tf.squeeze(logits, [1, 2])

               return logits


注意output_stride参数是配合语义分割模型,在DeepLab中output_stride取8或者16,它表示的是特征图最高下采样率,当超过这个值时,采用空洞卷积。
其它的模型超参数,我们可以通过slim的arg_scope来控制,这是slim框架我最喜欢的一个功能:
def mobilenet_arg_scope(is_training=True,
                       weight_decay=0.00004,
                       stddev=0.09,
                       dropout_keep_prob=0.8,
                       bn_decay=0.997):
   """Defines Mobilenet default arg scope.
   Usage:
    with tf.contrib.slim.arg_scope(mobilenet.training_scope()):
      logits, endpoints = mobilenet_v2.mobilenet(input_tensor)
    # the network created will be trainble with dropout/batch norm
    # initialized appropriately.
   Args:
       is_training: if set to False this will ensure that all customizations are
           set to non-training mode. This might be helpful for code that is reused
       across both training/evaluation, but most of the time training_scope with
       value False is not needed. If this is set to None, the parameters is not
       added to the batch_norm arg_scope.
       weight_decay: The weight decay to use for regularizing the model.
       stddev: Standard deviation for initialization, if negative uses xavier.
       dropout_keep_prob: dropout keep probability (not set if equals to None).
       bn_decay: decay for the batch norm moving averages (not set if equals to
           None).
   Returns:
       An argument scope to use via arg_scope.
   """
   # Note: do not introduce parameters that would change the inference
   # model here (for example whether to use bias), modify conv_def instead.
   batch_norm_params = {
       'center': True,
       'scale': True,
       'decay': bn_decay,
       'is_training': is_training
   }
   if stddev < 0:
       weight_intitializer = slim.initializers.xavier_initializer()
   else:
       weight_intitializer = tf.truncated_normal_initializer(stddev=stddev)

   # Set weight_decay for weights in Conv and FC layers.
   with slim.arg_scope(
       [slim.conv2d, slim.fully_connected, slim.separable_conv2d],
       weights_initializer=weight_intitializer,
       normalizer_fn=slim.batch_norm,
       activation_fn=tf.nn.relu6), \
       slim.arg_scope([slim.batch_norm], **batch_norm_params), \
       slim.arg_scope([slim.dropout], is_training=is_training,
                    keep_prob=dropout_keep_prob), \
       slim.arg_scope([slim.conv2d, slim.separable_conv2d],
                      biases_initializer=None,
                      padding="SAME"), \
       slim.arg_scope([slim.conv2d],
                    weights_regularizer=slim.l2_regularizer(weight_decay)), \
       slim.arg_scope([slim.separable_conv2d], weights_regularizer=None) as s:
       return s


最后可以通过如下代码测试模型(预训练模型官方实现中已经给出):

inputs = tf.placeholder(tf.uint8, [None, None, 3])
   images = tf.expand_dims(inputs, 0)
   images = tf.cast(images, tf.float32) / 128. - 1
   images.set_shape((None, None, None, 3))
   images = tf.image.resize_images(images, (224, 224))

   with slim.arg_scope(mobilenet_arg_scope(is_training=False)):
       logits = mobilenetv2(images)

   # Restore using exponential moving average since it produces (1.5-2%) higher
   # accuracy
   ema = tf.train.ExponentialMovingAverage(0.999)
   vars = ema.variables_to_restore()

   saver = tf.train.Saver(vars)

   print(len(tf.global_variables()))
   for var in tf.global_variables():
       print(var)
   checkpoint_path = ""
   image_file = ""
   with tf.Session() as sess:
       saver.restore(sess, checkpoint_path)

       img = cv2.imread(image_file)
       img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

       print(np.argmax(sess.run(logits, feed_dict={inputs: img})[0]))


整个实现代码可以见我的GitHub(xiaohu2015/DeepLearning_tutorials),欢迎star。


小结


这里比较简单地介绍了MobileNetv2,相比v1其核心区别就在于bottleneck和residual结构。但是,对于paper里面一些理论分析这里没有给出,感兴趣的可以自己啃一下。


参考

  1. MobileNetV2: Inverted Residuals and Linear Bottlenecks

  2. MobileNet version 2 (部分图来源这个blog)

  3. MobileNetv2的官方代码



  END



往期回顾【1】 TensorFlow Serving入门【2】 从傅立叶变换到Gabor滤波器【3】 《Computer vision》笔记-AlexNet(1)【4】 torch.uls.trainer包解析【5】 【Nature】DeepMind探索AI医疗黑箱问题,眼科疾病诊断水平超人类专家




机器学习算法工程师

                            一个用心的公众号

长按,识别,加关注

进群,学习,得帮助你的关注,我们的热度,我们一定给你学习最大的帮助


阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: