介绍
SENet 是 ImageNet 2017(ImageNet 收官赛)的冠军模型,具有复杂度低,参数少和计算量小的优点。另外,SENet 思路很简单,很其中的 SE 模块很容易扩展在已有网络结构如 Inception 和 ResNet 中。
SE 模块
已经有很多工作在空间维度上来提升网络的性能,如 Inception 等,而 SENet 将关注点放在了特征通道之间的关系上。其具体策略为:通过学习的方式来自动获取到每个特征通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征,这又叫做“特征重标定”策略。具体的 SE 模块如下图所示:
给定一个输入
x
x
x,其特征通道数为
c
1
c_1
c1,通过一系列卷积等一般变换
F
t
r
F_{tr}
Ftr 后得到一个特征通道数为
c
2
c_2
c2 的特征。与传统的卷积神经网络不同,我们需要通过下面三个操作来重标定前面得到的特征。
- 1、首先是 Squeeze 操作,我们顺着空间维度来进行特征压缩,将一个通道中整个空间特征编码为一个全局特征,这个实数某种程度上具有全局的感受野,并且输出的通道数和输入的特征通道数相等,例如将形状为 (1, 32, 32, 10) 的 feature map 压缩成 (1, 1, 1, 10)。此操作通常采用采用 global average pooling 来实现。
- 2、得到了全局描述特征后,我们进行 Excitation 操作来抓取特征通道之间的关系,它是一个类似于循环神经网络中门的机制:
这里采用包含两个全连接层的 bottleneck 结构,即中间小两头大的结构:其中第一个全连接层起到降维的作用,并通过 ReLU 激活,第二个全连接层用来将其恢复至原始的维度。进行 Excitation 操作的最终目的是为每个特征通道生成权重,即学习到的各个通道的激活值(sigmoid 激活,值在 0~1 之间)。 - 3、最后是一个 Scale 的操作,我们将 Excitation 的输出的权重看做是经过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定,从而使得模型对各个通道的特征更有辨别能力,这类似于attention机制。
SE 模块在其他网络上的应用
SE模块的灵活性在于它可以直接应用现有的网络结构中。以 Inception 和 ResNet 为例,我们只需要在 Inception 模块或 Residual 模块后添加一个 SE 模块即可。具体如下图所示:
上图分别是将 SE 模块嵌入到 Inception 结构与 ResNet 中的示例,方框旁边的维度信息代表该层的输出,r 表示 Excitation 操作中的降维系数。
模型效果
SE 模块很容易嵌入到其它网络中,为了验证 SE 模块的作用,在其它流行网络如 ResNet 和 VGG 中引入 SE 模块,测试其在 ImageNet 上的效果,如下表所示:
SE 模块代码实现
import tensorflow as tf
class Squeeze_excitation_layer(tf.keras.Model):
def __init__(self, filter_sq):
# filter_sq 是 Excitation 中第一个卷积过程中卷积核的个数
super().__init__()
self.filter_sq = filter_sq
self.avepool = tf.keras.layers.GlobalAveragePooling2D()
self.dense = tf.keras.layers.Dense(filter_sq)
self.relu = tf.keras.layers.Activation('relu')
self.sigmoid = tf.keras.layers.Activation('sigmoid')
def call(self, inputs):
squeeze = self.avepool(inputs)
excitation = self.dense(squeeze)
excitation = self.relu(excitation)
excitation = tf.keras.layers.Dense(inputs.shape[-1])(excitation)
excitation = self.sigmoid(excitation)
excitation = tf.keras.layers.Reshape((1, 1, inputs.shape[-1]))(excitation)
scale = inputs * excitation
return scale
SE = Squeeze_excitation_layer(16)
inputs = np.zeros((1, 32, 32, 32), dtype=np.float32)
SE(inputs).shape
TensorShape([1, 32, 32, 32])
SE 模块应用到 ResNet 代码实现
这里只提供一个简单的实现代码来展示 SE 模块在 ResNet 网络中的应用,暂不涉及 ResNet 中的下采样过程。有关 ResNet 的实现过程可以参考 Tensorflow2.0之自定义ResNet 一文。
import tensorflow as tf
class SEBottleneck(tf.keras.Model):
def __init__(self, stride=1):
super().__init__()
self.conv1 = tf.keras.layers.Conv2D(16, kernel_size=1)
self.bn1 = tf.keras.layers.BatchNormalization()
self.conv2 = tf.keras.layers.Conv2D(16, kernel_size=3, strides=stride, padding='same')
self.bn2 = tf.keras.layers.BatchNormalization()
self.conv3 = tf.keras.layers.Conv2D(32, kernel_size=1)
self.bn3 = tf.keras.layers.BatchNormalization()
self.relu = tf.keras.layers.Activation('relu')
self.se = Squeeze_excitation_layer(16)
def call(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)
out += residual
out = self.relu(out)
return out
SEB = SEBottleneck()
inputs = np.zeros((1, 32, 32, 32), dtype=np.float32)
SEB(inputs).shape
TensorShape([1, 32, 32, 32])