SKNet讲解
- 0. 引言
- 1. 网络结构
- 1.1 Split部分
- 1.2 Fuse部分
- 1.3 Select部分
- 1.4 三分支的情况
- 2. SKNet网络体系结构
- 3. 分析与解释
- 4. 代码
- 总结
0. 引言
视皮层神经元的感受野大小受刺激的调节,即对不同刺激
,卷积核的大小应该不同
,但在构建CNN时一般在同一层只采用一种卷积核
,很少考虑因图片大小不同采用不同卷积核。
于是提出了SKNet
。在SKNet
中,不同大小的感受视野(卷积核)对于不同尺度的目标会有不同的效果。尽管在Inception
中使用多个卷积核来适应不同尺度图像,但是卷积核权重相同
,参数就是被计算好的了。而SKNet
对不同输入使用的卷积核感受野不同,参数权重也不同
,可以根据输入大小自适应的进行处理。
在SKNet
中,提出了一种动态选择机制
,允许每个神经元根据输入信息的多个尺度自适应
调整其接受野的大小。设计了一种称为选择性内核(SK)单元的构建模块,在该模块中,由不同内核大小的多个分支的信息引导,使用softmax
的注意力进行融合
。对这些分支的不同关注导致融合层神经元有效感受野的大小不同。
论文地址:https://arxiv.org/pdf/1903.06586.pdf
代码地址:https://github.com/implus/SKNet
1. 网络结构
SKNet
网络主要由三个部分组成:Split
、Fuse
、Select
。其中,Split
部分将输入信息X
分别输入不同的核大小(这里是2个卷积核,卷积核大小分别为:3*3
和 5*5
);Fuse
部分进行特征融合;Select
部分根据计算得到的权重对对应的特征进行选择操作。
1.1 Split部分
对于输入信息X:[h,w,C]
,在Split
中,分别输入两个卷积层(默认为2个,根据需要可以设计多个),两个卷积层分别为3*3
和 5*5
。其中,每个卷积层都是由高效的分组/深度卷积
、批处理归一化
和ReLU
函数依次组成的。另外,为了进一步提高效率,将具有5*5
核的传统卷积替换为具有3×3核
和膨胀大小为2
的扩展卷积
。最终得到的输出分别为
U
~
\widetilde{U}
U
、
U
^
\hat{U}
U^ 。
1.2 Fuse部分
基本思想是使用门来控制来自多个分支的信息流,这些分支携带不同尺度的信息到下一层的神经元中。为实现这一目标,门需要整合来自所有分支的信息。我们首先通过element-wise summation
融合来自多个分支的结果:
U
=
U
~
+
U
^
U=\widetilde{U}+\hat{U}
U=U
+U^
F
g
p
F_{gp}
Fgp为全局平均池化操作
,
F
f
c
F_{fc}
Ffc 为先降维再升维的两层全连接层
。需要注意的是输出的两个矩阵a
和b
,其中矩阵b为冗余矩阵
,在如图两个分支的情况下b=1-a
。
通过简单地使用全局平均池化以生成channel-wise
统计信息
s
∈
R
C
s\in{R^C}
s∈RC 来生成全局信息。具体来说,s的第C个元素
是通过空间尺寸h×w
收缩U
来计算的:
s
c
=
F
g
p
(
U
c
)
=
1
h
×
w
∑
i
=
1
h
∑
j
=
1
w
U
c
(
i
,
j
)
s_c = F_{gp}(U_c) = \frac{1}{h\times{w}}\sum_{i=1}^{h}\sum_{j=1}^{w}{U_c(i,j)}
sc=Fgp(Uc)=h×w1i=1∑hj=1∑wUc(i,j)
此外,还创建了一个紧凑的特征
z
∈
R
d
×
1
z\in{R^{d\times1}}
z∈Rd×1,以便为精确和自适应选择提供指导。这是通过一个简单的完全连接(fc
)层实现的,降低了维度以提高效率:
z
=
F
f
c
(
s
)
=
δ
(
B
(
W
s
)
)
z = F_{fc}(s)=\delta(B(Ws))
z=Ffc(s)=δ(B(Ws))
其中
δ
\delta
δ 是ReLU
函数,
B
B
B 表示批量标准化,
W
∈
R
d
×
C
W\in{R^{d\times C}}
W∈Rd×C。为了研究d
对模型效率的影响,我们使用reduction ratio (r)
来控制其值:
d
=
m
a
x
(
C
/
r
,
L
)
d=max(C/r, L)
d=max(C/r,L)
其中L
表示d
的最小值(L=32
是我们实验中的典型设置)。
1.3 Select部分
Select
操作使用a
和b
两个权重矩阵对
U
~
\widetilde{U}
U
和
U
^
\hat{U}
U^ 进行加权操作,然后求和得到最终的输出向量V
。
跨通道的软关注
用于自适应地选择信息的不同空间尺度,空间尺度由紧凑特征描述符z
引导。具体而言,softmax
运算符应用于channel-wise
数字:
a
c
=
e
A
c
z
e
A
c
z
+
e
B
c
z
,
b
c
=
e
B
c
z
e
A
c
z
+
e
B
c
z
a_c = \frac{e^{A_cz}}{e^{A_cz}+e^{B_cz}},b_c = \frac{e^{B_cz}}{e^{A_cz}+e^{B_cz}}
ac=eAcz+eBczeAcz,bc=eAcz+eBczeBcz
其中
A
,
B
∈
R
C
×
d
A,B\in{R^{C\times d}}
A,B∈RC×d ,a、b表示
U
~
\widetilde{U}
U
和
U
^
\hat{U}
U^ 的soft attention
,
A
c
A_c
Ac 是A
的第c
行,$a_c $ 是a
的第c
个元素。在两个分支的情况下,矩阵B是冗余的
,因为
a
c
+
b
c
=
1
a_c +b_c = 1
ac+bc=1。最终的特征映射V
是通过各种卷积核的注意力权重获得的:
V
c
=
a
c
∗
U
~
+
b
c
∗
U
^
,
a
c
+
b
c
=
1
V_c = a_c * \widetilde{U} + b_c * \hat{U}, a_c +b_c = 1
Vc=ac∗U
+bc∗U^,ac+bc=1
其中
V
=
[
V
1
,
V
2
,
.
.
.
,
V
C
]
V=[V_1,V_2,...,V_C]
V=[V1,V2,...,VC],
V
c
∈
R
h
×
w
V_c∈R^{h×w}
Vc∈Rh×w,注意,这里我们提供了一个双分支情况的公式,并且可以通过扩展轻松推断出具有更多分支的情况。
1.4 三分支的情况
这里以三分支为例,绘制网络结构。后续多分支结构与此类似。
2. SKNet网络体系结构
-
与ResNeXt相似,
SKNet
主要由一堆重复的瓶颈(bottleneck)块组成,称为“ SK单元”。 -
每个SK单元由1×1卷积,SK卷积和1×1卷积的序列组成。
-
通常,ResNeXt中原始瓶颈块中的所有大内核卷积都将由
SK卷积代替
。 -
与ResNeXt-50相比,SKNet-50仅会使参数数量增加10%,计算成本增加5%。
3. 分析与解释
为什么SKNet会取得这么好的效果?
为了理解自适应卷积核选择的工作原理,我们通过输入相同的目标对象但在不同的尺度上分析注意力。我们从ImageNet验证集中获取所有图像实例,并通过中心裁剪和随后的大小调整逐步将中心对象从1.0倍扩大到2.0倍。
|
|
由上述两个例子对比可知:在大多数通道
中,当目标对象扩大
时,大核(5×5)的注意力权重就会增加
,这表明神经元的RF大小会自适应
地变大,这与预期相符。
关于跨深度的自适应选择的作用,还有一个令人惊讶的特点:目标对象越大,通过低级和中级阶段
(例如,SK 23,SK 34)的“选择性内核”
机制,对更大内核的关注就越多
。 但是,在更高的层
上(例如,SK 5_3),所有比例尺信息都丢失了
,这种模式消失了。
随着目标规模的增长
,5×5内核的重要性不断提高
。在网络的底层部分
(前部),可以根据对象大小的语义意识
来选择
合适的内核大小,从而有效地调整这些神经元的RF大小
。但是,这种模式在像SK 5_3这样的较高层中并不存在
,因为对于高级表示,“比例”部分编码在特征向量中,并且与较低层的情况相比,内核大小的重要性较小
。
4. 代码
最后,附上SKNet
的代码实现。
import torch
import torch.nn as nn
class SKConv(nn.Module):
def __init__(self, features, M: int = 2, G: int = 32, r: int = 16, stride: int = 1, L: int = 32):
super().__init__()
d = max(features / r, L)
self.M = M
self.features = features
# 1.split
self.convs = nn.ModuleList([])
for i in range(M):
self.convs.append(nn.Sequential(
nn.Conv2d(features, features, kernel_size=3, stride=stride, padding=1+i, dilation=1+i, groups=G, bias=False),
nn.BatchNorm2d(features),
nn.ReLU(inplace=True)
))
# 2.fuse
self.gap = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(nn.Conv2d(features, d, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(d),
nn.ReLU(inplace=True))
# 3.select
self.fcs = nn.ModuleList([])
for i in range(M):
self.fcs.append(
nn.Conv2d(d, features, kernel_size=1, stride=1)
)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
batch_size = x.shape[0]
# 1.split
feats = [conv(x) for conv in self.convs]
feats = torch.cat(feats, dim=1)
feats = feats.view(batch_size, self.M, self.features, feats.shape[2], feats.shape[3])
print('feats.shape', feats.shape)
# 2.fuse
feats_U = torch.sum(feats, dim=1)
feats_S = self.gap(feats_U)
feats_Z = self.fc(feats_S)
print('feats_U.shape', feats_U.shape)
print('feats_S.shape', feats_S.shape)
print('feats_Z.shape', feats_Z.shape)
# 3.select
attention_vectors = [fc(feats_Z) for fc in self.fcs]
attention_vectors = torch.cat(attention_vectors, dim=1)
print('attention_vectors.shape', attention_vectors.shape)
attention_vectors = attention_vectors.view(batch_size, self.M, self.features, 1, 1)
print('attention_vectors.shape', attention_vectors.shape)
attention_vectors = self.softmax(attention_vectors)
feats_V = torch.sum(feats * attention_vectors, dim=1)
print('feats_V.shape', feats_V.shape)
return feats_V
if __name__ == '__main__':
inputs = torch.randn(4, 64, 512, 512)
net = SKConv(64)
outputs = net(inputs)
得到的输出如下所示:
feats.shape torch.Size([4, 2, 64, 512, 512])
feats_U.shape torch.Size([4, 64, 512, 512])
feats_S.shape torch.Size([4, 64, 1, 1])
feats_Z.shape torch.Size([4, 32, 1, 1])
attention_vectors.shape torch.Size([4, 128, 1, 1]) #a, b
attention_vectors.shape torch.Size([4, 2, 64, 1, 1]) #a, b
feats_V.shape torch.Size([4, 64, 512, 512])
总结
SKNet
中使用了不同的卷积核,且卷积核权重
是不同的,该创新点是突出的!相信一定能在某些情况下取得不错的效果。最后,如果有什么疑问欢迎在评论区提出,对于共性问题可能会后续添加到文章介绍中。