paper核心思想:通过固定比例去缩放网络的宽度,深度,输入图像的分辨率来提高网络性能
1.EfficientNet介绍
扩大卷积神经网络被广泛用于提高精度。然而,扩大卷积神经网络的过程从来没有被很好地理解,目前有许多方法可以做到这一点。最常见的方法是通过深度或宽度(其实也就是增加channels)来放大卷积网。另一种不太常见但越来越流行的方法是根据图像分辨率放大模型。在以前的工作中,通常只缩放三个维度中的一个——深度、宽度和图像大小。虽然可以任意伸缩两个或三个维度,但任意伸缩需要繁琐的手动调优,而且通常仍会产生次优的精度和效率。
在EfficientNet中,从作者的研究证明平衡网络宽度/深度/分辨率的所有维度是至关重要的,令人惊讶的是,这种平衡可以通过固定比例缩放每个维度来实现。与传统的任意缩放这些因素不同,我们的方法统一缩放网络的宽度,深度,以及分辨率,一组固定的比例系数来解决问题。
传统的方法大多将卷积神经网络的规模分为以下几个维度之一:
- Depth ( d ):更深层次的ConvNet能够捕获更丰富、更复杂的特性,并能很好地概括新任务。然而,由于梯度消失问题,更深的网络也更难以训练。虽然skip connections(和批处理归一化等技术缓解了训练问题,但是非常深的网络的准确率提高降低
- Width ( w ):更广泛的网络往往能够捕获更多的细粒度特征,更容易训练。然而,极宽但较浅的网络往往难以捕获更高层次的特征。当网络变得更宽时,随着w的增大,准确率很快饱和
- Resolution ( r ):增加输入网络的图像分辨率能够潜在得获得更高细粒度的特征模板,但对于非常高的输入分辨率,准确率的增益也会减小。并且大分辨率图像会增加计算量。
结论一:
- 放大网络宽度、深度或分辨率的任何维度都能提高精度,但对于更大的模型,精度增益会减小。
根据经验,我们观察到不同的尺度并不是独立的。直观地说,对于更高分辨率的图像,我们应该增加网络深度,这样更大的接受域可以帮助捕获更大图像中包含更多像素的相似特征。相应地,当分辨率较高时,我们也应该增加网络宽度,为了在高分辨率图像中捕获更多像素的更细粒度的模式。这些直觉表明,我们需要协调和平衡不同的尺度,而不是传统的一维尺度。
结论二:
- 为了追求更好的准确性和效率,在ConvNet缩放过程中,平衡网络宽度、深度和分辨率的所有维度是至关重要的。
2.EfficientNet参数选择
针对以上所提出的问题,现在所需要的求得在有限的资源里面如何确定平衡这网络的深度d,通道的宽度c,还有输入图像的分辨率r。
为了能够处理这个问题,作者将问题进行了抽象:
在实践中,ConvNet层往往被划分为多个阶段,每个阶段的所有层都具有相同的架构。例如,ResNet有五个阶段,每个阶段的所有层都具有相同的卷积类型,除了第一层执行降采样(通过stride进行改变shape)。因此,我们可以将卷积网络定义为:
下面这一段感觉可能难以理解:常规的ConvNet设计主要关注于寻找最佳的层架构Fi,而模型缩放试图在不改变基线网络的Fi预定义的情况下扩展网络长度(Li)、宽度(Ci)和/或分辨率(Hi, Wi)。我理解的应该就是不改变网络快结构,只是改变深度,宽度与输入的分辨率。通过修正F i FiFi,模型缩放简化了新的资源约束下的设计问题,但仍有很大的设计空间去探索不同的L i , C i , H i , W i 每一层。为了进一步减少设计空间,我们限制所有层必须按恒定比例均匀缩放。我们的目标是在任何给定的资源约束下最大化模型的精度。
而为了能够统一w i d t h , d e p t h , r e s o l u t i o n, width,depth,resolution的参数,作者使用了混合缩放方法 ( compound scaling method) ,利用一个混合因子φ 去统一缩放这三者,具体的计算公式如下:
接下来作者在基准网络EfficientNetB-0使用NAS来搜索α , β , γ 这三个参数
- input_size:代表训练网络时输入网络的图像大小
- width_coefficient:代表channel维度上的倍率因子,比如在 EfficientNetB0中Stage1的3x3卷积层所使用的卷积核个数是32,那么在B6中就是 32 × 1.8 = 57.6接着取整到离它最近的8的整数倍即56,其它Stage同理
- depth_coefficient:代表depth维度上的倍率因子( 仅针对Stage2 到Stage8),比如在EfficientNetB0中Stage7的L = 4 ,那么在B6中就是 4 × 2.6 = 10.4,接着向上取整即11
- drop_connect_rate:是在MBConv结构中dropout层使用的drop_rate,在官方keras模块的实现中MBConv结构的drop_rate是从0递增到drop_connect_rate的(具体实现可以看下官方源码,注意,在源码实现中只有使用shortcut的时候才有Dropout层)。还需要注意的是,这里的Dropout层是Stochastic Depth,即会随机丢掉整个block的主分支(只剩捷径分支,相当于直接跳过了这个block)也可以理解为减少了网络的深度。
- dropout_rate:是最后一个全连接层前的dropout层(在stage9的Pooling与FC之间)的dropout_rate。
3.EfficientNet网络结构
下表为EfficientNet-B0的网络框架(B1-B7就是在B0的基础上修改Resolution,Channels以及Layers),可以看出网络总共分成了9个Stage,第一个Stage就是一个卷积核大小为3x3步距为2的普通卷积层(包含BN和激活函数Swish),Stage2~Stage8都是在重复堆叠MBConv结构(最后一列的Layers表示该Stage重复MBConv结构多少次),而Stage9由一个普通的1x1的卷积层(包含BN和激活函数Swish)一个平均池化层和一个全连接层组成。表格中每个MBConv后会跟一个数字1或6,这里的1或6就是倍率因子n即MBConv中第一个1x1的卷积层会将输入特征矩阵的channels扩充为n倍,其中k3x3或k5x5表示MBConv中Depthwise Conv所采用的卷积核大小。Channels表示通过该Stage后输出特征矩阵的Channels。
MBConv结构
MBConv其实就是MobileNetV3网络中的InvertedResidualBlock,但也有些许区别。一个是采用的激活函数不一样(EfficientNet的MBConv中使用的都是Swish激活函数),另一个是在每个MBConv中都加入了SE(Squeeze-and-Excitation)模块。下图是引用参考资料的MBConv结构。
如图所示,MBConv结构主要由一个1x1的普通卷积(升维作用,包含BN和Swish),一个kxk的Depthwise Conv卷积(包含BN和Swish)k的具体值可看EfficientNet-B0的网络框架主要有3x3和5x5两种情况,一个SE模块,一个1x1的普通卷积(降维作用,包含BN),一个Droupout层构成。搭建过程中还需要注意几点:
- 第一个升维的1x1卷积层,它的卷积核个数是输入特征矩阵channel的n倍n ∈ { 1 , 6 }
- 当n = 1 时,不要第一个升维的1x1卷积层,即Stage2中的MBConv结构都没有第一个升维的1x1卷积层(这和MobileNetV3网络类似)
- 关于shortcut连接,仅当输入MBConv结构的特征矩阵与输出的特征矩阵shape相同时才存在(代码中可通过stride == 1 and inputc_channels == output_channels条件来判断)
- SE模块如下所示,由一个全局平均池化,两个全连接层组成。第一个全连接层的节点个数是输入该MBConv特征矩阵channels的 1 /4 ,且使用Swish激活函数。第二个全连接层的节点个数等于Depthwise Conv层输出的特征矩阵channels,且使用Sigmoid激活函数。
EfficientNet(B0-B7)参数
4.EfficientNet性能统计
EfficientNet-B7超过了现有最好的GPipe精度,但使用的参数少了8.4倍,推理速度快了6.1倍。与广泛使用的ResNet-50 相比,EfficientNet-B4在类似FLOPS的情况下,将top-1的准确率从76.3%提高到了83.0%(+6.7%)。在8个广泛使用的数据集中有5个具有最先进的精度,同时比现有的ConvNets减少至多21倍的参数。
EfficientNet-B7实现了新的最先进的84.3%的顶级精度,但比GPipe小8.4倍,快6.1倍。EfficientNet-B1比ResNet-152小7.6倍,快5.7倍。
5.EfficientNet的pytorch实现
参考github代码:
import math import torch import torch.nn as nn # 激活函数需要继承nn.Module,然后进行正向传播 class Swish(nn.Module): def forward(self, x): return x * torch.sigmoid(x) # 3x3 DW卷积 其中'//'是取整运算符,eg: 3//2=1, # 其中stride默认为1,也就是一般不需要进行尺寸减半操作,而通过padding扩充使得对于kernel_size为3/5均可自动补充 def ConvBNAct(in_channels,out_channels,kernel_size=3, stride=1, groups=1): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=stride, padding=kernel_size//2, groups=groups), nn.BatchNorm2d(out_channels), Swish() ) # pw卷积含激活函数 def Conv1x1BNAct(in_channels,out_channels): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1), nn.BatchNorm2d(out_channels), Swish() ) # pw卷积不含激活函数 def Conv1x1BN(in_channels,out_channels): return nn.Sequential( nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1), nn.BatchNorm2d(out_channels) ) def Conv1(in_planes, places, stride=2): return nn.Sequential( nn.Conv2d(in_channels=in_planes,out_channels=places,kernel_size=7,stride=stride,padding=3, bias=False), nn.BatchNorm2d(places), Swish(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1) ) # 打平操作 class Flatten(nn.Module): def forward(self, x): return x.view(x.shape[0], -1) # SE结构 class SEBlock(nn.Module): def __init__(self, channels, ratio=16): super().__init__() # 这里压缩的倍数的16倍,而在MobileNetv3中压缩的倍数是4倍 mid_channels = channels // ratio # 基于通道的注意力 self.se = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, mid_channels, kernel_size=1, stride=1, padding=0, bias=True), Swish(), nn.Conv2d(mid_channels, channels, kernel_size=1, stride=1, padding=0, bias=True), ) def forward(self, x): return x * torch.sigmoid(self.se(x)) # MobileNetv3中的结构块,所以是MBBlock,需要注意,channels的扩充有因子expansion_factor=6 class MBConvBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, expansion_factor=6): super(MBConvBlock, self).__init__() self.stride = stride self.expansion_factor = expansion_factor mid_channels = (in_channels * expansion_factor) self.bottleneck = nn.Sequential( # 首先PW卷积升维,扩充倍数是6倍 Conv1x1BNAct(in_channels, mid_channels), # 然后DW卷积,维度不变,并且通过这一步将尺寸缩放 ConvBNAct(mid_channels, mid_channels, kernel_size, stride, groups=mid_channels), # 添加一个注意力模块 SEBlock(mid_channels), # 最后PW卷积降维操作,变回原来的channels数 Conv1x1BN(mid_channels, out_channels) ) # 残差相加 if self.stride == 1: self.shortcut = Conv1x1BN(in_channels, out_channels) def forward(self, x): out = self.bottleneck(x) out = (out + self.shortcut(x)) if self.stride==1 else out return out # 主网络 class EfficientNet(nn.Module): params = { 'efficientnet_b0': (1.0, 1.0, 224, 0.2), 'efficientnet_b1': (1.0, 1.1, 240, 0.2), 'efficientnet_b2': (1.1, 1.2, 260, 0.3), 'efficientnet_b3': (1.2, 1.4, 300, 0.3), 'efficientnet_b4': (1.4, 1.8, 380, 0.4), 'efficientnet_b5': (1.6, 2.2, 456, 0.4), 'efficientnet_b6': (1.8, 2.6, 528, 0.5), 'efficientnet_b7': (2.0, 3.1, 600, 0.5), } def __init__(self, subtype = 'efficientnet_b0', num_classes = 5): super(EfficientNet, self).__init__() self.width_coeff = self.params[subtype][0] # 元组的第一个参数: 宽度的扩充因子 width_coefficient self.depth_coeff = self.params[subtype][1] # 元组的第二个参数: 深度的扩充因子 depth_coefficient self.dropout_rate = self.params[subtype][3] # 元组的第四个参数: 随机丢弃比例 最后一个全连接层前的dropout层 self.depth_div = 8 # 对于b0层: 输出的channels为32,特征尺寸需要减半处理,所以stride=2 self.stage1 = ConvBNAct(3, self._calculate_width(32), kernel_size=3, stride=2) # 此处stride=1,所以尺寸不变 self.stage2 = self.make_layer(self._calculate_width(32), self._calculate_width(16), kernel_size=3, stride=1, block=self._calculate_depth(1)) self.stage3 = self.make_layer(self._calculate_width(16), self._calculate_width(24), kernel_size=3, stride=2, block=self._calculate_depth(2)) self.stage4 = self.make_layer(self._calculate_width(24), self._calculate_width(40), kernel_size=5, stride=2, block=self._calculate_depth(2)) self.stage5 = self.make_layer(self._calculate_width(40), self._calculate_width(80), kernel_size=3, stride=2, block=self._calculate_depth(3)) # 此处stride=1,所以尺寸不变 self.stage6 = self.make_layer(self._calculate_width(80), self._calculate_width(112), kernel_size=5, stride=1, block=self._calculate_depth(3)) self.stage7 = self.make_layer(self._calculate_width(112), self._calculate_width(192), kernel_size=5, stride=2, block=self._calculate_depth(4)) self.stage8 = self.make_layer(self._calculate_width(192), self._calculate_width(320), kernel_size=3, stride=1, block=self._calculate_depth(1)) # 池化操作 self.pooling = nn.Sequential( Conv1x1BNAct(self._calculate_width(320), self._calculate_width(1280)), nn.AdaptiveAvgPool2d(1), nn.Dropout2d(0.2), ) # 全连接层 self.fc = nn.Sequential( Flatten(), nn.Linear(self._calculate_width(1280), num_classes) ) self.init_weights() # 参数初始化 def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode="fan_out") elif isinstance(m, nn.Linear): init_range = 1.0 / math.sqrt(m.weight.shape[1]) nn.init.uniform_(m.weight, -init_range, init_range) # width_coefficient因子取整到离它最近的8的整数倍 def _calculate_width(self, x): # 对于输入的channels,需要乘上宽度的扩展因子 x *= self.width_coeff # 当x偏向8整数倍的下方,通过加上4来判断 new_x = max(self.depth_div, int(x + self.depth_div / 2) // self.depth_div * self.depth_div) if new_x < 0.9 * x: new_x += self.depth_div return int(new_x) # depth_coefficient因子得到小数后向上取整 def _calculate_depth(self, x): return int(math.ceil(x * self.depth_coeff)) # ceil向上取整函数 eg: ceil(4.01)=5 def make_layer(self, in_places, places, kernel_size, stride, block): layers = [] # 通过块结构的第一层改变尺寸,所以需要传入stride layers.append(MBConvBlock(in_places, places, kernel_size, stride)) # 块结构的其他层不改变channels。注意,此处的in_channels = out_channels = places for i in range(1, block): layers.append(MBConvBlock(places, places, kernel_size)) return nn.Sequential(*layers) # 以基本结构b0为例: # input: torch.Size([1, 3, 224, 224]) def forward(self, x): x = self.stage1(x) # torch.Size([1, 32, 112, 112]) x = self.stage2(x) # torch.Size([1, 16, 112, 112]) x = self.stage3(x) # torch.Size([1, 24, 56, 56]) x = self.stage4(x) # torch.Size([1, 40, 28, 28]) x = self.stage5(x) # torch.Size([1, 80, 14, 14]) x = self.stage6(x) # torch.Size([1, 112, 14, 14]) x = self.stage7(x) # torch.Size([1, 192, 7, 7]) x = self.stage8(x) # torch.Size([1, 320, 7, 7]) x = self.pooling(x) # torch.Size([1, 1280, 1, 1]) x = self.fc(x) # torch.Size([1, 5]) return x # 最基本结构 def EfficientNet_b0(): return EfficientNet('efficientnet_b0') # 以下结构为按比例的扩充模型 def EfficientNet_b1(): return EfficientNet('efficientnet_b1') def EfficientNet_b2(): return EfficientNet('efficientnet_b2') def EfficientNet_b3(): return EfficientNet('efficientnet_b3') def EfficientNet_b4(): return EfficientNet('efficientnet_b4') def EfficientNet_b5(): return EfficientNet('efficientnet_b5') def EfficientNet_b6(): return EfficientNet('efficientnet_b6') def EfficientNet_b7(): return EfficientNet('efficientnet_b7') if __name__== '__main__': # model = EfficientNet_b0() # model = EfficientNet_b1() # model = EfficientNet_b2() # model = EfficientNet_b3() # model = EfficientNet_b4() # model = EfficientNet_b5() # model = EfficientNet_b6() model = EfficientNet_b7() # print(model) input = torch.randn(1, 3, 224, 224) out = model(input) print(out.shape) torch.save(model.state_dict(), 'EfficientNet_b7.mdl')
模型的大小如图所示:
可见,模型不算很小,所以这也是EfficientNetV2改进的一点。
参考资料:
https://blog.csdn.net/qq_37541097/article/details/114434046
https://www.bilibili.com/video/BV1XK4y1U7PX