MobileNetV3架构解析与代码复现

850 阅读9分钟

@toc

参考论文:Searching for MobileNetV3

作者:Andrew Howard, Mark Sandler, Grace Chu, Liang-Chieh Chen, Bo Chen, Mingxing Tan, Weijun Wang, Yukun Zhu, Ruoming Pang, Vijay Vasudevan, Quoc V. Le, Hartwig Adam;

前置知识:MobileNetV1架构解析

MobileNetV2架构解析

SENet架构-通道注意力机制

1、MobileNetV3改进

  • 更新了MobileNetV2中的倒残差结构
  • 使用NAS搜索参数
  • 重新设计耗时层结构
  • 重新设计激活函数。

2、 MobileNetV1

img

2.1 Depthwise Separable Convolution

  MobileNet模型基于深度可分离卷积,这是一种分解卷积的形式,将标准卷积分解为深度卷积和1*1的点卷积。对于MobileNet,深度卷积将单个滤波器应用于每个输入通道,然后,逐点卷积应用1*1卷积将输出与深度卷积相结合。

  标准卷积在一个步骤中将输入滤波并组合成一组新的输出。深度可分离卷积将标准的卷积层分解为两层来做:

  首先是各个通道单独做卷积运算,称之为Depthwise Convolution 然后用一个1*1的标准卷积层进行各个通道间的合并,称之为Pointwise Convolution   论文中原图如下所示: image-20220803201346429

2.2 Depthwise卷积

在这里插入图片描述

  • 卷积核channel=1
  • 输入特征矩阵channel=卷积核个数=输出特征矩阵channel

  DW卷积中的每一个卷积核只会和输入特征矩阵的一个channel进行卷积计算,所以输出的特征矩阵就等于输入的特征矩阵。

2.3 Pointwise卷积

  Pointwise卷积和普通的卷积一样,只不过使用了1*1卷积核。

在这里插入图片描述

3、MobileNetV2

3.1 MobileNetV1与MobileNetV2架构对比

  MobileNetV1先使用3*3深度卷积,再使用1*1逐点卷积,全部采用ReLU6激活函数

  MobileNetV2先使用1*1卷积升维,在高维空间下使用3*3的深度卷积,在使用1*1卷积降维,在降维时采用线性激活函数。当步长为1时,使用残差连接输入和输出;当步长为2时,不适用残差连接,因为此时的输入特征矩阵和输出特征矩阵的shape不相等。

image-20220804170325471

img

3.2 普通残差块与逆残差块对比

image-20220804161835593

  普通残差是先使用1*1卷积降维,再进行3*3卷积,最后使用1*1卷积升维(两边厚,中间薄)

  逆残差块是先使用1*1卷积进行升维,然后使用3*3深度可分离卷积,最后使用1*1卷积进行降维(两边薄,中间厚)

img

img

3.3 ReLU6激活函数

image-20220804162651537

   relu6函数在低精度浮点数下有比较好的表示性能

3.4 MobileNetV2的卷积块

image-20220804162735742

    ==注意:这里只有当stride=1且输入特征矩阵与输出特征矩阵的shape相同时才能进行shortcut连接。==

4、MobileNetV3

4.1 SENet注意力机制

image-20220816114225350

具体实现如下:

  • 对输入进来的特征层进行全局平均池化。
  • 然后进行两次全连接(这两个全连接可用1*1卷积代替),第一次全连接神经元个数较少,第二次全连接神经元个数和输入特征层个数相同。
  • 在完成两次全连接之后,再取一次sigmoid讲值固定到0-1之间,此时我们获得了输入特征层每一个通道的权值(0-1之间)。
  • 在获得这个权值之后,讲这个权值与原输入特征层相乘即可。

4.2 bneck结构

  bneck结构如下所示:

image-20220826172511468

它综合了以下四个特点:

  • MobileNetV2的逆残差结构

  • MobileNetV1的深度可分离卷积(depthwise separable convolutions)

  • SENet注意力机制模型

  • 用h-swish代替swish激活函数,也可以说是用h-sigmoid代替sigmoid激活函数。

4.3 网络改进

4.3.1 重新设计耗时层的结构

image-20220826173339788

  图 5. 原始最后阶段与有效最后阶段的比较。这个更有效的最后阶段能够在网络末端丢弃三个昂贵的层,而不会损失准确性。

  当前基于 MobileNetV2 的倒置瓶颈结构和变体的模型使用 1x1 卷积作为最后一层,以便扩展到更高维的特征空间。该层对于具有丰富的预测特征至关重要。然而,这是以额外的延迟为代价的。为了减少延迟并保留高维特征,我们将这一层移到最终的平均池化之后。最后一组特征现在以 1x1 空间分辨率而不是 7x7 空间分辨率计算。

  (1)减少第一个卷积层的卷积核个数。将卷积核个数从32个降低到16个之后,同时使用ReLU或swish保持与32个卷积核相同的精度。这额外节省了2毫秒时间和1000万个乘加量。

  (2)简化最后的输出层。删除多余的卷积层,将延迟减少了 7 毫秒,这是运行时间的 11%,并且减少了 3000 万次 MAdd 的操作次数,几乎没有损失准确性。

4.3.2 重新设计激活函数

  引入了一种称为 swish 的非线性,当用作 ReLU 的替代品时,它显着提高了神经网络的准确性。非线性定义为:

swishx=xσ(x)swishx=x\cdot \sigma (x)

  虽然这种非线性提高了准确性,但它在嵌入式环境中具有非零成本,因为在移动设备上计算 sigmoid 函数的成本要高得多。

  将sigmoid替换为h-sigmoid激活函数:

hsigmoid=ReLU6(x+3)6h-sigmoid=\frac{ReLU6(x+3)}{6}

  则swish硬版本变成:

hswish[x]=xReLU6(x+3)6h-swish[x]=x\frac{ReLU6(x+3)}{6}

image-20220826174636058

   sigmoid 和 swish 非线性的软和硬版本的比较如图 6 所示。在我们的实验中,我们发现所有这些功能的硬版本在准确性上没有明显差异,但从部署的角度来看具有多种优势。首先,ReLU6 的优化实现几乎可以在所有软件和硬件框架上使用。其次,在量化模式下,它消除了由近似 sigmoid 的不同实现引起的潜在数值精度损失。最后,在实践中,h-swish 可以实现为分段函数,以减少内存访问次数,从而大幅降低延迟成本。

4.3.3 Large squeeze-and-excite

  在“MnasNet: Platform-Aware Neural Architecture Search for Mobile”论文中,squeeze-and-excite 瓶颈的大小是相对于卷积瓶颈的大小。相反,我们将它们==全部替换为固定为扩展层通道数的 1/4==。我们发现这样做可以提高准确性,同时适度增加参数数量,并且没有明显的延迟成本。

4.4 网络结构

  MobileNetV3 定义为两个模型:MobileNetV3Large 和 MobileNetV3-Small。这些模型分别针对高资源和低资源用例。

image-20220826175028770

image-20220826175038368

表1为MobileNetV3-Large模型,表2为MobileNetV3-Small模型。

SE 表示该块中是否存在 Squeeze-And-Excite。

NL 表示使用的非线性类型。这里,HS 表示 h-swish,RE 表示 ReLU。

NBN 表示没有批量标准化。

s 表示步幅。

exp size表示第一个1*1升维的卷积核个数

#out表示1*1降维的卷积核个数。

==注意:第一个bneck的input_channel和exp size的channel的大小一致,所以第一个bneck中没有1*1升维的卷积核。==

5、Tensorflow代码复现MobileNetV3结构。

import tensorflow as tf
import tensorflow.keras.layers as layers
from tensorflow.keras.models import Model
from typing import Union

5.1 channel个数调整

# 将传递进来的channel个数调整为离他最近的8的整数倍的一个数字
def _make_divisible(v, divisor=8, min_value=None):
  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

5.2 计算步长为2的padding

# 当卷积的stride=2的时候,计算一个合适的padding进行填充
def correct_pad(input_size: Union[int, tuple], kernel_size: int):
    """Returns a tuple for zero-padding for 2D convolution with downsampling.

    Arguments:
      input_size: Input tensor size.
      kernel_size: An integer or tuple/list of 2 integers.

    Returns:
      A tuple.
    """

    if isinstance(input_size, int):
        input_size = (input_size, input_size)

    kernel_size = (kernel_size, kernel_size)

    adjust = (1 - input_size[0] % 2, 1 - input_size[1] % 2)
    correct = (kernel_size[0] // 2, kernel_size[1] // 2)
    return ((correct[0] - adjust[0], correct[0]),
            (correct[1] - adjust[1], correct[1]))

5.3 h-sigmoid和h-swish激活函数

# hard_sigmoid 激活函数
def hard_sigmoid(x):
    x=layers.Activation('hard_sigmoid')(x)
    return x

# hard_swish激活函数
def hard_swish(x):
    x=layers.Multiply()([hard_sigmoid(x),x]);
    return x

5.4 SENet注意力机制模块

# SENet注意力机制模块
def _se_block(inputs, filters, prefix, se_ratio=1 / 4.):
    # [batch, height, width, channel] -> [batch, channel]
    x = layers.GlobalAveragePooling2D(name=prefix + 'squeeze_excite/AvgPool')(inputs)

    # Target shape. Tuple of integers, does not include the samples dimension (batch size).
    # [batch, channel] -> [batch, 1, 1, channel]
    x = layers.Reshape((1, 1, filters))(x)

    # fc1
    x = layers.Conv2D(filters=_make_divisible(filters * se_ratio),
                      kernel_size=1,
                      padding='same',
                      name=prefix + 'squeeze_excite/Conv')(x)
    x = layers.ReLU(name=prefix + 'squeeze_excite/Relu')(x)

    # fc2
    x = layers.Conv2D(filters=filters,
                      kernel_size=1,
                      padding='same',
                      name=prefix + 'squeeze_excite/Conv_1')(x)
    # hard sigmoid激活函数
    x = hard_sigmoid(x)

    x = layers.Multiply(name=prefix + 'squeeze_excite/Mul')([inputs, x])
    return x

5.5 倒残差结构

# 倒残差结构
def _inverted_res_block(x,
                        input_c: int,  # input channel
                        kernel_size: int,  # kennel size(DW卷积对应的kernel_size)
                        exp_c: int,  # expanded channel
                        out_c: int,  # out channel
                        use_se: bool,  # 是否使用SE注意力机制模块
                        activation: str,  # ReLU or HardSwish
                        stride: int,
                        block_id: int,
                        alpha: float = 1.0):
    shortcut=x
    prefix='expanded_conv'
    input_c=_make_divisible(input_c*alpha)
    exp_c=_make_divisible(exp_c*alpha)
    out_c=_make_divisible(out_c*alpha)
    
    if activation=='RE':
        act=layers.ReLU()
    elif activation=='HS':
        act=hard_swish
    
    # 当block=0的时候(第一个bneck结构)没有1*1升维卷积。
    if block_id:
         # expand channel
        prefix = 'expanded_conv_{}/'.format(block_id)
        # 1*1卷积升维
        x=layers.Conv2D(exp_c,
                        kernel_size=1,
                        padding='same',
                        use_bias=False,
                        name=prefix+'expand')(x)
        # BN
        x=layers.BatchNormalization(epsilon=1e-3,momentum=0.999,name=prefix+'expamd/BatchNorm')(x)
        x=act(x)
    # stride=2的时候先进行padding
    if stride==2:
        input_size=(x.shape[1],x.shape[2]) # height,width
        x=layers.ZeroPadding2D(padding=correct_pad(input_size,kernel_size),
                                     name=prefix + 'depthwise/pad')(x)
    # DW卷积
    x=layers.DepthwiseConv2D(kernel_size=kernel_size,
                            strides=stride,
                            padding='same' if stride==1 else 'valid',
                            use_bias=False,
                            name=prefix+'depthwise')(x)
    x=layers.BatchNormalization(epsilon=1e-3,momentum=0.999,name=prefix+'depthwise/BatchNorm')(x)
    x=act(x)
        
    # 是否需要注意力机制模块
    if use_se:
        x=_se_block(x,filters=exp_c,prefix=prefix)
    # 1*1卷积降维
    x=layers.Conv2D(filters=out_c,
                    kernel_size=1,
                    padding='same',
                    use_bias=False,
                    name=prefix+'project')(x)
    x=layers.BatchNormalization(epsilon=1e-3,momentum=0.999,name=prefix+'project/BatchNorm')(x)
        
    # 只有stride==1且input_channel==output_channel的时候,才有shortcut连接
    if stride==1 and input_c==out_c:
        x=layers.Add(name=prefix+'Add')([shortcut,x])
    return x

5.6 MobileNetV3-Large模型

def mobilenet_v3_large(input_shape=(224,224,3),
                       num_class=1000,
                       alpha=1.0,
                       include_top=True):  #是否需要包含最后的全连接层
    input=layers.Input(shape=input_shape)
    
    x=layers.Conv2D(filters=16,kernel_size=3,strides=(2,2),padding='same',
                   use_bias=False,name='Conv')(input)
    x=layers.BatchNormalization(epsilon=1e-3,momentum=0.999,name='Conv/BatchNorm')(x)
    x=hard_swish(x)
    
    #15个bneck
    # input, input_c, k_size, expand_c,out_c, use_se, activation, stride, block_id
    x = _inverted_res_block(x, 16, 3, 16, 16, False, "RE", 1, 0 ,alpha)
    x = _inverted_res_block(x, 16, 3, 64, 24, False, "RE", 2, 1 ,alpha)
    x = _inverted_res_block(x, 24, 3, 72, 24, False, "RE", 1, 2 ,alpha)
    x = _inverted_res_block(x, 24, 5, 72, 40, True, "RE", 2, 3 ,alpha)
    x = _inverted_res_block(x, 40, 5, 120, 40, True, "RE", 1, 4 ,alpha)
    x = _inverted_res_block(x, 40, 5, 120, 40, True, "RE", 1, 5 ,alpha) 
    x = _inverted_res_block(x, 40, 3, 240, 80, False, "HS", 2, 6 ,alpha)
    x = _inverted_res_block(x, 80, 3, 200, 80, False, "HS", 1, 7 ,alpha)
    x = _inverted_res_block(x, 80, 3, 184, 80, False, "HS", 1, 8 ,alpha)
    x = _inverted_res_block(x, 80, 3, 184, 80, False, "HS", 1, 9 ,alpha)
    x = _inverted_res_block(x, 80, 3, 480, 112, True, "HS", 1, 10 ,alpha)
    x = _inverted_res_block(x, 112, 3, 672, 112, True, "HS", 1, 11 ,alpha)
    x = _inverted_res_block(x, 112, 5, 672, 160, True, "HS", 2, 12 ,alpha)
    x = _inverted_res_block(x, 160, 5, 960, 160, True, "HS", 1, 13 ,alpha)
    x = _inverted_res_block(x, 160, 5, 960, 160, True, "HS", 1, 14 ,alpha)
    
    last_c=_make_divisible(160*6*alpha)
    last_point_c=_make_divisible(1280*alpha)
    
    x=layers.Conv2D(filters=last_c,
                   kernel_size=1,
                   padding='same',
                   use_bias=False,
                   name='Conv_1')(x)
    x=layers.BatchNormalization(epsilon=1e-3,momentum=0.999,name='Conv_1/BatchNorm')(x)
    x=hard_swish(x)
    
    if include_top is True:
        x=layers.GlobalAveragePooling2D()(x)
        x=layers.Reshape((1,1,last_c))(x)
        
        # fc1
        x=layers.Conv2D(filters=last_point_c,
                       kernel_size=1,
                       padding='same',
                       name='Conv_2')(x)
        x=hard_swish(x)
        
        # fc2
        x = layers.Conv2D(filters=num_class,
                          kernel_size=1,
                          padding='same',
                          name='Logits/Conv2d_1c_1x1')(x)
        x = layers.Flatten()(x)
        x = layers.Softmax(name="Predictions")(x)
    model=Model(input,x,name='MobileNetV3-Large')
    return model
model=mobilenet_v3_large()
model.summary()

5.8 MobileNetV3-Large模型结构图

5.9 MobileNetV3-Small模型结构图

References

Howard A, Sandler M, Chu G, et al. Searching for mobilenetv3[C]//Proceedings of the IEEE/CVF international conference on computer vision. 2019: 1314-1324.

MobileNetV3的Tensorflow官方代码

MobileNetV3 代码复现,网络解析,附Tensorflow完整代码

睿智的目标检测47——Keras 利用mobilenet系列(v1,v2,v3)搭建yolov4目标检测平台

深度学习之图像分类(十二)--MobileNetV3 网络结构

MobileNetv3网络详解

github.com/WZMIAOMIAO/…