SE Block 理解:通道注意力与重标定


Squeeze-and-Excitation(SE)学习笔记

一. 理论部分

1) 背景:卷积做了什么、还缺了什么?

  • 普通卷积会在局部感受野里,把空间信息(哪个位置)和通道信息(第几张特征图、检测到什么模式)混在一起做加权求和。
  • 过去提升性能,大家多从空间增强入手,本质都在怎么让网络看更大的空间范围

但一个很自然的问题是:通道与通道之间的关系是否也值得建模?
换句话说:这一层里有 C 张特征图,哪些该多听一点,哪些该压小一点


2) SE 的核心思想

把每个卷积层(或一个残差/瓶颈块)的输出看成 $C$ 个通道:($F \in \mathbb{R}^{C\times H\times W}$)。
SE 做三步:Squeeze → Excitation → Scale(压缩→激发→重标定),让每个通道得到一个自适应权重 ($s_c\in(0,1)$)。

(a) Squeeze:挤掉空间,只保留全局语义

对每个通道做全局平均池化(Global Average Pooling):$ z_c=\frac{1}{H W}\sum_{i=1}^{H}\sum_{j=1}^{W}F_c(i,j)$,得到长度为 C 的向量 ($z$)。

直觉:用一个数概括这一通道整体有多重要。

(b) Excitation:学会通道间关系

把 ($z$) 送进一个小的两层感知机产生通道权重:$ s=\sigma\big(W_2,\delta(W_1 z)\big), \quad s\in\mathbb{R}^{C}$

  • ($\delta$) 是 ReLU,($\sigma$) 是 Sigmoid;
  • 为了轻量,($W_1$) 先把维度从 ($C$) 降到 ($C/r$),再升回 ($C$)。

(c) Scale:按权重重标定每个通道

$ \tilde{F}_c = s_c \cdot F_c$
把有用的通道放大,没用的压小。不改变空间分辨率,仅通道重标定。

一句话:SE = 通道注意力。它不改动卷积核本身,而是学一个每层的通道音量调节器。


3) 怎么插到主干网络里?

  • SE-Inception:在 Inception 模块的分支聚合后接一个 SE。
  • SE-ResNet:在每个残差(或瓶颈)块的卷积后、与主分支相加之前插一个 SE。
    残差分支先卷积 → 全局池化 → 两层 FC(降维再升维)→ Sigmoid → 通道缩放 → 再与恒等分支相加。

好处:几乎不改结构,只在 block 末尾加一个很小的门控,易于复用到各种架构(ResNet、ResNeXt、Inception-v2/BN-Inception…)。


4) 代价与复杂度

  • 参数量:+2% ~ +10%(取决于通道数和 r)。
  • 计算/时间:GPU 端推理大约 +10%,CPU < +2%(量级参考文中统计)。
  • 这些代价换来显著更低的错误率,性价比高。

5) 实验现象(ImageNet-1k)

文中做了三类验证:

  1. 随网络加深的收益:在 ResNet-50/101/152 上,SE 版本训练/验证曲线更低,说明更稳定、泛化更好。
  2. 与现代结构结合:把 SE 接到 ResNeXt、BN-Inception、Inception-v2 上,同样获得一致增益,说明是通用插件
  3. 和 SOTA 对比:单模型单裁剪评测下,SE 版本的错误率更低
    他们的参赛队(WMW)在 ILSVRC 2017 分类任务拿到 Top-5 error ≈ 2.251% 的水平(榜单图上有)。

关键观察:SE 的收益在浅层与深层都有,尤其深网络也能稳稳受益;曲线图里 SE 版本(红/蓝)整体更低。


6) 为什么它有效?

  • 卷积的盲点:标准卷积更擅长空间模式(边缘/角点/纹理),但不显式地建模通道间依赖(哪些特征彼此关联、此消彼长)。

  • SE 的作用:让网络显式学习通道关系,像 EQ 一样动态强调当前任务/当前图像更重要的语义特征。

  • 训练上:门控是端到端学习的,全局池化让它聚合全图上下文做决策,稳定、易优化。

  • SE 是一种通道注意力,只给通道打权重,不做空间位置的注意力

二. 代码部分

1)原理图

展示了 SENet 的核心三步:

  • Squeeze:全局平均池化 → 把每个通道 ($C\times H\times W$) 压缩成 ($C\times1\times1$)。
  • Excitation:用两个 1×1 卷积(或 FC)建模通道间关系 → 学到每个通道的权重 ($s_c$)。
  • Scale:把这些权重乘回原来的 feature map → 调整通道强弱(特征重标定)。

图里三个彩色方块对应这三步,底下三行蓝绿橙文字也说明:

Squeeze:空间压缩
Excitation:通道建模
Scale:通道加权

2) 代码部分

下面是用 PyTorch 写的两个类:

(1) BasicBlock

这是一个基础卷积块 + SE 模块嵌入的实现。核心在于:

  • 两个卷积 + BN + ReLU;
  • 中间插入 SE 部分(两层 1×1 conv 模拟 FC);
  • 最后通道加权,再加 shortcut。

(2) SENet

把上面的块堆叠成完整网络(类似 ResNet-18 结构:2,2,2,2 层数)。主要包括:

  • 第一层卷积;
  • 4 层 BasicBlock 组成的主干;
  • 平均池化 + 全连接输出。
import torch
import torch.nn as nn
import torch.nn.functional as F

# ============================================
# 定义一个包含 SE 模块的基本卷积块 BasicBlock
# ============================================
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super(BasicBlock, self).__init__()
# 两个标准卷积层 + BN + ReLU
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)

# Shortcut 分支(如果维度不同,用 1×1 conv 调整)
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
else:
self.shortcut = nn.Sequential()

# ========== SE 模块部分 ==========
# Squeeze: 全局平均池化 (H×W → 1×1)
# Excitation: 两个 1×1 卷积实现 FC 降维升维(通道注意力)
self.fc1 = nn.Conv2d(out_channels, out_channels // 16, kernel_size=1) # 降维
self.fc2 = nn.Conv2d(out_channels // 16, out_channels, kernel_size=1) # 升维
# =================================

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))

# Squeeze 操作:全局平均池化
w = F.avg_pool2d(out, out.size(2)) # 输出维度 [B, C, 1, 1]

# Excitation 操作:两层 1x1 conv + ReLU + Sigmoid
w = F.relu(self.fc1(w))
w = torch.sigmoid(self.fc2(w))

# Scale 操作:用 w 对每个通道加权
out = out * w

# 加上 shortcut(残差连接)
out += self.shortcut(x)
out = F.relu(out)
return out


# ============================================
# 定义完整的 SENet 网络结构(类似 ResNet-18)
# ============================================
class SENet(nn.Module):
def __init__(self, num_classes=10): # 可根据任务改分类数
super(SENet, self).__init__()
# 初始卷积层
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64)

# 构建四层主干(每层由多个 BasicBlock 组成)
self.layer1 = self._make_layer(64, 64, 2, stride=1)
self.layer2 = self._make_layer(64, 128, 2, stride=2)
self.layer3 = self._make_layer(128, 256, 2, stride=2)
self.layer4 = self._make_layer(256, 512, 2, stride=2)

# 平均池化 + 全连接分类层
self.fc = nn.Linear(512, num_classes)

def _make_layer(self, in_channels, out_channels, blocks, stride):
layers = []
# 第一个块可能需要改变尺寸(stride != 1)
layers.append(BasicBlock(in_channels, out_channels, stride))
for _ in range(1, blocks):
layers.append(BasicBlock(out_channels, out_channels))
return nn.Sequential(*layers)

def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.layer1(out)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = F.avg_pool2d(out, 4)
out = out.view(out.size(0), -1)
out = self.fc(out)
return out


# 测试网络是否可运行
if __name__ == "__main__":
x = torch.randn(1, 3, 32, 32) # 模拟输入
model = SENet(num_classes=10)
y = model(x)
print(y.shape) # 输出 torch.Size([1, 10])
模块 功能 对应图中部分
Conv + BN + ReLU 提取基本局部特征 左侧 x → $F_tr$
GlobalAvgPool 全局聚合空间信息(Squeeze) 蓝色部分
1×1 Conv + ReLU + 1×1 Conv + Sigmoid 建模通道依赖并生成权重(Excitation) 绿色部分
out * w 特征加权(Scale) 橙色部分
shortcut + ReLU 保持梯度稳定的残差连接 输出层右侧箭头

Author: linda1729
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source linda1729 !
评论
  TOC