论文:https://arxiv.org/abs/1709.01507
1 核心思想
SENet论文提出了一种新的特征处理方法,就是SE Block,全称为Squeeze-and-Excitation block。其处理过程如下图所示:
对于SE Block,输入为图像
X
∈
R
H
′
×
W
′
×
H
′
X \in R^{H^{'}\times W^{'}\times H^{'}}
X∈RH′×W′×H′经过变换后的特征图
U
∈
R
H
×
W
×
H
U \in R^{H \times W \times H}
U∈RH×W×H。输出仍然是相同大小的特征图。处理过程是实现了不同channel之间的特征相互影响。
SE Block主要处理过程包括Squeeze和Excitation两个过程。
Squeeze:
Squeeze模块是对输入特征应用全局平均池化操作,即将大小为
H
×
W
×
C
H \times W \times C
H×W×C的特征图变为
1
×
1
×
C
1 \times 1 \times C
1×1×C,计算公式是:
普通的卷积操作只把卷积核作用于局部的输入特征块,Squeeze操作对特征进行逐通道全局平均池化操作后,就使得输出特征不只是受到局部输入块的影响,而是能够受到空间全局特征的影响。作者在论文中对比了全局平均池化和全局最大池化,都可以取得较好的正向分类和检测效果,只不过全局平均池化的效果更好一些,当然也可以其他空间聚合手段。
Excitation:
Excitation对输入的
1
×
1
×
C
1 \times 1 \times C
1×1×C的特征进行跨通道的相互影响,论文中的做法是使用了两个全连接层。第一个全连接层使用了bottleneck的操作,输入通道数为C,输出通道数为
C
r
\frac{C}{r}
rC,
r
r
r是超参数,论文中设置为16。第一个全连接层之后使用Relu激活函数。第二个全连接层输入通道数为
C
r
\frac{C}{r}
rC,输出通道数为C,把特征的通道数再变换回去。最后使用Sigmoid激活函数,得到
1
×
1
×
C
1 \times 1 \times C
1×1×C的输出。
Excitation的具体处理过程为:
最终将Excitation输出和SE Block的输入
U
∈
R
H
×
W
×
H
U \in R^{H \times W \times H}
U∈RH×W×H相乘得到最终变换后的输出。
小结:
SE Block的作用是对不同的通道应用不同的权重系数,使得模型对更加关注信息量大的channel,这也可以看作是一种attention机制。
SE Block增加的参数数量很有限,具体增加的参数数量为:
2
r
∑
s
=
1
S
N
s
C
s
2
\frac{2}{r}\sum_{s=1}^{S}N_s C_s^2
r2s=1∑SNsCs2
论文实验中
r
r
r取16,但作者也提到可以对不同层的SE Block应用不同大小的
r
r
r。
2 应用
SE Block可以应用到已有的各CNN网络中,作者实验了将其应用于VGG、Inception、ResNet、mobilenet及shufflenet结构中,都在参数数量略有增加的情况下取得了更好的分类和识别效果。
作者在实验中捕获了不同深度网络层应用SE块之后的激活值,发现前面层不同类别的激活值很接近,这是因为低层网络在学习通用的特征。随着网络层数的加深,不同类别的激活值之间的差异在变大,说明开始学习具有辨别力的特征。但随着网络深度的进一步加深,发现激活值趋于饱和,所以作者提到可以移除最高层的SE块,这样可以减少参数数量,同时实验证明也没有造成太大的准确率下降。
总之,SE块通过进行跨通道的特征修正,在基本不增加计算量的前提下提升了分类精度,同时由于SE块具有良好的普适性,可以应用于各成熟的网络结构中,因此具有良好的应用前景。
3 pyTorch实现
代码参考自:https://zhuanlan.zhihu.com/p/65459972
class SELayer(nn.Module):
def __init__(self, channel, reduction=16):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction, bias=False),
nn.ReLU(inplace=True),
nn.Linear(channel // reduction, channel, bias=False),
nn.Sigmoid()
)
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y.expand_as(x)
对于SE-ResNet模型,只需要将SE模块加入到残差单元(应用在残差学习那一部分)就可以:
class SEBottleneck(nn.Module):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, reduction=16):
super(SEBottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.se = SELayer(planes * 4, reduction)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out = self.se(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out