SENet

SENet 并不是一个完整的网络结构,而是一个可以插入任意网络结构的模块。 与以往的网络设计思路不同,SENet 显式考虑了特征图的通道之间的关系, 使用了一种 self-attention 机制,从特征图中学习出不同通道的重要性,并对特征图的每个通道重新加权。 SENet 于 2017 年由 Momenta 公司和牛津大学视觉组提出,并获得了当年 ILSVRC 竞赛图像分类项目的冠军。

模块设计

SENet 模块的设计并不复杂,整体结构如下图所示。

图中,Ftr\mathbf{F}_{tr} 表示被插入 SENet 模块的网络中的某个卷积层,可以是任意网络结构中的任意一层,U\mathbf{U} 即为网络输出的特征图。 U\mathbf{U}X~\mathbf{\tilde{X}} 的部分为 SENet 模块。

SENet 是 Squeeze and Excitation Network 的缩写。从这个名字中可以看出,这个模块中包含一个压缩过程和一个激发过程,分别对应图中的 Fsq()F_{sq}(\cdot)Fex(,W)F_{ex}(\cdot, \mathbf{W}) 部分。

压缩过程 Fsq()F_{sq}(\cdot) 是一个简单的全局平均池化层(Global Average Pooling)。 它可以将特征图 U\mathbf{U} 压缩成一个 1×1×C1\times 1\times C 的向量。 向量中的每一个元素是同通道全部空间位置的神经元的均值, 因而一定程度上包含了特征图 U\mathbf{U} 全部空间位置的信息。

激发过程 Fex(,W)F_{ex}(\cdot, \mathbf{W}) 由两个全连接层构成,是一个可学习的模块。 第一个全连接层将 CC 维度的全局特征映射到低维空间,并使用 ReLU 函数激活。 第二个全连接层再将低维向量映射回 CC 维,并使用 Sigmoid 激活。 激发过程的具体构造如下图所示,其中 rr 为通道的压缩系数。

这两个全连接层可以表示一个相对复杂的非线性映射。而通道数先减再增的瓶颈结构可以一定程度上降低计算量。

最后,再用激发后的特征向量(以下简称激发态向量,记为 s\mathbf{s} )对原特征图 U\mathbf{U} 的每个通道进行加权。 由于激发过程最后以 Sigmoid 函数作为激活函数,因此所有通道的权重介于 0 到 1 之间。 一个通道的重要性越低,它所对应的系数就越接近 0,在最终输出的特征图 X~\mathbf{\tilde{X}} 中就会被削弱。

使用 SENet 改造已有网络

SENet 的使用相对自由,可以插入到任何一个已有网络的任何一个卷积层之后(具体而言是非线性激活层之后),实现对网络的改造。 SENet 原始论文中给出了 Inception 以及残差网络 ResNet 和 ResNeXt 的改进方式,如下图所示。

对于 Inception 网络,我们可以将其直接插入到 Inception 结构之后。 对于 ResNet 和 ResNeXt,我们可以将其插入到残差分支的基本模块或瓶颈模块之后,并保留直通分支不变。

SENet 的 Pytorch 实现

mmclassification 在 backbone 中实现了加入了 SENet 模块的 SE-ResNet 和 SE-ResNeXt。

由于 SENet 是一个插入模块。因此对于通过工厂函数生成的 ResNet 和 ResNeXt 模型,只需要重新定义 SEBottleNeck ,在残差分支的最后加入 SELayer 就可以实现 SE-ResNet 和 SE-ResNeXt。

SELayer 即为 SENet 核心逻辑的实现,定义在在 se_layer.py 中,代码如下:

class SELayer(nn.Module):
    def __init__(self,
                 channels,
                 ratio=16,
                 conv_cfg=None,
                 act_cfg=(dict(type='ReLU'), dict(type='Sigmoid'))):
        super(SELayer, self).__init__()
        if isinstance(act_cfg, dict):
            act_cfg = (act_cfg, act_cfg)
        assert len(act_cfg) == 2
        assert mmcv.is_tuple_of(act_cfg, dict)
        self.global_avgpool = nn.AdaptiveAvgPool2d(1)
        self.conv1 = ConvModule(
            in_channels=channels,
            out_channels=int(channels / ratio),
            kernel_size=1,
            stride=1,
            conv_cfg=conv_cfg,
            act_cfg=act_cfg[0])
        self.conv2 = ConvModule(
            in_channels=int(channels / ratio),
            out_channels=channels,
            kernel_size=1,
            stride=1,
            conv_cfg=conv_cfg,
            act_cfg=act_cfg[1])

    def forward(self, x):
        out = self.global_avgpool(x)
        out = self.conv1(out)
        out = self.conv2(out)
        return x * out

可以看到,该模块的前传函数由一个全局平均每池化层和两个 1x1 的卷积层串联而成。 池化层即为压缩过程,两个卷积层则等价实现了通道维度的两个全连接层,即激发过程。 在这里,通道的压缩比率默认为 16 。 最后一个带广播的乘法完成了加权操作。

Last updated