网络设计是基于MobileNetV1。它保持了简单性,同时显著提高了精度,在移动应用的多图像分类和检测任务上达到了最新的水平。主要贡献是一个新的层模块:具有线性瓶颈的倒置残差。该模块将输入的低维压缩表示首先扩展到高维并用轻量级深度卷积进行过滤。随后用线性卷积将特征投影回低维表示。
在介绍MobilenetV2网络结构之前需要先了解一下网络内部的细节。
深度可分卷积
这是一种分解卷积的形式,它将一个标准卷积分解为深度卷积和一个1×1卷积,1x1卷积又叫称为点卷积,下图是MobilenetV1中论文的图。
标准的卷积是输入一个 D F D_{F} DF x D F D_{F} DF x M M M 的特征图,输出一个 D F D_{F} DF x D F D_{F} DF x N N N 的特征图。
标准的卷积层的参数是 D K D_{K} DK x D K D_{K} DK x M M M x N N N ,其中 D K D_{K} DK是卷积核的小大, M M M是输入通道数, N N N是输出通道数。
所以标准的卷积计算量如下:
D K D_{K} DK x D K D_{K} DK x M M M x N N N x D F D_{F} DF x D F D_{F} DF
深度可分离卷积由两层组成:深度卷积和点卷积。
我们使用深度卷积来为每个输入通道(输入深度)应用一个过滤器。然后使用点态卷积(一个简单的1×1卷积)创建深度层输出的线性组合。
其计算量为: D K D_{K} DK x D K D_{K} DK x M M M x D F D_{F} DF x D F D_{F} DF
相对于标准卷积,深度卷积是非常有效的。然而,它只过滤输入通道,并没有将它们组合起来创建新的功能。因此,为了生成这些新特征,需要一个额外的层,通过1 × 1的卷积计算深度卷积输出的线性组合。
其计算量为: M M M x N N N x D F D_{F} DF x D F D_{F} DF
例子:
标准卷积:
假设有一个56 x 56 x 16的特征图,其卷积核是3 x 3大小,输出通道为32,计算量大小就是56 x56 x16 x 32 x 3 x 3 = 14450688
深度可分离卷积:
假设有一个56 x 56 x 16的特征图,输出通道为32,先用16个3 x 3大小的卷积核进行卷积,接着用32个1 x 1大小的卷积核分别对这用16个3 x 3大小的卷积核进行卷积之后的特征图进行卷积,计算量为3 x 3 x 16 x 56 x 56 + 16 x 32 x 56 x 56 = 2057216
从而可见计算量少了许多。
在论文中,实验者们发现,使用线性层是至关重要的,因为它可以防止非线性破坏太多的信息,在瓶颈中使用非线性层确实会使性能降低几个百分点,如下图。线性瓶颈
模型的严格来说比非线性模型要弱一些,因为激活总是可以在线性状态下进行,并对偏差和缩放进行适当的修改。然而,我们在图a中展示的实验表明,线性瓶颈改善了性能,为非线性破坏低维空间中的信息提供了支持。
倒残差瓶颈块
与残差块类似,其中每个块包含一个输入,然后是几个瓶颈,然后是扩展,下图是论文中给大家展示的残差块与倒残差瓶颈块的区别。
主要区别就是残差块先进行降维再升维,而倒残差瓶颈块是先进行升维再降维,结构如下:
倒残差瓶颈块从 k k k转换为 k ′ k′ k′个通道,步长为 s s s,扩展系数为 t t t。
网络使用使用ReLU6作为非线性,因为用于低精度计算时它的鲁棒性。
其中:
from os import name
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import (
Activation, BatchNormalization, Conv2D, DepthwiseConv2D, Dropout,ZeroPadding2D, Add, Dense,
GlobalAveragePooling2D, Input, Reshape
)
from tensorflow.keras.models import Model
from plot_model import plot_model
from tensorflow.keras import backend as K
def correct_pad(inputs, kernel_size):
img_dim = 1
input_size = K.int_shape(inputs)[img_dim:(img_dim + 2)]
if isinstance(kernel_size, int):
kernel_size = (kernel_size, kernel_size)
if input_size[0] is None:
adjust = (1, 1)
else:
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]))
# 保证特征层为8得倍数
def _make_divisible(v, divisor, min_value=None):
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
if new_v < 0.9 * v:
new_v += divisor
return new_v
def relu6(x):
return K.relu(x, max_value=6)
def Conv2D_block(inputs, filters, kernel_size=(3, 3), strides=(1, 1)):
x = Conv2D(
filters=filters, kernel_size=kernel_size, padding='valid',
use_bias=False, strides=strides
)(inputs)
x = BatchNormalization(epsilon=1e-3,
momentum=0.999)(x)
x = Activation(relu6)(x)
return x
def bottleneck(inputs, expansion, stride, alpha, filters):
in_channels = K.int_shape(inputs)[-1]
pointwise_conv_filters = int(filters * alpha)
pointwise_filters = _make_divisible(pointwise_conv_filters, 8)
x = inputs
# 数据扩充
x = Conv2D(expansion * in_channels,
kernel_size=1,
padding='same',
use_bias=False,
activation=None)(x)
x = BatchNormalization(epsilon=1e-3,
momentum=0.999)(x)
x = Activation(relu6)(x)
if stride == 2:
x = ZeroPadding2D(padding=correct_pad(x, 3))(x)
# 深度卷积
x = DepthwiseConv2D(kernel_size=3,
strides=stride,
activation=None,
use_bias=False,
padding='same' if stride == 1 else 'valid')(x)
x = BatchNormalization(epsilon=1e-3,
momentum=0.999)(x)
x = Activation(relu6)(x)
# 1x1卷积用于改变通道数
x = Conv2D(pointwise_filters,
kernel_size=1,
padding='same',
use_bias=False,
activation=None)(x)
x = BatchNormalization(epsilon=1e-3,
momentum=0.999)(x)
if (in_channels == pointwise_filters) and stride == 1:
return Add()([inputs, x])
return x
def MobilenetV2(inputs, alpha=0.35, dropout=1e-3, classes=17):
first_block_filters = _make_divisible(32 * alpha, 8)
x = ZeroPadding2D(padding=correct_pad(inputs, 3))(inputs)
x = Conv2D_block(x, filters=first_block_filters, kernel_size=3, strides=(2, 2))
x = bottleneck(x, filters=16, alpha=alpha, stride=1, expansion=1)
x = bottleneck(x, filters=24, alpha=alpha, stride=2, expansion=6)
x = bottleneck(x, filters=24, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=32, alpha=alpha, stride=2, expansion=6)
x = bottleneck(x, filters=32, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=32, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=64, alpha=alpha, stride=2, expansion=6)
x = bottleneck(x, filters=64, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=64, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=64, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=96, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=96, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=96, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=160, alpha=alpha, stride=2, expansion=6)
x = bottleneck(x, filters=160, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=160, alpha=alpha, stride=1, expansion=6)
x = bottleneck(x, filters=320, alpha=alpha, stride=1, expansion=6)
if alpha > 1.0:
last_block_filters = _make_divisible(1280 * alpha, 8)
else:
last_block_filters = 1280
x = Conv2D_block(x, filters=last_block_filters, kernel_size=1, strides=(1, 1))
x = GlobalAveragePooling2D()(x)
shape = (1, 1, int(last_block_filters))
x = Reshape(shape, name='reshape_1')(x)
x = Dropout(dropout, name='dropout')(x)
x = Conv2D(classes, (1, 1),padding='same', name='conv_preds')(x)
x = Activation('softmax', name='act_softmax')(x)
x = Reshape((classes,), name='reshape_2')(x)
return x
if __name__ == '__main__':
is_show_picture = False
inputs = Input(shape=(224,224,3))
classes = 1000
model = Model(inputs=inputs, outputs=MobilenetV2(inputs=inputs, classes=classes))
model.summary()
print(len(model.layers))
# for i in range(len(model.layers)):
# print(i, model.layers[i])
if is_show_picture:
plot_model(model,
to_file='./nets_picture/MobilenetV2.png',
)
print("plot_model------------------------>")