# SENet

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

## 模块设计

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

![SE block](https://3632175219-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJhbbglPBd1Md8dFDXQ%2Fsync%2F63839555b60612bcbffa333f50c01b767811089d.png?generation=1609678680038079\&alt=media)

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

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

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

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

![SE Excitation](https://3632175219-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJhbbglPBd1Md8dFDXQ%2Fsync%2F246e346090b6946e2a4ed47d44fe550174e298aa.png?generation=1609678679862429\&alt=media)

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

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

## 使用 SENet 改造已有网络

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

![SE Mod](https://3632175219-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MJhbbglPBd1Md8dFDXQ%2Fsync%2Fd998d7ba9d9e26471489cd0ec2f1b69698330e00.png?generation=1609678673780085\&alt=media)

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

## SENet 的 Pytorch 实现

mmclassification 在 [backbone](https://github.com/open-mmlab/mmclassification/tree/master/mmcls/models/backbones) 中实现了加入了 SENet 模块的 SE-ResNet 和 SE-ResNeXt。

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

`SELayer` 即为 SENet 核心逻辑的实现，定义在在 [se\_layer.py](https://github.com/open-mmlab/mmclassification/blob/master/mmcls/models/utils/se_layer.py) 中，代码如下：

```python
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 。 最后一个带广播的乘法完成了加权操作。
