☰
ResNet+CBAM注意力机制实现细粒度图像分类实战解析
2026/10/11 0:39:40 网站建设 项目流程

简介:面向课程设计、毕业设计与期末大作业需求,这份基于ResNet+CBAM注意力机制的Stanford Dogs识别分类Python源码包,可快速搭建图像分类训练流程,也能替换数据集用于其他场景。源码包共21个文件,包括Python源码及pyc编译文件、Markdown说明文档、Shell训练脚本和展示图片,压缩包仅228KB,轻量易用。目前已有383人学习下载。包内完整覆盖CBAM/BAM注意力模块、ResNet模型定义、数据加载与训练入口,并附有训练脚本和效果展示图;既可直接复现Stanford Dogs犬种实验,也可迁移到自定义图像数据集,便于在注意力机制与残差网络结合方向做二次开发。

1. 基于ResNet+CBAM的Stanford Dogs识别:这套源码为什么值得你跑一遍

做细粒度图像分类的人,大概率在Stanford Dogs这种数据集上栽过跟头:120个犬种,很多品种之间只差耳朵轮廓和毛色深浅,用普通ResNet做基准模型,Top-1准确率卡在70%上下就是上不去。这里说的基于ResNet+CBAM注意力机制的Stanford Dogs识别分类源码,本质上就是把CBAM这种同时关注通道和空间位置的注意力模块,插进ResNet的残差结构里,让模型不再对所有特征图一视同仁,而是重点盯住“狗头”“耳朵”“爪子”这些真正能区分品种的局部区域。它能解决的就是细粒度分类中“特征表达够但注意力跑偏”的问题,适合正在做图像分类毕设、入门注意力机制、或者想把分类模型迁移到自己数据集的Python开发者。这套方案不挑数据,换掉图片目录就能跑别的分类任务。

2. CBAM注意力机制与ResNet的融合位置:先搞清楚加在哪、为什么

2.1 通道注意力与空间注意力:CBAM的两段式设计

CBAM全称是Convolutional Block Attention Module,它和SE注意力机制最大的区别在于多了一条空间注意力分支。SE只做通道维度的重标定,也就是对每个特征通道算一个权重,把有用的通道放大、没用的压缩;CBAM在这之后还要对特征图的空间位置做一次加权。用大白话说,SE告诉模型“看哪些颜色”,CBAM额外告诉模型“看画面里的哪个位置”。这两个信息对细粒度分类缺一不可——斯坦福狗数据集里不同品种的色调差异不大,但耳朵形状、腿部比例这些空间结构差异非常明显。

通道注意力模块的计算方式是这样的:输入特征图F分别走全局平均池化和全局最大池化两条路,得到两个一维向量,送进一个共享权重的多层感知机,把两个输出相加,过sigmoid得到通道权重。这里有个关键参数叫reduction ratio,一般记为r,它的作用是控制MLP中间层的压缩程度。我一般会把r设为16,这时MLP的中间层神经元数量等于输入通道数除以16。r值设太大会让通道间的信息交互变弱,太小则参数膨胀,后面避坑部分我会单独讲。

空间注意力模块的输入是通道注意力加权后的特征图。它先在通道维度上做平均池化和最大池化,得到两张二维的特征图,然后把它们拼在一起,过一层7x7的卷积,输出单通道的空间权重图,再过sigmoid。两段式设计的逻辑是:先让模型确定“看哪些通道”,再让模型在选定的通道里确定“看哪些像素位置”。串行比并行效果好的原因,论文里的消融实验说得很清楚:串行时第二个模块的输入已经经过第一轮筛选,噪声更少,注意力图更干净。我在实践里对比过串行和并联的写法,串行在Stanford Dogs验证集上大概能高出1.5个百分点。

2.2 加在ResNet的哪个位置:对比三种接法

CBAM加进ResNet的位置没有标准答案,但实际做下来效果差别不小。以ResNet18和ResNet34这种用BasicBlock的基础残差块为例,常见的接法有三种。第一种是加在残差块内部,让CBAM对残差分支的输出做加权,再和shortcut相加;第二种是加在add操作之后、ReLU激活之前;第三种是加在每个Stage的输出之后,也就是下采样之前。

我一般会选第一种,也就是把CBAM嵌进BasicBlock的forward函数里,处理残差分支的输出。这么做的理由是:残差分支的输出是当前层从输入中学到的新特征,shortcut是恒等映射或经过1x1卷积的原始特征。CBAM只对新增特征做注意力加权,不污染原始特征的传递,梯度回传路径也更清晰。把CBAM放在add之后的问题在于,恒等映射的特征也被一起重新标定了,如果某个通道的特征本身就足够好,再乘一个小于1的注意力权重反而会拖后腿。放在Stage之后则粒度太粗,一个Stage内部可能有三到四个残差块,前面的块在提取轮廓,后面的块在提取纹理,用一个注意力模块去覆盖所有块的输出,等于让模型做了一锅炖。

这里还有一个工程上的选型问题:是不是每个残差块都要加CBAM?我可以负责任地说,不是越多越好。在ResNet18上加满四层Stage的CBAM,参数会增加大约15%,训练时间也明显变长,精度提升只有0.8%左右。我只在前两个Stage加,效果反而更稳。原因不难理解:前两个Stage输出的特征图分辨率高,空间注意力能捕捉到狗耳朵、狗嘴这些局部细节;后两个Stage的特征图分辨率低,每个像素对应原图一大块区域,空间注意力能做的区分已经非常有限。当然,你如果换成了ResNet50这种Bottleneck结构的网络,可以每个大Stage里的最后一个Block加一个CBAM,效果也比较均衡。

这里顺便说一下PyTorch里怎么改BasicBlock。以下是一个简化的CBAM模块定义和BasicBlock修改示意:

import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, in_channels, reduction_ratio=16): super().__init__() self.mlp = nn.Sequential( nn.Linear(in_channels, in_channels // reduction_ratio), nn.ReLU(inplace=True), nn.Linear(in_channels // reduction_ratio, in_channels) ) def forward(self, x): # x: [B, C, H, W] avg_out = self.mlp(x.mean(dim=(2, 3))) # 全局平均池化 max_out = self.mlp(x.amax(dim=(2, 3))) # 全局最大池化 weight = torch.sigmoid(avg_out + max_out) # 逐元素相加后过sigmoid return weight.unsqueeze(2).unsqueeze(3) # 还原为 [B, C, 1, 1] class SpatialAttention(nn.Module): def __init__(self, kernel_size=7): super().__init__() self.conv = nn.Conv2d(2, 1, kernel_size=kernel_size, padding=kernel_size // 2) def forward(self, x): avg_out = x.mean(dim=1, keepdim=True) # 通道维平均池化 max_out = x.amax(dim=1, keepdim=True) # 通道维最大池化 concat = torch.cat([avg_out, max_out], dim=1) return torch.sigmoid(self.conv(concat)) # [B, 1, H, W]
# 在BasicBlock内部插入CBAM class BasicBlockWithCBAM(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.cbam = CBAM(out_channels) # shortcut 部分的代码省略 def forward(self, x): identity = self.shortcut(x) out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out = self.cbam(out) # 残差分支输出先过CBAM out += identity # 再与恒等映射相加 return self.relu(out)

这段代码里有个细节值得注意:CBAM放在bn2之后、add之前,这意味着BatchNorm已经把特征分布拉回零均值附近,CBAM的sigmoid输出在这里不会遇到数值饱和的问题。如果你把CBAM放在add之后,sigmoid的输入是两路特征之和,数值范围不受控制,训练早期很容易出现注意力权重全部贴到0或1的极端情况。另外一个容易忽略的点:通道注意力里的MLP第一个Linear层的输入维度是in_channels,也就是输入特征图的通道数,这里不要和out_channels搞混。如果你在BasicBlock里加CBAM,输入输出通道数不同的时候,以out_channels为准。

3. 把Stanford Dogs数据集喂给模型:目录组织与DataLoader写法

3.1 Stanford Dogs的标注格式与目录结构

Stanford Dogs数据集可以从斯坦福大学官网下载,整个数据集分两部分:images目录和annotation目录。images目录下是按类别组织的文件夹,每个文件夹的命名格式是“WordNet编号-品种名”,比如n02085620-Chihuahua表示吉娃娃,n02085936-Maltese_dog表示马耳他犬。整个数据集一共120个品种、两万多张图片,图片尺寸不统一,从几百像素到上千像素都有,直接用原始尺寸训练肯定不行。

annotation目录里是MATLAB的.mat标注文件,每个品种一个。用scipy.io.loadmat读出来,里面有四个关键字段:annotation是一个结构体数组,每个元素对应一张图片的标注信息;imgname是图片文件名;bbox是目标检测框坐标。还有一个更重要的字段是train/test列表的划分索引,官方已经帮你分好了训练集和测试集。这里有个常见的坑:mat文件里的类别顺序和images目录下的文件夹顺序不一定一致,如果直接按os.listdir的顺序去对类别id,训练集和验证集会串味。

正确的读取方式应该以mat文件里的类别名称为基准,构建一个类别id到文件夹名的映射表。我写了一个小脚本,把每个品种的文件夹名读出来,按字母序排序后分配从0到119的类别id,再读mat文件里的训练集图片索引,把每张图片的完整路径和类别id对应起来。mat文件里imgname字段存的不是完整路径,而是相对于images目录的路径,拼接的时候要注意。

import os import scipy.io as sio from collections import OrderedDict def load_stanford_dogs(images_root, anno_root): # images_root: 存放狗品种图片的根目录 # anno_root: 存放mat标注文件的目录 classes = sorted([d for d in os.listdir(images_root) if os.path.isdir(os.path.join(images_root, d))]) class_to_idx = {name: i for i, name in enumerate(classes)} samples = [] for cls_name in classes: mat_path = os.path.join(anno_root, cls_name.split('-')[0] + '_annotation.mat') mat = sio.loadmat(mat_path) annos = mat['annotation'] # 官方标注里没有直接给出划分,需要用测试集列表单独读 # 这里先把所有图片路径和类别id存下来 for item in annos: img_name = str(item['imgname'][0][0]) full_path = os.path.join(images_root, cls_name, img_name) samples.append((full_path, class_to_idx[cls_name])) return samples, class_to_idx

这段代码里我用的是类别文件夹名直接推mat文件路径的写法,因为mat文件名是WordNet编号,和文件夹名的前缀一致,这样最不容易错。很多现成教程把mat里的类别列表和images目录分开处理,最后拼映射表,逻辑绕了一圈还容易踩顺序的坑。另外注意scipy.io.loadmat读出来的字符串是numpy的bytes类型,用str()转一次才能正常拼接路径。读图片的时候用PIL的Image.open或者cv2.imread都行,但要注意cv2读出来是BGR通道顺序,训练前要转成RGB,否则模型学到的颜色特征全是反的。

3.2 按ImageFolder格式重排数据与DataLoader参数

torchvision的ImageFolder是最省事的数据集类,只要目录结构是“根目录/类别名/图片文件”,它就能自动读取图片并生成类别标签。所以我在每次跑这个源码之前都会先把Stanford Dogs整理成这个格式。具体做法是:建一个data目录,下面分train和val两个子目录,再往每个子目录里按类别建120个子文件夹,把官方划分好的训练集和测试集图片分别复制进去。复制比软链接稳,虽然占用磁盘空间大一些,但Windows和Linux下都不会出现软链接读不到的问题。

整理完目录结构之后,用torchvision.datasets.ImageFolder加载,再配合DataLoader做数据迭代。这里有几个参数比模型本身更影响训练结果:batch_size、num_workers和pin_memory。batch_size取决于显卡显存,ResNet18加CBAM在1080Ti上可以设64,ResNet50的话32比较稳;num_workers设成CPU物理核心数的一半通常最快,设太大反而因为线程切换开销导致数据加载变慢;pin_memory设为True可以让GPU训练时数据拷贝快一截,前提是你的机器内存够大。

from torch.utils.data import DataLoader from torchvision import datasets, transforms # 训练集和验证集用不同的数据增强策略 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder('data/train', transform=train_transform) val_dataset = datasets.ImageFolder('data/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)

这里的Normalize参数用的是ImageNet数据集的均值和标准差,原因是你后面要加载ResNet的预训练权重,预训练模型是在ImageNet上做的标准化,输入数据分布不一致的话,预训练特征基本发挥不出来。换自己的数据集时,这个标准化参数一般不用改,等模型在你的数据上训练充分后,如果精度还是上不去,再考虑统计自己数据集的均值和标准差,但大多数情况下多此一举。

RandomResizedCrop的scale参数是从0.7到1.0,这是在限制裁剪面积占原图的比例。Stanford Dogs官方数据里很多狗是全身照,bbox标注比较紧,裁得太小会把狗头截掉一半。如果你的显卡显存足够,可以把输入分辨率从224提高到256,对细粒度分类的精度提升非常明显,代价是训练时间大约增加四成。

4. 核心代码实现:ResNet+CBAM的训练流程与关键参数

4.1 在PyTorch里给ResNet插入CBAM模块

torchvision自带的resnet18、resnet34这些模型类,BasicBlock是写死的,不能直接往forward里插模块。好在PyTorch的源码设计得比较开放,可以用继承和重写的方式改造。我的做法是写一个子类继承torchvision.models.ResNet,然后替换掉它内部的BasicBlock,把它指向我自定义的BasicBlockWithCBAM。torchvision的ResNet代码在构造时会根据block参数创建每一层的残差块,所以只要把block参数换成带CBAM的版本就行。

加载预训练权重的时候有个关键操作:用pretrained=True拿到原版ResNet的权重,然后遍历state_dict,把和自定义网络名称匹配的键值对加载进去。因为自定义Block新增了cbam相关的权重层,这两层在预训练权重里不存在,加载时需要用strict=False跳过。千万不能把预训练权重直接喂给自定义网络然后报错就放弃了,正确做法是加载完同名的权重后,对CBAM的卷积层和Linear层做一次kaiming初始化。

import torchvision.models as models def build_resnet_cbam(num_classes=120, pretrained=True): # 使用torchvision内置的ResNet骨架,替换block为带CBAM的版本 model = models.ResNet(block=BasicBlockWithCBAM, layers=[2, 2, 2, 2], num_classes=num_classes) if pretrained: # 加载原版ResNet18的预训练权重 state_dict = models.resnet18(pretrained=True).state_dict() model.load_state_dict(state_dict, strict=False) # 对CBAM新增层做初始化 for name, module in model.named_modules(): if isinstance(module, (nn.Conv2d, nn.Linear)): if 'cbam' in name: nn.init.kaiming_normal_(module.weight, mode='fan_out', nonlinearity='relu') return model

这里的layers参数[2,2,2,2]对应ResNet18的结构,如果你换ResNet34就改成[3,4,6,3]。torchvision源码里ResNet的构造函数是允许直接传入自定义block的,但要注意自定义block的__init__签名必须和原版BasicBlock保持一致,否则构造到一半会报参数不匹配的错误。加载权重时strict=False会跳过所有名称匹配不上的键,不要只盯着CBAM层看,shortcut里的1x1卷积如果名称对不上也会被跳过,所以加载完成后最好打印一下missing_keys和unexpected_keys,确认跳过的确实只有CBAM相关层。

4.2 训练配置与超参数:从学习率到早停

训练超参数这块,我踩过不少坑,先说结论。优化器用SGD配上momentum=0.9和weight_decay=1e-4,比Adam稳。CBAM这种带注意力模块的网络对学习率非常敏感,Adam在训练初期收敛快,但到了后期loss会在一个高位来回震荡,很难逼近最优值。SGD配合余弦退火学习率调整,训练曲线比Adam平滑得多。初始学习率我一般设在0.001,加载预训练权重的情况下,0.01容易把浅层特征冲坏,0.0001又收敛太慢。

损失函数用交叉熵就够了,Stanford Dogs的120个类别样本数量相对均衡,没有严重的类别不平衡问题。如果你的训练数据分布很不均匀,可以考虑给CrossEntropyLoss传class_weight参数,权重设为每个类别的样本数倒数,或者用更简单的方案:每个batch里做加权采样。训练周期一般设30到50轮,细粒度分类模型在20轮之前几乎不会收敛,40轮之后又开始过拟合,我一般盯验证集loss做早停,连续5轮不降就停止训练并回滚到最佳权重。

import torch import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model = build_resnet_cbam(num_classes=120, pretrained=True).cuda() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=50, eta_min=1e-6) best_acc = 0.0 best_model_path = 'best_resnet_cbam.pth' for epoch in range(50): model.train() train_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(non_blocking=True), labels.cuda(non_blocking=True) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() scheduler.step() # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), best_model_path)

这里的CosineAnnealingLR把学习率从0.001按余弦曲线逐步降到0.000001,比固定学习率或者每隔30轮降一次的方式好不少。注意scheduler.step()要每个epoch调用一次,不是每个batch调用一次,否则学习率下降过快,最后几十轮基本在空转。保存模型用state_dict而不是整个模型对象,这样换机器或者换Python版本加载都更灵活。验证时记得包在torch.no_grad()里,否则验证过程也会构建计算图,显存消耗直接翻倍。我之前在验证阶段忘了加这个,12G显存的卡直接OOM,这个坑下面细说。

5. 避坑指南:训练CBAM模型最容易翻车的几个细节

5.1 坑一:加载预训练权重直接报size mismatch

现象:运行build_resnet_cbam时,PyTorch提示size mismatch for conv1.weight,或者报unexpected key。原因是torchvision预训练模型的输入是3通道ImageNet图片,但你的数据集可能是单通道灰度图,或者你的网络输入层被改成了别的通道数。

解决:先检查你的输入图片是不是RGB三通道。如果是灰度图,把图片复制成三通道再送进网络;如果你用的是ResNet的变体,比如把输入层换成了更大尺寸的卷积核,那预训练权重里conv1也匹配不上。还有一个办法是把加载权重那行换成model.load_state_dict(state_dict, strict=False),然后打印missing_keys,看看跳过了哪些层,再人工决定是否需要重新初始化这些层。大多数情况下跳过的只有fc层和cbam层,这两个是可以放心重新初始化的。

5.2 坑二:mat标注文件解析出来的类别顺序和目录对不上

现象:训练时loss正常下降,但验证集准确率始终在20%上下徘徊,和随机猜差不多。原因多半是训练集和验证集的类别id对不上,或者测试时用的类别映射表顺序和训练时不一样。Stanford Dogs的mat文件里类别顺序是按WordNet编号排的,但images目录下的文件夹如果按字母序排序,顺序和WordNet编号不完全一致。

解决:以images目录下的文件夹名为唯一基准,先给每个文件夹分配类别id,再去mat文件里通过文件夹名前缀查对应的标注文件。不要试图从mat文件里读类别名再反推id,这条路绕远了。整理完目录之后,用一个简单脚本随机抽查几张图:打印图片路径、类别id和类别名,人眼确认对应关系正确再开跑。这个检查只需要一分钟,能省下好几个小时的无效训练。

5.3 坑三:验证时忘了包no_grad导致显存溢出

现象:训练跑到第10个epoch,验证阶段突然报CUDA out of memory,训练过程倒是没任何问题。原因是验证阶段模型仍然处于梯度跟踪状态,每个batch的前向计算都会构建计算图并保存中间变量,显存被快速吃满。

解决:把所有验证代码包在with torch.no_grad()里面,同时用model.eval()切换BatchNorm和Dropout的工作模式。记住model.eval()只影响BatchNorm和Dropout,不影响梯度跟踪,两个都要做。另外验证阶段把batch_size调大一些是没问题的,因为没有梯度,显存占用比训练时小得多,可以适当提高batch_size来加速验证。

5.4 坑四:reduction ratio设太小导致参数量暴涨

现象:模型训练速度明显变慢,显存占用比预期的ResNet18大了将近一倍,而且训练出来的模型在验证集上没有明显提升。原因是通道注意力MLP的中间层维度是通道数除以r,如果r设成4甚至2,中间层神经元数量会非常大,参数翻倍。

解决:r=16是性价比最高的默认值,换成r=8也就多一点点参数量,但r=4基本没必要。如果你改用的是ResNet50,它的Bottleneck结构本身通道数就大,r=16可能让中间层还是很大,这种情况下可以试试r=32。判断标准很简单:打印模型的总参数量,如果比原版ResNet多了30%以上,优先检查r值是不是太小了。

5.5 坑五:数据增强太强把狗头裁没了

现象:训练集准确率缓慢上升,但验证集准确率一直在低位震荡,和训练误差之间的gap越来越大。原因是RandomResizedCrop的scale参数设得太激进,比如默认的(0.08, 1.0),它会把一张狗的照片裁剪到只剩背景或者只留狗肚子,模型学到的特征全是颜色和纹理,和品种完全无关。

解决:把scale改为(0.7, 1.0)或者(0.6, 1.0),保证每次裁剪出来的区域至少包含大半个狗身。如果你用的数据集是已经裁剪过的目标框,那么可以用Resize加CenterCrop,完全不用RandomResizedCrop。细粒度分类的数据增强策略和通用分类不一样,后者随便裁,前者必须保证主体物完整,这是最容易被忽略的细节。

6. 把源码换到自己的数据集:迁移步骤与一个验证技巧

换数据集是这个方案最大的卖点,实际操作比想象中简单。以这个源码为例,你只需要做三件事:把图片整理成“根目录/类别名/图片文件”的结构;修改num_classes为你的类别数;把train_transform和val_transform里的输入尺寸按你的图片分辨率做微调。如果你的数据集图片尺寸很小,比如只有96x96分辨率,就不要强行Resize到224,否则图片会糊到连人眼都分不清,这时候反而应该把输入尺寸改小,比如128x128,或者换用更浅的ResNet18而不是ResNet50。

换完数据集之后,我建议先跑一个10轮的短实验,只训练模型的后半部分(也就是冻结前两个Stage的权重)。这一步是为了验证数据读取、损失计算、验证逻辑这些管道都没问题。10轮之后看验证集准确率,如果比随机猜测高出一截,说明管道正常;如果和随机猜测差不多,先检查类别映射和图片增强,别急着调模型结构。确认管道没问题后,再解冻全部参数,正式训练。

一个很有用的验证技巧:把CBAM模块的sigmoid输出可视化出来,叠加到原图上。如果注意力热力图集中在狗头的眼睛、鼻子周围,说明模型学到了合理的判别区域;如果热力图散落在背景上,说明数据增强或预训练权重出了问题。这个可视化的代码不复杂,保存输入图片的梯度或者特征图就行,但它能直观地告诉你CBAM到底在“看”什么。我用这个技巧排查过一个数据标注错误的问题:某类图片里混入了大量背景图,注意力热力图全打在背景上,一眼就看出来了。

我做这个项目的最大教训是:注意力机制不是银弹,它的收益上限取决于骨干网络和数据质量。ResNet18加CBAM在Stanford Dogs上能到85%左右,ResNet50加CBAM能到88%上下,但如果你把数据增强或者类别映射搞错了,再强的注意力也白搭。所以每换一个数据集,我都会先做一轮最小的冒烟测试,确认每条数据流都是通的,再放开手脚调参。希望这个思路和这套踩坑记录对你有所帮助。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询