多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

176类森林树叶图像分类数据集实战:从数据到baseline

176类森林树叶图像分类数据集实战:从数据到baseline 简介这份资源是面向图像分类初学者与算法工程师的森林树叶识别数据集共176个类别已完成训练集与测试集划分可直接用ImageFolder加载无需额外预处理适合快速验证分类模型或作为课程实验、竞赛练手的数据基础。包内共2000个文件以1998张jpg图像为主另附1个json字典文件记录176种树叶的英文标签映射以及1个可视化py脚本随机传入4张图片即可展示并保存到当前目录无需修改即可运行。压缩包约133.63MBdata目录下train含14755张、test含3598张类别结构清晰也可直接用于yolov5分类任务。目前已有744人学习下载读者可借此省去数据采集与清洗成本专注模型搭建与调参同时借助字典文件与可视化脚本快速核对类别分布与样本质量。1. 176类森林树叶图像分类数据集从拿到手到跑出第一个baseline森林树叶图像分类这件事真正卡住大多数人的从来不是模型结构而是数据。你搜「图像分类数据集下载」能翻出一堆链接但真拿到一个已经划分好、类别数够多、每类样本不至于只有三五张的树叶数据集其实不多。176类森林树叶图像分类数据集的价值就在这它把最耗时的采集、清洗、划分三步替你做完了你拿到的是一个可以直接喂给训练脚本的目录结构而不是一堆需要自己按类别重组的原始图片。这个数据集适合三类人一是想验证自己图像分类模型在细粒度任务上表现的算法工程师树叶类间差异小、类内差异大是天然的细粒度测试场二是做林业、植物识别相关应用落地的开发者176类基本覆盖了温带常见树种三是刚入门图像分类、想找一个比CIFAR-10更真实但又不至于像ImageNet那样跑不动的练手数据集的同学。下面我按「先看清数据长什么样再选模型再跑通训练最后避坑」的顺序把整个流程拆开讲。2. 先摸清176类森林树叶数据集的目录结构与类别分布拿到一个图像分类数据集第一件事不是写模型是搞清楚它的组织方式。很多「已做数据集划分」的数据集划分方式和你手头的训练框架默认读取方式不一定对得上直接开跑大概率报路径错误或者类别错乱。2.1 典型目录结构与划分比例这类森林树叶数据集常见的组织方式是按train/val/test三个子目录划分每个子目录下再按类别名建文件夹。结构大致如下forest_leaves_176/ ├── train/ │ ├── Acer_campestre/ │ │ ├── img_0001.jpg │ │ └── ... │ ├── Quercus_robur/ │ └── ...共176个类别目录 ├── val/ │ └── ...同样176个类别目录 └── test/ └── ...同样176个类别目录划分比例常见的是 7:1.5:1.5 或 8:1:1。你需要确认的是val 和 test 里每个类别是否都存在。有些数据集划分时做了随机抽样导致某些稀有类别在 val 里一张都没有训练时验证集算出来的准确率会虚高或者直接报除零错误。用下面这段脚本快速统计每个类别的样本数确认划分是否均衡import os from collections import defaultdict def count_per_class(root, split): split_dir os.path.join(root, split) counts {} for cls in sorted(os.listdir(split_dir)): cls_dir os.path.join(split_dir, cls) if os.path.isdir(cls_dir): n len([f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .jpeg, .png))]) counts[cls] n return counts root ./forest_leaves_176 for split in [train, val, test]: c count_per_class(root, split) total sum(c.values()) print(f{split}: {len(c)} classes, {total} images, fmin{min(c.values())}, max{max(c.values())}, fmean{total/len(c):.1f})这段代码的逻辑很直接遍历每个 split 下的类别目录统计图片文件数量。重点看三个指标——类别数是否都是176、min 是否过小小于5就要警惕、max/min 比值是否超过10。如果某个类别训练集只有个位数样本后面训练时需要考虑过采样或者类别权重。2.2 图片尺寸、格式与命名规范树叶图像数据集常见的图片格式是 JPG尺寸不统一长边从 256 到 1024 都有。这不影响训练因为训练时统一 resize 就行但你要注意两点一是如果原图分辨率很低比如短边小于100像素resize 到 224 会糊得厉害细粒度特征丢失严重二是如果图片是 RGBA 四通道 PNG直接读入会报通道数不匹配。快速检查图片尺寸分布和通道数from PIL import Image import os, random root ./forest_leaves_176/train all_imgs [] for cls in os.listdir(root): cls_dir os.path.join(root, cls) if os.path.isdir(cls_dir): for f in os.listdir(cls_dir): if f.lower().endswith((.jpg, .jpeg, .png)): all_imgs.append(os.path.join(cls_dir, f)) sample random.sample(all_imgs, min(200, len(all_imgs))) sizes, modes [], set() for p in sample: with Image.open(p) as im: sizes.append(im.size) modes.add(im.mode) ws [s[0] for s in sizes] hs [s[1] for s in sizes] print(fwidth min/median/max: {min(ws)}/{sorted(ws)[len(ws)//2]}/{max(ws)}) print(fheight min/median/max: {min(hs)}/{sorted(hs)[len(hs)//2]}/{max(hs)}) print(fcolor modes: {modes})如果 modes 里出现RGBA或L训练前统一转成 RGB。如果中位数尺寸低于 200建议把输入分辨率降到 160 或 192别硬上 224。2.3 类别标签的两种映射方式训练框架读取数据时类别标签有两种常见映射按文件夹名排序后转索引或者读取一个单独的classes.txt。你需要确认数据集有没有附带类别映射文件。如果没有ImageFolder 这类接口会按字母序自动生成索引这本身没问题但你要把这个映射存下来推理时才能把索引还原成类别名。import json, os train_dir ./forest_leaves_176/train classes sorted([d for d in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, d))]) class_to_idx {c: i for i, c in enumerate(classes)} with open(class_to_idx.json, w, encodingutf-8) as f: json.dump(class_to_idx, f, ensure_asciiFalse, indent2) print(fsaved {len(class_to_idx)} classes)这个映射文件后面推理和部署都要用现在花十秒存下来比后面翻车了再回头找强。3. 用 torchvision 在本地跑通第一个训练从 DataLoader 到 176 类输出层数据摸清楚了接下来跑一个能出结果的 baseline。我一般先用 ResNet-50 或 EfficientNet-B0 跑一轮不追求最高精度目的是确认整条链路通、显存吃得下、每个 epoch 时间可接受。3.1 构建 Dataset 与 DataLoader 的关键参数torchvision 的ImageFolder能直接读上面那种目录结构配合transforms做增强。训练集和验证集的 transform 要分开写训练集加随机裁剪和翻转验证集只做 resize 和归一化。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), # 树叶方向不固定垂直翻转也合理 transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf 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]), ]) root ./forest_leaves_176 train_ds datasets.ImageFolder(f{root}/train, transformtrain_tf) val_ds datasets.ImageFolder(f{root}/val, transformval_tf) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers8, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers8, pin_memoryTrue) print(ftrain: {len(train_ds)}, val: {len(val_ds)}, fclasses: {len(train_ds.classes)})参数说明RandomResizedCrop的scale(0.6, 1.0)比默认的 (0.08, 1.0) 更保守因为树叶细粒度特征集中在叶片形状和纹理上裁太狠会把关键区域切掉。RandomVerticalFlip对树叶是合理的因为叶片在自然状态下朝向随机。num_workers设成 CPU 核数的 2/3 左右太多反而会因为进程调度拖慢。3.2 模型选择与 176 类输出层的修改ResNet-50 是稳妥的起点预训练权重能显著加快收敛。把最后的全连接层换成 176 类输出import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) model.fc nn.Linear(model.fc.in_features, 176) model model.to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)label_smoothing0.1在类别数多、部分类别样本少的情况下能抑制过拟合。学习率 3e-4 配 AdamW 是细粒度分类的常用起点如果你用 SGD改成 0.01 配 momentum 0.9。3.3 训练循环与验证指标训练循环本身不复杂关键是每个 epoch 后在验证集上算 top-1 和 top-5 准确率。176 类任务里 top-5 比 top-1 更能反映模型的真实区分能力。def evaluate(model, loader, device): model.eval() top1, top5, total 0, 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) _, pred out.topk(5, dim1) correct pred.eq(labels.view(-1, 1)) top1 correct[:, :1].sum().item() top5 correct.any(dim1).sum().item() total labels.size(0) return top1 / total, top5 / total for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() running_loss loss.item() * imgs.size(0) scheduler.step() acc1, acc5 evaluate(model, val_loader, device) print(fepoch {epoch1}: loss{running_loss/len(train_ds):.4f} ftop1{acc1:.4f} top5{acc5:.4f})如果显存不够把 batch_size 降到 32同时把学习率按比例降到 1.5e-4。如果每个 epoch 时间超过 10 分钟检查 num_workers 是否设得太低或者图片是否没做预 resize——直接在原图上做 RandomResizedCrop 比先缩到 256 再裁要慢不少。4. 森林树叶分类的避坑与排查从准确率异常到显存溢出这一章是我自己踩过的坑按「现象 → 原因 → 解决」写你遇到类似情况可以直接对号入座。4.1 训练准确率很高但验证准确率极低现象训练集 top-1 到 95% 以上验证集只有 20% 出头loss 震荡。原因最常见的是训练集和验证集的类别索引不一致。ImageFolder 按文件夹名排序生成索引如果 train 和 val 的类别目录名有细微差异比如大小写、下划线 vs 空格两个 split 的索引映射会错位。另一个原因是验证集 transform 里误加了数据增强。解决打印train_ds.class_to_idx和val_ds.class_to_idx确认完全一致。不一致的话手动指定class_to_idx参数或者统一用同一个映射文件。验证集 transform 只保留 resize、centercrop、ToTensor、Normalize。4.2 某些类别准确率始终为 0现象整体准确率还行但混淆矩阵里某几个类别几乎全错。原因这些类别训练样本太少或者和其他类别视觉上高度相似比如同属不同种的树叶。176 类里出现这种情况很正常。解决先统计这些类别的样本数如果训练集少于 20 张考虑过采样或者用WeightedRandomSampler给稀有类别更高采样权重。如果样本数不少但还是错说明特征区分度不够换更强的 backboneEfficientNet-B3 或 ConvNeXt-Tiny或者提高输入分辨率到 288。4.3 训练中途显存溢出现象前几个 epoch 正常突然报 CUDA out of memory。原因如果用了可变尺寸输入或者动态 batch某个 batch 的图片特别大就会爆。更常见的是验证阶段没加torch.no_grad()验证集前向传播也建了计算图。解决验证和推理一律包在with torch.no_grad():里。如果还爆用torch.cuda.empty_cache()在每个 epoch 结束后清一次缓存或者把 batch_size 再降一档。4.4 数据加载成为训练瓶颈现象GPU 利用率长期低于 50%每个 epoch 大部分时间花在等数据上。原因num_workers设得太小或者图片存放在机械硬盘上随机读取慢。解决num_workers设成 CPU 物理核数pin_memoryTruepersistent_workersTrue。如果数据集不大比如总共几万张可以先把所有图片解码后存成内存映射的 numpy 数组或者 LMDB训练时直接读速度能快好几倍。4.5 测试集准确率远低于验证集现象验证集 80%测试集只有 60%。原因验证集和测试集的分布不一致。有些数据集划分时没有做分层抽样导致测试集里难样本比例偏高。另一个可能是你在调参过程中反复用验证集选模型间接过拟合了验证集。解决确认测试集和验证集的类别分布是否接近。如果差异大以测试集为准重新评估。调参时用交叉验证或者留出独立的验证集别反复在同一份验证集上试几十组参数。5. 把 176 类树叶分类推到 90% 的几个实用技巧baseline 跑通之后想再往上提点精度下面几个技巧是我实际用过有效的。渐进式分辨率训练。先用 160 分辨率训 20 个 epoch再切到 224 微调 10 个 epoch。低分辨率阶段模型学的是全局形状高分辨率阶段学的是纹理细节比一上来就 224 收敛更稳。切换时记得同步调整 batch_size分辨率翻倍显存大概涨 1.8 倍。Mixup 和 CutMix 交替用。176 类细粒度任务里CutMix 通常比 Mixup 效果好因为它强迫模型关注局部判别区域。但 CutMix 对稀有类别不友好我一般前 2/3 epoch 用 CutMix后 1/3 关掉让模型在真实分布上收尾。import numpy as np def cutmix(imgs, labels, alpha1.0): lam np.random.beta(alpha, alpha) idx torch.randperm(imgs.size(0)).to(imgs.device) _, _, H, W imgs.shape cut_rat np.sqrt(1 - lam) cut_h, cut_w int(H * cut_rat), int(W * cut_rat) cy, cx np.random.randint(H), np.random.randint(W) y1, y2 max(cy - cut_h//2, 0), min(cy cut_h//2, H) x1, x2 max(cx - cut_w//2, 0), min(cx cut_w//2, W) imgs[:, :, y1:y2, x1:x2] imgs[idx, :, y1:y2, x1:x2] lam 1 - (y2 - y1) * (x2 - x1) / (H * W) return imgs, labels, labels[idx], lamloss 计算时用lam * CE(out, labels_a) (1-lam) * CE(out, labels_b)。alpha 设 1.0 是常用值想更激进可以到 2.0。测试时增强TTA。推理时对同一张图做原图、水平翻转、垂直翻转三次前向softmax 概率平均。176 类任务上 TTA 通常能涨 1-2 个点代价是推理时间翻三倍。如果部署延迟敏感可以只做水平翻转的 2x TTA。冻结 backbone 先训分类头。如果训练集不大每类几十张先用预训练 backbone 冻结只训 fc 层 5 个 epoch再解冻全部微调。这样能避免随机初始化的分类头在早期把 backbone 的预训练特征带偏。最后说一个我自己的习惯每次跑完实验把配置文件、类别映射、最优 epoch 的权重、验证集混淆矩阵存到同一个目录下命名带上日期和关键参数。树叶分类这种细粒度任务后面大概率要反复调没有后悔药可吃只有实验记录能救你。希望帮到你。本文还有配套的精品资源点击获取
返回列表