034、SE 注意力模块:Squeeze-Excitation 的全局平均池化到 FC 到 Sigmoid 数学推导
034、SE 注意力模块:Squeeze-Excitation 的全局平均池化到 FC 到 Sigmoid 数学推导
一个让我调了三天三夜的 bug
去年做 YOLOv5 轻量化部署的时候,我在 Neck 部分插入了 SE 模块,想着给特征图加个通道注意力,结果模型收敛速度反而变慢了。当时我盯着 loss 曲线,心里一万个草泥马——明明论文里说 SE 能提升几个点 mAP,怎么到我这就翻车了?
后来 debug 到凌晨三点,发现是全局平均池化后的维度处理出了问题。SE 模块的数学本质其实很简单,但实现细节稍不注意就会踩坑。今天咱们就从数学推导到代码实现,把 SE 的每个操作掰开揉碎。
Squeeze 操作:全局平均池化的数学本质
SE 模块的第一步是 Squeeze,用全局平均池化把每个通道的 H×W 特征图压缩成一个标量。假设输入特征图 X 的形状是 (C, H, W),全局平均池化的数学表达式是:
z_c = (1 / (H × W)) * Σ_{i=1}^{H} Σ_{j=1}^{W} x_c(i, j)
这里 z_c 是第 c 个通道的全局描述,x_c(i, j) 是第 c 个通道在位置 (i, j) 的像素值。这个操作的本质是什么?是把空间信息压缩成通道级的统计量,相当于告诉模型“这个通道整体激活程度如何”。
这里踩过坑:很多人以为全局平均池化就是简单的求均值,但在 PyTorch 里用nn.AdaptiveAvgPool2d(1)时,输出形状是 (B, C, 1, 1),如果你直接 squeeze 掉最后两维,batch 维度可能被误伤。正确做法是用view(B, C)或者flatten(2)。
Excitation 操作:从 FC 到 Sigmoid 的数学推导
Excitation 部分是两个全连接层加一个 Sigmoid,数学上可以写成:
s = σ(W_2 · δ(W_1 · z + b_1) + b_2)
其中 z 是 Squeeze 得到的 (C, 1) 向量,W_1 是 (C/r, C) 的降维矩阵,W_2 是 (C, C/r) 的升维矩阵,r 是缩减率(通常取 16),δ 是 ReLU 激活函数,σ 是 Sigmoid。
别这样写:直接把两个 FC 层堆叠起来,中间不加 BatchNorm。SE 模块的设计哲学就是轻量级,加 BN 反而会引入额外的参数量和计算量,而且 ReLU 后的分布已经够用了。
我们来推导一下 Sigmoid 的输入输出关系。假设经过第二个 FC 层后的输出是 u,那么 Sigmoid 定义为:
σ(u) = 1 / (1 + e^{-u})
这个函数的值域是 (0, 1),当 u 很大时 σ(u) 趋近于 1,当 u 很小时趋近于 0。在 SE 模块中,s 的每个元素代表对应通道的“重要性权重”,范围在 0 到 1 之间。
数学上的关键点:为什么用 Sigmoid 而不是 Softmax?因为通道之间不是互斥的,多个通道可以同时被强调或抑制。Softmax 会强制所有通道权重和为 1,这不符合注意力机制的本意。
Scale 操作:逐通道乘法的实现细节
最后一步是 Scale,把学习到的权重 s 乘到原始特征图上:
y_c = s_c · x_c
这里的乘法是逐通道的,s_c 是标量,x_c 是 H×W 的特征图。在 PyTorch 里实现时,需要把 s 的形状从 (B, C) 扩展成 (B, C, 1, 1),然后直接做乘法。
这里踩过坑:如果你用s.unsqueeze(-1).unsqueeze(-1)扩展维度,要确保 s 的维度顺序正确。我见过有人把 s 的形状搞成 (B, 1, C, 1),结果乘出来特征图全乱了。
完整的数学推导链
把三个步骤串起来,SE 模块的完整数学表达式是:
y = X · σ(W_2 · δ(W_1 · GAP(X) + b_1) + b_2)
其中 GAP 是全局平均池化,· 表示逐通道乘法。这个公式看起来简单,但每个操作符的维度变化都需要精确控制。
别这样写:在 forward 函数里直接写x * self.sigmoid(self.fc2(self.relu(self.fc1(self.gap(x)))))。虽然一行代码能搞定,但调试时根本看不出中间结果。建议拆成多行,每步打印 shape 检查。
我的经验性建议
缩减率 r 的选择:不是越小越好。r=16 是论文里的默认值,但在小模型上(比如 YOLOv5s),r 可以设到 8 甚至 4,因为通道数本来就少,降维太狠会丢失信息。我做过实验,r=4 在轻量级模型上比 r=16 高 0.3 个 mAP。
放置位置:SE 模块放在 Backbone 的每个 stage 后面效果最好,放在 Neck 部分反而可能干扰特征融合。YOLOv5 官方代码里只在 Backbone 加了 SE,Neck 没加,这是有道理的。
训练技巧:加了 SE 模块后,学习率要适当调低,因为 SE 的 Sigmoid 输出对梯度敏感。我习惯把初始学习率降低 20%,然后用余弦退火调度。
调试方法:训练时打印 SE 模块输出的权重分布,如果大部分权重集中在 0.5 附近,说明模块没学到有效信息,可能是缩减率太大或者位置不对。
部署优化:SE 模块的两个 FC 层可以用 1×1 卷积替代,这样在 TensorRT 部署时能利用卷积优化,速度提升 15% 左右。
最后说一句,SE 模块虽然简单,但数学推导和实现细节决定了它能不能真正发挥作用。下次遇到模型收敛慢,先检查你的全局平均池化维度对不对。
