一、参考资料
原始论文:[1]
NeurIPS22 Spotlight | 已开源 | 华为GhostNetV2:端侧小模型性能新SOTA
二、术语解析
廉价的线性变换/线性运算:cheap linear operations
;
线性变换的线性内核:linear kernels
;
深度可分离卷积:Depthwise Separable Convolution
,DSConv
;
逐深度卷积:Depthwise Convolution
,DWConv
;
逐点卷积:Pointwise Convolution
,PWConv
;
Ghost特征图:Ghost feature maps
;
注意力图:attention maps
;
解耦全连接注意力:decoupled fully connected attention
,DFC attention
;
表征能力:expressiveness
;
性能:capacity
;
三、GhostNetV2
相关介绍
1. 引言
关于 GhostNetV1
网络模型的详细介绍,请参考另一篇博客:通俗易懂理解GhostNetV1轻量级神经网络模型
智能手机等边缘设备计算资源有限,在设计模型时,不仅需要考虑模型的性能,更要考虑其实际的推理速度。最近计算机视觉领域爆火的Transformer模型在多个任务取得了很高精度,但在端侧设备上运行速度较慢,难以满足实时性的要求。经典的自注意力(self-attention)模块的计算复杂度较高,计算量随着输入分辨率的增加以二次方的速度增长。尽管目前主流的局部注意力模块(将图像切分为多个区域,在每个区域内分别部署注意力模块)降低了理论复杂度,但图像切分需要大量变换张量形状的操作(比如reshape、transpose等),在端侧设备上会产生很高的时延。比如,将局部注意力模块和轻量级模型GhostNet
结合,理论复杂度只增加了20%,但是实际的推理时延却翻了2倍。因此,为轻量化小模型专门设计硬件友好的注意力机制非常有必要。
2. DFC attention
2.1 移动端CNN的注意力模块
一个适用于移动端CNN的注意力模块应当满足3个条件:
- 对长距离空间信息的建模能力强。相比CNN,Transformer性能强大的一个重要原因是它能够建模全局空间信息,因此新的注意力模块也应当能捕捉空间长距离信息。
- 部署高效。注意力模块应该硬件友好,计算高效,以免拖慢推理速度,特别是不应包含硬件不友好的操作。
- 概念简单。为了保证注意力模块的泛化能力,这个模块的设计应当越简单越好。
2.2 DFC attention
原理
虽然自注意力操作可以很好地建模长距离依赖,但是部署效率低。相比自注意力机制,具有固定权重的FC层更简单,更容易实现,也可以用于生成具有全局感受野的 attention maps
。
给定特征图
Z
∈
R
H
×
W
×
C
\begin{array}{ccc}Z&\in&\mathbb{R}^{H\times W\times C}\end{array}
Z∈RH×W×C,它可以看作
h
w
hw
hw 的 tokens
,记作
z
i
∈
R
C
z_i\in\mathbb{R}^C
zi∈RC,也就是
Z
=
{
z
11
,
z
12
,
⋯
,
z
H
W
}
Z=\{z_{11},z_{12},\cdots,z_{HW}\}
Z={z11,z12,⋯,zHW}。FC层生成 attention map
的公式表达如下:
a
h
w
=
∑
h
′
,
w
′
F
h
w
,
h
′
w
′
⊙
z
h
′
w
′
,
(
3
)
\boldsymbol{a}_{hw}=\sum_{h^{\prime},w^{\prime}}F_{hw,h^{\prime}w^{\prime}}\odot\boldsymbol{z}_{h^{\prime}w^{\prime}}, \quad (3)
ahw=h′,w′∑Fhw,h′w′⊙zh′w′,(3)
其中,
⊙
\odot
⊙ 表示 element-wise multiplication
,
F
F
F 是FC层中可学习的权重,
A
=
{
a
11
,
a
12
,
⋯
,
a
H
W
}
A=\{a_{11},a_{12},\cdots,\boldsymbol{a}_{HW}\}
A={a11,a12,⋯,aHW}。
根据上述公式,将所有 tokens
与可学习的权重聚合在一起以提取全局信息,该过程比经典的自注意力简单的多。然而,该过程的计算复杂度仍然是二次方,特征图的大小为
O
(
H
2
W
2
)
)
2
\mathcal{O}({H^{2}W^{2}}))^{2}
O(H2W2))2,这在实际情况下是不可接受的,特别是当输入的图像是高分辨率时。例如,对于4层的GhostNet
网络的特征图具有
3136
(
56
×
56
)
3136 (56 \times 56)
3136(56×56)个 tokens
,这使得计算变得 attention maps
异常复杂。实际上,CNN中的特征图通常是低秩的,不需要将不同空间位置的所有输入和输出的 tokens
密集地连接起来。特征的2D尺寸很自然地提供一个视角,以减少FC层的计算量,也就是根据上述公式分解为两个FC层,分别沿水平方向和垂直方向聚合特征,其公式表达如下:
a
h
w
′
=
∑
h
′
=
1
H
F
h
,
h
′
w
H
⊙
z
h
′
w
,
h
=
1
,
2
,
⋯
,
H
,
w
=
1
,
2
,
⋯
,
W
,
(
4
)
\boldsymbol{a}'_{hw}=\sum\limits_{h'=1}^{H}F_{h,h'w}^{H}\odot\boldsymbol{z}_{h'w},h=1,2,\cdots,H,w=1,2,\cdots,W, \quad (4)
ahw′=h′=1∑HFh,h′wH⊙zh′w,h=1,2,⋯,H,w=1,2,⋯,W,(4)
a h w = ∑ w ′ = 1 W F w , h w ′ W ⊙ a h w ′ ′ , h = 1 , 2 , ⋯ , H , w = 1 , 2 , ⋯ , W , ( 5 ) \boldsymbol{a}_{hw}=\sum_{w'=1}^{W}F_{w,hw'}^{W}\odot\boldsymbol{a}_{hw'}^{\prime},h=1,2,\cdots,H,w=1,2,\cdots,W, \quad (5) ahw=w′=1∑WFw,hw′W⊙ahw′′,h=1,2,⋯,H,w=1,2,⋯,W,(5)
其中,
F
H
F^H
FH和
F
W
F^W
FW是变换的权重。输入原始特征
Z
Z
Z,并依次应用公式(4)和公式(5),分别提取沿两个方向的长距离依赖关系。 作者将此操作称为解耦全连接注意力(decoupled fully connected attention
,DFC attention
),其信息流如下图所示:
由于水平和垂直方向变换的解耦,注意力模块的计算复杂度可以降低到
O
(
H
2
W
+
H
W
2
)
\mathcal{O}(H^{2}W+HW^{2})
O(H2W+HW2)。对于 full attention
(公式3),正方形区域内的所有 patches
直接参与被聚合 patch
的计算。在 DFC attention
中,一个 patch
直接由其垂直方向和水平方向的 patch
进行聚合,而其他 patch
参与垂直线/水平线上的 patch
的生成,与被聚合的 token
有间接关系。因此,一个 patch
的计算也涉及到正方形区域的所有 patchs
。
公式(4)和公式(5)是 DFC attention
的一般表示,分别沿着水平和垂直方向聚合像素。通过共享部分变换权重,可以方便地使用卷积操作实现,省去了影响实际推理速度的耗时张量的reshape操作和transpose操作。为了处理不同分辨率的输入图像,卷积核的大小可以与特征图的大小进行解耦,也就是在输入特征上依次进行两个大小为
1
×
K
H
1 \times K_H
1×KH 和
K
W
×
1
K_W \times 1
KW×1的 DWConv
操作。当用卷积操作时,DFC attention
理论上的计算复杂度为
O
(
K
H
H
W
+
K
W
H
W
)
\mathcal{O}(K_{H}HW+K_{W}HW)
O(KHHW+KWHW)。这种策略得到了TFLite和ONNX等工具的良好支持,可以在移动设备上进行快速推理。
2.3 DFC attention
的性能
DFC attention
捕获了不同空间位置像素之间的长距离依赖关系,增强了模型的表征能力。
不同注意模块的 MobileNetV2
的实验结果如表4所示。SE[2]和CBAM[3]是两种广泛使用的注意力模块,CA[4]是最近提出的一种SOTA方法。本文提出的DFC attention
比现有方法具有更高的性能。例如,所提出的 DFC attention
将 MobileNetV2
的top-1精度提高了2.4%,而超过了CA(1.5%)。
3. GhostNetV2
将 DFC attention
插入到轻量化网络GhostNetV1
中可以提升表征能力,从而构建出新型视觉骨干网络 GhostNetV2
。
3.1 Enhancing Ghost Module
Ghost Module
中只有m个特征与其他像素交互,这影响了Ghost Module
提取空间信息(spatial information
)的能力。因此,作者使用 DFC attention
来增强 Ghost Module
的输出特征
Y
Y
Y,从而来捕获不同空间像素之间的长距离依赖关系。
输入特征
X
∈
R
h
×
w
×
c
X\in\mathbb{R}^{h\times w \times c}
X∈Rh×w×c 被送入两个分支,一个是 Ghost Module
分支,用于输出特征
Y
Y
Y,另一个是 DFC attention Module
分支,用于生成 attention map
,记作
A
A
A(公式(4)和公式(5)。 回想一下,在经典的自注意力中,线性变换层将输入特征图转换为计算 attention maps
的 query
和 key
。类似的,作者实现一个
1
×
1
1 \times 1
1×1 的卷积操作,将 Ghost Module
分支的输入
X
X
X 转换为 DFC module
分支的输入
Z
Z
Z。两个分支输出的乘积,即为最终输出
O
∈
R
H
×
W
×
C
O\in\mathbb{R}^{{H}\times W\times C}
O∈RH×W×C,可以表示为:
O
=
Sigmoid
(
A
)
⊙
V
(
X
)
,
(
6
)
O=\operatorname{Sigmoid}(A)\odot\mathcal{V}(X), \quad (6)
O=Sigmoid(A)⊙V(X),(6)
其中,
⊙
\odot
⊙ 表示element-wise multiplication
,
A
A
A 是attention map
,
Sigmoid
\operatorname {Sigmoid}
Sigmoid 是归一化函数以缩放到
(
0
,
1
)
(0, 1)
(0,1) 范围。
V
(
)
\mathcal{V}()
V() 表示 Ghost Module
,
X
X
X 为输入特征。则信息聚合过程如下图所示:
使用相同的输入特征,Ghost Module
和 DFC attention
是两个从不同角度提取信息的并行分支。输出特征是它们逐元素的信息,其中包含来自 Ghost Module
的特性和 DFC attention
的信息。每个 attention value
涉及到大范围的 patches
,以便输出的特征可以包含这些 patches
的信息。
3.2 GhostNetV1 bottleneck
the former enhances expanded features (expressiveness) while the latter improves the block’s capacity.
GhostNetV1 bottleneck
是反向残差bottlenet(inverted residual bottleneck
)结构,这种结构自然地解耦模型的表征能力(expressiveness
)和容量(capacity
)。对应的 GhostNetV1 bottleneck
包含两个Ghost Module
,第一个 Ghost Module
用于升维以增强扩展特征(表征能力),输出通道数增加;第二个 Ghost Module
用于降维以提高 GhostNetV1 bottleneck
的容量,输出通道数减少。简单理解,expressiveness
对应第一个 Ghost Module
,capacity
对应第二个 Ghost Module
。GhostNetV1 bottleneck
的结构如下图所示:
3.3 GhostNetV2 bottleneck
作者比较了将 DFC atttention
放到不同 Ghost Module
中的精度差异,实验结果如下图所示:
从实验结果发现,虽然两个 Ghost Module
中都放置 DFC atttention
对模型的 Top1-Acc
都有影响,但计算量也随之增大,且将 DFC atttention
放到第一个Ghost Module
中用来增强 expressiveness
更有效。因此默认设置下,只在第一个 Ghost Module
中加入DFC attention
。GhostNetV2 bottleneck
的结构如下图所示:
DFC attention
分支与第一个Ghost Module
并行,以增强扩展特征,然后将增强的特征传递到第二个Ghost Module
。
4. 实验结果
GhostNetV2
也可以作为骨干模型,用于目标检测、语义分割等下游任务。
本文在ImageNet图像分类、COCO目标检测、ADE语义分割等数据集上进行了实验,相比其他架构,GhostNetV2
取得了更快的推理速度和更高的模型精度。
4.1 Image Classification on ImageNet
在ImageNet数据集中进行图像分类任务,实验结果如下:
4.2 Object Detection on MS COCO
在MS COCO数据集中进行目标检测任务,实验结果如下:
4.3 Semantic Segmentation on ADE20K
在ADE20K数据集中进行语义分割任务,实验结果如下:
5. 代码实现
PyTorch代码:ghostnetv2_pytorch
MindSpore 代码:ghostnetv2
GhostNet v2(NeurIPS 2022 Spotlight)原理与代码解析
以下仅介绍核心代码。
GhostModuleV2
class GhostModuleV2(nn.Module):
def __init__(self, inp, oup, kernel_size=1, ratio=2, dw_size=3, stride=1, relu=True,mode=None,args=None):
super(GhostModuleV2, self).__init__()
self.mode=mode
self.gate_fn=nn.Sigmoid()
if self.mode in ['original']:
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
self.primary_conv = nn.Sequential(
nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
self.cheap_operation = nn.Sequential(
nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
elif self.mode in ['attn']:
self.oup = oup
init_channels = math.ceil(oup / ratio)
new_channels = init_channels*(ratio-1)
self.primary_conv = nn.Sequential(
nn.Conv2d(inp, init_channels, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(init_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
self.cheap_operation = nn.Sequential(
nn.Conv2d(init_channels, new_channels, dw_size, 1, dw_size//2, groups=init_channels, bias=False),
nn.BatchNorm2d(new_channels),
nn.ReLU(inplace=True) if relu else nn.Sequential(),
)
self.short_conv = nn.Sequential(
nn.Conv2d(inp, oup, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(oup),
nn.Conv2d(oup, oup, kernel_size=(1,5), stride=1, padding=(0,2), groups=oup,bias=False),
nn.BatchNorm2d(oup),
nn.Conv2d(oup, oup, kernel_size=(5,1), stride=1, padding=(2,0), groups=oup,bias=False),
nn.BatchNorm2d(oup),
)
def forward(self, x):
if self.mode in ['original']:
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]
elif self.mode in ['attn']:
res=self.short_conv(F.avg_pool2d(x,kernel_size=2,stride=2))
x1 = self.primary_conv(x)
x2 = self.cheap_operation(x1)
out = torch.cat([x1,x2], dim=1)
return out[:,:self.oup,:,:]*F.interpolate(self.gate_fn(res),size=(out.shape[-2],out.shape[-1]),mode='nearest')
DFC attention
分支
self.short_conv
就是 DFC attention
分支,DFC attention
的代码实现过程如下:
- 首先,采用
Average Pooling
进行down-sampling
; - 然后,经过 1x1卷积,扩充通道数;
- 接着,用卷积替代
horizontal FC
和vertical FC
,卷积核大小分别为(1, 5)、(5, 1); - 最后,经过sigmoid得到
DFC attention
分支的输出。 - 对于
DFC attention
分支的输出,采用bilinear插值进行up-sampling
,得到原始输入大小。然后与原始GHost Module
的输出相乘,得到最终输出。
self.gate_fn=nn.Sigmoid()
self.short_conv = nn.Sequential(
nn.Conv2d(inp, oup, kernel_size, stride, kernel_size//2, bias=False),
nn.BatchNorm2d(oup),
nn.Conv2d(oup, oup, kernel_size=(1,5), stride=1, padding=(0,2), groups=oup,bias=False),
nn.BatchNorm2d(oup),
nn.Conv2d(oup, oup, kernel_size=(5,1), stride=1, padding=(2,0), groups=oup,bias=False),
nn.BatchNorm2d(oup),
)
res=self.short_conv(F.avg_pool2d(x,kernel_size=2,stride=2))
F.interpolate(self.gate_fn(res),size=(out.shape[-2],out.shape[-1]),mode='nearest')
请注意,论文与代码不一致的地方:
- 论文中,通过消融实验发现
Max Pooling
效率高,默认的下采样方法为Max Pooling
,而代码中使用Average Pooling
; - 论文中,通过消融实现发现
bilinear interpolation
速度快,默认的上采样方法为bilinear interpolation
,而代码中使用nearest
;
GhostBottleneckV2
GhostBottleneckV2
由两个 GhostModuleV2
构成。
class GhostBottleneckV2(nn.Module):
def __init__(self, in_chs, mid_chs, out_chs, dw_kernel_size=3,
stride=1, act_layer=nn.ReLU, se_ratio=0.,layer_id=None,args=None):
# in_chs 表示输入特征图的通道数
# mid_chs 表示第一个 `Ghost Module` 进行升维得到的输出通道数
# out_chs 表示第二个 `Ghost Module` 进行降维得到的输出通道数
super(GhostBottleneckV2, self).__init__()
has_se = se_ratio is not None and se_ratio > 0.
self.stride = stride
# Point-wise expansion
if layer_id<=1:
self.ghost1 = GhostModuleV2(in_chs, mid_chs, relu=True,mode='original',args=args)
else:
self.ghost1 = GhostModuleV2(in_chs, mid_chs, relu=True,mode='attn',args=args)
# Depth-wise convolution
if self.stride > 1:
self.conv_dw = nn.Conv2d(mid_chs, mid_chs, dw_kernel_size, stride=stride,
padding=(dw_kernel_size-1)//2,groups=mid_chs, bias=False)
self.bn_dw = nn.BatchNorm2d(mid_chs)
# Squeeze-and-excitation
if has_se:
self.se = SqueezeExcite(mid_chs, se_ratio=se_ratio)
else:
self.se = None
self.ghost2 = GhostModuleV2(mid_chs, out_chs, relu=False,mode='original',args=args)
# shortcut
if (in_chs == out_chs and self.stride == 1):
self.shortcut = nn.Sequential()
else:
self.shortcut = nn.Sequential(
nn.Conv2d(in_chs, in_chs, dw_kernel_size, stride=stride,
padding=(dw_kernel_size-1)//2, groups=in_chs, bias=False),
nn.BatchNorm2d(in_chs),
nn.Conv2d(in_chs, out_chs, 1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(out_chs),
)
def forward(self, x):
residual = x
x = self.ghost1(x)
if self.stride > 1:
x = self.conv_dw(x)
x = self.bn_dw(x)
if self.se is not None:
x = self.se(x)
x = self.ghost2(x)
x += self.shortcut(residual)
return x