目前衡量模型复杂度的一个通用指标是 FLOPs,具体指的是计算在网络中的乘法操作和加法操作的数量,但是这却是一个间接指标,因为它不完全等同于速度。如下图中的(c)和(d),可以看到具有相同 FLOPs 的两个模型,其速度在不同系统架构下却存在差异。这种不一致主要归结为两个原因,首先除了 FLOPs 影响速度之外,还有内存使用量(memory access cost, MAC)等,这不能忽略,且对于 GPUs 来说可能会是瓶颈。另外模型的并行程度也影响速度,并行度高的模型速度相对更快。另外一个原因,模型在不同平台上的运行速度是有差异的,如 GPU 和 ARM,而且采用不同的库也会有影响。
据此,作者在特定的平台下研究 ShuffleNet V1 和 MobileNet V2 的运行时间,并结合理论与实验得到了 4 条实用的指导原则:
根据前面的 4 条准则,作者分析了 ShuffleNet V1 设计的不足,并在此基础上改进得到了 ShuffleNet V2,两者模块上的对比如下图(a 和 b 是 ShuffleNet V1 中的两种 units,c 和 d 是 ShuffleNet V2 中的两种 units)所示:
ShuffleNet V1 中有以下缺陷:
为了改善 V1 中的缺陷,V2 版本引入了一种新的运算:Channel split。具体来说,在开始时先将通道数为 c c c 的输入特征图在通道维度分成两个分支,每个分支的通道数都是 c / 2 c/2 c/2。这样做的好处是:
【注】对于下采样模块,即 ShuffleNet V2 中的第二个 unit,不再有 Channel split,而是每个分支都是直接复制一份输入,每个分支都有步长为 2 的下采样,最后 Concat 在一起后,特征图空间大小减半,但是通道数翻倍。
ShuffleNet V2 的整体结构如下表所示,基本与 V1 类似:
【注】ShuffleNet V2 在全局池化之前增加了一个卷积层。
import tensorflow as tf
def channel_shuffle(inputs, num_groups):
n, h, w, c = inputs.shape
x_reshaped = tf.reshape(inputs, [-1, h, w, num_groups, c // num_groups])
x_transposed = tf.transpose(x_reshaped, [0, 1, 2, 4, 3])
output = tf.reshape(x_transposed, [-1, h, w, c])
return output
def conv(inputs, filters, kernel_size, strides=1):
x = tf.keras.layers.Conv2D(filters, kernel_size, strides, padding='same')(inputs)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.Activation('relu')(x)
return x
def depthwise_conv_bn(inputs, kernel_size, strides=1):
x = tf.keras.layers.DepthwiseConv2D(kernel_size=kernel_size,
strides=strides,
padding='same')(inputs)
x = tf.keras.layers.BatchNormalization()(x)
return x
def ShuffleNetUnitA(inputs, out_channels):
shortcut, x = tf.split(inputs, 2, axis=-1)
x = conv(inputs, out_channels // 2, kernel_size=1, strides=1)
x = depthwise_conv_bn(x, kernel_size=3, strides=1)
x = conv(x, out_channels // 2, kernel_size=1, strides=1)
x = tf.concat([shortcut, x], axis=-1)
x = channel_shuffle(x, 2)
return x
def ShuffleNetUnitB(inputs, out_channels):
shortcut = inputs
in_channels = inputs.shape[-1]
x = conv(inputs, out_channels // 2, kernel_size=1, strides=1)
x = depthwise_conv_bn(x, kernel_size=3, strides=2)
x = conv(x, out_channels-in_channels, kernel_size=1, strides=1)
shortcut = depthwise_conv_bn(shortcut, kernel_size=3, strides=2)
shortcut = conv(shortcut, in_channels, kernel_size=1, strides=1)
output = tf.concat([shortcut, x], axis=-1)
output = channel_shuffle(output, 2)
return output
def stage(inputs, out_channels, n):
x = ShuffleNetUnitB(inputs, out_channels)
for _ in range(n):
x = ShuffleNetUnitA(x, out_channels)
return x
def ShuffleNet(inputs, first_stage_channels, num_groups):
x = tf.keras.layers.Conv2D(filters=24,
kernel_size=3,
strides=2,
padding='same')(inputs)
x = tf.keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(x)
x = stage(x, first_stage_channels, n=3)
x = stage(x, first_stage_channels*2, n=7)
x = stage(x, first_stage_channels*4, n=3)
x = tf.keras.layers.Conv2D(filters=1024,
kernel_size=1,
strides=1,
padding='same')(x)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
x = tf.keras.layers.Dense(1000)(x)
return x
inputs = np.zeros((1, 224, 224, 3), np.float32)
ShuffleNet(inputs, 144, 1).shape
TensorShape([1, 1000])
ShuffleNetV2:轻量级CNN网络中的桂冠