☰
176类森林树叶图像分类数据集实战:从数据到baseline
2026/10/1 19:29:50 网站建设 项目流程

简介:这份资源是面向图像分类初学者与算法工程师的森林树叶识别数据集,共176个类别,已完成训练集与测试集划分,可直接用ImageFolder加载,无需额外预处理,适合快速验证分类模型或作为课程实验、竞赛练手的数据基础。包内共2000个文件,以1998张jpg图像为主,另附1个json字典文件记录176种树叶的英文标签映射,以及1个可视化py脚本,随机传入4张图片即可展示并保存到当前目录,无需修改即可运行。压缩包约133.63MB,data目录下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, ' f'min={min(c.values())}, max={max(c.values())}, ' f'mean={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(f'width min/median/max: {min(ws)}/{sorted(ws)[len(ws)//2]}/{max(ws)}') print(f'height min/median/max: {min(hs)}/{sorted(hs)[len(hs)//2]}/{max(hs)}') print(f'color 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', encoding='utf-8') as f: json.dump(class_to_idx, f, ensure_ascii=False, indent=2) print(f'saved {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', transform=train_tf) val_ds = datasets.ImageFolder(f'{root}/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=8, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=8, pin_memory=True) print(f'train: {len(train_ds)}, val: {len(val_ds)}, ' f'classes: {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(weights=models.ResNet50_Weights.IMAGENET1K_V2) model.fc = nn.Linear(model.fc.in_features, 176) model = model.to(device) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)

label_smoothing=0.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, dim=1) correct = pred.eq(labels.view(-1, 1)) top1 += correct[:, :1].sum().item() top5 += correct.any(dim=1).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(f'epoch {epoch+1}: loss={running_loss/len(train_ds):.4f} ' f'top1={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给稀有类别更高采样权重。如果样本数不少但还是错,说明特征区分度不够,换更强的 backbone(EfficientNet-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_memory=True,persistent_workers=True。如果数据集不大(比如总共几万张),可以先把所有图片解码后存成内存映射的 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, alpha=1.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], lam

loss 计算时用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 的权重、验证集混淆矩阵存到同一个目录下,命名带上日期和关键参数。树叶分类这种细粒度任务,后面大概率要反复调,没有后悔药可吃,只有实验记录能救你。希望帮到你。

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

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

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

立即咨询