简介这是一份面向CUB-200-2011鸟类数据集的CNN分类训练资源适合正在学习图像识别、打算用经典细粒度数据集练手的开发者与研究者。资源基于Python构建包含完整训练流程数据加载、模型定义与训练入口能够帮助读者快速搭建针对200种鸟类的分类实验环境。压缩包按代码结构组织共5个文件其中3个Python脚本承担主要逻辑2个pyc文件为缓存编译产物整体仅15KB非常轻量便于直接阅读与修改。当前已有648人学习/下载适合用于课程设计、论文对比实验或入门深度学习分类任务。通过这份资源使用者可以了解CUB数据集的读取方式、CNN模型的组织形式以及训练脚本的基本写法在此基础上替换自己的数据或网络结构即可扩展出更多实验降低从零起步的时间成本。1. CUB200 不是普通分类200 类细粒度识别先看这份资源能干什么做鸟类识别的人大概都绕不开 CUB-200-2011 这个数据集200 种鸟类、约 1.2 万张图片宏观上比 ImageNet 小得多难度却一点不低。不同鸟种之间可能就是胸脯颜色、翼尖纹路的差别类间差异极小这也是细粒度图像识别的经典入门场景。这份资源里是一套用 CNN 分类 CUB_200 数据集中 200 种鸟类的完整训练代码包含 train.py 训练入口、model.py 网络定义、data.py 数据加载三个核心文件环境是 Python 3.6跑起来就能从零把 CUB200 的 200 类分类模型训练出来。如果你已经在用 PyTorch 但还没碰过细粒度数据集或者刚下载完 CUB-200-2011 正愁怎么组织训练它可以给你省掉半天整理时间。2. 数据集与代码结构CUB-200-2011 的文件组织与 train.py 入口2.1 先认识 CUB-200-2011 的目录和标签文件CUB-200-2011 官方解压后主要包含 images按 200 个类目子目录存放图片、classes.txt类名清单、image_class_labels.txt每张图的类别编号、train_test_split.txt每张图属于训练集还是测试集以及 attributes 等标注文件。这份资源里的 data.py 核心职责就是把这些文件读进来转换成 PyTorch 能直接消费的数据集对象。理解 data.py 之前我建议你把官方目录和这份代码放在同一级路径关系在 data.py 里写死了一版自己改时最常翻车的也是路径。常见做法是先把 train_test_split.txt 读成「图片名到归属」的映射再按训练/测试划分复制或软链图片组织成 ImageFolder 标准结构。下面这段代码演示了如何把官方划分映射到 image 文件路径# 把官方 train_test_split.txt 转成按目录划分的数据集 mkdir -p data/train data/test while read image_path is_train; do class_id$(grep ${image_path} image_class_labels.txt | awk {print $1}) class_name$(sed -n ${class_id}p classes.txt) if [ $is_train -eq 1 ]; then mkdir -p data/train/${class_name} ln -sf /path/to/CUB_200_2011/images/${image_path} data/train/${class_name}/ else mkdir -p data/test/${class_name} ln -sf /path/to/CUB_200_2011/images/${image_path} data/test/${class_name}/ fi done train_test_split.txt这段脚本把官方平铺的 images 目录转成 PyTorch 的 ImageFolder 布局train 与 test 都按「类名/图片」组织后续给 torchvision.datasets.ImageFolder 设置 root 参数就能用。注意 class_name 里带空格比如 Black footed Albatross目录名含空格没问题但你在 Linux 命令行里操作时要给变量加引号上面代码已经处理了。is_train1 表示训练样本is_train0 表示测试样本这是官方划分的固定语义不要自己再随机切分否则和论文指标没法对比。如果你不想建软链data.py 里另一种典型读法是把 train_test_split.txt 直接作为数据集过滤条件。下面给出 data.py 中常见的构造方式也是这份资源里最可能采用的实现from torch.utils.data import Dataset from PIL import Image import os class CUBDataset(Dataset): def __init__(self, root, split_file, is_trainTrue, transformNone): self.root root # 指向 CUB_200_2011/images self.transform transform self.samples [] with open(split_file, r) as f: for line in f: img_path, flag line.strip().split() if int(flag) 1 and is_train: self.samples.append(img_path) elif int(flag) 0 and not is_train: self.samples.append(img_path) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path self.samples[idx] label int(img_path.split(/)[0]) - 1 # 目录名即类别编号 1~200 img Image.open(os.path.join(self.root, img_path)).convert(RGB) if self.transform: img self.transform(img) return img, label这段自定义 Dataset 直接从 train_test_split.txt 过滤样本省去了重新组织目录的步骤。图片路径形如 001.Black_footed_Albatross/Black_Footed_Albatross_0001_796111.jpg目录名以三位数字开头也就是类别编号。label 需要减一因为官方类别从 1 开始编号而 PyTorch 训练要求从 0 开始。如果你发现训练出来的模型在 classes.txt 里错位一位多半是这里没处理。2.2 train.py 的入口骨架train.py 是整个资源的入口结构上依次是解析参数、构建数据加载器、初始化模型、定义优化器、循环训练与验证、保存模型。阅读时你只要关心几个主循环变量batch_size、epochs、learning_rate、checkpoint 保存路径。下面是最常出现的训练主循环骨架import torch import torch.nn as nn from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in dataloader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / total, correct / totaltrain.py 的关键是每轮都把数据搬到 deviceGPU 或 CPU、每个 batch 清零梯度、前向算损失、反向传播、更新参数。main 函数里还会包一层 epoch 循环并在每轮结束后切到验证模式计算 val accuracy。新手容易漏的是 model.train() 和 model.eval() 的切换特别是模型里有 BatchNorm 和 Dropout 时忘了切会导致验证集指标虚高或略微飘忽。2.3 pyc 文件的作用与坑压缩包里能看到 data.cpython-36.pyc、model.cpython-36.pyc这是 Python 3.6 环境运行后生成的字节码缓存。它说明作者确实以 Python 3.6 跑通过这个项目但对使用者来说这是最常见的坑别人机器上残留的 pyc 可能和你当前代码版本不一致Python 会优先加载 pyc。如果你改完 data.py 发现行为没变化先删掉pycache再跑。这是我复现任何带 pyc 项目时的第一步。3. 模型与数据管线ResNet 做特征提取DataLoader 的增强与归一化3.1 为什么选预训练 ResNet 而不是从零训练CUB-200-2011 只有约 1.2 万张训练图片分到 200 类平均每类 30 张训练样本。从零训练一个深层 CNN 在这个数据量级上极容易欠拟合——模型参数远多于可用样本训练损失降不下去验证集准确率卡在 30% 左右是常态。所以 model.py 里的模型定义常见做法是加载 ImageNet 预训练权重把最后一层全连接改成 200 类。这属于迁移学习里的特征提取微调范式前几层学到的是通用边缘、纹理、颜色特征鸟类专用特征体现在高层语义上。细粒度分类比普通分类更依赖这种迁移。200 种鸟可能在 ImageNet 里只覆盖了一部分但预训练模型对「羽毛质感」「眼睛周围纹理」「喙的形状」这类中层特征有很强的表达能力微调后收敛速度明显快于从零训练。如果训练资源充足还可以把 backbone 解冻用更小学习率整体微调但这份资源的定位显然是快速跑通流程。3.2 model.py 的模型构造方式model.py 中最常见的实现是直接用 torchvision 的 ResNet50替换全连接层。下面给出等价实现import torch.nn as nn import torchvision.models as models def build_model(num_classes200, pretrainedTrue): model models.resnet50(pretrainedpretrained) in_features model.fc.in_features # ResNet50 是 2048 model.fc nn.Linear(in_features, num_classes) return model参数说明num_classes 固定 200pretrainedTrue 表示加载在 ImageNet 上预训练好的权重。model.fc.in_features 读取的是原全连接层输入维度ResNet50 对应 2048ResNet18 对应 512这样写的好处是换 backbone 时不用手改数字。你也可以用 resnet18 或 resnet101我一般先跑 ResNet50显存不够再退到 ResNet18省得一上来就 OOM 折腾人。如果你是后来打开 model.py 看到里面不是 torchvision而是自己写了一个类继承 nn.Module也不要慌。多数改良版本会在 ResNet 后加一个 Global Average Pooling 和两层全连接改成瓶颈结构比如 Linear(2048, 512) ReLU Dropout Linear(512, 200)这种结构在数据增强不足时泛化更好。换模型时唯一要保证的是输出维度必须等于 200。3.3 数据增强与归一化参数data.py 的 transform 部分不会做太激进的操作。CUB 图像基本都是鸟居中的自然照片最有效的增强是随机裁剪、水平翻转和颜色抖动。下面是一组经过验证的配置也是我在类似细粒度项目里惯用的参数from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.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]) ])RandomResizedCrop 的 scale 我调到 0.7 到 1.0比默认的 0.08 到 1.0 更保守因为切得太狠鸟的主体容易只截到一小块羽毛反而制造噪声样本。ColorJitter 三个增强幅度都取 0.2太大会让白鹭变成粉鹭属于细粒度分类特别需要注意的边界。val_transform 不做随机增强只做 Resize 到短边 256、CenterCrop 到 224这样测试时每个样本只跑一次前向结果稳定可复现。Normalize 的 mean 和 std 必须沿用 ImageNet 统计值因为预训练模型就是在该分布上训练的。这里有个新手必踩的坑把 Normalize 忘了或者顺序放错比如放在 ToTensor 前面会直接引发张量类型错误。接着是 DataLoader 的配置batch size 和 num_workers 两个参数最影响训练体验train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)shuffleTrue 只在训练集开测试集如果也 shuffle验证曲线会抖得没法看。num_workers 按 CPU 核数放4 或 8 都行Windows 下要注意 num_workers 大于 0 时某些 IDE 会报 multiprocessing 错误解决方法是把训练代码放进 ifname main或者干脆改成 0。pin_memoryTrue 配合 GPU 训练能省掉一部分 CPU 到 GPU 的拷贝等待。4. 训练主流程损失、优化器、学习率调度与断点续练4.1 损失函数与优化器选择CUB200 是单标签分类类别互斥标准做法用 CrossEntropyLoss。它内部先把模型输出的 logits 做 softmax 转成概率分布再计算交叉熵所以模型最后一层不需要额外接 softmax。如果你的代码里有 nn.LogSoftmax 再接 NLLLoss 也可以但 CrossEntropyLoss 一步到位更不容易错。使用方式很简单criterion nn.CrossEntropyLoss()优化器这块我给两个方向。数据量小、预训练权重为重时用 Adam 加较小学习率收敛快不容易在初始阶段震荡SGD 加 Momentum 最终精度通常更高但调参门槛高一些。这份资源的场景建议先用 Adam把流程跑通看指标再换 SGD 精调。下面是我惯用的一组配置optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, verboseTrue)4.2 学习率调度与训练轮数细粒度分类常见训练轮数是 30 到 50。轮数太少最后一层刚收敛 backbone 还没跟上轮数太多验证集准确率会在某个值附近打转甚至掉点。这里有个更关键的设置冻结 backbone 只训练新增的全连接层跑 5 到 10 轮再把 backbone 解冻整体微调。两份资源里的 train.py 如果没做这个区分我建议你手动拆成两阶段第一阶段的代码大致如下# 阶段一只训练最后一层 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True # 注意优化器要在设置 requires_grad 之后新建 optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)第二阶段再把所有 requires_grad 置 True学习率下调到 1e-4 继续训练。注意优化器必须等 requires_grad 设置完再创建否则 optimizer 里已经捕获了所有参数冻结不会生效。很多人在这段代码上翻了车训练日志显示 loss 从第一步就在降但验证集纹丝不动十有八九是过滤条件没生效。整体训练轮数上资源里作者一般会写 epoch30。以 ResNet50 加单张 1080Ti 为例batch size 32 时一个 epoch 约 3 到 5 分钟30 个 epoch 两小时左右跑完。如果只有 CPU一个 epoch 可能需要二十分钟以上建议先把 num_workers 调到 0、batch size 调小先验证流程正确再谈全量训练。4.3 训练中的观察指标与模型保存训练脚本里每一轮至少打印三个数字train loss、train accuracy、validation accuracy。我还会额外打印当前学习率因为 ReduceLROnPlateau 静默降 lr不打印你都不知道它在哪个 epoch 动了刀。好的实践经验是每隔一个 epoch 保存一次 checkpoint文件名带 epoch 和 val accbest_acc 0.0 for epoch in range(start_epoch, total_epochs): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device) val_acc evaluate(model, val_loader, criterion, device) scheduler.step(val_acc) # 只在验证集最优时保存防止后期过拟合覆盖好模型 if val_acc best_acc: best_acc val_acc torch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), best_acc: best_acc, }, checkpoints/best_model.pth)注意 scheduler.step 的入参是 val_acc 而不是 val_loss如果 monitor 模式和入参不匹配ReduceLROnPlateau 的降速会和你想的不一样。保存 optimizer_state_dict 是为了断点续训续练时加载后 schedule 状态也一并恢复只有加载 model_state_dict 的话可以将就继续评估但不建议续训。这个训练脚本跑完正常会在验证集上得到 70% 到 80% 的准确率这个数字对 CUB200 的纯图像分类属正常水平别指望一两小时内到 90%那需要更强 backbone 和更精细的多阶段训练。5. 训练 CUB 的避坑记录类别不平衡、过拟合、显存溢出与坏图5.1 验证集准确率一直上不去卡在 30% 附近现象训练集 loss 稳步下降验证集准确率始终徘徊在 30% 到 40%和随机猜 0.5% 相比是高了但远低于同类项目报告的 70% 以上。原因我遇到的最常见情况是 label 没减一类别编号从 1 到 200而 CrossEntropyLoss 期望标签从 0 开始。第二个常见原因是模型没加载预训练权重torchvision 里 pretrained 参数写成了 False 或者干脆自己手写了网络从零训练。1.2 万张图训练一个深层网络拟合能力接近随机。解决先打印 dataloader 返回的 label 范围确认最小值是 0 最大值是 199再打印 model.fc 的输出层维度最后在模型构建处打印 model 加载的权重是否来自 torchvision 缓存文件。这三步检查一遍能排查掉 90% 的「跑得动但没效果」。5.2 训练到一半报 CUDA out of memory现象前几个 epoch 正常跑到第几个 batch 时突然报 CUDA out of memory程序终止。原因CUB 图片大多是高分辨率原图RandomResizedCrop 缩到 224 前会先把整图解码到内存里DataLoader 虽然只放缩后的图但中间张量峰值很高。更隐蔽的是验证集评估时忘了 torch.no_grad()梯度图全部保留显存在验证阶段持续累积。解决先把 batch_size 从 32 降到 16 或 8是最快的办法再把 num_workers 降到 2减少 CPU 侧积压最后在评估函数里包一层 with torch.no_grad()。如果你用了更大的输入分辨率比如把 224 提到 320 想涨点显存占用是按平方涨的32 的 batch 在 224 下能跑到 320 可能 8 都吃力别硬撑。5.3 训练过程中出现 broken image 或 Pillow 解码报错现象某个 epoch 跑到固定位置报 cannot identify image file 或 image file is truncated每次报错的样本可能不同。原因CUB200 从官网下载的压缩包完整性通常没有问题但解压过程、磁盘坏道或某次非正常中断可能导致个别 JPEG 文件截断。Pillow 默认遇到截断图直接抛异常导致一个样本毁掉整个 epoch。解决在 Dataset 的getitem里捕获 OSError 和 PIL.UnidentifiedImageError返回相邻样本替代。常见做法是在init里扫描一遍全部图片打开失败的直接过滤掉代价是第一次加载数据集会多花几十秒换回训练过程稳定。具体过滤逻辑如下from PIL import Image, ImageFile ImageFile.LOAD_TRUNCATED_IMAGES True # 允许 Pillow 读截断图 valid_samples [] for img_path in all_samples: try: with Image.open(os.path.join(root, img_path)) as im: im.verify() # 只校验不完整解码 valid_samples.append(img_path) except (OSError, IOError): print(fskip broken image: {img_path})注意 verify() 后要重新 openverify 会搞坏文件句柄所以上面代码里用 with 打开后立即校验通过后再把路径加入列表。LOAD_TRUNCATED_IMAGES 打开以后某些严重损坏的图读出来是半张灰图视觉上不检查根本发现不了所以配合过滤逻辑更稳妥。5.4 加载 pyc 缓存导致代码改了不生效现象改了 model.py 里的结构重新运行 train.py打印出来的网络结构还是旧的或者报错信息指向一个早已删除的函数。原因pycache目录下的 data.cpython-36.pyc 和 model.cpython-36.pyc 是 Python 3.6 编译缓存Python 会按 mtime 判断 pyc 是否过期。但如果两台机器时间戳差异大或者 pyc 是从压缩包直接解压出来的时间戳可能比 py 新Python 就优先加载 pyc。解决解压后第一步把pycache整个删掉后面每次改代码前手动删一次。用命令 find . -type d -namepycache-exec rm -rf {} 可以批量清。如果之后还遇到诡异问题再检查 PYTHONDONTWRITEBYTECODE 环境变量是否被设置成了 1。5.5 训练集和测试集划分不对导致准确率虚高现象验证集准确率异常高接近 95%但拿去跑真正的测试图片发现很差怀疑数据集有泄漏。原因自己在 data.py 里用 random.split 切了训练测试而 CUB200 官方划分中同一只鸟的不同图片全部分在同一侧且按物种分层。随机分割可能让同一只鸟的图片同时出现在训练和测试模型记住的是个体特征而不是物种特征测试时遇到没见过的新个体就露馅。解决严格使用 train_test_split.txt 作为划分唯一依据千万不要在代码里再引入随机种子切分。CUB200 的划分本来就考虑了个体和类别的平衡这是官方基准对比的前提。我一开始为了「提高」训练占比自己重切过一次验证集好看得不得了后来提交实验才意识到完全不可信从那以后我每次拿到数据集都强制走一遍官方划分检查流程。6. 从训练到验证混淆矩阵、Top-5 准确率与导出预测结果模型训练完只得到一个 best_model.pth 还不够我一般会写一个小脚本做三件事算 Top-1 和 Top-5 准确率、画混淆矩阵找出最易混的鸟种、把预测结果存成 CSV 供后续分析。CUB200 的标准评估指标就是 Top-1 和 Top-5因为有些鸟种从人眼看都分不清Top-5 更能反映模型实际可用性。import torch import pandas as pd model.eval() top1_correct 0 top5_correct 0 total 0 results [] with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, top5_pred outputs.topk(5, 1) top1_correct (top5_pred[:, 0] labels).sum().item() for i, label in enumerate(labels): top5_correct (label in top5_pred[i]).item() total labels.size(0) results.extend(zip(labels.cpu().tolist(), top5_pred[:, 0].cpu().tolist())) print(fTop-1 Accuracy: {top1_correct / total:.4f}) print(fTop-5 Accuracy: {top5_correct / total:.4f}) df pd.DataFrame(results, columns[true_label, pred_label]) df.to_csv(cub200_pred_results.csv, indexFalse)这段脚本输出两个指标的同时把每张测试图的真实标签和 Top-1 预测导出后续可以基于 CSV 去查哪一类的召回率低、被哪些类混淆。混淆矩阵的绘制需要 sklearn 的 confusion_matrix 和 matplotlib200 类的矩阵画出来做成 20 x 10 的热力图主要看两类同一属内部互相混淆的鸟比如各种啄木鸟以及颜色纹理相近的鸟。如果你发现混淆集中的鸟种恰恰是你没有做数据增强的类别说明增强还不够可以单独针对这些难分种类增加随机裁剪范围。这里有个经验数值可以参考ResNet50 加官方划分Top-1 在 70% 到 80%、Top-5 在 90% 以上属于健康水平。如果 Top-1 冲到 85% 以上很可能是训练集和测试集由同源图片组成导致的乐观评估实际部署时要警惕。验证完成以后我习惯把 best_model.pth 连同 classes.txt 一起留档并把 CSV 里误判样本的图片路径抽出来单独放一个文件夹下次调数据增强时直接拿这些样本做回归测试。从那以后我每次训 CUB200 都强制走一遍「官方划分确认、pyc 清理、验证集 no_grad、指标导出 CSV」这四条再也没出现过模型训完但拿不出可信报告的情况。希望这份资源对你的 CUB200 细粒度分类项目也有同样的帮助。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?