【图像分类】【深度学习】【轻量级网络】【Pytorch版本】EfficientNet_V1模型算法详解
文章目录
前言
EfficientNet_V1是由谷歌公司的Tan, Mingxing等人《EfficientNet:Rethinking Model Scaling for Convolutional Neural Networks【ICML-2019】》【论文地址】一文中提出的模型,通过复合缩放把网络缩放的深度、宽度和分辨率组合起来按照一定规则缩放,从而提高网络的效果。
EfficientNet_V1讲解
卷积神经网络通常是在固定的资源预算下开发的,如果有更多的资源可用,那么卷积神经网络就可以扩展以获得更好的精度。目前有许多方法可以做到这一点,最常见的方法是按其深度或宽度扩大卷积神经网络,另一种不太常见但越来越流行的方法是通过扩大图像分辨率扩大模型。在以前的工作中,通常只缩放三个维度中的一个—深度、宽度和图像大小。虽然可以任意缩放二维或三维,但任意缩放需要冗长的手动调整,而且通常会产生次优的精度和效率。因此,本论文对卷积神经网络的扩展过程进行了研究和反思,系统地研究了模型缩放,并发现仔细平衡网络深度、宽度和分辨率可以获得更好的性能。基于这一观察提出了一种新的缩放方法,该方法使用一个简单但高效的复合系数统一缩放深度/宽度/分辨率的所有尺寸。
以下是原论文提出的缩放方法和传统方法之间的区别的示意图:
研究的核心问题:是否有一个理论性的方法来扩展卷积神经网络,以实现更好的准确性和效率?
平衡网络宽度/深度/分辨率的所有维度是至关重要的,这种平衡可以通过简单地用恒定比率缩放每个维度来实现,因此论文提出了一种简单而有效的复合缩放法,使用一组固定的缩放系数统一缩放网络宽度、深度和分辨率。例如,如果我们想使用
2
N
2^N
2N倍以上的计算资源,那么我们可以简单地将网络深度增加
α
N
\alpha^N
αN,宽度增加
β
N
\beta^N
βN,图像大小增加
γ
N
\gamma ^N
γN,其中
α
\alpha
α,
β
\beta
β,
γ
\gamma
γ是由原始小模型上的小网格搜索确定的常数系数。论文使用神经架构搜索来开发一个新的基网络,并将其扩展以获得一系列模型,称为EfficientNets。
问题辨析(Problem Formulation)
一个卷积神经网络的层
i
i
i可以被描述为函数
Y
i
=
F
i
(
X
i
)
Y_i=F_i(X_i)
Yi=Fi(Xi),其中
F
i
F_i
Fi是卷积操作,
Y
i
Y_i
Yi是输出的张量,
X
i
X_i
Xi 是输入的张量且张量的形状为
<
H
i
,
W
i
,
C
i
>
< {H_{\rm{i}}},{{\rm{W}}_{\rm{i}}},{C_{\rm{i}}} >
<Hi,Wi,Ci>,
H
i
H_i
Hi和
W
i
W_i
Wi是空间维度上的尺寸,
C
i
C_i
Ci是通道维度的尺寸。一个卷积网络N可以表示为
N
=
F
k
⊙
.
.
.
⊙
F
2
⊙
F
1
(
X
1
)
=
⊙
j
=
1...
k
F
j
(
X
1
)
{\rm N} = {F_k} \odot ... \odot {F_2} \odot {F_1}({X_1}) = { \odot _{j = 1...k}}{F_j}({X_1})
N=Fk⊙...⊙F2⊙F1(X1)=⊙j=1...kFj(X1)。实际上,卷积神经网络层通常分为多个阶段,每个阶段的所有层都具有相同的体系结构,因此将卷积神经网络定义为:
N
=
⊙
i
=
1...
s
F
i
L
i
(
X
<
H
i
,
W
i
,
C
i
>
)
{\rm N} = \mathop \odot \limits_{i = 1...s} F_i^{{L_i}}({X_{ < {H_{\rm{i}}},{{\rm{W}}_{\rm{i}}},{C_{\rm{i}}} > }})
N=i=1...s⊙FiLi(X<Hi,Wi,Ci>)
其中
F
i
L
i
F_i^{{L_i}}
FiLi表示
F
i
F_i
Fi架构在第
i
i
i个阶段被重复
L
i
L_i
Li次,
<
H
i
,
W
i
,
C
i
>
< {H_{\rm{i}}},{{\rm{W}}_{\rm{i}}},{C_{\rm{i}}} >
<Hi,Wi,Ci>是第
i
i
i层输入的张量
X
X
X形状。不同于之前常规的网络设计是集中在寻找更好的
F
i
F_i
Fi架构,模型缩放则是扩展网络长度
L
i
L_i
Li,宽度
C
i
C_i
Ci和分辨率
(
H
i
,
W
i
)
({H_{\rm{i}}},{{\rm{W}}_{\rm{i}}})
(Hi,Wi),而不改变基网络的
F
i
F_i
Fi。为了进一步减少设计空间的大小,限制所有参数必须以恒定的比例均匀地缩放。目标是为了在给定资源限制时最大化模型精度,可以被定义为一个优化问题:
max
d
,
w
,
r
A
c
c
u
r
a
c
y
(
N
(
d
,
w
,
r
)
)
N
(
d
,
w
,
r
)
=
⊙
i
=
1...
s
F
i
d
⋅
L
i
∧
(
X
⟨
r
⋅
H
i
,
r
⋅
W
i
,
w
⋅
C
i
⟩
)
M
e
m
o
r
y
(
N
)
≤
t
a
r
g
e
t
_
m
e
m
o
r
y
F
L
O
P
S
(
N
)
≤
t
a
r
g
e
t
_
f
l
o
p
s
\begin{array}{l} \mathop {\max }\limits_{d,w,r} Accuracy\left( {N\left( {d,w,r} \right)} \right)\\ N\left( {d,w,r} \right) = \mathop \odot \limits_{i = 1...s} \mathop {F_i^{d \cdot {L_i}}}\limits^ \wedge \left( {{X_{\left\langle {r \cdot {H_{\rm{i}}},r \cdot {{\rm{W}}_{\rm{i}}},w \cdot {C_{\rm{i}}}} \right\rangle }}} \right)\\ Memory\left( N \right) \le target\_memory\\ FLOPS\left( N \right) \le target\_flops \end{array}
d,w,rmaxAccuracy(N(d,w,r))N(d,w,r)=i=1...s⊙Fid⋅Li∧(X⟨r⋅Hi,r⋅Wi,w⋅Ci⟩)Memory(N)≤target_memoryFLOPS(N)≤target_flops
其中
w
w
w、
d
d
d、
r
r
r为缩放网络宽度、深度和分辨率的系数;
F
^
i
{\hat F_i}
F^i、
H
^
i
{\hat H_i}
H^i、
W
^
i
{\hat W_i}
W^i、
C
^
i
{\hat C_i}
C^i是基础网络中预定义的参数,具体数据如下图是原论文中所示:
缩放尺寸(Scaling Dimensions)
缩放尺寸主要难点是最优的 d d d、 w w w、 r r r相互依赖,并且在不同的资源约束下值会发生变化。由于这一困难,传统的方法大多是在其中一个维度上进行缩放。
- Depth( d d d): 缩放网络深度是许多卷积神经网络最常用的方法,更深层次的卷积神经网络可以捕获更丰富、更复杂的特性,并很好地概括新的任务。然而由于梯度消失问题,更深层次的网络也更加难以训练。虽然有一些技术,如跳过连接和批处理标准化,缓解了训练问题,但非常深的网络的精度增益会减少。原论文下图(左)对不同深度系数 d d d的基础模型进行缩放的实证研究,进一步表明对于非常深的卷积神经网络精度收益递减。
- Width( w w w): 小型模型通常采用网络宽度缩放,更广泛的网络往往能够捕获更细粒度的特点,更容易训练。然而非常宽但很浅的网络往往难以捕获更高层次的特性。原论文下图(中)中得到的经验结果表明,当网络越宽 w w w越大时,准确率会迅速饱和。
- Resolution( r r r): 使用更高分辨率的输入图像,卷积神经网络可以捕获更细粒度的模式。早期的卷积神经网络通常使用224x224分辨率的图像,目前的卷积神经网络则使用299x299或331x331分辨率的图像来获得更好的精度。最近有网络使用480x480分辨率的图像在ImageNet上实现了最优秀的精度。更高的分辨率如600x600也广泛应用于目标检测卷积神经网络。原论文下图(右)中显示了缩放网络分辨率的结果,证明更高的分辨率可以提高精度,但是对于非常高的分辨率,精度增益会减小( r = 1.0 r = 1.0 r=1.0表示分辨率224x224)。
上述分析使我们得出了第一个观察结果:增大网络宽度、深度或分辨率的任何维度都会提高精度,但对于较大的模型,精度增益会减小。
复合缩放(Compound Scaling)
根据以往经验可以得到一种假设:不同的尺度尺度并不是独立的。比如,对于高分辨率的图像应该增加网络深度,因为较大的感受野可以在较大的图像中帮助捕获包含更多像素的特征。这种假设表明,卷积神经网络网络性能的提升需要协调和平衡不同的尺度而不是传统的单一尺度。
为了验证这种假设,原论文中比较了不同网络深度和分辨率下的宽度缩放,如下图所示。如果只缩放网络宽度
w
w
w (蓝线) 而不改变深度(
d
=
1.0
d=1.0
d=1.0)和分辨率(
r
=
1.0
r=1.0
r=1.0),则精度很快达到饱和。随着更深的网络和更高的分辨率,在相同的计算成本下,宽度缩放可以获得更好的精度。
上述分析使我们得出了第二个观察结果:为了追求更好的精度和效率,在卷积神经网络缩放期间平衡网络宽度、深度和分辨率的所有维度至关重要。
在论文中,我们提出了一种新的复合缩放方法,它使用复合系数
ϕ
\phi
ϕ以理论性的方式均匀缩放网络的宽度、深度和分辨率:
d
e
p
t
h
:
d
=
α
ϕ
w
i
d
t
h
:
w
=
β
ϕ
r
e
s
o
l
u
t
i
o
n
:
r
=
γ
ϕ
α
⋅
β
2
⋅
γ
2
≈
2
α
≥
1
,
β
≥
1
,
γ
≥
1
\begin{array}{l} {\rm{depth: d = }}{\alpha ^\phi }\\ {\rm{width: w = }}{\beta ^\phi }\\ {\rm{resolution: r = }}{\gamma ^\phi }\\ \alpha \cdot {\beta ^2} \cdot {\gamma ^2} \approx 2\\ \alpha \ge 1,\beta \ge 1,\gamma \ge 1 \end{array}
depth:d=αϕwidth:w=βϕresolution:r=γϕα⋅β2⋅γ2≈2α≥1,β≥1,γ≥1
其中
α
\alpha
α,
β
\beta
β,
γ
\gamma
γ是可以通过小网格搜索确定的常数。
ϕ
\phi
ϕ是一个用户指定的系数,它控制着有多少资源可用于模型缩放,而
α
\alpha
α,
β
\beta
β,
γ
\gamma
γ则分别指定如何将这些额外的资源分配给网络宽度、深度和分辨率。
常规卷积运算的浮点运算FLOPs与 d d d、 w 2 w^2 w2、 r 2 r^2 r2成比例,即网络深度增加一倍将使FLOPs也增加一倍,但网络宽度或分辨率增加一倍将使FLOPs增加四倍。
由于卷积运算通常在卷积神经网络中占主导地位,用上述公式缩放卷积神经网络将使总的FLOPs增加 ( α × β 2 × γ 2 ) ϕ {\left( {\alpha \times {\beta ^2} \times {\gamma ^2}} \right)^\phi } (α×β2×γ2)ϕ,原论文约束 α ⋅ β 2 ⋅ γ 2 ≈ 2 \alpha \cdot {\beta ^2} \cdot {\gamma ^2} \approx 2 α⋅β2⋅γ2≈2,使得对于任何一个新的 ϕ \phi ϕ,总的Flops都将大约增加 2 ϕ 2^{\phi} 2ϕ。
EfficientNet_V1的模型结构
模型缩放不会改变基础网络中的层操作符
F
i
F_i
Fi,因此基础网络很重要。原论文使用MnasNet的方法搜索,利用多目标神经网络架构搜索,同时优化准确率和FLOPS产生了一个高效的网络,将其命名为 EfficientNet-B0(FLOPS为400M),下图是原论文给出的关于 EfficientNet-B0模型结构的详细示意图:
固定 α \alpha α, β \beta β, γ \gamma γ并使用不同的 ϕ \phi ϕ对基础网络进行扩展,得到了EfficientNet-B1到B7
EfficientNet_V1在图像分类中分为两部分:backbone部分: 主要由MBConv基础单元、卷积层组成,分类器部分:由卷积层、全局池化层和全连接层组成 。
神经网络架构搜索的技术路线参考:
以下内容是原论文中没有的补充内容,关于EfficientNet_V1结构的更细节描述。
SE模块(Squeeze Excitation)
对所通道输出的特征图进行加权: SE模块显式地建立特征通道之间的相互依赖关系,通过学习能够计算出每个通道的重要程度,然后依照重要程度对各个通道上的特征进行加权,从而突出重要特征,抑制不重要的特征。
SE模块的示意图如下图所示:
- 压缩(squeeze): 由于卷积只是在局部空间内进行操作,很难获得全局的信息发现通道之间的关系特征,因此采用全局平局池化将每个通道上的空间特征编码压缩为一个全局特征完成特征信息的进行融合。
- 激励(excitation): 接收每个通道的全局特征后,采用俩个全连接层预测每个通道的重要性(激励)。为了降低计算量,第一个全连接层带有缩放超参数起到减少通道、降低维度的作用;第二个全连接层则恢复原始维度,以保证通道的重要性与通道的特征图数量完全匹配。
- 加权(scale): 计算出通道的重要性后,下一步对通道的原始特征图进行加权操作,各通道权重分别和对应通道的原始特征图相乘获得新的加权特征图。
EfficientNet_V1中的SE模块:
反向残差结构 MBConv
ResNet【参考】中证明残差结构(Residuals) 有助于构建更深的网络从而提高精度,MobileNets_V2【参考】中以ResNet的残差结构为基础进行优化,提出了反向残差(Inverted Residuals) 的概念。
反向残差结构的过程: 低维输入->1x1膨胀卷积(升维)-> bn层+swish激活->3x3深度卷积(低维)->bn层+swish激活->1x1点卷积(降维)->bn层->与残差相加。
EfficientNet_V1常规的反向残差结构分为俩种,当stride=2时,反向残差结构取消了shortcut连接和drop_connect。
这里的stride=2只是当前stage的首个MBConv,而不是整个stage的所有MBConv
EfficientNet_V1还有一个特殊的反向残差结构,它没有用于升维的1x1膨胀卷积。
在MobileNets_V2都是使用ReLU6激活函数,但EfficientNet_V1使用现在比较常用的是swish激活函数,即x乘上sigmoid激活函数:
s
w
i
s
h
(
x
)
=
x
σ
(
x
)
{\rm{swish}}(x) = x\sigma (x)
swish(x)=xσ(x)
其中sigmoid激活函数:
σ
(
x
)
=
1
1
+
e
−
x
\sigma (x) = \frac{1}{{1 + {e^{ - x}}}}
σ(x)=1+e−x1
反向残差结构组 Stage
EfficientNet_V1由多个反向残差结构组构成,除了stride的细微差异,每个反向残差结构组具有相同的网络结构,以下是EfficientNet-B0模型参数以及对应的网络结构图。
EfficientNet_V1 Pytorch代码
卷积块: 3×3/5×5卷积层+BN层+Swish激活函数(可选)
# 卷积块:3×3/5×5卷积层+BN层+Swish激活函数(可选)
class ConvBNAct(nn.Sequential):
def __init__(self,
in_planes, # 输入通道
out_planes, # 输出通道
kernel_size=3, # 卷积核大小
stride=1, # 卷积核步长
groups=1, # 卷积层组数
norm_layer= None, # 归一化层
activation_layer=None): # 激活层
# 计算padding
padding = (kernel_size - 1) // 2
# BN层
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# Swish激活函数
if activation_layer is None:
# nn.SiLU 等价于 x * torch.sigmoid(x)
activation_layer = nn.SiLU
super(ConvBNAct, self).__init__(nn.Conv2d(in_channels=in_planes,
out_channels=out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
groups=groups,
bias=False),
norm_layer(out_planes),
activation_layer())
SE注意力模块:: 全局平均池化+1×1卷积+Swish激活函数+1×1卷积+sigmoid激活函数
# SE注意力模块:对各通道的特征分别强化
class SqueezeExcitation(nn.Module):
def __init__(self,
input_c, # 1×1卷积输出通道数(降维)
expand_c, # se模块整体输入输出通道
squeeze_factor=4): # 降维系数
super(SqueezeExcitation, self).__init__()
# 降维通道数=降维卷积输出通道数//降维系数
squeeze_c = input_c // squeeze_factor
# 1×1卷积(降维)
self.fc1 = nn.Conv2d(expand_c, squeeze_c, 1)
# Swish激活函数
self.ac1 = nn.SiLU()
# 1×1卷积(特征提取)
self.fc2 = nn.Conv2d(squeeze_c, expand_c, 1)
# Sigmoid激活函数(0~1重要性加权)
self.ac2 = nn.Sigmoid()
def forward(self, x):
scale = F.adaptive_avg_pool2d(x, output_size=(1, 1))
scale = self.fc1(scale)
scale = self.ac1(scale)
scale = self.fc2(scale)
scale = self.ac2(scale)
# 特征按通道分别进行加权
return scale * x
反向残差结构: 1×1膨胀卷积层+BN层+Swish激活函数+3×3/5×5深度卷积层+BN层+Swish激活函数+1×1点卷积层+BN层
# 反残差结构:1×1膨胀卷积层+BN层+Swish激活函数+3×3/5×5深度卷积层+BN层+Swish激活函数+1×1点卷积层+BN层
class MBConvBlock(nn.Module):
def __init__(self,
kernel, # 卷积核大小 3 or 5
input_c, # 基础输入通道数
out_c, # 基础输出通道数
expanded_ratio, # 膨胀系数 1 or 6
stride, # 卷积核步长
use_se, # 启用se注意力模块
drop_rate, # 通道随机失活率
norm_layer): # 归一化层
super(MBConvBlock, self).__init__()
# 膨胀通道数 = 输入通道数*膨胀系数
expanded_c = input_c * expanded_ratio
# 步长必须是1或者2
if stride not in [1, 2]:
raise ValueError("illegal stride value.")
# 深度卷积步长为2则没有shortcut连接
self.use_res_connect = ( stride == 1 and input_c == out_c)
layers = OrderedDict()
# Swish激活函数
activation_layer = nn.SiLU
# 1×1膨胀卷积(膨胀系数>1) 升维
if expanded_c != input_c:
layers.update({"expand_conv": ConvBNAct(input_c,
expanded_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=activation_layer)})
# 3×3或5×5深度卷积
layers.update({"dwconv": ConvBNAct(expanded_c,
expanded_c,
kernel_size=kernel,
stride=stride,
groups=expanded_c,
norm_layer=norm_layer,
activation_layer=activation_layer)})
# 启用se注意力模块
if use_se:
layers.update({"se": SqueezeExcitation(input_c,
expanded_c)})
# 1×1点卷积
layers.update({"project_conv": ConvBNAct(expanded_c,
out_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=nn.Identity)})
self.block = nn.Sequential(layers)
# 只有在使用shortcut连接时才使用drop_connect层
if self.use_res_connect and drop_rate > 0:
self.drop_connect = DropConnect(drop_rate)
else:
self.drop_connect = nn.Identity()
def forward(self, x):
result = self.block(x)
result = self.drop_connect(result)
# 反残差结构随机失活
if self.use_res_connect:
result += x
return result
反残差结构随机失活
# 反残差结构随机失活:batchsize个样本随机失活,应用于反残差结构的主路径
class DropConnect(nn.Module):
def __init__(self, drop_prob=0.5):
super(DropConnect, self).__init__()
self.keep_prob = None
self.set_rate(drop_prob)
# 反残差结构的保留率
def set_rate(self, drop_prob):
if not 0 <= drop_prob < 1:
raise ValueError("rate must be 0<=rate<1, got {} instead".format(drop_prob))
self.keep_prob = 1 - drop_prob
def forward(self, x):
# 训练阶段随机丢失特征
if self.training:
# 是否保留取决于固定保留概率+随机概率
random_tensor = self.keep_prob + torch.rand([x.size(0), 1, 1, 1],
dtype=x.dtype,
device=x.device)
# 0表示丢失 1表示保留
binary_tensor = torch.floor(random_tensor)
# self.keep_prob个人理解对保留特征进行强化,概率越低强化越明显
return torch.mul(torch.div(x, self.keep_prob), binary_tensor)
else:
return x
完整代码
import math
from functools import partial
from collections import OrderedDict
import torch
import torch.nn as nn
from torch.nn import functional as F
from torchsummary import summary
def _make_divisible(ch, divisor=8, min_ch=None):
'''
int(ch + divisor / 2) // divisor * divisor)
目的是为了让new_ch是divisor的整数倍
类似于四舍五入:ch超过divisor的一半则加1保留;不满一半则归零舍弃
'''
if min_ch is None:
min_ch = divisor
new_ch = max(min_ch, int(ch + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_ch < 0.9 * ch:
new_ch += divisor
return new_ch
# 反残差结构随机失活:batchsize个样本随机失活,应用于反残差结构的主路径
class DropConnect(nn.Module):
def __init__(self, drop_prob=0.5):
super(DropConnect, self).__init__()
self.keep_prob = None
self.set_rate(drop_prob)
# 反残差结构的保留率
def set_rate(self, drop_prob):
if not 0 <= drop_prob < 1:
raise ValueError("rate must be 0<=rate<1, got {} instead".format(drop_prob))
self.keep_prob = 1 - drop_prob
def forward(self, x):
# 训练阶段随机丢失特征
if self.training:
# 是否保留取决于固定保留概率+随机概率
random_tensor = self.keep_prob + torch.rand([x.size(0), 1, 1, 1],
dtype=x.dtype,
device=x.device)
# 0表示丢失 1表示保留
binary_tensor = torch.floor(random_tensor)
# self.keep_prob个人理解对保留特征进行强化,概率越低强化越明显
return torch.mul(torch.div(x, self.keep_prob), binary_tensor)
else:
return x
# 卷积块:3×3/5×5卷积层+BN层+Swish激活函数(可选)
class ConvBNAct(nn.Sequential):
def __init__(self,
in_planes, # 输入通道
out_planes, # 输出通道
kernel_size=3, # 卷积核大小
stride=1, # 卷积核步长
groups=1, # 卷积层组数
norm_layer= None, # 归一化层
activation_layer=None): # 激活层
# 计算padding
padding = (kernel_size - 1) // 2
# BN层
if norm_layer is None:
norm_layer = nn.BatchNorm2d
# Swish激活函数
if activation_layer is None:
# nn.SiLU 等价于 x * torch.sigmoid(x)
activation_layer = nn.SiLU
super(ConvBNAct, self).__init__(nn.Conv2d(in_channels=in_planes,
out_channels=out_planes,
kernel_size=kernel_size,
stride=stride,
padding=padding,
groups=groups,
bias=False),
norm_layer(out_planes),
activation_layer())
# SE注意力模块:对各通道的特征分别强化
class SqueezeExcitation(nn.Module):
def __init__(self,
input_c, # 1×1卷积输出通道数(降维)
expand_c, # se模块整体输入输出通道
squeeze_factor=4): # 降维系数
super(SqueezeExcitation, self).__init__()
# 降维通道数=降维卷积输出通道数//降维系数
squeeze_c = input_c // squeeze_factor
# 1×1卷积(降维)
self.fc1 = nn.Conv2d(expand_c, squeeze_c, 1)
# Swish激活函数
self.ac1 = nn.SiLU()
# 1×1卷积(特征提取)
self.fc2 = nn.Conv2d(squeeze_c, expand_c, 1)
# Sigmoid激活函数(0~1重要性加权)
self.ac2 = nn.Sigmoid()
def forward(self, x):
scale = F.adaptive_avg_pool2d(x, output_size=(1, 1))
scale = self.fc1(scale)
scale = self.ac1(scale)
scale = self.fc2(scale)
scale = self.ac2(scale)
# 特征按通道分别进行加权
return scale * x
# 反残差结构:1×1膨胀卷积层+BN层+Swish激活函数+3×3/5×5深度卷积层+BN层+Swish激活函数+1×1点卷积层+BN层
class MBConvBlock(nn.Module):
def __init__(self,
kernel, # 卷积核大小 3 or 5
input_c, # 基础输入通道数
out_c, # 基础输出通道数
expanded_ratio, # 膨胀系数 1 or 6
stride, # 卷积核步长
use_se, # 启用se注意力模块
drop_rate, # 通道随机失活率
norm_layer): # 归一化层
super(MBConvBlock, self).__init__()
# 膨胀通道数 = 输入通道数*膨胀系数
expanded_c = input_c * expanded_ratio
# 步长必须是1或者2
if stride not in [1, 2]:
raise ValueError("illegal stride value.")
# 深度卷积步长为2则没有shortcut连接
self.use_res_connect = (stride == 1 and input_c == out_c)
layers = OrderedDict()
# Swish激活函数
activation_layer = nn.SiLU
# 1×1膨胀卷积(膨胀系数>1) 升维
if expanded_c != input_c:
layers.update({"expand_conv": ConvBNAct(input_c,
expanded_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=activation_layer)})
# 3×3或5×5深度卷积
layers.update({"dwconv": ConvBNAct(expanded_c,
expanded_c,
kernel_size=kernel,
stride=stride,
groups=expanded_c,
norm_layer=norm_layer,
activation_layer=activation_layer)})
# 启用se注意力模块
if use_se:
layers.update({"se": SqueezeExcitation(input_c,
expanded_c)})
# 1×1点卷积
layers.update({"project_conv": ConvBNAct(expanded_c,
out_c,
kernel_size=1,
norm_layer=norm_layer,
activation_layer=nn.Identity)})
self.block = nn.Sequential(layers)
# 只有在使用shortcut连接时才使用drop_connect层
if self.use_res_connect and drop_rate > 0:
self.drop_connect = DropConnect(drop_rate)
else:
self.drop_connect = nn.Identity()
def forward(self, x):
result = self.block(x)
result = self.drop_connect(result)
# 反残差结构随机失活
if self.use_res_connect:
result += x
return result
class EfficientNetV1(nn.Module):
def __init__(self,
width_coefficient, # 宽度缩放
depth_coefficient, # 深度缩放
num_classes=1000, # 输出类别
num_features=1280,
dropout_rate: float = 0.2, # 通道随机失活率
drop_connect_rate: float = 0.2, # 反残差结构随机失活概率
block= None, # block模块(这里是MBConvBlock反残差结构)
norm_layer= None # 归一化层
):
super(EfficientNetV1, self).__init__()
# 反残差结构: 卷积核大小, 输入通道数, 输出通道数, 膨胀系数, 卷积核步长, 启用se模块, 反残差结构随机失活概率, 重复次数
default_cnf = [[3, 32, 16, 1, 1, True, drop_connect_rate, 1],
[3, 16, 24, 6, 2, True, drop_connect_rate, 2],
[5, 24, 40, 6, 2, True, drop_connect_rate, 2],
[3, 40, 80, 6, 2, True, drop_connect_rate, 3],
[5, 80, 112, 6, 1, True, drop_connect_rate, 3],
[5, 112, 192, 6, 2, True, drop_connect_rate, 4],
[3, 192, 320, 6, 1, True, drop_connect_rate, 1]]
# 调整网络深度
def adjust_repeats(repeats):
# 网络深度 = 基础网络深度*深度缩放
return int(math.ceil(repeats * depth_coefficient))
# 调整网络宽度
def adjust_channels(channels):
return _make_divisible(channels * width_coefficient, 8)
# block(这里是MBConvBlock)
if block is None:
block = MBConvBlock
# 配置bn层参数
if norm_layer is None:
norm_layer = partial(nn.BatchNorm2d, eps=1e-3, momentum=0.1)
self.stem = ConvBNAct(in_planes=3,
out_planes=adjust_channels(default_cnf[0][1]),
kernel_size=3,
stride=2,
norm_layer=norm_layer) # 激活函数默认是SiLU
# 当前反残差结构序号
block_id = 0
# 反残差结构总数
total_blocks = float(sum(adjust_repeats(i[-1]) for i in default_cnf))
blocks = []
for cnf in default_cnf:
# 每个stage的反残差结构数数
repeats = adjust_repeats(cnf.pop(-1))
for i in range(repeats):
# 反残差结构随机失活概率随着网络深度等差递增,公差为drop_connect_rate/total_blocks,范围在[0,drop_connect_rate)
blocks.append(block(kernel=cnf[0],
input_c=adjust_channels(cnf[1] if i == 0 else cnf[2]),
out_c=adjust_channels(cnf[2]),
expanded_ratio=cnf[3],
stride=cnf[4] if i == 0 else 1,
use_se=cnf[5],
drop_rate=drop_connect_rate * block_id / total_blocks,
norm_layer=norm_layer))
block_id += 1
head_input_c = adjust_channels(cnf[2])
# 主干网络
self.blocks = nn.Sequential(*blocks)
head = OrderedDict()
head.update({"project_conv": ConvBNAct(in_planes=head_input_c,
out_planes=adjust_channels(num_features),
kernel_size=1,
norm_layer=norm_layer)})
head.update({"avgpool": nn.AdaptiveAvgPool2d(1)})
head.update({"flatten": nn.Flatten()})
if dropout_rate > 0:
head.update({"dropout": nn.Dropout(p=dropout_rate, inplace=True)})
head.update({"classifier": nn.Linear(adjust_channels(num_features), num_classes)})
# 分类器
self.head = nn.Sequential(head)
# 初始化权重
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out")
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.BatchNorm2d):
nn.init.ones_(m.weight)
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.zeros_(m.bias)
def forward(self, x):
# 主干网络
x = self.stem(x)
x = self.blocks(x)
# 分类器
x = self.head(x)
return x
# 不同的网络模型对应不同的分辨率
def efficientnet_b0(num_classes=1000):
# input image size 224x224
return EfficientNetV1(width_coefficient=1.0,
depth_coefficient=1.0,
dropout_rate=0.2,
num_classes=num_classes)
def efficientnet_b1(num_classes=1000):
# input image size 240x240
return EfficientNetV1(width_coefficient=1.0,
depth_coefficient=1.1,
dropout_rate=0.2,
num_classes=num_classes)
def efficientnet_b2(num_classes=1000):
# input image size 260x260
return EfficientNetV1(width_coefficient=1.1,
depth_coefficient=1.2,
dropout_rate=0.3,
num_classes=num_classes)
def efficientnet_b3(num_classes=1000):
# input image size 300x300
return EfficientNetV1(width_coefficient=1.2,
depth_coefficient=1.4,
dropout_rate=0.3,
num_classes=num_classes)
def efficientnet_b4(num_classes=1000):
# input image size 380x380
return EfficientNetV1(width_coefficient=1.4,
depth_coefficient=1.8,
dropout_rate=0.4,
num_classes=num_classes)
def efficientnet_b5(num_classes=1000):
# input image size 456x456
return EfficientNetV1(width_coefficient=1.6,
depth_coefficient=2.2,
dropout_rate=0.4,
num_classes=num_classes)
def efficientnet_b6(num_classes=1000):
# input image size 528x528
return EfficientNetV1(width_coefficient=1.8,
depth_coefficient=2.6,
dropout_rate=0.5,
num_classes=num_classes)
def efficientnet_b7(num_classes=1000):
# input image size 600x600
return EfficientNetV1(width_coefficient=2.0,
depth_coefficient=3.1,
dropout_rate=0.5,
num_classes=num_classes)
if __name__ == '__main__':
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = efficientnet_b0().to(device)
summary(model, input_size=(3, 224, 224))
summary可以打印网络结构和参数,方便查看搭建好的网络结构。
总结
尽可能简单、详细的介绍了复合缩放的原理和过程,讲解了EfficientNet_V1模型的结构和pytorch代码。