ARTICLE DETAIL

资讯详情

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

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

176类森林树叶图像分类数据集:从数据划分到baseline实战 简介这份资源是面向计算机视觉初学者与图像分类实践者的森林树叶图像分类数据集共涵盖176个树叶类别已完成训练集与测试集划分可直接用于深度学习模型训练与算法验证。压缩包内共2000个文件以1998张jpg图像为主另附1个json字典文件记录176种树木树叶的英文标签映射以及1个可视化py脚本整体大小约133.63MB。目录下分为train与test两个子目录训练集14755张、测试集3598张按类别文件夹存放使用ImageFolder即可直接加载无需额外预处理也可作为YOLOv5分类数据集使用。可视化脚本随机传入4张图片即可展示并保存到当前目录无需修改即可运行。目前已有744人学习下载适合希望快速上手图像分类任务、验证模型效果或开展迁移学习实验的读者参考使用。1. 176 类森林树叶图像分类数据集从拿到手到跑通第一个 baseline上周帮一个做生态监测的朋友看模型他拿 30 万张手机拍的树叶照片训 ResNet验证集准确率卡在 61% 死活上不去。我让他把类别分布导出来一看——176 个类里最多的类有 4000 多张最少的只有 12 张而且训练集和验证集是随机切的同一片叶子正反面被切进了两边。这不是模型的问题是数据集划分的问题。今天要拆的这份森林树叶图像分类数据集恰好把这件事做对了176 个细粒度类别已经完成训练/验证/测试划分目录结构直接对齐ImageFolder和tf.keras.utils.image_dataset_from_directory的读取约定。它解决的不是有没有数据的问题而是数据能不能直接进训练循环、划分是否可信的问题。适合三类人想练细粒度分类但懒得自己爬数据的学生、需要快速验证 backbone 迁移效果的算法工程师、以及做植物识别产品原型的小团队。下面按这份资源长什么样 → 怎么接进训练管线 → 划分和增强上容易翻车的地方 → 怎么把 176 类压到能跑动的程度这条线走一遍。2. 数据集结构与读取方式先搞清楚目录约定再写 DataLoader拿到一个分类数据集第一件事不是写模型是tree一下看目录。这份数据的组织方式决定了你后面用ImageFolder还是自己写Dataset也决定了类别索引到标签名的映射怎么存。2.1 目录层级与 ImageFolder 的隐式约定常见做法是root/类别名/图片文件这种两层结构torchvision.datasets.ImageFolder会按文件夹名的字典序自动生成class_to_idx。这里有个血泪经验字典序不是自然序class_10会排在class_2前面。如果你后面要把预测结果映射回中文树名必须把dataset.class_to_idx存下来别自己另写一套映射。import os from torchvision import datasets, transforms DATA_ROOT ./forest_leaves_176 # 先确认划分目录是否存在 for split in [train, val, test]: p os.path.join(DATA_ROOT, split) print(split, exists:, os.path.isdir(p)) # ImageFolder 读取类别索引按文件夹名字典序生成 train_ds datasets.ImageFolder( os.path.join(DATA_ROOT, train), transformtransforms.ToTensor() ) print(类别数:, len(train_ds.classes)) print(前 5 个类别:, train_ds.classes[:5]) print(样本数:, len(train_ds)) # 关键把 class_to_idx 落盘推理时对齐用 import json with open(class_to_idx.json, w, encodingutf-8) as f: json.dump(train_ds.class_to_idx, f, ensure_asciiFalse, indent2)这段代码做了三件事验证三个划分目录都在、用ImageFolder建立索引、把class_to_idx序列化。参数上唯一要注意的是transform——这里先用ToTensor()占位真正的增强管线在 3.2 节展开。len(train_ds.classes)应该输出 176如果不是说明目录里混进了非类别文件夹比如.ipynb_checkpoints或__MACOSX这是解压后最常见的污染源。2.2 用 image_dataset_from_directory 走 TensorFlow 路线如果团队用 Keras读取逻辑一样但接口换成了image_dataset_from_directory。它默认按文件名排序同样存在字典序问题而且label_mode的选择会直接影响损失函数。import tensorflow as tf IMG_SIZE (224, 224) BATCH 32 train_ds tf.keras.utils.image_dataset_from_directory( ./forest_leaves_176/train, image_sizeIMG_SIZE, batch_sizeBATCH, label_modeint, # 配合 sparse_categorical_crossentropy shuffleTrue, seed42 ) val_ds tf.keras.utils.image_dataset_from_directory( ./forest_leaves_176/val, image_sizeIMG_SIZE, batch_sizeBATCH, label_modeint, shuffleFalse # 验证集不要打乱方便对齐混淆矩阵 ) print(类别名:, train_ds.class_names[:5])label_modeint输出整数标签配sparse_categorical_crossentropy如果改成categorical就是 one-hot配categorical_crossentropy。这两个配错不会报错但 loss 会一直不降属于典型的玄学不收敛来源。shuffleFalse用在验证集上是习惯目的是让预测顺序和文件顺序一致画混淆矩阵时不用再查索引。2.3 先做一次类别分布体检176 类几乎不可能完全均衡。在写训练脚本之前花两分钟统计每个类的样本数能省掉后面几小时的调参。from collections import Counter import matplotlib.pyplot as plt targets [y for _, y in train_ds.samples] # torchvision 的 .samples 是 (path, label) cnt Counter(targets) counts [cnt[i] for i in range(len(train_ds.classes))] print(最多:, max(counts), 最少:, min(counts)) print(均值: %.1f % (sum(counts)/len(counts))) plt.hist(counts, bins30) plt.title(train per-class sample count) plt.xlabel(samples); plt.ylabel(num classes) plt.savefig(class_dist.png, dpi120)如果max/min超过 20 倍后面 4.1 节的采样策略就必须上否则模型会明显偏向头部类。这一步不产出任何模型但它是判断要不要做重采样的唯一依据。3. 训练管线搭建从增强策略到第一个能跑的 baseline数据读进来只是开始真正决定 baseline 能不能在 176 类上跑到可用水平的是输入分辨率、增强强度和 backbone 选择这三件事的配合。3.1 输入分辨率与 backbone 的匹配树叶分类是细粒度任务叶脉纹理、边缘锯齿这些判别特征在高分辨率下才明显。但 176 类意味着显存压力不小。常见做法是训练用 224×224配合RandomResizedCrop让模型见到不同尺度如果显存够比如 24G 卡可以上 320 或 384细粒度任务上通常有 2~4 个点的提升。backbone输入尺寸参数量176 类预期 top-1显存(bs32)ResNet-5022425M78~83%~6GEfficientNet-B02245.3M76~81%~4GConvNeXt-Tiny22428M82~86%~8GViT-B/16 (预训练)22486M84~88%~14G表里的预期区间是基于同类细粒度数据集的常见经验值不是这份数据的实测承诺——具体数字取决于划分质量和类间相似度。选型逻辑很简单显存紧就 EfficientNet-B0追求精度且能等就 ConvNeXt-Tiny想试 transformer 就 ViT-B/16 但必须用预训练权重从零训基本没戏。3.2 增强管线哪些增强对树叶有效哪些是负作用树叶图像的类内差异主要来自拍摄角度、光照和背景类间差异来自形状和纹理。所以几何增强要克制颜色增强可以放开。from torchvision import transforms train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), # 尺度抖动别低于 0.5 transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), # 小角度树叶有方向性 transforms.ColorJitter(0.3, 0.3, 0.3, 0.05), # 亮度/对比度/饱和度放开 transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]) ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]) ])RandomRotation(15)而不是 90是因为叶子有明确的生长方向大角度旋转会制造不存在的形态。ColorJitter的第四个参数是 hue只给 0.05色相大幅偏移会让不同树种的颜色特征混淆。RandomResizedCrop的scale下限设 0.6再低会把叶脉切没反而伤害细粒度特征。验证集只用 resize centercrop不做任何随机增强这是评估可信度的底线。3.3 一个能直接跑的 baseline 训练脚本把前面的读取和增强串起来配一个带 warmup 的余弦退火就是一份可复现的起点。import torch, torch.nn as nn, torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, models, transforms from torch.optim.lr_scheduler import CosineAnnealingLR device cuda if torch.cuda.is_available() else cpu train_ds datasets.ImageFolder(./forest_leaves_176/train, transformtrain_tf) val_ds datasets.ImageFolder(./forest_leaves_176/val, transformval_tf) train_ld DataLoader(train_ds, batch_size32, shuffleTrue, num_workers8, pin_memoryTrue) val_ld DataLoader(val_ds, batch_size32, shuffleFalse, num_workers8, pin_memoryTrue) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) model.fc nn.Linear(model.fc.in_features, 176) # 换成 176 类 model model.to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1) # 细粒度任务加 label smoothing optimizer optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for x, y in train_ld: x, y x.to(device), y.to(device) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval(); correct total 0 with torch.no_grad(): for x, y in val_ld: x, y x.to(device), y.to(device) pred model(x).argmax(1) correct (pred y).sum().item(); total y.size(0) print(fepoch {epoch}: val_acc{correct/total:.4f})几个参数值得说label_smoothing0.1在类别多、标注可能有噪声时能稳住训练AdamW的lr3e-4是微调预训练模型的常用起点从零训要降到 1e-3 以下num_workers8按机器核数调设太大会在 epoch 切换时卡顿。跑完 30 epochResNet-50 在这类数据上通常能到 80% 上下如果只有 60% 多先回去看第 4 章的划分和采样问题。4. 避坑与排查176 类细粒度分类最容易翻车的五个地方这一章是我自己踩过和帮人排查过的真实问题每条按现象 → 原因 → 解决写遇到对应症状直接对号入座。4.1 验证集准确率远高于测试集现象val_acc 85%test_acc 只有 70%差距超过 10 个点。原因验证集和测试集分布不一致或者验证集被无意中参与过模型选择比如反复用 val 调超参导致 val 虚高。另一个常见原因是划分时按图片随机切但同一片叶子的多张照片被切到了不同集合。解决先确认这份数据的划分是不是按个体/拍摄批次切的而不是纯随机。如果是随机切测试集的可信度要打问号。自己复现时用GroupShuffleSplit按拍摄对象分组切分别用train_test_split。4.2 训练 loss 正常下降但准确率不动现象loss 从 5.2 降到 1.8但 train_acc 一直在 1% 左右。原因标签和输出维度对不上或者label_mode与损失函数配错。比如 Keras 里用了label_modeint却配categorical_crossentropy或者 PyTorch 里model.fc没改成 176 类。解决打印一个 batch 的y.max()和model(x).shape[-1]前者应该小于 176后者应该等于 176。这两个数一对问题立刻现形。4.3 显存溢出发生在第二个 epoch现象第一个 epoch 跑得好好的第二个 epoch 开始报 CUDA out of memory。原因验证阶段没加torch.no_grad()计算图一直累积或者num_workers太大每个 worker 都缓存了一份数据。解决验证循环强制套with torch.no_grad():num_workers从 4 开始试别一上来就 16。另外pin_memoryTrue只在 GPU 训练时有意义CPU 训练要关掉。4.4 类别极度不均衡导致尾部类全错现象整体准确率 80%但混淆矩阵里后 50 个类几乎全预测成头部类。原因176 类里长尾分布严重交叉熵被头部类主导。解决三种做法按代价排序——给CrossEntropyLoss传weight按类频率倒数、用WeightedRandomSampler过采样尾部类、或者对尾部类做针对性增强。我一般先上 sampler因为它不改损失函数调起来直观。4.5 推理时类别索引对不上现象训练时准确率很高部署后预测结果全是乱的。原因推理脚本重新用ImageFolder建了索引但推理目录里只有部分类别class_to_idx和训练时不一致。解决训练时把class_to_idx.json存下来推理时直接加载这个文件不要重新扫描目录。这是 2.1 节强调落盘的原因。5. 把 176 类压到能跑动分层采样与混淆矩阵驱动的迭代技巧176 类在单卡上跑完整训练不是不行但迭代一次要等太久调参效率低。我的习惯是先做一个可快速迭代的子集用混淆矩阵找出最难分的类对再决定要不要上更重的模型。具体做法是先用全部数据训一个轻量 backboneEfficientNet-B0224 输入拿到 baseline 混淆矩阵然后按混淆矩阵把类别聚成若干超类在超类层面先调通增强和采样策略最后再回到 176 类精调。import numpy as np from sklearn.metrics import confusion_matrix # 假设已经收集了验证集的 preds 和 labels cm confusion_matrix(labels, preds, labelslist(range(176))) np.fill_diagonal(cm, 0) # 抹掉对角只看错分 top_pairs np.dstack(np.unravel_index(np.argsort(cm.ravel())[::-1][:20], cm.shape))[0] for a, b in top_pairs: print(f{train_ds.classes[a]} - {train_ds.classes[b]}: {cm[a,b]})这段代码输出错分最多的 20 个类对。拿到之后常见做法是只对这几对做针对性处理要么补数据要么在损失里给这几对加 margin。别一上来就换大模型细粒度任务里数据侧的收益往往比模型侧大。另一个实用技巧是分层学习率backbone 用 1e-5分类头用 1e-3。176 类的分类头参数量不小从头学需要更大学习率而预训练 backbone 只需要微调。head_params list(model.fc.parameters()) backbone_params [p for n, p in model.named_parameters() if not n.startswith(fc.)] optimizer optim.AdamW([ {params: backbone_params, lr: 1e-5}, {params: head_params, lr: 1e-3}, ], weight_decay1e-4)这样配完通常比统一学习率快 5~8 个 epoch 收敛。验证方法很简单跑 5 个 epoch看 val_acc 有没有在前 3 个 epoch 就超过统一学习率的同期水平。从那以后我每次接一个新的多类数据集都强制先跑一遍类别分布统计和混淆矩阵再动模型——这两步花不了十分钟但能省掉后面反复试错的几个小时。希望帮到你。本文还有配套的精品资源点击获取
返回列表