尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ResNet加注意力机制:Keras图像分类工程实现详解

发布时间:2026/9/16 1:28:12

资讯中心
01
ARTICLE

ResNet加注意力机制:Keras图像分类工程实现详解

ResNet加注意力机制:Keras图像分类工程实现详解
简介注意力机制与ResNet的结合是当前图像分类任务中提升特征表达能力的有效途径。该资源提供了一套基于Python和Keras实现的完整分类网络方案包含ResNet V1/V2、ResNeXt、DenseNet、Inception等主流模型源码并重点加入了双重注意力机制模块适合有一定深度学习基础、希望开展图像分类实验或对比模型效果的研究者与开发者。压缩包共53个文件大小约5.8MB以22个Python脚本为核心辅以8张网络结构图、训练记录文档及项目配置文件结构与注释清晰便于直接运行和二次修改。内容还附带了训练记录docx包含损失曲线、准确率变化等关键信息可帮助复盘优化过程。目前已有2835人学习是快速上手注意力机制ResNet图像分类建模的实用参考资料。1. ResNet加注意力机制的分类网络这个Python包解决了什么问题同样的残差网络加一个几十行的注意力模块验证集准确率往往能再涨一个点。ResNet解决的是梯度消失和深层训练的稳定性注意力机制解决的是另一码事特征图里哪些通道可信、哪些像素位置值得看。这个zip包含一套可跑的Keras图像分类工程代码按模块拆得比较规整——resnet_v1_v2.py实现两代残差网络attention_module.py和se.py分别给出CBAM与SE注意力模块double_eye_main.py把这两条注意力分支同时并入主干做训练。配合overview.png、submodule.png两张结构图和训练记录.docx不需要边翻论文边对照代码。适合准备给ResNet预训练模型接入注意力做细粒度图像分类的工程师和学生。2. resnet_v1_v2.py拆读预激活与瓶颈结构怎么影响分类精度resnet_v1_v2.py是这套代码的网络核心里面同时维护了v1和v2两套残差结构方便同一份训练脚本只改一个参数就能切换主干。看懂这一层后面装注意力模块才知道装在哪。2.1 从resnetv1_network-v1.png看v1残差块的顺连方式包里的resnetv1_network.png和resnetv1_network-v1.png是两张配套图一张画整个网络一张展开单个v1残差块。v1块的常规顺序是输入先过卷积做完BatchNormalization再激活也就是“卷积→BN→ReLU”这样一组组堆。这种后激活写法在浅层网络里没有明显问题但深度超过50层后连续的非线性变换会把回传梯度的幅度压得很小单纯靠堆层数很难继续提升精度。v1的解决办法是恒等映射shortcut分支不做任何变换直接把输入加到主路径输出上。stride1时shortcut就是原张量本身Add层做逐元素加法只有当空间尺寸减半、通道数翻倍时才在shortcut上补一个1x1卷积来对齐shape。这里有个容易被忽略的细节这个1x1卷积只负责维度对齐不承担特征表达所以它的权重初始化和主路径保持同一个方差范围即可不需要单独调低。很多复现代码在v1的shortcut上加了BN严格说是偏离原论文的训练时可能没问题但迁移预训练权重时会对不上统计量。2.2 表格式对比v2批量归一化提前与瓶颈结构v2版本把BN和ReLU挪到了卷积前面顺序变成“BN→ReLU→Conv”也就是预激活。主路径的第一个操作从卷积改成了激活恒等映射通道保持完全干净梯度从深层传回浅层时不会再经过任何激活函数。实际训练里最直观的差别是v2对初始学习率的容忍度更高收敛曲线也更平滑。对比维度ResNet v1ResNet v2预激活块内顺序Conv→BN→ReLUBN→ReLU→Conv跳跃连接stride1恒等stride2补1x1卷积同上shortcut后不再接激活瓶颈结构可选1x1降维→3x3→1x1升维有且每个卷积前都先做BN训练表现对初始lr敏感深层容易振荡对大lr更耐受收敛稳定读代码方式resnet_v1.pyresnet_v2.pyv1_v2.py里两者并存这个表格在项目里对应三种文件resnet_v1.py、resnet_v2.py、resnet_v1_v2.py。前两个是独立实现后一个把两代网络合并为同一套函数训练时用一个version参数切换。对比训练记录.docx里的loss曲线v2在相同epoch数下通常更快进入平台期这并不代表它更优只是说明BN提前让前几轮的特征分布更稳定。2.3 用Keras函数式API搭残差块的关键参数残差结构有跨层分支Sequential模型表达不了必须用函数式API。一个能跑的基础残差块长这样def res_block(x, filters, stride1, preactTrue, use_bottleneckTrue): shortcut x if preact: # v2预激活每个卷积前都先过BNReLU x BatchNormalization(axis3)(x) x Activation(relu)(x) x Conv2D(filters, (1, 1), strides(stride, stride))(x) x BatchNormalization(axis3)(x) x Activation(relu)(x) x Conv2D(filters, (3, 3), paddingsame)(x) x BatchNormalization(axis3)(x) x Activation(relu)(x) conv_out Conv2D(filters * 4 if use_bottleneck else filters, (1, 1))(x) else: # v1写法卷积在BN之前 x Conv2D(filters, (3, 3), strides(stride, stride), paddingsame)(x) x BatchNormalization(axis3)(x) x Activation(relu)(x) conv_out x # 对齐shortcut的通道数和空间尺寸 if stride ! 1 or shortcut.shape[-1] ! conv_out.shape[-1]: shortcut Conv2D(conv_out.shape[-1], (1, 1), strides(stride, stride))(shortcut) out Add()([conv_out, shortcut]) return out这里有几个参数坑。axis3默认数据是channels_last换到channels_first后端整个BN就全错。preact决定用v1还是v2顺序同一份数据v2的前几个epoch loss一般比v1高一点那是BN统计量在重新校准别急着改学习率。use_bottleneck为True时第一第二层卷积用较窄的filters最后用filters*4升维这是ResNet50以上深度的标配小数据集用False省参。真实网络按层表重复这个块并在不同stage切换stride我这里只保留了最小组件。提示加载resnet预训练模型做迁移时第一层stem的padding、步长、池化位置必须和原结构一致任何一个不一致都会让起始几层的特征分布整体偏移后面注意力模块加得再好也补不回来。3. 通道注意力与CBAM空间注意力attention_module.py的实现拆解注意力机制在图像分类里的角色不是替换主干而是对主干特征做重标定。se.py与attention_module.py分别给出两种模块SE做通道维度的权重学习CBAM在通道之外再加一个空间分支。两者不冲突可以像double_eye_main.py那样串起来用。3.1 se.py里的SE通道注意力压缩比ratio怎么调SE是“通道注意力机制”的代表做法步骤是压缩、激励、重标定。全局平均池化把每个通道压成一个标量两个Dense层对通道关系建模sigmoid输出0到1之间的门控权重再乘回原特征图。se.py里的实现核心只有几行from keras import backend as K from keras.layers import GlobalAveragePooling2D, Dense, Reshape, multiply def se_block(input_tensor, ratio16, namese): # 每个通道先平均池化成1个数值得到通道描述符 x GlobalAveragePooling2D()(input_tensor) # 瓶颈结构先降维再升维控制参数量 x Dense(int(input_tensor.shape[-1]) // ratio, activationrelu, namename _fc1)(x) x Dense(int(input_tensor.shape[-1]), activationsigmoid, namename _fc2)(x) # 把(C,)的权重reshape成(1,1,C)再逐通道乘 x Reshape((1, 1, int(input_tensor.shape[-1])))(x) return multiply([input_tensor, x])ratio是中间瓶颈层的压缩比取16时512通道的特征图中间只有32个节点计算开销几乎可以忽略。小数据集上把ratio从16降到8弱特征更容易被保留涨点往往更明显取32则省参数但通道间的关系建模太粗。这个超参对最终效果的影响比Dense层用什么初始化器更敏感。两个Dense都带了name为的是后面用get_layer取中间输出做热区可视化。包里带了一份se_resnext.py是SE插到ResNeXt分组卷积后的版本代码结构和se.py同源。做baseline对比时直接用这份文件就能控制变量地比较SE-ResNeXt和普通ResNeXt在同一份训练配置下的差距不用另外改主干。3.2 attention_module.py里的CBAM通道-空间协同注意力机制CBAM比SE多了一个维度。通道分支同时用全局平均池化和全局最大池化描述每个通道两个描述向量共享同一个MLP输出相加后得到通道注意力空间分支对通道维度求平均和最大值拼成两个通道的空间描述符再过一个7x7卷积输出空间权重。两个分支串联起来就是典型的“通道-空间协同注意力机制”。与只考虑通道的SE、以及引入坐标位置信息的CA注意力机制不同CBAM的空间权重完全由卷积在局部感受野内学习。from keras import backend as K from keras.layers import (GlobalAveragePooling2D, GlobalMaxPooling2D, Dense, Reshape, Add, Concatenate, Conv2D, Lambda, multiply) def cbam_block(input_tensor, ratio16, kernel_size7, namecbam): # 通道分支avg和max并联描述避免只看均值忽略强激活 avg_pool GlobalAveragePooling2D()(input_tensor) max_pool GlobalMaxPooling2D()(input_tensor) shared_dense1 Dense(int(input_tensor.shape[-1]) // ratio, activationrelu) shared_dense2 Dense(int(input_tensor.shape[-1]), activationsigmoid) avg_att shared_dense2(shared_dense1(avg_pool)) max_att shared_dense2(shared_dense1(max_pool)) channel_att Reshape((1, 1, int(input_tensor.shape[-1])))(Add()([avg_att, max_att])) x multiply([input_tensor, channel_att]) # 空间分支对通道求均值/最大值拼成两通道的空间描述符 avg_spatial Lambda(lambda t: K.mean(t, axis3, keepdimsTrue))(x) max_spatial Lambda(lambda t: K.max(t, axis3, keepdimsTrue))(x) x_cat Concatenate(axis3)([avg_spatial, max_spatial]) # 7x7卷积把两通道融合成一张空间注意力图 spatial_att Conv2D(1, kernel_size, paddingsame, activationsigmoid)(x_cat) return multiply([x, spatial_att])这段代码有三个容易写错的地方。第一两个共享Dense必须复用同一个层实例而不是重新构造重新new出来的Dense是独立权重参数翻倍还达不到共享效果。第二空间分支里的Lambda必须保持axis3这里只要写错维度输出shape会从(H,W,1)变成(H,1,W)之类的错位最终multiply时直接报shape错误。第三7x7卷积核在低分辨率阶段感受野覆盖全图权重图趋近全1空间注意力就失效了特征图缩到几十像素以下时kernel_size改回3。3.3 注意力模块插入网络的三种位置与对应效果同样是CBAM插的位置不同效果差异很大。每个basic block的Add之后立刻接注意力处理粒度最细但计算开销和显存占用都高每个stage末尾接一处整个网络只有4处模块CBAM论文里最常用的就是这种折中只在全局池化之前接一处对高分辨率中间特征没有约束效果最软。我拆这个包时看到的做法是混合式SE放在每个stage输出之后做通道重标定CBAM放在主干最后一层、全局池化之前做空间聚焦。插完模块先别急着全量训练。加载了预训练权重时我一般先冻结全部主干只训练注意力模块跑两三个epoch看到loss在下降再解冻整个网络。原因很简单预训练特征分布已经稳定注意力模块的Dense初始权重如果不做缩小SGD第一步就可能把特征分布拉歪。给注意力模块的Dense设一个stddev0.01的随机初始化是让第一轮训练更平滑的常规做法。4. double_eye_main.py训练流程把注意力模块装进ResNet跑起来double_eye_main.py是本项目的主程序入口double_eye的语义就是双注意力网络里同时有SE的通道注意力分支和CBAM的空间注意力分支。主程序做的事情分三块拼网络、读数据、记录训练过程分别对应resnet_v1_v2.py、load_img.py、训练记录.docx。4.1 组装带双注意力的分类网络从resnet_v1_v2.py拿残差主干注意力模块按上一章说的位置接进去最后接全局池化和分类头。代码层面就是函数式API的层与层串联def build_dual_attention_model(input_shape(224, 224, 3), num_classes1000, use_seTrue, use_cbamTrue): inputs Input(shapeinput_shape) # 残差主体按resnet_v1_v2.py的backbone接口取网络最后一级输出 x resnet_v1_v2.backbone(inputs, versionv2, weightsNone) if use_se: # 通道注意力每个stage末尾做通道重标定 x se_block(x, ratio16, namese_last) if use_cbam: # 空间注意力主干末尾、池化前做空间聚焦 x cbam_block(x, kernel_size3, namecbam_final) x GlobalAveragePooling2D()(x) outputs Dense(num_classes, activationsoftmax)(x) return Model(inputs, outputs)到这里backbone输出已经是1/32输入分辨率的特征图SE的全局池化几乎不增加计算量CBAM的kernel_size我一般从默认7改成3避免低分辨率下的感受野溢出。从零训练时可以在全连接前加Dropout(0.2)如果加载预训练权重做微调我一般把Dropout去掉让原有特征分布尽量不被破坏。训练结束保存的h5文件里模型结构、权重和优化器状态都在换机器恢复训练直接keras.models.load_model就能接上。4.2 load_img.py的数据加载与训练参数load_img.py负责数据侧逻辑不复杂但决定了训练能不能复现。它读入图片路径统一resize到模型输入尺寸转float32再归一化。如果backbone用的是ImageNet预训练权重归一化要和预训练时一致否则BN统计量在第一轮就会被冲乱。参数常用值说明target_size(224,224)或(299,299)必须与模型Input层一致batch_size16或32显存8G以下用16并同步下调学习率normalize0~1或ImageNet均值方差与预训练权重配套不一致会明显掉点augment水平翻转、小幅旋转小数据集必开否则注意力更容易过拟合老版本Keras的主训练循环长这样项目里的saved_models目录也是给这个流程用的model.compile( optimizerSGD(lr0.01, momentum0.9, nesterovTrue), losscategorical_crossentropy, metrics[accuracy]) checkpoint ModelCheckpoint( saved_models/best.h5, monitorval_acc, save_best_onlyTrue, modemax) lr_scheduler ReduceLROnPlateau( factor0.5, patience5, cooldown2, verbose1) model.fit_generator( train_generator, steps_per_epochlen(train_files) // batch_size, epochs120, callbacks[checkpoint, lr_scheduler])这里有三个点要说明。优化器用SGD加momentum而不是AdamAdam前几百轮收敛快但微调ResNet后期验证集准确率容易停在平台期SGD对学习率更敏感最终精度普遍更好。ReduceLROnPlateau的patience设5连续5个epoch验证loss不降就把lr减半比手动固定epoch降lr省心。监控指标写的val_acc在TensorFlow 2.x里要改成val_accuracy这是老项目迁到新环境最常见的一行报错。4.3 从训练记录.docx看懂优化过程的几个信号训练记录.docx是逐epoch的记录exp4.png、exp5.png如果按方案编号命名对应的就是不同注意力组合下的曲线。展开docx先别盯准确率按这几个字段排查记录字段信号含义处理建议train_loss / val_loss训练降而验证不降加数据增强或Dropoutval_acc前10轮不动学习率偏大曲线在震荡lr降到0.001试三个epochloss快速到0.1但acc低模型学到捷径注意力失效检查attention层输出是否恒为1同lr下v1与v2曲线差异两代结构收敛行为不同记录里优先看v2加注意力那组最容易误判的是第二种加了注意力模块后前几个epoch的loss高于纯ResNet这是新加卷积层破坏了原有BN统计通常两三个epoch后回落。超过5个epoch仍不回落才需要考虑注意力插入位置或kernel_size的问题。曲线对比的重点放在训练后半段注意力模块对特征的重标定作用通常在主干拟合基本完成后才开始体现。5. 从图看模型有没有真学到东西注意力热区可视化验证准确率上去不代表模型关注对了位置。图像分类里best模型的直觉判断就是拿几张验证集图片看注意力热区。5.1 把空间注意力权重导出为热区def visualize_attention(model, img): # 用层名取到CBAM空间注意力分支的输出 att_layer model.get_layer(cbam_final) get_att K.function([model.input], [att_layer.output]) att_map get_att([img[np.newaxis]])[0][0] # att_map shape: (H, W, 1)取单通道做热区叠加 heatmap att_map[:, :, 0] plt.imshow(img) plt.imshow(heatmap, cmapjet, alpha0.5) plt.axis(off) plt.show()get_layer(cbam_final)取到的正是4.1节给注意力模块起的层名这也是为什么前面强调name参数。输出热区后看两点热区是否集中在目标区域以及换一张同类图片热区位置是否稳定。热区全亮或集中在四角多半是kernel_size偏大或者模块加在分辨率过低的层上先把7改成3跑一轮对比。5.2 消融对比与ECA快速尝试热区验证通过后做量化对比固定随机种子保持epoch、batch size、初始lr完全一致分别跑ResNet v2、v2SE、v2CBAM三组比较val_acc和top5。再跑一组去掉所有注意力分支的纯v2作为基线能排除涨点其实来自数据增强的可能性。想继续压榨涨点空间可以试ECA注意力机制。SE的两个Dense瓶颈会丢通道间的位置关系ECA把通道注意力换成1D卷积卷积核大小按通道数自适应计算实现里常见直接取5参数量接近零实现就是把SE里的两个Dense层替换成Conv1D后直接multiply。工程上我喜欢先动ratio和kernel_size这两个超参再做ResNet版本切换一次只改一个变量出问题时分得清是注意力配置的问题还是主干结构的问题。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。