ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

通道注意力机制SE详解:原理、PyTorch实现与部署选型

通道注意力机制SE详解:原理、PyTorch实现与部署选型 去年帮一个团队调工业质检的分类模型他们基于 ResNet-50 的骨架已经卡在 98.2% 的准确率上两周没动改了学习率、换了优化器、加了数据增强曲线就是平的。后来我在每个 bottleneck 里塞了一个二十来行的小模块训练三轮之后验证集涨到了 99.1%而参数量只多了 250 万左右。那个小模块就是 SE全称 Squeeze-and-Excitation也就是这几年被说烂了的通道注意力机制。但说实话注意力机制这个词现在被用得太泛很多人听到 SE 第一反应是 Transformer 里那套自注意力其实两者从动机到计算方式完全不是一回事。这篇东西我想把 SE 从动机、公式、代码、参数量、训练坑、部署坑一路拆到底顺带把它和 CBAM、CA、ECA、多头自注意力放在一起比一比方便你在自己的任务里判断该不该用、该怎么用。1. Squeeze-and-Excitation 究竟在解决卷积的什么短板1.1 卷积核视角的通道盲区先说一个很多人没细想过的事实标准卷积在通道维度上是民主的。一个 3×3×512 的卷积核在计算输出的时候会把输入的 512 个通道按同一套空间权重卷积、求和得到下一层的某个通道。整个过程里卷积核只学会了在空间上怎么加权却没有学会在通道上怎么加权。换句话说512 个输入通道对输出的贡献是由卷积核的数值隐式决定的而且这个决定是局部的、静态的——同一组权重不管面对什么输入图像都给出同样的通道混合比例。这在 AlexNet 那个年代问题不大因为那时候大家关心的是有没有学到边缘和纹理。但当骨干网络走到 ResNet-50、ResNet-101 这个量级通道数动辄 2048通道之间的分工已经非常明确有的通道专门响应纹理有的响应颜色有的响应特定朝向的边缘还有大量通道在特定输入下几乎不激活。这时候如果网络能根据当前输入动态判断这 2048 个通道里哪几个是重要的把资源往重要的通道上倾斜理论上就能在不增加多少计算量的前提下提升表达能力。SE 就是干这个的。它的核心假设非常朴素通道之间存在重要性差异而且这个差异是输入相关的。同一张特征图输入一只猫和一辆车需要被强调的通道应该是不同的。1.2 从堆深度到重分配的思路切换2017 年之前提升精度的主流思路是往两个方向堆加深ResNet 从 18 堆到 152和加宽Wide ResNet、ResNeXt 的 group conv。这两条路都有明确的边际递减ResNet-152 比 ResNet-50 参数量翻了 2.4 倍ImageNet top-1 只涨了 2 个点左右而且训练时间线性增长。SE 走的是第三条路——不改变特征图的数量只改变它们的权重。你可以把它想象成一个调音台原来 512 个音轨全部按固定音量混合输出现在加了一个自动混音器先听一遍每个音轨的平均能量再动态决定每个音轨该放大还是衰减。设备没换乐手没换只是混音策略变了。这也是为什么 SE 的额外参数量可以做到很小。它的参数只跟通道数 C 有关跟空间尺寸 H×W 完全无关所以无论你的输入是 224×224 还是 512×512SE 的开销是一样的。相比之下如果在空间维度上做注意力比如 CBAM 的空间分支开销就跟 H×W 强相关了。一个常见误解值得提前澄清SE 不是给特征图加权而是给通道加权。它输出的是一组长度为 C 的标量每个标量对应一个通道然后这组标量被广播乘到整张特征图上。所以 SE 不会改变特征图的空间结构也不会引入任何位置信息——这是它和后面要讲的 CA、自注意力最本质的区别。2. Squeeze、Excitation、Scale 三步的数学过程和设计取舍2.1 Squeeze把每个通道压成一个数为什么选全局平均池化给定输入特征图 U形状是 C×H×W这里按通道优先写实际实现里是 NCHWSqueeze 这一步做的事极其简单z_c (1 / (H*W)) * Σ_{i1..H} Σ_{j1..W} U_c(i, j)也就是对第 c 个通道做全局平均池化得到一个标量 z_c。C 个通道走完得到一个长度为 C 的向量 z。为什么用平均而不是最大这里的逻辑是Squeeze 的目的是描述一个通道的整体活跃程度而不是峰值响应。平均池化对整张空间图的响应做了一次无偏汇总梯度回传到每个空间位置都是均匀的 1/(H*W)训练非常稳定。最大池化只挑最强的那个位置梯度只回传到那一个点上在噪声大的数据集上容易放大异常激活导致训练震荡。不过话说回来这个选择在具体任务里未必总是最优。我做过一个遥感图像的小目标检测实验图像里目标只占几个像素全局平均池化会把目标响应和一大片背景平均掉SE 的收益几乎为零。后来换成 GAP 和 GMP 并联两个分支各出一个 C 维向量相加后再送进 Excitation小目标的召回率才提上来。CBAM 的通道分支用的就是这个并联方案某种程度上是对纯 GAP 的一个修正。另一个细节是Squeeze 的输出维度是 C跟 batch 无关。这意味着 SE 模块本身的参数量不含 batch 维度它是逐样本计算的——每个样本算出自己的一组通道权重。这一点在做 batch 推理优化的时候要注意SE 的 FC 层不能像 BN 那样在推理时把 batch 统计量折叠掉。2.2 Excitation瓶颈结构里为什么必须降维r 该取多少得到 z 之后Excitation 用两个全连接层加一个 Sigmoid 把它映射成一组权重s σ( W2 · δ( W1 · z ) )其中 W1 的形状是 (C/r) × CW2 的形状是 C × (C/r)δ 是 ReLUσ 是 Sigmoidr 是降维比。为什么要降维再用升回来而不是直接用一个 C×C 的矩阵原因有三个我按重要性排一下。第一是参数控制。一个 C×C 的矩阵在 C2048 时有 419 万参数插到 ResNet 的最后一个 stage 里三个 block 就是 1200 多万直接把模型撑大三成。而用 r16 的瓶颈结构参数量降到 2C²/r 52 万只有原来的 1/8。第二是非线性。降维再升维这个结构天然引入了一层 ReLU让通道之间的交互不局限于线性组合。如果是单个 C×C 矩阵那整个 SE 就退化成一次线性变换表达能力弱很多。第三是隐式的正则化。瓶颈结构强迫网络用一个低维子空间来描述哪些通道重要这本身是一种信息压缩在训练数据不够大的时候反而有帮助。r 取多少原论文在 ImageNet 上做了网格搜索扫了 4、8、16、32 几档发现 r16 在精度和参数量的平衡点上最好。但这个结论有很强的任务依赖性我自己的经验是ImageNet 这类千类分类r16 基本可以闭眼用。细粒度分类比如区分几十种鸟通道分工更细r 建议降到 8甚至 4。小数据集做迁移学习r 可以放大到 32减少过拟合风险。分割和检测里如果骨架已经很强r16 就够了再小收益不明显。有一个坑后面会展开讲r 设得过小时瓶颈层会成为信息瓶颈。比如 C64 的浅层网络r16 意味着中间层只有 4 个神经元要把 64 个通道的重要性排序塞进 4 维空间损失的信息太多反而掉点。2.3 Scale广播乘法的位置决定了梯度怎么走最后一步是把 s 乘回原特征图X_c s_c · U_c实现上通常写成x * s.view(N, C, 1, 1)靠广播完成。这一步没有任何参数但它的位置很有讲究。在 ResNet 的 bottleneck 里SE 有两种插法插在残差相加之前或者插在相加之后。原始 SENet 用的是前者消融实验显示在残差分支的末端做重标定效果更好。原因可以这样理解残差相加之后shortcut 分支的恒等映射也被 SE 的权重影响了恒等通路被破坏了而相加之前做shortcut 仍然是干净的恒等映射梯度可以无损地回传SE 只影响主分支的表达。另一件事是初始化的尺度问题。Sigmoid 输出范围是 0 到 1如果训练初期 s 普遍在 0.5 附近等于把整张特征图缩小了一半会扰动预训练权重的尺度。有几种常见对策把第二个 FC 的权重初始化得非常小接近 0这样 s 接近 0.5 但方差很小或者把第二个 FC 的 bias 设成 2 到 3让 sigmoid 输出落在 0.88 到 0.95 之间接近恒等映射。后者在加载自己随机初始化的 SE 做迁移学习时特别有用能把训练初期的 loss 尖峰压下去。3. 一份可以直接跑通的 PyTorch 实现与集成细节3.1 二十行的最小实现逐行说明下面是我在项目里用了好几年的版本用 1×1 卷积代替 nn.Linear好处是兼容任意尺寸输入也方便和卷积网络的其它层统一处理。import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() hidden max(channels // reduction, 4) # 防止浅层通道数太少 self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Conv2d(channels, hidden, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(hidden, channels, 1, biasTrue), nn.Sigmoid() ) # 第二个卷积的 bias 设正让初始输出接近恒等映射 nn.init.zeros_(self.fc[2].weight) nn.init.constant_(self.fc[2].bias, 2.0) def forward(self, x): s self.fc(self.pool(x)) return x * s几个值得说的点。hidden max(channels // reduction, 4)这行是我自己加的保险。很多开源实现直接写channels // reduction当 channels32、reduction16 时中间层只有 2 个神经元甚至当 channels16 时会变成 1 甚至 0直接报错或者退化成常数。加一个下界能避免浅层网络出问题。用Conv2d(C, hidden, 1)而不是Linear是因为前者天然接受 NCHW 输入不需要 view 来 view 去。有些人担心 1×1 卷积比 Linear 慢实测在 GPU 上差别在 1% 以内反倒是少了两次 reshape代码更干净。nn.init.zeros_(self.fc[2].weight)把升维层的权重清零、bias 设成 2这样初始化时 sigmoid(2)≈0.88SE 的输出就是原特征图乘 0.88非常接近恒等映射。这个技巧能显著改善在预训练骨架上新加 SE 分支时的训练稳定性。注意这是我自己项目里的做法原论文用的是默认初始化如果你的训练数据量足够大、从头训用默认的也没问题。3.2 插进 ResNet Bottleneck 的正确位置以标准的 Bottleneck 为例1×1 降维 → 3×3 → 1×1 升维 → 残差相加SE 应该插在最后那个 1×1 升维卷积之后、残差相加之前class SEBottleneck(nn.Module): expansion 4 def __init__(self, inplanes, planes, stride1, downsampleNone, reduction16): super().__init__() width planes self.conv1 nn.Conv2d(inplanes, width, 1, biasFalse) self.bn1 nn.BatchNorm2d(width) self.conv2 nn.Conv2d(width, width, 3, stridestride, padding1, biasFalse) self.bn2 nn.BatchNorm2d(width) self.conv3 nn.Conv2d(width, planes * self.expansion, 1, biasFalse) self.bn3 nn.BatchNorm2d(planes * self.expansion) self.relu nn.ReLU(inplaceTrue) self.se SEBlock(planes * self.expansion, reduction) self.downsample downsample def forward(self, x): identity x out self.relu(self.bn1(self.conv1(x))) out self.relu(self.bn2(self.conv2(out))) out self.bn3(self.conv3(out)) out self.se(out) # 重标定发生在相加之前 if self.downsample is not None: identity self.downsample(x) out identity return self.relu(out)注意 SE 作用在planes * expansion上也就是 bottleneck 的输出通道数不是中间的 width。这一点经常有人搞错把 SE 接到 conv2 后面通道数少了一半参数量省了但效果也差了。SENet 原文的设定就是作用在 block 输出上。还有个小顺序问题self.se(out)放在bn3之后、加 identity 之前。如果放在 ReLU 之后重标定作用在非负的特征上理论上也可以但实测差别不大跟原论文保持一致就行。3.3 实测账本多出来的参数和 FLOPs 到底有多少拿 ResNet-50 做例子SE 加在四个 stage 的所有 bottleneck 上通道数分别是 256、512、1024、2048block 数分别是 3、4、6、3r16Stage通道数 CBlock 数单块 SE 参数 2C²/r小计layer12563819224,576layer2512432,768131,072layer310246131,072786,432layer420483524,2881,572,864合计—16—约 2.51MResNet-50 原始参数量是 25.6M加上 SE 变成约 28.1M增幅 9.8%。FLOPs 方面SE 的额外计算主要来自两部分全局平均池化约 H×W×C 次加法和两个 1×1 卷积约 2C²/r 次乘加。以 layer4 在 7×7 分辨率下为例一个 block 的 GAP 是 7×7×2048 ≈ 10 万次加法FC 是 2×2048×2048/16 ≈ 52 万次乘加合计 62 万。而 layer4 一个 bottleneck 的卷积计算量在 1×1×2048×512×7×7 这个量级大约是 5000 万次乘加。SE 占比 1% 出头。整网算下来FLOPs 增加通常在 0.2% 到 0.5% 之间。这个性价比是 SE 当年能火的核心原因参数增加约 10%FLOPs 增加不到 1%ImageNet top-1 提升 1.5 个点左右ResNet-50 从 76.1% 到 77.6%。相比之下把 ResNet-50 换成 ResNet-101参数量增加 75%FLOPs 增加 80%也只涨 1.7 个点。4. 训练和部署里那些文档不会写的坑4.1 小 batch 与 BN 的相互干扰SE 本身和 BN 没有直接冲突但两者放在一起会放大小 batch 的问题。原因在于 SE 的权重是逐样本计算的而 GAP 又对空间维度做平均当 batch 很小、特征图分辨率又低的时候GAP 的估计噪声很大导致同一个通道在不同样本上的权重波动剧烈。再叠加上 BN 在小 batch 下统计量不准整个 block 的输出方差会明显变大。我在一个 batch size 只能开到 4 的医学影像项目上踩过这个坑加了 SE 之后训练 loss 曲线毛刺非常明显验证精度比不加 SE 还低 0.6 个点。解决方式有两个一是换用 GroupNorm 或者把 BN 的 momentum 调小比如从 0.1 调到 0.01让统计量更平滑二是把 SE 里的 GAP 换成带温度的平均也就是先对空间做一次轻量的 1×1 卷积再加权平均增加一点可学习参数来稳定估计。第二种做法在 batch 特别小的场景下收益比较明显。4.2 特征图空间尺寸太小时的收益衰减GAP 在 7×7 和 1×1 上的行为完全不同。当特征图已经降到 1×1比如某些分类头之前的全局池化层SE 就退化成对单个标量的重标定等价于在通道维度乘一个数几乎没有额外信息可用。这时候加 SE 的收益为零只是白白增加参数。我的经验阈值是特征图空间尺寸低于 4×4 时SE 的收益开始明显衰减低于 2×2 基本没有意义。所以如果你在做高分辨率输入的小目标检测SE 加在 stride 8 和 stride 16 的层上效果最好加到 stride 32 那层就要谨慎评估。反过来如果输入分辨率被压得很低比如 96×96 的分类任务最深的 stage 特征图只有 3×3SE 的收益会比你预期的小很多。4.3 降维比 r 引发的信息瓶颈前面提过一句这里展开。r 的作用是控制瓶颈层的宽度但它同时控制了能表达多少种通道重要性模式。当 C/r 太小时网络只能用极少的维度来描述通道重要性遇到需要精细区分的场景就会力不从心。有个很直观的验证方法把训练好的 SE 权重 W1 拿出来做 SVD看前几个奇异值占比。如果前两三个奇异值就占了 90% 以上说明这个瓶颈已经严重压缩网络只能表达少数几种通道重要性模式。我在一个 200 类细粒度分类任务上做过这个分析r16 时前 3 个奇异值占 93%把 r 降到 4 之后降到 78%对应的 top-1 提升了 0.9 个点。当然代价是 SE 参数量翻了 4 倍。对于通道数很小的浅层C64 或 128建议直接把 r 设成 8 甚至 4或者干脆不用瓶颈用一个 C×C 的矩阵。小通道数下 C×C 矩阵的参数量本来就不大C64 时只有 4096 个参数完全可以承受。4.4 迁移学习和权重重初始化的顺序问题这个坑比较隐蔽。当你拿一个预训练的普通 ResNet-50往里面插 SE 模块然后在小数据集上微调时如果 SE 分支用默认初始化训练前几百个 step 的 loss 会有个明显尖峰有时候直接把学习率预热阶段的动力学搞乱。原因是预训练权重的特征图尺度是经过精心校准的插入一个输出期望在 0.5 附近的 SE等于给每一层都加了一个 0.5 倍的缩放后面所有层的输入分布都偏了。三个处理方式按推荐程度排序直接用官方预训练的 SE-ResNet 权重。torchvision 里有se_resnet50这个结构吗其实没有但很多第三方库比如 timm提供了完整的 SE-ResNet 预训练权重直接拿来微调最省事。如果必须自己插把 SE 的第二个卷积权重清零、bias 设成 2.0前面代码里的做法让初始化时 SE 接近恒等映射再配合 500 到 1000 step 的 warmup。前几个 epoch 冻结 SE 分支把它的学习率设成 0 或者非常小等主干稳定后再解冻联合训练。4.5 量化部署时的动态范围问题SE 里最难量化的是 Sigmoid。它的输出集中在 0 到 1 之间而且是高度非均匀分布——大多数值挤在 0.3 到 0.7 这个窄区间里。做 INT8 量化的时候如果按 0 到 1 的满量程去分 256 个桶实际用到的可能只有中间 100 来个桶量化误差一下就上来了。我在 TensorRT 上做过对比测试整网 INT8 量化的情况下如果 SE 部分也强制 INT8ImageNet 上的 top-1 会掉 1.2 到 1.8 个点。把 SE 内部的两个 1×1 卷积和 Sigmoid 保留成 FP16其余部分仍然 INT8掉点能控制在 0.3 个点以内推理速度基本没有损失——因为 SE 的计算量本来就小保留 FP16 不会成为瓶颈。另一个做法是把 Sigmoid 换成 Hard-Sigmoidhard_sigmoid(x) clamp(x/6 0.5, 0, 1)这样整个 SE 只包含加减乘和 clamp量化友好度大幅提升。MobileNetV3 用的就是这个方案它在移动端的量化部署里表现比标准 Sigmoid 稳定得多。代价是表达能力稍有下降在 ImageNet 这种大任务上大约损失 0.2 个点但在移动端场景里这是划算的。5. 通道注意力家族的横向对比与选型判断5.1 从 SE 到 ECA一条减法的演化路线SE 之后通道注意力这个方向基本沿着两条线走一条是加更多维度一条是减掉冗余计算。ECAEfficient Channel Attention是减法路线的代表。它指出 SE 的降维操作其实是有害的——降维破坏了通道和权重之间的直接对应关系。ECA 的做法是去掉两个全连接层用一个长度为 k 的一维卷积直接在 GAP 输出上做跨通道交互class ECABlock(nn.Module): def __init__(self, channels, k3): super().__init__() self.pool nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizek, paddingk // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): y self.pool(x) # N, C, 1, 1 y y.squeeze(-1).transpose(-1, -2) # N, 1, C y self.sigmoid(self.conv(y)) y y.transpose(-1, -2).unsqueeze(-1) # N, C, 1, 1 return x * yECA 的参数量几乎为零k 通常取 3 或 5只有几个参数效果却能追平甚至略超 SE。我在几个移动端项目里做过替换测试把 SE 换成 ECA精度基本持平差距在 0.1 个点内参数量少了 250 万推理延迟在 CPU 上降低约 6%。所以现在做移动端我基本默认用 ECA 而不是 SE。5.2 CBAM 与 CA把空间信息也拉进来CBAM 在通道注意力之外并联了一个空间注意力分支先把通道维压成 2 个一个平均、一个最大再过一个 7×7 卷积得到一张 H×W 的权重图。它的优势是能定位哪里重要在检测和分割任务里比纯通道注意力更有用。代价是额外计算量与 H×W 相关高分辨率输入下开销不可忽视。CACoordinate Attention的思路更巧妙它不做全局平均而是分别沿水平方向和垂直方向做池化得到两个一维向量长度分别是 W 和 H分别编码后再合并。这样做的好处是把位置信息嵌进了通道注意力里同时计算量只跟 HW 有关比 CBAM 的空间分支省得多。在移动端分割任务上CA 的表现通常优于 SE 和 CBAM。方法注意力维度额外参数额外计算适合场景SE通道约 2C²/r与 C² 相关占比很小分类骨干、通道数大的深层ECA通道约 k 个几乎为零移动端、轻量模型CBAM通道 空间与 C² 和 H×W 都相关中等偏高检测、分割CA通道 位置中等与 HW 相关移动端分割、密集预测多头自注意力空间 通道与 C² 相关且较大与 H²W² 相关大模型、长程依赖建模5.3 多头自注意力和 SE 到底差在哪这两个东西经常被混为一谈因为都叫注意力但实际上从问题设定就不一样。SE 的注意力是逐通道的一维权重它对所有空间位置一视同仁——如果某个通道被判定为重要那么整张图上这个通道的所有位置都被同等放大。它建模的是通道之间的关系。多头自注意力作用在序列上每个位置都会生成自己的 Query、Key、Value然后计算所有位置两两之间的相似度得到一张 L×L 的注意力矩阵L 是序列长度。它建模的是任意两个位置之间的关系是一种内容寻址的全局交互。从计算复杂度看SE 是 O(C²)跟空间尺寸无关自注意力是 O(L²)随序列长度平方增长。这就是为什么 ViT 在高分辨率输入下需要窗口注意力、线性注意力这类变体来降复杂度而 SE 从来不需要考虑这个问题。一个实用的判断法如果你的瓶颈在于通道之间的分工没有被充分利用用 SE如果瓶颈在于模型需要看到长距离的空间依赖那得上自注意力。两者不冲突很多混合架构比如 CoAtNet 那一类就是卷积 SE 注意力的组合。5.4 时序任务里的通道注意力变体SE 的原始设计针对二维特征图但在时序建模里把 (C, T) 当作 (C, H) 来用Squeeze 换成沿时间轴的全局平均Excitation 照旧一样能用。我做过一个传感器信号分类的项目输入是 12 通道的时序信号窗口长度 512。直接套二维 SE把时间轴当空间轴能带来约 1.2 个点的准确率提升尤其是当某些通道在特定工况下完全无响应时SE 能自动把它压下去。这里有个容易忽略的点时序数据的通道数通常很小8 到 32远小于图像里的几百上千所以 r 一定要设小或者干脆不要瓶颈。我用的是 C12、r2 的配置中间层还有 6 个神经元勉强够用。如果照搬图像里的 r16中间层就是 0直接退化成无效模块。6. 在真实项目里把 SE 用出效果的一些具体经验6.1 什么任务加 SE 收益最大按我自己的项目经验排个序收益从高到低多通道传感器数据融合排第一。当输入有多个来源比如摄像头、雷达、红外或者多路传感器时各通道的信噪比差异很大SE 能自动降权噪声通道效果通常比图像分类里还明显。我做过一个三路传感器融合的分类任务加 SE 之后准确率提升 2.3 个点。细粒度分类排第二。通道分工越细的任务SE 的价值越大。之前说的那个 200 类细粒度项目r4 的 SE 带来了 1.8 个点的提升。通用图像分类排第三典型提升 0.5 到 1.5 个点任务越难类别越多、类间差异越小提升越明显。目标检测和分割要看具体结构。加在 backbone 上通常有小幅提升加在 FPN 或者检测头上收益不稳定有时候会掉点因为检测任务对空间位置敏感纯通道注意力可能干扰定位精度。这时候应该考虑 CBAM 或者 CA。已经在用大模型或者强增强的任务收益最小。如果你的骨干已经是 EfficientNet 或者 ConvNeXt 这类本身带注意力设计的架构再叠一层 SE 经常是负收益——重复的注意力机制会互相干扰。6.2 推理延迟的真实开销和几条优化经验纸上说的 FLOPs 增加 0.5%不代表推理时间只增加 0.5%。SE 里有个 GAP它是典型的 reduce 操作在很多推理框架里延迟并不低因为需要跨线程同步。我在几个平台上实测过 ResNet-50 加 SE 的延迟变化平台输入分辨率无 SE 延迟有 SE 延迟增幅服务器 GPU (FP16)224×2243.1 ms3.4 ms9.7%桌面 CPU 单线程224×22448 ms61 ms27%移动端 NPU224×2248.2 ms8.9 ms8.5%CPU 上的增幅明显偏高主要就是 GAP 和 sigmoid 的开销。优化方向有这么几个一是合并 GAP 的实现。有些框架里 AdaptiveAvgPool2d(1) 会走一条通用但慢的路径换成显式的 mean 操作比如x.mean(dim(2,3), keepdimTrue)在某些版本上能快 20%。二是把两个 1×1 卷积合并成一个。如果你不介意放弃非线性可以把 W2·W1 预乘成一个矩阵推理时只做一次矩阵乘。代价是失去 ReLU 的非线性精度会掉一点但在极端延迟敏感的场景下值得一试。三是只在部分 stage 加 SE。实测下来只在 layer3 和 layer4 加 SE能保住大约 80% 的精度收益参数量和延迟开销减半。这是个很实用的折中方案。6.3 几个调参的土办法最后分享几条我从项目里攒下来的经验都是文档里不会写的。看 SE 权重的分布判断模块是否在工作。训练完之后把所有 batch 的 SE 输出收集起来看直方图。如果所有值都挤在 0.5 附近说明这个 SE 基本没学到东西可能是 r 太大或者学习率不合适。如果分布很分散比如在 0.1 到 0.9 之间说明它在积极工作。我在一个项目里就是靠这个发现 layer1 的 SE 完全没起作用直接把那层去掉省了参数还涨了 0.1 个点。用 Grad-CAM 对比加 SE 前后的热力图。如果加了 SE 之后热力图反而更分散了说明通道权重的调整干扰了空间定位这在检测任务里是危险信号。微调时给 SE 单独设学习率。SE 是从零初始化的而主干是预训练的两者需要的更新幅度不一样。我通常给 SE 分支设 10 倍于主干的学习率让它快速适应通常两三个 epoch 就能收敛到合理值。当然那个 bias2.0 的初始化能让这个需求不那么强烈。别在 stride 太大的层上用大 r。前面提过空间尺寸的问题这里补一句stride 32 那层特征图只有 7×7GAP 对整张图的表示已经很粗了此时用 r16 会导致瓶颈层非常小表达能力受限。我的做法是在最深层用 r8浅层用 r16效果比统一 r 好一些。如果你在复现论文注意 SE 是加在所有 bottleneck 上还是只加部分。有些实现为了省事只在每个 stage 的第一个 block 加性能会差 0.3 到 0.5 个点。做对比实验的时候这点差异足以让你得出错误结论。最后别迷信 SE。它是一个性价比很高的模块但不是万能药。当你的模型已经很强、数据量已经很大、任务本身对通道重要性不敏感时SE 带来的收益可能还不如调一下数据增强策略。我现在的习惯是先不加任何注意力模块把 baseline 训到一个稳定的水平再逐个尝试 SE、ECA、CA用消融实验说话而不是一上来就堆模块。
返回列表