基于ResNet18的CUB-200-2011鸟类细粒度识别实战
2026/9/8 17:28:26 网站建设 项目流程

简介:基于ResNet18预训练实现CUB-200-2011鸟类数据集识别分类的完整项目源码包,面向计算机相关专业学生、教师及企业开发者,适合毕业设计、课程设计、大作业或迁移学习入门。项目包含PyTorch源码、使用说明、数据集下载链接及训练评估脚本,代码稳定可运行,并针对随机初始化与预训练两种方式、三组不同学习率进行对比实验,最高验证准确率达68.33%,可清晰观察不同配置对收敛速度和泛化性能的影响。压缩包内共37个文件,以py源码、md说明、txt文本、xml/iml配置及TensorBoard训练日志为主,整体仅54KB,结构清晰便于二次开发。已有440人浏览学习,可直接基于项目改写分类任务,也可作为论文实验或答辩演示基础。配套说明和排错提示均为中文,注意项目名与路径需使用英文,解压后按说明运行即可复现全部结果。 拿到CUB-200-2011鸟类数据集做识别分类,很多人第一反应就是"把图片丢进网络里硬训练",但这个思路在细粒度分类任务上往往行不通。这个项目选择ResNet18预训练模型作为基底,在PyTorch里做迁移学习微调,目标是让一个200类的鸟类细粒度识别任务在单张消费级显卡上跑出稳定可用的精度。整个项目包含完整Python源码、使用说明文档,以及数据集的下载方式,属于典型的中等规模图像分类实战,适合刚入门深度学习图像方向、想跑通一次完整训练验证流程的读者,也适合用Pytorch做分类任务但想看看别人的数据加载、微调策略怎么设计的同学。

1. CUB-200-2011到底难在哪,值得单独做一个项目

1.1 数据集的体量与官方划分

CUB-200-2011全称是Caltech-UCSD Birds-200-2011,是细粒度图像识别领域最常用的benchmark之一。数据集里一共200种鸟类,总计11788张图片,其中官方通过train_test_split.txt把数据划分为训练集5994张、测试集5794张,每类大约30张训练图、29张测试图。

这个数据量放在深度学习分类任务里属于非常小的规模。ImageNet有128万张训练图,CUB连它的零头都不到。但数据量小不代表任务简单,恰恰因为每一类的训练样本只有几十张,模型很容易在训练集上记住个别样本的纹理、背景和姿态,在测试集上却无法泛化。这也是为什么这个数据集一直用来验证模型的细粒度识别能力和迁移学习策略。

除了图片本身,CUB-200-2011还提供了每张图的标注框、关键点位置和312个二值属性标注。不过在基于ResNet18的分类基线里,通常只使用图片和类别标签,目标是把Top-1识别精度跑上去,并不需要先用检测框抠出鸟类主体。属性标注可以留到后面做多任务学习或者可解释性分析时再用。

1.2 细粒度分类为什么"细"在容易混淆

普通图像分类任务,比如猫狗分类,类间差异非常大,模型哪怕只看轮廓也能分个大概。但CUB这种细粒度数据集的难点在于,不同类别的鸟长相高度相似。比如不同种类的海鸥、燕鸥之间,可能只是嘴巴颜色、翅膀斑纹、腿部颜色的细微差别,普通卷积网络如果不做针对性设计,很容易把注意力放在背景、姿态这些干扰信息上。

更麻烦的是类内差异大。同一类鸟在不同季节、不同性别、不同姿态下外观差异明显,一只展翅的鸟和一只缩在树枝上的鸟,视觉特征差别比跨类别的还大。这种类间距离小、类内距离大的数据分布,让模型天然倾向过拟合。

ResNet18预训练模型在这里的价值就体现出来了。它在ImageNet上已经学到了足够通用的边缘、纹理、形状组合特征,从预训练权重开始微调,相当于让模型"带着常识进入考场",不需要在6000张图上从头摸索什么是翅膀、什么是喙、什么是羽毛纹理,只需要学会这些通用特征在200种鸟类之间如何组合和区分。

2. 为什么基座选ResNet18预训练,而不是直接上更大的模型

2.1 模型容量和训练成本的平衡

很多人在这个数据集上会纠结:ResNet50不是更强吗?为什么不动用EfficientNet或者ViT?这里的关键不是模型上限有多高,而是训练数据量太小,模型容量过大反而容易过拟合。

ResNet18的参数量大约1120万,ResNet50是2550万,ViT-Base参数量超过8600万。CUB训练集只有6000张左右,ViT这种大模型如果没有大规模预训练和复杂的正则化手段,在CUB上fine-tune未必比ResNet18强,还容易陷入严重的过拟合。ResNet18的容量对这个规模的数据集来说刚好够用,训练速度也快很多,在主流GPU上几十秒就能跑完一个epoch,调参周期短,非常适合作为细粒度分类任务的第一个可行基线。

另一个实际考量是部署和复现成本。ResNet18在224x224输入下的单次前向推理延迟非常低,CPU上也能跑,这对后面做实际应用落地很重要。如果你一开始就用最大的模型,可能光调整超参数就要花几倍时间,而最终精度提升可能只有一两个点。

2.2 预训练权重在迁移学习中扮演的角色

预训练权重解决的核心问题是如何在数据量不足时依然训练出泛化能力强、收敛稳定的模型。在ImageNet上训练过的ResNet18,前面几层学到的是边缘、颜色块、纹理基元这类与任务无关的通用特征,后面几层学到的是物体部件级别的模式,比如眼睛、轮子、翅膀之类的组合特征。

拿到CUB数据集后,我们不是让这些特征推倒重来,而是在已有特征表示基础上做"局部修正"。底层特征基本不需要动,高层特征和新的全连接分类头则需要根据200类鸟类的分布重新拟合。这个迁移过程比随机初始化从头训练省力得多。

实测中随机初始化ResNet18在CUB上从头训练,训练集上精度可能也很高,但验证集通常很难超过50%,而且loss下降缓慢,稍不注意就过拟合。使用预训练权重微调后,哪怕只是简单微调几个epoch,验证精度也能快速爬到70%以上。这就是预训练模型的真正价值:在小数据集上,好的起点比好的结构更关键。

这里要注意torchvision的API更新问题。老代码里常见的models.resnet18(pretrained=True)写法在新版本中会提示deprecated,现在推荐这样写:

import torchvision.models as models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)

如果网络下载权重失败,可以单独下载pth文件放到缓存目录,具体路径取决于你的torchvision版本,一般在~/.cache/torch/hub/checkpoints/下。

3. 环境准备与数据管线搭建

3.1 环境清单与数据集目录整理

我复现这个项目时的环境是Python 3.10、PyTorch 2.0、torchvision 0.15、CUDA 11.8,显卡是RTX 3060 12G。实际上PyTorch 1.10以上的版本基本都能直接跑,没必要追求最新版本,关键是cuda、torch、torchvision三者的版本号要匹配。

数据集解压后的目录结构大概是这样的:

CUB_200_2011/ ├── images/ │ ├── 001.Black_footed_Albatross/ │ │ ├── Black_Footed_Albatross_0001_796111.jpg │ │ └── ... │ ├── 002.Laysan_Albatross/ │ └── ... ├── train_test_split.txt ├── classes.txt ├── images.txt └── ...

images/下每个子目录代表一个类别,目录名是固定的格式:三位编号加点号加类名,比如001.Black_footed_Albatrosstrain_test_split.txt每行是一个图片相对路径加一个0或1的标记,1表示训练集,0表示测试集。

3.2 用ImageFolder配合官方划分文件构建数据集

torchvision.datasets.ImageFolder可以直接读取这种按类别分目录的结构,它会自动把每个子目录映射成0到199的类别索引。但CUB的官方train/test划分不是按目录分的,而是由train_test_split.txt控制,所以需要分两步操作:先分别创建应用训练增强和应用测试增强的两个ImageFolder实例,然后根据分割文件筛选出对应的样本索引,再用Subset切出训练集和验证集。

from torchvision import datasets, transforms from torch.utils.data import Subset import os data_dir = "./CUB_200_2011" images_dir = os.path.join(data_dir, "images") # 注意:这里用两个不同的transform分别创建两个ImageFolder train_ds_all = datasets.ImageFolder(images_dir, transform=train_transform) test_ds_all = datasets.ImageFolder(images_dir, transform=test_transform) # 读取官方划分 split_info = {} with open(os.path.join(data_dir, "train_test_split.txt")) as f: for line in f: rel_path, flag = line.strip().split() split_info[rel_path] = int(flag) train_idx, test_idx = [], [] for i, (path, _) in enumerate(train_ds_all.samples): rel = os.path.relpath(path, images_dir).replace("\\", "/") if split_info.get(rel, 0) == 1: train_idx.append(i) else: test_idx.append(i) train_dataset = Subset(train_ds_all, train_idx) test_dataset = Subset(test_ds_all, test_idx)

我在Windows上第一次跑的时候,就因为在拼接相对路径时没有用replace("\\", "/"),导致一小部分图片的路径匹配不上分割文件,后来把这些样本全部当成了验证集,训练集数量莫名少了一截。这个问题在下一节的踩坑部分会展开讲。

3.3 数据增强与ImageNet标准化

CUB训练集每类只有约30张图,不做数据增强的话模型很快过拟合。我用的训练增强配置是RandomResizedCrop随机裁剪缩放、随机水平翻转和轻度颜色扰动。测试集只用Resize(256)CenterCrop(224),保证评估结果的稳定性。

train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.6, 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]), ]) test_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]), ])

标准化用的0.485、0.456、0.406是ImageNet数据集的RGB均值,0.229、0.224、0.225是标准差。使用预训练模型时一定要沿用预训练阶段的统计值,否则输入分布和权重期望的分布不一致,微调效果会大打折扣。随机裁剪的scale参数我特意设成了0.6到1.0,比默认的0.08到1.0更温和,因为鸟类通常占图片主体,裁剪太狠会把鸟截掉一半,反而制造噪声样本。

4. 微调ResNet18:改网络结构、冻结策略和学习率安排

4.1 替换全连接层

torchvision加载的ResNet18最后一层是fc = nn.Linear(512, 1000),这里的512来自最后卷积输出经过平均池化后的特征维度,1000对应ImageNet的类别数。要让模型输出200类的分类概率,只需把fc替换成输出维度200的线性层。

import torch.nn as nn model.fc = nn.Linear(model.fc.in_features, 200)

这里有个容易写错的点:很多人以为ResNet18的fc输入维度是512,就直接写nn.Linear(512, 200),这在标准ResNet18上确实没错,但一旦换到ResNet34或ResNet50,维度就会变成不同的值。更稳妥的写法是像上面这样从model.fc.in_features动态取输入维度,这样即便换模型也不用改代码。

4.2 冻结还是不冻结?两种策略怎么选

微调时可以冻结backbone,只训练新加的fc层,也可以让全部参数都参与训练。CUB有6000张训练图,而且鸟类外观跟ImageNet里的常见物体差异比较大,只训练fc层会把识别能力限制在ImageNet预训练特征能表示的范围内,精度上限不高。我实测全冻结时验证精度大概在65%到70%之间,放开全部参数微调后可以到75%以上。

对于数据量更小的场景,比如每类只有十几张图,可以采取折中策略:冻结前几层和所有BatchNorm层的统计量,只微调后几层特征和fc层。BN训练时统计量的更新在数据量过小时会让模型很不稳定,冻结后能缓解不少。

对于CUB这个规模,我建议直接全量微调,然后把重心放在学习率的控制上。全量微调不等于所有层都用同一个学习率,预训练得到的特征需要保护,而新替换的fc层需要从头学,两者对梯度的敏感度差别很大。

4.3 优化器、损失函数和调度策略

损失函数直接用CrossEntropyLoss,多分类任务的默认选择,内部已经包含了softmax,不需要在网络输出后再手动加softmax。

优化器我用的是带动量的SGD,这是微调CNN非常成熟的选择。相比Adam,SGD配合低学习率和余弦退火在迁移学习场景下往往能获得更平滑、更稳健的收敛结果。注意要给fc层设置更高的学习率,预训练特征层设置更低的学习率。

import torch.optim as optim from torch.optim import lr_scheduler base_params = [p for name, p in model.named_parameters() if "fc" not in name] optimizer = optim.SGD([ {"params": base_params, "lr": 1e-4}, {"params": model.fc.parameters(), "lr": 1e-3}, ], momentum=0.9, weight_decay=1e-4) scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=40)

这样设置的原因是,预训练层已经处于一个较好的局部最优点附近,学习率太大会把学到的特征破坏掉;而fc层是随机初始化的,如果跟预训练层用同样的小学习率,收敛会非常慢。两组参数用不同的学习率,本质上兼顾了特征保护和分类头快速拟合两个目标。

余弦退火调度器让学习率随着训练进程从初始值平滑衰减到接近0,训练后期参数在很小步长下可以更精细地收敛到想要的位置。T_max设为训练总epoch数即可。

5. 训练实测:超参配置与精度变化记录

5.1 最终采用的训练方案

我把一套跑通且效果稳定的配置整理成了表格,直接按这套参数复现即可。

配置项取值
输入尺寸224x224
训练集/验证集5994 / 5794
Batch Size64
Epoch40
优化器SGD (momentum=0.9, weight_decay=1e-4)
特征层学习率1e-4
fc层学习率1e-3
学习率调度CosineAnnealingLR, T_max=40
损失函数CrossEntropyLoss

Batch Size为64时,在RTX 3060上单卡即可训练,显存占用大约4到5GB,如果显卡显存只有4GB,可以把Batch Size降到32,学习率也同步下调一半。

训练循环本身不复杂,关键是要保证训练和验证阶段前向计算状态切换正确。PyTorch里model.train()model.eval()会影响BatchNorm和Dropout的行为,用预训练模型时这些状态尤其重要。

for epoch in range(epochs): model.train() total_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) scheduler.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) val_acc = correct / total print(f"Epoch {epoch+1:02d} | Loss {total_loss/len(train_dataset):.4f} | Val Acc {val_acc:.4f}")

训练结束后用torch.save(model.state_dict(), "cub_resnet18.pth")保存权重,之后推理时先加载模型结构,再load_state_dict,这一点在踩坑部分还会强调。

5.2 训练曲线的走势与精度预期

模型在第1个epoch结束时验证精度大概在55%左右,到第5个epoch就能突破70%,之后提升速度明显放缓。40个epoch跑下来,验证集Top-1精度稳定在76%到78%之间。训练集精度大概到90%以上,说明模型仍然存在一定程度的过拟合,但验证精度能维持在76%以上,说明学习到的特征已经有很好的泛化能力。

有些同学复现时可能只有72%或者到80%,这都正常。精度浮动主要来自数据增强策略、随机种子和RandomResizedCrop的裁剪范围,略微调大裁剪尺度或者增加翻转之外的角度增强,都可能带来一两个点的波动。ResNet18这个模型在CUB上的合理精度区间就是70%到80%,低于65%说明训练配置大概率出了问题,高于82%则说明额外用了集成、注意力模块或更复杂的增强策略。

6. 踩坑实录:从loss不降到标签错位

6.1 忘了做ImageNet标准化,loss卡住不动

这是我第一次跑这个项目时遇到的最诡异的问题。训练loss在2.0左右徘徊,验证精度始终在5%以下,看起来像是模型没在学,实际上问题出在input预处理上。加载图片后直接缩放到224送进网络,没有做Normalize。预训练模型在ImageNet训练时,输入分布是均值为0.485、0.456、0.406,标准差为0.229、0.224、0.225的标准化分布,而原始图片像素值分布在0到1区间,分布完全错位。

排查过程其实很简单,把训练输入在送入网络前打印一下均值和方差,发现完全不符合预训练权重的期望,再加上看到验证精度接近随机猜测,基本就锁定了预处理环节。加上标准化后,loss立刻从2.0降到了0.8以下。这个坑非常隐蔽,因为不报错、不崩溃,看起来只是训练效果差,很多新手会把它误判成模型结构问题。

6.2 Windows下路径分隔符导致标签错位

第二个坑出现在数据加载环节。我在Linux上开发时一切正常,后来把代码挪到Windows上跑,发现准确率下降了10个百分点,仔细检查才发现问题出在这一行:

rel = os.path.relpath(path, images_dir)

在Windows上返回的相对路径分隔符是反斜杠\,而train_test_split.txt里用的是正斜杠/。字符串匹配不上时,我原代码的兜底逻辑又把它默认划到了测试集,结果就是训练集样本数大幅减少,验证集里混入了大量训练样本。评估时看到的精度虚高,但训练过程中的loss曲线异常跳变。

解决方式就是在生成相对路径时主动把分隔符统一替换成正斜杠:

rel = os.path.relpath(path, images_dir).replace("\\", "/")

这个问题也提醒我,凡是涉及外部配准文件的加载,一定要检查路径统一性和编码格式,不能指望不同操作系统自动兼容。

6.3 验证阶段忘了model.eval(),精度忽高忽低

训练结束后,我在评估阶段没有调用model.eval(),直接对测试集跑了一遍循环。得到的结果是每次运行精度都不一样,第一遍78%,第二遍74%,有时甚至差5个百分点以上。原因是模型处于训练模式下时,BatchNorm会使用当前batch的均值和方差来归一化,测试阶段batch内统计量波动比训练时数据增强后的分布更不稳定,导致输出乱跳。

正确做法是评估前必须调用model.eval(),让BN层使用训练阶段累计的全局均值方差。另外,包裹验证循环的torch.no_grad()也不能省,在model.eval()状态下虽然梯度计算不会影响BN,但如果不关闭梯度追踪,模型参数会保存大量梯度和计算图中间变量,显存占用上升,推理速度也明显下降。

6.4 Batch Size调小后,学习率不跟着调

训练到一半想增大Batch Size时发现显存不够,于是从64降到32,但忘了调整学习率。这里有个基本规律:Batch Size减小一半,梯度噪声变大,如果学习率不变,loss曲线会明显震荡,收敛速度变慢。我实测在Batch Size 32、学习率仍保持1e-3时,验证精度只能到73%左右,把学习率调成5e-4后精度就恢复到正常水平。

如果你用AdamW这类自适应优化器,对学习率的敏感度比SGD低一些,但也不能完全无视。迁移学习场景下,微调阶段的超参数调整必须遵循"动了一个变量,就重新验证一轮"的原则,不能想当然。

跑通这个ResNet18基线之后,后面再想提精度,我已经试过的方向包括换ResNet50做backbone、在fc前加一层1024维的瓶颈层、以及用标注框把鸟类区域先裁切出来再分类,这些做法各有收益也各有代价。先把这篇的基线和坑解决掉,后面的事情就顺理成章了。

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

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

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

立即咨询