ARTICLE DETAIL

资讯详情

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

CNN注意力机制:SE、ECA、CBAM的PyTorch实现与ResNet实战

CNN注意力机制:SE、ECA、CBAM的PyTorch实现与ResNet实战 做图像分类的模型跑不动、涨点慢的时候我第一个想到的补丁往往不是换 backbone而是往卷积块里塞一个轻量的注意力模块。CNN 的注意力机制这几年已经攒下了一大批成熟实现SE、ECA、CBAM 是最常用的三个代码短、参数少、几乎不用改训练流程插进 ResNet、MobileNet 甚至 YOLO 的 backbone 里很多时候都能拿到零点几个点到一两个点的提升。这篇就把这三个模块从头到尾拆一遍原理讲透PyTorch 代码给全包括怎么插进 ResNet 的 Bottleneck、怎么核算多出来的参数量、以及我在实际训练里踩过的那些坑。如果你刚搭好 PyTorch 环境能跑通一个 ResNet 训练脚本那这篇内容可以直接抄作业如果你已经用过注意力模块但总觉得加了没效果后面关于插入位置和超参的部分应该更对你有用。1. 卷积到底缺了什么注意力机制要补的短板先别急着看代码。理解一个模块为什么这么设计比记住它的实现更重要因为真正决定涨不涨点的是你把它插在哪、为了补什么缺陷而不是模块本身。1.1 局部性与固定权重的先天限制卷积操作有两个刻在骨子里的性质局部连接和平移等变性。一个 3×3 卷积核只看得到 3×3 邻域靠层层堆叠来扩大感受野同时同一个卷积核在整张特征图上滑动时权重完全共享也就是说不管这个位置是猫的耳朵还是背景的树叶网络用的都是同一套权重。这套设计非常高效参数量小、泛化性好是 CNN 能在图像任务上碾压全连接网络的根本原因。但它的短板也很明显权重一旦训练完就固定了推理时不会根据输入内容做任何调整。一张图里真正有用的信息可能只集中在少数通道和少数空间位置上其余大部分计算其实是在处理背景噪声。理想情况下网络应该有能力看一眼当前这张图再决定把注意力放在哪几个通道、哪几块区域上。注意力机制干的就是这件事。它的核心动作叫特征重标定feature recalibration先对特征做一个全局统计得到一组描述当前输入里什么重要的系数再把这组系数乘回原特征上。这个过程是数据驱动的同一层卷积在不同图片上会产生不同的注意力权重这就补上了卷积权重固定的短板。你可以把它理解成给每个通道、每个位置动态调音量——该响的地方调大背景噪声压下去。1.2 通道与空间的两个维度注意力该加在哪特征图的形状是(B, C, H, W)注意力可以从三个维度下手通道维哪个通道重要、空间维哪个位置重要、以及两者结合。目前主流的做法基本都在通道和空间这两个维度上做文章原因也很直白通道本质上代表某种语义特征的检测器空间代表这个特征出现在哪里这两者对分类、检测任务的影响最大。把三个模块放在一起对比你会发现它们的差异其实就在怎么算权重和算哪个维度上模块注意力维度权重的计算方式额外参数量ResNet-50主要特点SE通道全局平均池化 两层 FC 降维升维 Sigmoid约 2.5M10%结构简单、通用性强是后续大量模块的模板ECA通道全局平均池化 一维卷积无降维几千个几乎可忽略去掉降维用自适应卷积核轻量且效果不弱于 SECBAM通道 空间通道用平均池化最大池化共享 MLP空间用 7×7 卷积约 1.5M~2M串行组合通道在前空间在后通用性最好表格里的参数量是按 ResNet-50 整体估算的具体数字和你的实现细节有关下面第 3 章我会把计算过程完整推一遍你可以照着算自己模型的情况。这里只需要先建立一个直觉SE 最重ECA 最轻CBAM 介于中间但覆盖了两个维度。还有一点值得提前说明这三个模块都属于即插即用的 plugin不改变特征图的尺寸和通道数所以可以塞进任何现成网络的任何位置不需要改后续结构。这是它们能被大量工程直接采用的关键原因——改动成本几乎为零收益却相对确定。2. SE、ECA、CBAM 三个模块的原理与设计取舍三个模块出自三篇论文思路有继承关系SE 提出通道注意力的基本范式ECA 指出 SE 里降维这一步其实有害CBAM 则把注意力扩展到空间维度。按这个顺序看设计动机就很清楚了。2.1 SE压缩-激励通道重标定的开山之作SE 模块Squeeze-and-Excitation把整个流程拆成三步命名就来自前两步。第一步是 Squeeze压缩。对每个通道做全局平均池化把(B, C, H, W)压成(B, C, 1, 1)。这一步的作用是让每个通道拿到一个能代表自己整体活跃程度的标量。为什么要用全局平均池化而不是别的因为它把空间信息做了加权平均得到的数值对图像里目标的位置不敏感——不管猫在左上角还是右下角只要这个通道在响应猫池化后的值就大。这种位置无关性正是通道注意力需要的。第二步是 Excitation激励。把C维向量过两层全连接第一层把维度降到C/rr 是 reduction ratio默认 16过 ReLU第二层再升回C最后过 Sigmoid 得到 0~1 之间的权重。写成公式是s sigmoid(W2 * ReLU(W1 * z))这里的降维-升维结构是个瓶颈bottleneck它做了两件事一是限制参数量二是引入非线性让模块能学到通道之间的组合关系而不是简单地按通道自身的均值排序。ReLU 保证了权重是非负的稀疏表达Sigmoid 则负责把输出压到 0~1方便当作乘性系数。第三步是 Reweight重标定。把(B, C, 1, 1)的权重广播乘回原特征图。注意这里用的是乘法而不是加法或者拼接好处是不改变特征的数值尺度也不增加通道数后续层的接口完全不用动。有两个实现细节新手经常忽略。一个是两层 FC 都不加 bias因为后面紧跟着 BN 和归一化性质的 Sigmoid加 bias 属于冗余参数原论文实现里也是biasFalse。另一个是 ReLU 要用inplaceTrue省一点显存不过在需要梯度检查或者做可视化的时候要小心inplace 操作会覆盖输入张量可能影响反向传播中间变量的读取。SE 的效果在原始报告里是很扎实的ResNet-50 在 ImageNet 上 top-1 提升大致在 1 个点上下代价是参数量增加约 10%、计算量几乎不变因为池化和 FC 的开销相对卷积可以忽略。它的局限也很明显两次全连接引入了2*C*C/r的参数量通道数大的时候比如 2048这部分开销不小而且降维操作会损失一部分通道信息。2.2 ECA把降维全连接换成自适应一维卷积ECAEfficient Channel Attention的核心洞察很有意思作者做了一组对比实验发现 SE 里那个降维步骤对最终的通道注意力效果是负面的。维数降下去再升回来虽然省了参数但把通道之间的部分关联信息也一并压掉了。于是 ECA 的做法是干脆不降维直接在通道维度上做一维卷积。流程是这样全局平均池化得到(B, C, 1, 1)reshape 成(B, 1, C)用一个长度为k的一维卷积在这条通道序列上滑动输出还是(B, 1, C)再过 Sigmoid最后乘回原特征。每个通道的权重由它自己加上左右各(k-1)/2个邻居通道共同决定——这就是所谓的局部跨通道交互。关键问题是k取多少。ECA 没有把它设成固定值而是设计了一个和通道数挂钩的自适应公式k | (log2(C) b) / gamma |_odd其中gamma 2、b 1| |_odd表示取最近的奇数。用 ResNet-50 的各个 stage 算一下就很直观了。stage2 输出通道 C256log2(256)8(81)/24.5取整为 4 是偶数向上取到 5所以 k5stage3 的 C512log29(91)/25本身就是奇数k5stage4 的 C1024log210(101)/25.5取整 5k5stage5 的 C2048log211(111)/26偶数取 7k7。看出来了吗通道数从 256 涨到 2048卷积核只从 5 变到 7。这就是 ECA 的聪明之处高层语义的通道数翻了好几倍但真正需要交互的邻居通道数并不需要等比例增长用对数关系描述足够了。整个模块的参数量就是k个ResNet-50 上总共几千个参数和 SE 的两百多万完全不在一个量级。注意实现一维卷积时padding必须设为kernel_size // 2这样输出长度才等于输入长度。由于 k 一定取奇数这个整除是精确的不会出现长度对不齐的问题。另外卷积核要用biasFalse因为后面接 Sigmoid偏置项没有意义。2.3 CBAM通道与空间串行组合为什么不是并行CBAMConvolutional Block Attention Module把注意力拆成两个子模块串行处理先通道注意力再空间注意力。通道注意力部分和 SE 长得像但有个重要区别SE 只用平均池化CBAM 同时用平均池化和最大池化两者各自过一个共享权重的 MLP两层 1×1 卷积先降维到C/r再升回来结果相加后过 Sigmoid。用最大池化的理由很实在平均池化会把特征图上的显著响应平均掉如果某个通道只有一小块区域有强响应平均之后这个通道的权重会被拉低最大池化能把这个峰值保留下来两种统计量互补。空间注意力部分则换了思路在通道维度上做平均池化和最大池化各得到一张(B, 1, H, W)的单通道图把两张图拼接成(B, 2, H, W)再用一个 7×7 卷积把 2 通道压成 1 通道过 Sigmoid得到每个空间位置的权重图广播乘回原特征。这里用 7×7 而不是 3×3是因为空间注意力需要看到足够大的邻域才能判断这块区域是不是目标感受野太小会退化成逐像素的局部操作。**为什么串行而不是并行**这个问题我在读论文时也纠结过。原论文做过消融串行通道在前、空间在后的效果最好。我的理解是通道注意力做的是选哪类特征的粗筛它会先改变每个通道的整体强度空间注意力在这个基础上再问这个特征图上哪块位置值得保留。反过来先做空间再做通道相当于在还没筛选语义的情况下先划位置两个阶段的判断会互相干扰。另外通道注意力输出仍是(B, C, H, W)可以直接喂给空间注意力接口天然对齐不需要做任何 reshape。CBAM 的参数量主要在通道注意力的 MLP 上。由于用的是 1×1 卷积实现且两层共享权重avg 和 max 走同一个 MLP参数量约等于2*C*C/r和 SE 同量级但略小因为它的空间部分只有 49 个参数7×7×2→1biasFalse。3. PyTorch 完整实现与逐行注释原理讲完接下来是可运行的代码。我把三个模块都写成独立类接口统一为输入输出同形状这样你可以在任何地方插拔。所有代码都在 PyTorch 1.x/2.x 上实测通过不需要额外依赖。3.1 SEBlock 实现import torch import torch.nn as nn class SEBlock(nn.Module): Squeeze-and-Excitation 通道注意力 def __init__(self, channels, reduction16): super().__init__() # 全局平均池化把 (B, C, H, W) 压成 (B, C, 1, 1) self.avg_pool nn.AdaptiveAvgPool2d(1) # 瓶颈结构C - C/r - C self.fc nn.Sequential( nn.Linear(channels, channels // reduction, biasFalse), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels, biasFalse), nn.Sigmoid() ) def forward(self, x): b, c, _, _ x.size() # Squeeze y self.avg_pool(x).view(b, c) # Excitation y self.fc(y).view(b, c, 1, 1) # Reweightexpand_as 保证广播形状和输入一致 return x * y.expand_as(x)用Linear而不是Conv2d(1x1)实现全连接是因为AdaptiveAvgPool2d(1)之后特征图已经退化成一个向量用Linear语义更清楚、写法更短。expand_as是个小技巧它在(B, C, 1, 1)和(B, C, H, W)之间做广播乘法不会真的复制内存比手写repeat更省。reduction参数默认 16这是原论文在 ResNet 上搜索出来的结果。如果你用的是 MobileNet 这类通道数本来就很少的网络比如某个 block 只有 16 个通道那16 // 16 1中间层只有一个神经元信息几乎被压没了这时候应该把 reduction 调成 4 或者 8。这是个很常见的坑后面还会提到。3.2 ECABlock 实现import math class ECABlock(nn.Module): Efficient Channel Attention用一维卷积做局部跨通道交互 def __init__(self, channels, gamma2, b1): super().__init__() # 自适应卷积核大小 t int(abs((math.log(channels, 2) b) / gamma)) k t if t % 2 else t 1 self.k k self.avg_pool nn.AdaptiveAvgPool2d(1) self.conv nn.Conv1d(1, 1, kernel_sizek, paddingk // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): # (B, C, H, W) - (B, C, 1, 1) y self.avg_pool(x) # (B, C, 1, 1) - (B, 1, C)把通道当成序列长度 y y.squeeze(-1).transpose(-1, -2) # 一维卷积(B, 1, C) - (B, 1, C) y self.conv(y) # 变回 (B, C, 1, 1) y y.transpose(-1, -2).unsqueeze(-1) return x * self.sigmoid(y)这里transpose(-1, -2)配合squeeze(-1)完成形状变换比view更安全因为它在处理非连续张量时不会报错。有几个细节必须留心int()是截断取整不是四舍五入当(log2(C)b)/gamma落在 4.5 这种位置时int(4.5)得到 4然后因为 4 是偶数再加 1 变成 5。这个行为和论文公式里的取整方式一致。paddingk // 2保证了输出长度不变。由于 k 必然是奇数k//2在二维意义上是对称 padding两端正好对齐。卷积核的值在训练前是随机初始化的代码里没有对它做特殊初始化。原论文是这样做的不必额外处理。关于k的一个实用建议如果你的通道数在训练过程中会有变化或者你打算把这个模块塞进一个通道数很小的网络最好在__init__里打印一下算出来的k确认它是合理的奇数一般落在 3~9 之间。我见过有人手抖把gamma写成 1结果 k 变成 11 甚至更大一维卷积核长过通道数padding 之后就相当于在常数 0 上做卷积输出完全失真。3.3 CBAM 实现CBAM 拆成两个类写会更清晰便于单独使用或者替换其中一个子模块。class ChannelAttention(nn.Module): CBAM 的通道注意力分支平均池化 最大池化共享 MLP def __init__(self, channels, reduction16): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) # 用 1x1 卷积实现 MLP方便在两个分支间共享权重 self.mlp nn.Sequential( nn.Conv2d(channels, channels // reduction, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1, biasFalse) ) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out self.mlp(self.avg_pool(x)) max_out self.mlp(self.max_pool(x)) return x * self.sigmoid(avg_out max_out) class SpatialAttention(nn.Module): CBAM 的空间注意力分支通道维池化 7x7 卷积 def __init__(self, kernel_size7): super().__init__() assert kernel_size in (3, 7), kernel_size 建议取 3 或 7 self.conv nn.Conv2d(2, 1, kernel_size, paddingkernel_size // 2, biasFalse) self.sigmoid nn.Sigmoid() def forward(self, x): avg_out torch.mean(x, dim1, keepdimTrue) # (B,1,H,W) max_out, _ torch.max(x, dim1, keepdimTrue) # (B,1,H,W) y torch.cat([avg_out, max_out], dim1) # (B,2,H,W) return x * self.sigmoid(self.conv(y)) class CBAM(nn.Module): 通道注意力在前空间注意力在后串行组合 def __init__(self, channels, reduction16, kernel_size7): super().__init__() self.ca ChannelAttention(channels, reduction) self.sa SpatialAttention(kernel_size) def forward(self, x): x self.ca(x) x self.sa(x) return x几点说明。第一通道注意力里 MLP 用Conv2d而不是Linear实现好处是输入输出都是 4D 张量avg 分支和 max 分支直接复用同一个self.mlp对象权重天然共享不用手动做参数绑定。第二torch.max返回的是(values, indices)的元组只取第一个值所以必须写max_out, _ ...这个细节写错的话会得到一个 tuple后面cat直接报类型错误。第三空间注意力用torch.mean和torch.max在dim1上做池化注意keepdimTrue否则维度会被压掉cat的时候对不上。kernel_size参数我加了个断言限制只能取 3 或 7。理论上任意奇数都能跑但论文里只做过这两个的消融7 的效果略好。如果你在检测任务的小目标分支上用可以试试 3感受野小一些可能对小目标更友好这算是我自己做检测时的一个经验。3.4 插进 ResNet Bottleneck位置、代码与参数量核算模块写好了接下来是最关键的一步插在哪。以 ResNet-50 的 Bottleneck 为例它的结构是1x1 降维 - 3x3 卷积 - 1x1 升维然后加残差。注意力模块应该加在升维之后、残差相加之前。原因有两层。一是通道数问题SE 和 CBAM 都需要知道通道数来构建 FC如果在 1x1 升维之前插通道数是planes升维后是planes*4两次插就没意义了放在最后通道数是确定的planes*4。二是残差结构的语义残差分支输出的是要叠加到主干上的增量在这个增量进入加法之前做重标定等于是在告诉网络这一层的这 4 倍通道里哪部分对当前样本更重要然后再和恒等映射相加。如果加在加法之后重标定就会同时作用到恒等路径上破坏了残差无阻碍直连的设计初衷。class Bottleneck(nn.Module): expansion 4 def __init__(self, inplanes, planes, stride1, downsampleNone, norm_layerNone, attnse, reduction16): super().__init__() if norm_layer is None: norm_layer nn.BatchNorm2d width planes self.conv1 nn.Conv2d(inplanes, width, 1, biasFalse) self.bn1 norm_layer(width) self.conv2 nn.Conv2d(width, width, 3, stridestride, padding1, biasFalse) self.bn2 norm_layer(width) self.conv3 nn.Conv2d(width, planes * self.expansion, 1, biasFalse) self.bn3 norm_layer(planes * self.expansion) self.relu nn.ReLU(inplaceTrue) self.downsample downsample self.stride stride out_ch planes * self.expansion if attn se: self.attn SEBlock(out_ch, reduction) elif attn eca: self.attn ECABlock(out_ch) elif attn cbam: self.attn CBAM(out_ch, reduction) else: self.attn nn.Identity() # 关闭注意力做消融对比用 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.attn(out) # 注意力在残差相加之前 if self.downsample is not None: identity self.downsample(x) out identity return self.relu(out)这个attn参数设计成字符串开关好处是同一份代码可以通过改一个参数在四种配置之间切换做消融实验时特别省事。nn.Identity()是个空操作直接返回输入等价于不加注意力用它做基线最干净。接下来算参数量这是很多人关心的实际问题。ResNet-50 的四个 stage 输出通道分别是 256、512、1024、2048每个 stage 的 block 数分别是 3、4、6、3。以 SE 为例单个 block 的额外参数是两层 FCC - C/16 - C都无 bias所以是2*C*(C/16) C²/8。逐 stage 计算Stage输出通道 Cblock 数单 block 额外参数该 stage 合计stage22563256² / 8 819224576stage35124512² / 8 32768131072stage4102461024² / 8 131072786432stage5204832048² / 8 5242881572864加起来约 2.51M而 ResNet-50 本身约 25.6M所以增幅接近 10%。FLOPs 几乎没变化因为全局池化和 FC 的计算量相对卷积可以忽略。这个参数量涨 10%、精度涨 1 个点的性价比在当年是相当划算的。ECA 就没什么好算的了每个 block 的额外参数只有k个k 取值 5 或 716 个 block 加起来不到 100 个参数。CBAM 的通道分支约2*C*C/16和 SE 同量级空间分支固定 98 个参数7×7×2无 bias整体比 SE 略小。提示如果你要做严格的对比实验记得把基线也重新训练一遍。注意力模块带来的提升有时候和训练轮数、数据增强策略有交互直接拿论文里的基线数字对比自己的实验结论很容易失真。4. 训练实测、超参选择与排错清单代码能跑通只是第一步真正决定效果的是插入位置、超参和训练策略。这部分我按自己踩坑的顺序来讲。4.1 模块怎么选、插在哪一层先说选型。如果你不确定用哪个我的建议顺序是先试 ECA再试 SE最后试 CBAM。理由很实际ECA 几乎不增加参数和显存对训练稳定性影响最小能快速验证这个任务到底吃不吃注意力如果 ECA 有提升说明通道维度确实存在冗余再换 SE 看能不能拿到更多如果通道注意力效果一般但你的任务是检测、分割这类对位置敏感的任务那 CBAM 的空间分支值得一试。再说插入位置。这里有个反直觉的经验不是插得越多越好。SE 原论文其实做过对比只在每个 stage 的最后一个 block 插入或者全部 block 都插入两者差距不大但全部插入的参数量和显存占用明显更高。我在自己的数据集上做过一轮消融结论是插入策略相对提升我的实验观察显存增幅建议每个 stage 全部 block 插入基准最大参数量敏感时不用每个 stage 只插最后一个 block与全部插入接近明显更小推荐默认方案只插 stage3 和 stage4略低于全插最小算力紧张时的折中加在残差相加之后提升不稳定有时掉点相同不推荐高层stage4、stage5的通道数大、语义强注意力能发挥的空间也大低层stage2的特征还比较底层加了注意力收益有限反而增加参数量。所以预算有限时优先保证高层。最后提醒一点关于 reduction ratio 的设置。如果某个 block 的通道数是 256256/1616中间层 16 维是合理的但如果你的网络比较轻量某个 block 输出只有 32 通道32/162中间层只剩 2 维信息压缩过度效果可能反而变差。这种情况下把 reduction 改成 4中间层 8 维或者 8中间层 4 维更稳妥。判断标准很简单中间层维度不要低于 8。4.2 常见问题速查表下面这些坑我在不同项目里基本都踩过一遍整理成表方便你对照排查。现象可能原因排查与解决训练 loss 震荡验证集不涨注意力模块的 Sigmoid 输出乘回特征后某些通道权重趋近 0梯度被抑制检查 reduction 是否过大尝试把模块只加在部分 block降低初始学习率形状报错 size mismatchSE/CBAM 的通道数和实际特征图通道不一致打印x.shape确认通道注意planes*expansion而不是planesECA 的 k 算出来是偶数int()截断后没做奇偶修正确认写了k t if t % 2 else t 1训练变慢明显、显存爆掉每个 block 都插了 CBAM空间分支会额外产生(B,2,H,W)中间张量只在 stage4/stage5 插入或者改用 ECA加了注意力反而不如基线插入位置在残差相加之后或 reduction 过大移到相加之前把 reduction 从 16 调到 8推理速度下降很多CBAM 的 7×7 卷积在高分辨率特征图上开销大把空间分支的 kernel_size 改成 3或者只在低分辨率阶段加 CBAM多卡训练时精度异常用了inplaceTrue的 ReLU 配合某些版本的 DDP调试阶段先把inplace关掉定位问题后再打开关于第一行loss 震荡这个现象多说两句。SE 的 Sigmoid 输出在极端情况下会非常接近 0这时候对应的通道在前向传播里几乎被关掉反向传播时梯度也接近 0。训练早期权重是随机的如果某一批数据让某些通道被判为不重要这些通道就学不动了表现出来就是 loss 忽上忽下。我的处理方式是把注意力模块的输出和原特征做一个带系数的加权也就是out x * (1 alpha * attn)用一个小的alpha比如 0.1做残差式的注意力。这样即使注意力全为 0也不至于把通道直接关死。这个方法不是标准做法属于工程上的小修补但在小数据集上确实更稳。4.3 我自己的几条实操心得实验做多了有些经验是文档里不会写的这里一并交代。第一注意力的收益和数据规模强相关。在小数据集比如几万张上SE 带来的提升可能只有零点几个点甚至不显著而且容易过拟合——因为注意力模块本身也是要学的参数。数据量足够大十万张以上时它的价值才比较明显。如果你手上数据不多我建议先用 ECA 这种参数极少的模块试水别一上来就上 SE 或者 CBAM。第二先验证基线再加模块。我见过太多人拿一个没调好的基线去加注意力结果模块成了背锅侠。正确的做法是先把基线训练到收敛、确认超参合理然后只改动一处加注意力其他条件完全不变这样才能得到干净的对比结论。如果一次改了三四个地方涨了不知道是谁的功劳掉了也不知道该回退哪个。第三可视化注意力权重来判断模块是否真的在起作用。方法很直接在 forward 里把 SE 的y或者 CBAM 的通道权重存下来对验证集的一批图片做平均画成柱状图看分布。如果所有通道的权重都挤在 0.5 附近说明模块没学到东西基本等价于恒等映射如果分布明显分化有高有低说明它确实在做筛选。空间注意力可以上采样成热力图叠加到原图上看它关注的区域是否落在目标上。这个检查花不了十分钟但能省下几轮无效训练。第四注意 BN 和注意力的顺序。在 Bottleneck 里注意力模块接在bn3之后、残差相加之前也就是说它作用在已经归一化过的特征上。如果在 BN 之前插入特征的数值尺度还没统一全局池化的统计量会受到 batch 内样本差异的影响效果不稳定。这个顺序尽量不要改。第五迁移学习场景下要留意预训练权重。如果你加载的是官方 ResNet 的预训练权重插入注意力模块后新增的层是随机初始化的其余层是预训练的。这时候不要立刻用大学习率全局微调容易把预训练特征冲掉。我的做法是分两段训练先冻结 backbone 只训练新增的注意力层和分类头几个 epoch再解冻全局用小学习率微调。这样收敛更稳最终精度也更高一些。最后再补一个关于推理部署的提醒。ECA 用了一维卷积CBAM 用了 7×7 卷积SE 用了一维池化加 FC这些算子在常见推理框架里的支持情况不一样。导出的时候建议先跑一遍计算图确认没有算子被拆成不支持的组合如果某个分支导出失败可以把 SE 的全连接换成等价的 1×1 卷积两者在数学上完全等价但卷积算子的兼容性通常更好。我在实际使用中的体会是注意力模块本身并不神秘它就是个参数很少的小网络学的是一组动态权重。真正花时间的地方在于选哪个模块、插在哪、reduction 和 kernel_size 怎么定、以及怎么设计一组能说明问题的对比实验。把这几件事做扎实比反复换模块带来的收益大得多。
返回列表