参考论文:EfficientNetV2: Smaller Models and Faster Training
1、EfficientNetV2简介
图 1. ImageNet ILSVRC2012 top-1 准确度与训练时间和参数的关系——标记为 21k 的模型在 ImageNet21k 上进行了预训练,而其他模型则直接在 ImageNet ILSVRC2012 上进行了训练。训练时间是用 32 个 TPU 核心测量的。所有 EfficientNetV2 模型都经过渐进式学习的训练。我们的 EfficientNetV2 的训练速度比其他方法快 5 到 11 倍,同时使用的参数最多可减少 6.8 倍。
这篇文章介绍了EfficientNetV2,与以前的模型相比,它具有更快的训练速度和更好的参数效率。为了开发这些模型,我们结合使用训练感知神经架构搜索和缩放,共同优化训练速度和参数效率。这些模型是从富含新操作(如 Fused-MBConv)的搜索空间中搜索的。我们的实验表明,EfficientNetV2 模型的训练速度比最先进的模型快得多,同时体积缩小了 6.8 倍。
通过渐进式学习,我们的 EfficientNetV2 在 ImageNet 和 CIFAR/Cars/Flowers 数据集上显着优于以前的模型。通过在相同的 ImageNet21k 上进行预训练,我们的 EfficientNetV2 在 ImageNet ILSVRC2012 上实现了 87.3% 的 top-1 准确度,比最近的 ViT 提高了 2.0%,同时使用相同的计算资源训练速度提高了 5x-11x。
2、EfficientNet存在的问题
- 非常大的图像尺寸的训练速度很慢。
- Depthwise Convolution在浅层网络中速度很慢。
- 同等地放大每个stage是次优的。
3、渐进式学习的改进方法
在早期训练阶段,我们训练图像尺寸小、正则化弱(例如dropout和数据增强)的网络,然后逐渐增加图像尺寸并添加更强的正则化。基于渐进式调整大小,但通过动态调整正则化,我们的方法可以加快训练速度而不会导致准确率的下降。
4、EfficientNetV2与EfficientNet的区别
- EfficientNetV2在浅层网络中广泛使用MBConv和Fused-MBConv。
- EfficientNetV2更偏向MBConv的较小的expansion rario,因为较小的expansion ratio往往具有较小的内存访问开销。
- EfficientNetV2更偏向更小的
3*3
卷积核,但它增加了更多的层来补偿由于小卷积核导致的感受野减少。 - EfficientNetV2完全移除了原始EfficientNet中的最后一个stride-1阶段,这可能是由于其较大的参数大小和内存访问开销。
5、MBConv和Fused-MBConv
将 MBConv (Sandler et al., 2018; Tan & Le, 2019a) 中的 depthwise conv3x3 和 expansion conv1x1 替换为单个常规 conv3x3,如图 2 所示。
表 3. 用 Fused-MBConv 替换 MBConv。 No fused表示所有stage都使用MBConv,Fused stage1-3表示在stage {2,3,4}中用Fused-MBConv替换MBConv。
6、EfficientNetV2架构
这部分参考:https://blog.csdn.net/qq_37541097/article/details/116933569
表 4. EfficientNetV2-S 架构——MBConv 和 FusedMBConv 块在图 2 中进行了描述。
6.1 Fused-MBConv
Fused-MBConv模块名称后跟的1,4表示expansion ratio,k3x3表示kenel_size为3x3,下面是我参考大佬的结构图,注意当expansion ratio等于1时是没有expand conv的,还有这里是没有使用到SE结构的(原论文图中有SE)。
注意当stride=1且输入输出Channels相等时才有shortcut连接。
还需要注意的是,当有shortcut连接时才有Dropout层,而且这里的Dropout层是Stochastic Depth
,即会随机丢掉整个block的主分支(只剩捷径分支,相当于直接跳过了这个block)也可以理解为减少了网络的深度。具体可参考Deep Networks with Stochastic Depth这篇文章。
6.2 MBConv模块
MBConv模块**和EfficientNetV1中是一样的,其中模块名称后跟的4,6表示expansion ratio,
SE0.25表示使用了SE模块,0.25表示SE模块中第一个全连接层的节点个数是输入该MBConv模块特征矩阵channels的 $\frac{1}{4}$ ,下面是大佬重绘的MBConv模块结构图。
注意当stride=1且输入输出Channels相等时才有shortcut连接。同样这里的Dropout层是Stochastic Depth。
Stride就是步距,注意每个Stage中会重复堆叠Operator模块多次,只有第一个Opertator模块的步距是按照表格中Stride来设置的,其他的默认都是1。#Channels表示该Stage输出的特征矩阵的Channels,
#Layers表示该Stage重复堆叠Operator的次数。
7、代码复现
这部分参考:
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Model, layers
7.1 swish激活函数
def swish(x):
x = x *layers.Activation('sigmoid')(x)
return x
7.2 标准卷积模块
def conv_block(inputs, filters, kernel_size, stride, activation=True):
# 卷积+BN+激活
x = layers.Conv2D(filters=filters,
kernel_size=kernel_size,
strides=stride,
padding='same',
use_bias=False)(inputs)
x = layers.BatchNormalization()(x)
if activation: # 如果activation==True就使用激活函数
x = swish(x)
return x
7.3 SENet注意力机制模块
def se_block(inputs, in_channel, ratio=0.25):
'''
inputs: 深度卷积层的输出特征图
input_channel: MBConv模块的输入特征图的通道数
ratio: 第一个全连接层的通道数下降为MBConv输入特征图的几倍
'''
squeeze = int(in_channel * ratio) # 第一个FC降低通道数个数
excitation = inputs.shape[-1] # 第二个FC上升通道数个数
# 全局平均池化 [h,w,c]==>[None,c]
x = layers.GlobalAveragePooling2D()(inputs)
# [None,c]==>[1,1,c]
x = layers.Reshape(target_shape=(1, 1, x.shape[-1]))(x)
# [1,1,c]==>[1,1,c/4]
x = layers.Conv2D(filters=squeeze, # 通道数下降1/4
kernel_size=(1, 1),
strides=1,
padding='same')(x)
x = swish(x) # swish激活
# [1,1,c/4]==>[1,1,c]
x = layers.Conv2D(filters=excitation, # 通道数上升至原来
kernel_size=(1, 1),
strides=1,
padding='same')(x)
x = tf.nn.sigmoid(x) # sigmoid激活,权重归一化
# [h,w,c] * [1,1,c] ==> [h,w,c]
outputs = layers.multiply([inputs, x])
return outputs
7.4 MBConvm模块
def MBConv(x, expansion, kernel_size, stride, out_channel, dropout_rate):
'''
expansion: 第一个卷积层特征图通道数上升的倍数
kernel_size: 深度卷积层的卷积核size
stride: 深度卷积层的步长
out_channel: 第二个卷积层下降的通道数
dropout_rate: Dropout层随机丢弃输出层的概率,直接将输入接到输出
'''
# 残差边
residual = x
# 输入特征图的通道数
in_channel = x.shape[-1]
# 1*1标准卷积升维
x = conv_block(inputs=x,
filters=in_channel * expansion, # 上升通道数为expansion倍
kernel_size=(1, 1),
stride=1,
activation=True)
# 3*3深度卷积
x = layers.DepthwiseConv2D(kernel_size=kernel_size,
strides=stride,
padding='same',
use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = swish(x)
# SE注意力机制,输入特征图x,和MBConv模块输入图像的通道数
x = se_block(inputs=x, in_channel=in_channel)
# 1*1标准卷积降维,使用线性激活
x = conv_block(inputs=x,
filters=out_channel, # 上升通道数
kernel_size=(1, 1),
stride=1,
activation=False) # 不使用swish激活
# ⑥ 只有步长=1且输入等于输出shape,才使用残差连接输入和输出
if stride == 1 and residual.shape == x.shape:
# 判断是否进行dropout操作
if dropout_rate > 0:
# 参数noise_shape一定的概率将某一层的输出丢弃
x = layers.Dropout(rate=dropout_rate, # 丢弃概率
noise_shape=(None, 1, 1, 1))
# 残差连接输入和输出
x = layers.Add([residual, x])
return x
# 如果步长=2,直接输出1*1卷积降维后的结果
return x
7.5 Fused_MBConv模块
def Fused_MBConv(x, expansion, kernel_size, stride, out_channel, dropout_rate):
# 残差边
residual = x
# 输入特征图的通道数
in_channel = x.shape[-1]
# 如果通道扩展倍数expansion==1,就不需要升维
if expansion != 1:
# 3*3标准卷积升维
x = conv_block(inputs=x,
filters=in_channel * expansion, # 通道数上升为原来的expansion倍
kernel_size=kernel_size,
stride=stride)
# 判断卷积的类型
# 如果expansion==1,变成3*3卷积+BN+激活;
# 如果expansion!=1,变成1*1卷积+BN,步长为1
x = conv_block(inputs=x,
filters=out_channel, # FusedMBConv模块输出特征图通道数
kernel_size=(1, 1) if expansion != 1 else kernel_size,
stride=1 if expansion != 1 else stride,
activation=False if expansion != 1 else True)
# 当步长=1且输入输出shape相同时残差连接
if stride == 1 and residual.shape == x.shape:
# 判断是否使用Dropout层
if dropout_rate > 0:
x = layers.Dropout(rate=dropout_rate, # 随机丢弃输出层的概率
noise_shape=(None, 1, 1, 1)) # 代表不是杀死神经元,是丢弃输出层
# 残差连接输入和输出
outputs = layers.Add([residual, x])
return outputs
# 若步长等于2,直接输出卷积层输出结果
return x
7.6 堆叠MBConv和Fused-MBConv
#每个模块重复执行num次
# Fused_MBConv模块
def Fused_stage(x, num, expansion, kernel_size, stride, out_channel, dropout_rate):
for _ in range(num):
# 传入参数,反复调用Fused_MBConv模块
x = Fused_MBConv(x, expansion, kernel_size, stride, out_channel, dropout_rate)
return x
# MBConv模块
def stage(x, num, expansion, kernel_size, stride, out_channel, dropout_rate):
for _ in range(num):
# 反复执行MBConv模块
x = MBConv(x, expansion, kernel_size, stride, out_channel, dropout_rate)
return x
7.7 搭建EfficientNetV2-S网络结构
def efficientnetv2(input_shape, classes, dropout_rate):
# 构造输入层
inputs = keras.Input(shape=input_shape)
# 标准卷积层[224,224,3]==>[112,112,24]
x = conv_block(inputs, filters=24, kernel_size=(3, 3), stride=2)
# [112,112,24]==>[112,112,24]
x = Fused_stage(x, num=2, expansion=1, kernel_size=(3, 3),
stride=1, out_channel=24, dropout_rate=dropout_rate)
# [112,112,24]==>[56,56,48]
x = Fused_stage(x, num=4, expansion=4, kernel_size=(3, 3),
stride=2, out_channel=48, dropout_rate=dropout_rate)
# [56,56,48]==>[32,32,64]
x = Fused_stage(x, num=4, expansion=4, kernel_size=(3, 3),
stride=2, out_channel=64, dropout_rate=dropout_rate)
# [32,32,64]==>[16,16,128]
x = stage(x, num=6, expansion=4, kernel_size=(3, 3),
stride=2, out_channel=128, dropout_rate=dropout_rate)
# [16,16,128]==>[16,16,160]
x = stage(x, num=9, expansion=6, kernel_size=(3, 3),
stride=1, out_channel=160, dropout_rate=dropout_rate)
# [16,16,160]==>[8,8,256]
x = stage(x, num=15, expansion=6, kernel_size=(3, 3),
stride=2, out_channel=256, dropout_rate=dropout_rate)
# [8,8,256]==>[8,8,1280]
x = conv_block(x, filters=1280, kernel_size=(1, 1), stride=1)
# [8,8,1280]==>[None,1280]
x = layers.GlobalAveragePooling2D()(x)
# dropout层随机杀死神经元
if dropout_rate > 0:
x = layers.Dropout(rate=dropout_rate)
# [None,1280]==>[None,classes]
logits = layers.Dense(classes)(x)
# 构建网络
model = Model(inputs, logits)
return model
注意,这里最后的全连接层暂时每加softmax激活函数,根据个人情况吧,
compile
的时候加也可以。
if __name__ == '__main__':
model = efficientnetv2(input_shape=[224, 224, 3],classes=1000,dropout_rate=0)
# 查看模型摘要
model.summary()
我去看源码的时候,发现Tensorflow的2.9版本实现了这个模型,所以版本高的可以直接去调用API,用迁移学习的方法训练自己的数据集(自己实现会忽略掉很多细节,这里的代码也是才考别人实现的,源码中那种代码风格感觉不太优雅)。
7.8 模型结构大图(长图)
其中,这个地方是swish激活函数的实现。
References
EfficientNetV2: Smaller Models and Faster Training