ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

细粒度图像分类实训:CUB-200-2011从88%到95%的调优指南

细粒度图像分类实训:CUB-200-2011从88%到95%的调优指南 简介面向数字图像处理课程综合实训的细粒度图像分类项目基于CUB-200-2011鸟类数据集源自作者97分的期末大作业。项目适合计算机相关专业学生及有图像分类实战需求的学习者完整覆盖图像预处理、特征提取、模型选择与训练、模型评估与调参优化的技术流程目标是将细粒度分类准确率提升至95分以上。压缩包共14个文件大小仅4.76MB包含Python源码脚本、PDF格式的项目报告与细分类讲解、PPT答辩幻灯片、docx标注说明以及README使用文档结构清晰可直接运行。目前已有104人学习下载。通过该项目可拿到完整可复现的分类方案理解双线性卷积网络与迁移学习两种建模思路还能参考97分项目的报告撰写和展示材料作为课程设计、毕业设计或学科竞赛的起步模板节省从零搭建环境与调试代码的时间。1. 从85分到95分细粒度图像分类实训的真正门槛在哪里数字图像处理综合实训里细粒度图像分类选CUB-200-2011数据集几乎是标配——200种鸟类、近1.2万张图像类别之间只差在喙形、翼斑和眼色上肉眼都容易认错。做这个实训最典型的场景是用ResNet50随便finetune一把验证集能到88%~90%然后卡住不动了。目标分数超95分难的不是“跑通”而是“把每一分都从模型结构、数据管线和训练策略里抠出来”。这篇文章就是照着CUB-200-2011这个数据集把从预处理到推理的完整路径拆开讲哪些参数直接决定你能不能跨过95分这条线哪些坑会让你的分数一夜回到解放前。2. 吃透CUB-200-2011目录结构、标注格式与输入管线搭建2.1 CUB-200-2011的文件布局四个你必须知道的文件CUB-200-2011解压后是一个CUB_200_2011目录里面没有现成的train/val子目录所有图片平铺在images/下按类别子目录组织。你真正要处理的是四个核心标注文件缺一个训练就跑不起来。CUB_200_2011/ ├── images.txt # 图片文件名列表 ├── image_class_labels.txt # 每张图片的类别标签1-200 ├── train_test_split.txt # 每张图片属于train(1)还是test(0) ├── bounding_boxes.txt # 每张图片的目标框 [x, y, width, height] └── images/ # 200个类别子目录共11788张图片images.txt的行号就是图像的ID其他所有文件都以这个ID为索引对齐。比如bounding_boxes.txt每行是image_id x y w htrain_test_split.txt每行是image_id is_training。这个设计很关键你不能按文件名匹配必须按行号对齐。我一般会先写一段脚本把id和文件名映射关系固化下来顺带统计train/test各自的数量。标准划分是5994张训练、5794张测试比例接近1:1如果发现数量对不上大概率是读文件时索引错位了。提示bounding_boxes.txt里的坐标是相对于原图的像素值不是归一化坐标。用的时候要么除以图片宽高要么在Dataset里直接用像素坐标裁剪。2.2 用PyTorch写Dataset边界框裁剪与归一化细粒度分类和普通图像分类最大的区别在于目标在整张图中的占比差异很大背景干扰严重。直接用整图训练模型很容易学背景纹理而不是鸟的特征。所以第一步是“目标裁剪”按边界框把鸟裁出来再resize到模型输入尺寸。import torch import torchvision.transforms as T from PIL import Image from torch.utils.data import Dataset class CUBDataset(Dataset): def __init__(self, root, split, img_size448, use_bboxTrue, transformNone): self.root root self.split split # train or test self.use_bbox use_bbox self.img_size img_size # 读取四个标注文件 with open(f{root}/images.txt) as f: images [line.strip().split() for line in f.readlines()] with open(f{root}/image_class_labels.txt) as f: labels [int(line.strip().split()[1]) - 1 for line in f.readlines()] with open(f{root}/train_test_split.txt) as f: splits [int(line.strip().split()[1]) for line in f.readlines()] with open(f{root}/bounding_boxes.txt) as f: bboxes [line.strip().split() for line in f.readlines()] self.samples [] for img_id, (_, img_path), label, is_train, bbox_line in zip( range(1, len(images) 1), images, labels, splits, bboxes): if (split train and is_train 1) or (split test and is_train 0): # bbox: [x, y, width, height] x, y, w, h map(float, bbox_line[1:]) self.samples.append((img_path, label, (x, y, w, h))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label, bbox self.samples[idx] img Image.open(f{self.root}/images/{img_path}).convert(RGB) if self.use_bbox: x, y, w, h bbox img img.crop((int(x), int(y), int(x w), int(y h))) img img.resize((self.img_size, self.img_size)) if self.transform: img self.transform(img) return img, torch.tensor(label, dtypetorch.long)这段代码的核心逻辑有三处。self.samples里同时存路径、标签和bbox一次对齐后面就不需要反复读文件了。use_bbox控制是否裁剪这是后面做消融实验的开关别写死。img_size448不是随手填的——CUB里很多鸟的原始边界框长宽比接近1:1448是兼顾细节和显存的折中值比224能多保留约4倍的像素信息对细粒度分类的提升非常明显。2.3 数据增强策略哪些该开、哪些该关CUB的痛点不是数据量太少而是类间差异太小。数据增强的度要拿捏好普通分类里常用的RandomResizedCrop在这里可能帮倒忙——它会随机裁掉目标的一部分而鸟的判别特征往往就集中在喙或者翼尖那几块像素上。train_transform T.Compose([ T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees10), T.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) test_transform T.Compose([ T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomRotation(degrees10)是小角度旋转模拟拍摄角度轻微变化的同时不会把鸟翻转成不自然姿态。ColorJitter里hue色调只给0.05因为鸟类识别里颜色是关键特征色调扰动过大等于篡改标签。测试阶段不做任何随机增强只做归一化这是为了保证推理结果的稳定性和可复现性。这里有个常见分歧要不要在训练时对bbox区域做随机缩放我试过把use_bbox和RandomResizedCrop组合下限是精度没变上限是掉了0.5个百分点。原因在于CUB的bbox标注精度本身存在噪声再叠加随机裁剪会导致目标区域不稳定。实训项目里如果你不确定标注质量最稳妥的做法是严格按bbox裁不额外加随机裁剪。3. 模型选型与迁移学习从ResNet50到ViT的细粒度改造3.1 细粒度分类为什么让通用模型集体翻车CUB-200-2011的难点非常特殊200个类别全是鸟类间差异可能只集中在翅膀上的一小撮羽毛颜色或者嘴型的弧度上。通用分类模型学到的特征是“全局表征”比如整体轮廓、主导色块这些对区分“狗 vs 猫”有效但对区分“黑嘴海雀 vs 白令鸬鹚”远远不够。细粒度分类的解决方案在思路上大概分为三类基于部件检测的、基于注意力定位的、以及用高分辨率输入的。实训场景里部件检测需要额外的关键点标注CUB自带的零件标注往往超出实训范围和应用场景最成熟的做法是把注意力机制嵌入主干网络让模型自己学会“看哪里”。3.2 用torchvision的Transformer主干替换ResNet如果你的开发环境允许使用预训练权重我建议直接从Swin Transformer或者ViT入手。ViT在CUB上有一个天然优势它的自注意力机制天然适合捕捉远距离部件之间的关系比如“翅膀末端颜色 头部纹理”这种跨区域的判别线索。import torch.nn as nn from torchvision import models def build_model(num_classes200, backbonevit_b_16, pretrainedTrue): if backbone vit_b_16: model models.vit_b_16(weightsmodels.ViT_B_16_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.heads.head.in_features model.heads.head nn.Linear(in_features, num_classes) elif backbone swin_t: model models.swin_t(weightsmodels.Swin_T_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.head.in_features model.head nn.Linear(in_features, num_classes) elif backbone resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model替换分类头时注意ViT的最后一层在model.heads.headSwin的最后一层在model.head这两者结构不同但都叫“head”写错了会直接报维度不匹配的错误。保留除head外的全部预训练参数这是迁移学习的核心——ImageNet上亿张图学到的通用特征对鸟类边缘、纹理的底层响应依然有效。我个人实测的结论供参考ViT-B/16在输入448时精度上限最高能到93%以上Swin-T收敛更快但上限略低ResNet50最稳但需要更长的训练周期才能摸到91%。如果你想冲95分ViT是性价比最高的起点。3.3 迁移学习的层次化学习率策略直接对整个模型用同一个学习率是新手最容易犯的错。预训练骨干网络已经接近局部最优给它大学习率等于用随机梯度把预训练权重冲散。正确做法是把骨干和分类头拆开、设置不同的学习率。def get_optimizer(model, backbone_lr2e-5, head_lr2e-4, weight_decay1e-4): backbone_params [] head_params [] for name, param in model.named_parameters(): if head in name: head_params.append(param) else: backbone_params.append(param) optimizer torch.optim.AdamW([ {params: backbone_params, lr: backbone_lr}, {params: head_params, lr: head_lr} ], weight_decayweight_decay) return optimizerbackbone_lr2e-5和head_lr2e-4相差10倍这是经过多轮验证的经验值。骨干层只需要微调分类头是从零初始化的需要更激进的更新速度。weight_decay用1e-4比通用分类的5e-4要小——CUB数据集本身只有6000张训练图过强的正则化会抑制模型拟合细微特征的能力。哪天你发现训练loss降得极慢先查学习率而不是模型结构。4. 训练策略与调参把验证集从88分推到95分4.1 学习率调度余弦退火的优势与阈值设置训练细粒度模型学习率调度和损失函数设计是决定上限的两根支柱。CUB最容易出现的现象是前10个epoch验证集一路涨到88%之后开始震荡怎么调都突破不了90%。这通常意味着优化器在鞍点附近反复试探需要的是“跳出局部最优”而非“降低学习率”。from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts scheduler CosineAnnealingWarmRestarts( optimizer, T_010, # 第一个周期的epoch数 T_mult2, # 每个周期后周期翻倍 eta_min1e-6 # 最低学习率 )CosineAnnealingWarmRestarts和普通ReduceLROnPlateau最大的区别是学习率会周期性回升。这种“热重启”能让模型跳出尖锐的局部极小值换一个更平滑的收敛点——这对细粒度分类很重要因为尖锐极小值往往对应着对训练集特征的过度拟合泛化能力差。T_010和T_mult2意味着学习率先在10个epoch内从峰值降到谷底然后跳回高点并开始一个20个epoch的新周期。训练60个epoch就能经历完整的三轮“升温-降温”过程。eta_min1e-6设得很低目的是让模型在周期末段能以极小的步长精细逼近最优解。4.2 损失函数与标签平滑交叉熵损失是细粒度分类的默认选项但在类别极度相似的数据集上硬标签one-hot会迫使模型为“错误但相近”的类别输出极端置信度。标签平滑label smoothing能缓解这个问题它把正确类别的目标概率从1.0降为1 - epsilon余下的概率均匀分给所有类别。import torch.nn as nn loss_fn nn.CrossEntropyLoss(label_smoothing0.1)label_smoothing0.1是一个经验值。太大会导致模型对正确类别的置信度不足太小则没有效果。用这个损失函数替换普通CrossEntropyLossCUB验证集通常能涨0.5~1个百分点而且在测试阶段不会出现“某个类别预测概率0.99 其余全0.005”这种过度自信的表现。4.3 训练超参数速查表这里给出一份可以直接照抄的超参数配置覆盖了从数据加载到优化调度的全过程。每项参数的取值都对应着上面分析过的原理不要只抄数值不记逻辑。参数名推荐值说明输入尺寸448x448bbox裁剪后直接resize保留细节Batch Size16ViT/ 64ResNet50ViT受显存限制无法开大batchEpochs60配合cosine restarts3个完整周期优化器AdamW比SGD收敛快适合迁移学习骨干学习率2e-5预训练权重只能微调分类头学习率2e-4新初始化层更新速率更快学习率调度CosineAnnealingWarmRestartsT_010, T_mult2Label Smoothing0.1缓解类别相似导致的过拟合权重衰减1e-4数据量少正则化不宜过强关于batch size有两点补充。ViT-B/16在448分辨率下单卡16G显存开batch size16就已经接近极限了ResNet50可以开到64实测梯度更稳定但每个epoch的迭代次数变少需要适当增加epochs来补偿。如果你的显卡是24Gbatch size可以翻倍但学习率要同步调整——简单做法是batch翻倍时把学习率乘1.5倍这是线性缩放规则的简化版。4.4 如何判断模型是欠拟合还是过拟合做完上面这些配置最关键的环节是训练过程中的监控。我在实训里见过太多人看到train loss持续下降就松了口气结果验证集已经在悄悄掉点了。现象诊断对策train loss下降慢且val loss不降学习率太小或骨干被冻结检查是否忘了解冻骨干层把backbone_lr调到5e-5train loss趋近0val loss上升过拟合增强数据增强强度或增加weight_decay到5e-4val accuracy在45-50附近震荡数据泄漏train/test混在一起检查train_test_split.txt是否正确按行号对齐val accuracy在10个epoch内暴增后不动分类头过拟合骨干没适配调低head_lr到1e-4增加backbone_lrtrain和val loss同步下降但acc不动类别不平衡或评估代码bug检查每个类别的recall分布第5种情况容易被忽略。CUB虽然总体类别均衡但有些类别只有30张训练图个别类别可能出现模型完全预测错误的情况。正确做法是打印混淆矩阵找出预测精度最低的Top-10类别针对性检查这些类别的图像质量和bbox标注是否有问题。5. 低分翻车排查细粒度实训中5个让分数断崖下跌的坑5.1 数据泄漏train和test混在一起分数虚高成笑话现象验证集准确率莫名其妙到了99%比任何公开榜单都高。原因数据集按文件名遍历时训练集中混入了部分或全部测试图片。CUB的文件索引是ID1到11788train和test是乱序分布在ID空间中的如果直接从images.txt取前6000张当训练集就会把大量测试图放进训练集。解决严格按train_test_split.txt的标记过滤。如果发现自己的训练准确率和验证准确率几乎一样高先检查这条。顺便在训练之前单独确认train/test的数量之和等于11788。5.2 不做bbox裁剪模型学了一堆背景纹理现象验证集在88%附近反复横跳无论怎么调学习率都上不去。原因CUB原始图像里鸟类主体平均只占图像的30%-40%剩下全是天空、树叶、水面。模型很容易学到“蓝色像素多就是某类鸟”这种背景捷径。解决在Dataset里use_bboxTrue按bounding_boxes.txt裁剪后再训练。只做这一步就能把精度从88%推到91%以上。这里有一个细节裁剪后要做边缘扩展比如扩5%因为标注框有时会贴着鸟的身体边缘把喙或尾羽截断扩一圈可以让模型看到完整的轮廓。5.3 自己搭CNN从零训练收敛慢且上限低现象训练20个epoch后验证集只有70%模型还在缓慢爬升。原因CUB只有6000张训练图从零训练一个深度CNN数据量不够学出泛化性好且稳定的特征。解决加载ImageNet预训练权重冻结前几层卷积只微调后面的特征层并用2e-4~2e-5的分层学习率。这是做细粒度分类最快提升到90%的路径不要自己设计网络结构。5.4 学习率开太大loss曲线像心电图上蹿下跳现象loss曲线不是平滑下降而是断崖式暴跌后突然暴涨验证集波动剧烈。原因骨干网络学习率用了和分类头一样的值比如3e-4预训练权重被大梯度破坏模型表达能力骤降。解决把骨干学习率降到1/10甚至1/20。ViT对学习率尤其敏感我用Swin-T时的经验值是骨干最多给5e-5再高就会让预训练层的特征响应出现异常。5.5 评估代码写错Top-1还是Top-5没搞清楚现象训练精度高测试时手工验证发现模型明明认对了鸟代码却给出错误的评估分数。原因底层的评估逻辑问题不改肉眼可见的错误。最常见的是把CUB的标签当成全局ID1到11788而不是类别ID1到200导致所有标签整体偏移200倍模型输出和标签永远错位。解决标签统一从0开始编号减1image_class_labels.txt里的标注范围是1到200PyTorch的CrossEntropyLoss要求标签从0到199忘记减1的话所有样本都会被打上错误的类别标签。6. 冲上95分的关键技巧测试时增强 多模型投票的低成本组合到了这个阶段你的模型验证集应该在92%~94%之间。要从这里再往上走核心思想是降低测试阶段的随机性——把“只看一次”改成“看多次然后投票”也就是测试时增强Test-Time AugmentationTTA。CUB的测试图只有一个视角但模型如果在训练时见过翻转、小幅旋转和轻微颜色偏移的图像测试时把这几种变换分别推理一次取平均概率就能抵消单次推理时的偶然偏差。实现方式如下def predict_with_tta(model, img, device): model.eval() tta_transforms [ T.Compose([T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])]), T.Compose([T.RandomHorizontalFlip(p1.0), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])]), T.Compose([T.RandomRotation(degrees5), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])]), ] with torch.no_grad(): probs [] for t in tta_transforms: x t(img).unsqueeze(0).to(device) logits model(x) probs.append(torch.softmax(logits, dim1)) avg_probs torch.mean(torch.cat(probs, dim0), dim0) pred avg_probs.argmax(dim1).item() return pred这里做了3次推理原图、水平翻转、小角度旋转。torch.mean求的是概率平均而不是logits平均原因是概率空间直接对应置信度混合样本之间的置信度冲突能被平均过程平滑掉。从经验上看3次TTA约能稳定贡献0.3~0.8个百分点的提升推理时间变为原来的3倍但精度受益明显。如果再想往上走可以考虑用两个不同初始化种子训练同一个ViT模型推理时把两者的softmax概率相加。这种做法和TTA组合起来在CUB上通常能额外带来0.4~0.5个点的提升。但代价是显存翻倍、训练时间翻倍属于性价比偏低的“保底手段”——如果你的验证集已经到94%强烈建议先做TTA这个投入产出比是最高的。回到开头那个问题95分难不难做完整套流程你会觉得难的不是学模型不是写代码而是“每一步都做对”。我自己第一次跑CUB裁掉bbox之后忘了调整归一化参数精度反而掉了2个点排查了大半天才意识到训练和测试的预处理不一致。从那以后我把“先跑通10个epoch看训练loss是否下降”当成一切调参的前提模型结构、数据管线、评估逻辑三处都对精度是水到渠成的事。希望这些踩过的坑能帮你在实训里少走一段弯路。本文还有配套的精品资源点击获取
返回列表