SE-NET与ResNet融合实战:如何通过注意力机制提升图像分类性能(附代码)

张开发
2026/4/17 4:50:20 15 分钟阅读

分享文章

SE-NET与ResNet融合实战:如何通过注意力机制提升图像分类性能(附代码)
1. 从ResNet到SE-NET为什么需要注意力机制我第一次用ResNet做图像分类时发现一个奇怪现象模型对某些类别的识别准确率总是偏低。比如在花卉分类任务中模型经常把白色雏菊误判为白色玫瑰。后来才发现这是因为ResNet对所有通道特征一视同仁而实际上花瓣纹理等关键特征通道应该获得更多关注。这就是**SE-NETSqueeze-and-Excitation Network**要解决的问题。它通过一个聪明的注意力开关让网络自动学会哪些特征通道更重要。想象你在看一幅画时眼睛会不自觉聚焦在关键细节上——SE块就是让神经网络具备这种能力。传统卷积有个固有缺陷当它在局部感受野计算时会平等对待所有通道的特征。就像用相同力度听交响乐中所有乐器的声音反而让主旋律被淹没。SE-NET通过两个精妙步骤解决这个问题挤压Squeeze把全局空间信息压缩成通道描述符相当于先听清整个乐章的旋律轮廓激励Excitation用全连接层学习通道间关系就像指挥家决定哪些乐器组需要加强实测在CIFAR-10数据集上加入SE块的ResNet-34错误率从7.8%降到6.2%而且参数量仅增加约3%。这种小投入大回报的特性让SE模块成为计算机视觉领域的经典设计。2. SE块解剖三明治结构的智慧2.1 挤压层的全局视野很多教程把全局平均池化GAP简单带过其实这里有精妙设计。假设我们有个512×7×7的特征图GAP不是简单取平均值而是让每个7×7的二维特征图坍缩成一个统计量。这相当于让网络暂时忽略在哪里先关注有什么。我做过一个对比实验用最大池化代替GAP模型准确率下降了1.5%。因为最大池化只关注最显著特征而GAP保留了所有位置的信息贡献这对细粒度分类特别重要。# 挤压层核心代码 self.squeeze nn.AdaptiveAvgPool2d(1) # 无论输入尺寸多大输出都是1×12.2 激励层的通道外交激励层的瓶颈结构bottleneck设计充满智慧。假设原始通道数C512缩减比率r16那么先用512→32的全连接层降维就像把问题简化经过ReLU激活引入非线性再用32→512的全连接层恢复维度最后用sigmoid输出0-1之间的权重值这种设计既节省参数又保证了足够的表达能力。我在实践中发现r16在大多数场景都是甜点值过小会导致欠拟合过大则增加计算量但收益递减。# 激励层实现 self.excitation nn.Sequential( nn.Linear(channels, channels // r), # 降维 nn.ReLU(inplaceTrue), nn.Linear(channels // r, channels), # 升维 nn.Sigmoid() # 输出权重 )3. 当ResNet遇上SE三种融合策略对比3.1 SE-PRE先注意力后残差这种结构把SE块放在残差连接之前相当于先对输入特征做通道筛选再进行卷积运算。在ImageNet上的实验显示SE-PRE对浅层网络效果更好因为低层特征更需要全局指导。但要注意梯度爆炸风险。我在一个项目中曾遇到训练不稳定的情况后来在SE块后加入LayerNorm才解决。关键代码修改如下class SE_PRE_Block(nn.Module): def forward(self, x): se_weight self.se_block(x) # 先计算注意力权重 residual self.conv1(x * se_weight) # 加权后再卷积 return residual self.shortcut(x)3.2 SE-POST经典组合方案这是论文推荐的做法在残差之后应用SE块。好比先让网络自由发挥再对结果进行微调。在ResNet-50上这种结构比原始模型提升1.8%的Top-1准确率。有个实用技巧当stride2进行下采样时建议在shortcut分支也加入SE块。这能保持两个分支的注意力机制同步我在多个项目中验证过其有效性。3.3 SE-Identity并行注意力流最有趣的变体是让SE块与残差块并行。这相当于让网络同时学习原始特征和注意力加权的特征。在细粒度分类任务中这种结构表现出色比如鸟类识别准确率提升2.3%。但要注意计算开销。我的经验是只在最后两个stage使用这种结构既能提升性能又不会显著增加FLOPs。4. 实战用PyTorch实现SE-ResNet4.1 搭建基础SE块先实现一个可复用的SE模块。这里有个细节优化原始论文使用全连接层但我们可以用1×1卷积替代这样能避免view操作带来的维度问题。实测训练速度提升约15%。class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.squeeze nn.AdaptiveAvgPool2d(1) self.excitation nn.Sequential( nn.Conv2d(channels, channels//reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels//reduction, channels, 1), nn.Sigmoid() ) def forward(self, x): weights self.excitation(self.squeeze(x)) return x * weights4.2 改造ResNet模块以ResNet的BasicBlock为例我们需要在残差连接处插入SE块。注意三个关键点在第二个卷积之后应用SE保证shortcut分支与主分支同步缩放对降采样块特殊处理class SEBasicBlock(nn.Module): expansion 1 def __init__(self, in_planes, planes, stride1, reduction16): super().__init__() self.conv1 nn.Conv2d(in_planes, planes, 3, stride, 1, biasFalse) self.bn1 nn.BatchNorm2d(planes) self.conv2 nn.Conv2d(planes, planes, 3, 1, 1, biasFalse) self.bn2 nn.BatchNorm2d(planes) self.se SEBlock(planes, reduction) self.shortcut nn.Sequential() if stride !1 or in_planes ! self.expansion*planes: self.shortcut nn.Sequential( nn.Conv2d(in_planes, self.expansion*planes, 1, stride, biasFalse), nn.BatchNorm2d(self.expansion*planes) ) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.se(out) # 在此处应用SE out self.shortcut(x) return F.relu(out)4.3 训练技巧与参数调优在训练SE-ResNet时有几个经验证有效的技巧学习率预热前5个epoch使用线性增长的lr避免早期不稳定标签平滑用0.1的平滑系数缓解过拟合混合精度训练用AMP加速同时节省显存# 优化器配置示例 optimizer torch.optim.SGD(model.parameters(), lr0.1, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambdalambda epoch: (epoch 1) / 5 if epoch 5 else 0.1 ** (epoch // 30) )5. 效果验证与对比实验5.1 在CIFAR-100上的基准测试我对比了四种结构的性能表现训练200个epoch模型参数量(M)FLOPs(G)Top-1 Acc(%)ResNet-3421.31.1674.2SE-ResNet-3421.81.1876.5 (2.3)ResNet-5023.51.3176.8SE-ResNet-5024.11.3478.9 (2.1)可以看到SE模块以约2%的参数量增加换取了2%以上的准确率提升。5.2 注意力可视化分析通过可视化SE块的权重分布我发现一些有趣现象在低层网络SE块倾向于加强颜色和边缘通道在高层网络注意力更关注语义特征通道分类错误样本往往表现出异常的注意力分布# 可视化代码片段 def visualize_se_weights(model, layer_idx): se_block model.features[layer_idx].se weights se_block.excitation[2].weight.data plt.matshow(weights.cpu().numpy()) plt.colorbar()6. 进阶应用与优化方向6.1 轻量化设计MobileNetSE当把SE模块引入轻量级网络时需要特别注意计算开销。我的解决方案是只在关键stage添加SE块使用分组卷积减少FC层计算量采用更激进的reduction ratio如r32class LiteSEBlock(nn.Module): def __init__(self, channels, reduction32): super().__init__() self.squeeze nn.AdaptiveAvgPool2d(1) self.excitation nn.Sequential( nn.Conv2d(channels, channels//reduction, 1, groups4), nn.ReLU(), nn.Conv2d(channels//reduction, channels, 1, groups4), nn.Sigmoid() )6.2 动态调整reduction ratio固定比例的reduction可能不是最优的。我实现了一个动态调整策略根据当前epoch数逐渐增大r值让网络早期专注粗粒度特征后期再细化调整。这带来了约0.5%的额外提升。6.3 跨模态应用尝试在尝试将SE-ResNet用于多模态任务时我发现一个改进点对RGB和深度图分别使用独立的SE块最后再融合比共享SE块效果更好。这启示我们注意力机制可能需要根据输入特性进行定制。

更多文章