
简介这份常见果蔬多类别图像分类数据集面向计算机视觉初学者与图像分类、分割网络改进的开发者提供已标注、可直接输入分类网络的训练素材省去自行采集与清洗的繁琐环节。压缩包共约2000个文件以1998张jpg图像为主体另含1个py可视化脚本与1个json类别说明文件整体约364.87MB采用7z格式打包。数据覆盖香蕉、苹果、梨、葡萄、橙子、黄瓜、胡萝卜、辣椒、洋葱、土豆等36个类别并已划分训练集、测试集与验证集各类图片分目录存放便于直接加载训练与评估。运行包内show脚本即可快速预览样本分布与图像质量json文件则给出完整类别映射方便核对标签。目前已有119人学习下载适合用于图像分类模型训练、迁移学习实验以及分割网络改进的对比验证也可作为课程设计与毕业设计的现成数据基础。1. 常见果蔬多类别图像分类数据集4,200 张已标注数据能跑出什么效果果蔬识别这件事听起来像是图像分类里的新手村任务但真做过的人都知道它比猫狗分类难缠得多。同一颗苹果红富士和黄元帅在低分辨率下几乎同色青椒和尖椒的轮廓差异在侧拍角度下会被完全抹平更别提超市货架上那种堆叠、遮挡、反光同时出现的场景。我拿到这份约 4,200 张、已标注的常见果蔬多类别图像分类数据集时第一反应不是数据量够不够而是类别边界干不干净——因为果蔬分类的翻车八成不是模型不行是类别定义本身就有歧义。这份数据集的核心价值在于已标注三个字。图像分类数据集下载容易但下载完发现标签是文件名、类别不均衡、脏图混入清洗成本往往超过训练成本。4,200 张这个量级属于单卡能跑、迁移学习能出活的甜点区既不至于小到过拟合无法收敛也不至于大到需要多机多卡。它适合三类人想快速验证图像分类算法改动的算法工程师、要做果蔬识别系统设计与实现的学生、以及需要一个小规模干净数据集来调试训练管线的从业者。下面我按先看清数据、再跑通基线、最后压榨精度的顺序把这条链路讲透。2. 先摸清数据底细4,200 张果蔬图的类别分布与清洗判断在写任何训练代码之前我会先花二十分钟把数据集翻一遍。这一步省不得因为果蔬数据集的坑几乎都藏在类别分布和图像质量里而不是在模型结构上。2.1 目录结构与标注格式的三种常见形态图像分类数据集最常见的组织方式是按类别分文件夹这也是最省事的格式因为 PyTorch 的ImageFolder和 TensorFlow 的image_dataset_from_directory都能直接吃。但实际拿到的数据集可能是三种形态之一处理方式完全不同形态典型结构处理方式按类分目录train/apple/xxx.jpg直接用 ImageFolder单目录 标签文件images/labels.csv自己写 Dataset 读 csv单目录 文件名编码apple_001.jpg从文件名解析类别我一般先跑一段脚本把结构摸清楚顺便统计每个类别的样本数。这一步能直接暴露类别不均衡问题——果蔬数据集里苹果香蕉这类常见品类往往有几百张而杨桃火龙果可能只有几十张。import os from collections import Counter from pathlib import Path root Path(fruits_veggies) # 数据集根目录 # 假设是按类分目录的结构遍历一级子目录 class_counts {} for cls_dir in sorted(root.iterdir()): if cls_dir.is_dir(): # 只统计常见图片后缀避免把 .DS_Store 之类算进去 imgs [f for f in cls_dir.iterdir() if f.suffix.lower() in (.jpg, .jpeg, .png, .bmp)] class_counts[cls_dir.name] len(imgs) total sum(class_counts.values()) print(f总类别数: {len(class_counts)}, 总图片数: {total}) for name, cnt in sorted(class_counts.items(), keylambda x: -x[1]): print(f{name:20s} {cnt:5d} {cnt/total*100:5.1f}%)这段脚本的关键在两点一是用后缀白名单过滤果蔬数据集里经常混入缩略图缓存或系统文件二是按数量降序打印方便一眼看出长尾类别。如果最大类和最小类样本数差距超过 10 倍后面训练就必须上重采样或类别权重否则模型会把小类直接忽略。2.2 用图像尺寸和通道统计揪出脏数据标注正确不代表图像干净。我遇到过数据集里混着灰度图、CMYK 模式的图、以及分辨率低到 32×32 的缩略图。这些图不处理训练时要么报错要么悄悄拉低精度。下面这段脚本统计尺寸分布和通道数是性价比最高的体检手段。from PIL import Image import numpy as np sizes, modes [], [] for cls_dir in root.iterdir(): if not cls_dir.is_dir(): continue for img_path in cls_dir.iterdir(): if img_path.suffix.lower() not in (.jpg, .jpeg, .png, .bmp): continue try: with Image.open(img_path) as im: sizes.append(im.size) # (宽, 高) modes.append(im.mode) # RGB / L / CMYK except Exception as e: print(f损坏文件: {img_path} - {e}) # 统计尺寸分布看是否高度不统一 w [s[0] for s in sizes] h [s[1] for s in sizes] print(f宽度 min/median/max: {min(w)}/{int(np.median(w))}/{max(w)}) print(f高度 min/median/max: {min(h)}/{int(np.median(h))}/{max(h)}) print(通道模式分布:, Counter(modes))参数说明im.size返回的是宽高元组注意 PIL 的顺序是 (宽, 高)别和 numpy 的 (行, 列) 搞混。如果modes里出现L灰度或CMYK统一转成 RGB 再训练否则三通道模型会直接报维度错误。尺寸中位数如果和最大值差得远说明数据集里混了大图训练前统一 resize 到 224×224 或 256×256 即可果蔬分类这个分辨率足够。提示清洗阶段不要急着删图先记录到一份bad_files.txt人工抽查几张再决定。果蔬数据集里有些脏图其实是难样本删了反而降低模型鲁棒性。3. 从零跑通果蔬分类基线迁移学习比从头训练省 90% 时间数据摸清之后下一步是尽快跑出一个能看的基线。我的原则是小数据集永远先上迁移学习不要一上来就自己搭网络从头训。4,200 张图从头训 ResNet大概率欠拟合或过拟合二选一而用预训练权重微调半天就能出结果。3.1 数据划分与增强管线的参数怎么设果蔬分类的增强策略和通用图像分类略有不同。颜色抖动ColorJitter要慎用因为果蔬的类别区分很大程度依赖颜色——你把一个红苹果的色调抖成偏黄模型就学废了。我的经验是几何增强翻转、随机裁剪可以放开用颜色增强幅度要小。import torch from torchvision import datasets, transforms from torch.utils.data import random_split, DataLoader # 训练增强几何为主颜色轻微扰动 train_tf transforms.Compose([ transforms.Resize((256, 256)), # 先放大再裁剪保留细节 transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 裁剪比例别太狠 transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), # hue 一定要小 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), # ImageNet 统计量 ]) # 验证/测试只做 resize 和归一化不做随机增强 val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) full datasets.ImageFolder(root, transformtrain_tf) n_val int(len(full) * 0.2) n_train len(full) - n_val # 固定随机种子保证每次划分一致方便复现 g torch.Generator().manual_seed(42) train_set, val_set random_split(full, [n_train, n_val], generatorg) val_set.dataset datasets.ImageFolder(root, transformval_tf) # 验证集换增强 train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers4)逻辑说明RandomResizedCrop的scale(0.7, 1.0)是关键参数果蔬图主体通常占画面比例大裁剪太狠会把主体裁掉0.7 是安全下限。ColorJitter里hue0.05是我踩过坑后的保守值早期设 0.1 导致青椒和黄瓜的类别混淆率明显上升。归一化用 ImageNet 统计量因为后面要用 ImageNet 预训练权重这一步必须对齐否则微调效果打折。3.2 用 ResNet18 微调冻结策略与学习率设置网络选型上果蔬分类这种中等难度任务ResNet18 或 EfficientNet-B0 足够。参数量再大4,200 张图喂不饱反而容易过拟合。我一般先冻结主干只训分类头再解冻全部微调这套两段式策略在小数据集上非常稳。import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(full.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 替换分类头 model model.to(device) # 第一阶段冻结主干只训分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False criterion nn.CrossEntropyLoss() # 分类头学习率可以大一些因为它是随机初始化的 optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10) def run_epoch(loader, trainTrue): model.train() if train else model.eval() total_loss, correct, n 0.0, 0, 0 with torch.set_grad_enabled(train): for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) loss criterion(out, labels) if train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (out.argmax(1) labels).sum().item() n imgs.size(0) return total_loss / n, correct / n for epoch in range(10): tr_loss, tr_acc run_epoch(train_loader, True) va_loss, va_acc run_epoch(val_loader, False) scheduler.step() print(fEpoch {epoch1:02d} | train {tr_loss:.3f}/{tr_acc:.3f} f| val {va_loss:.3f}/{va_acc:.3f})参数说明weight_decay1e-4是 AdamW 的常规值小数据集上正则化很重要。CosineAnnealingLR的T_max设成第一阶段 epoch 数让学习率平滑降到接近 0。第一阶段跑完后把requires_grad全部打开学习率降到 1e-4 再跑 10~15 个 epoch通常验证集准确率还能再涨 3~5 个百分点。如果第一阶段验证准确率就卡在某个值不动先别急着解冻检查是不是类别不均衡导致小类全被预测成大数类。注意random_split之后我重新赋值了val_set.dataset这是为了让验证集用不同的 transform。直接对 Subset 改 transform 不会生效因为它引用的是原始 dataset 对象这个坑我踩过不止一次。4. 果蔬分类避坑与排查5 个让精度卡住不动的真实原因模型跑起来只是开始真正耗时间的是精度上不去但不知道为啥。下面这 5 条是我在果蔬数据集上反复遇到的每条都按现象、原因、解决来写。4.1 验证准确率远高于测试准确率现象验证集 95%换一批真实拍摄的果蔬图掉到 70%。原因验证集和训练集来自同一批拍摄条件分布一致模型学到的是拍摄背景而非果蔬特征。解决划分数据时按拍摄批次或来源分组划分而不是随机划分同时加强背景相关的增强比如随机裁剪时允许裁到更多背景。4.2 某些类别永远预测不对现象混淆矩阵里青椒和黄瓜互相误判严重。原因这两类在颜色和形状上高度相似且数据集中可能存在标注不一致——同一张图有人标青椒有人标黄瓜。解决先人工抽查这两个类的样本统一标注标准如果确实难分考虑合并成瓜椒类或引入更细的纹理特征。4.3 训练 loss 震荡不收敛现象loss 忽高忽低准确率原地踏步。原因学习率太大或者 batch 里混入了尺寸异常的大图导致梯度爆炸。解决先把学习率降一个数量级试同时确认所有图都经过了统一 resize没有漏网的原始大图。4.4 小类别样本被完全忽略现象整体准确率还行但长尾类别召回率为 0。原因交叉熵损失被大类主导。解决用WeightedRandomSampler做重采样或者给CrossEntropyLoss传weight参数权重设为类别频率的倒数。from torch.utils.data import WeightedRandomSampler import numpy as np # 统计每个训练样本的类别计算采样权重 targets [full.classes.index(full.classes[label]) for _, label in train_set] # 简化写法实际按 Subset 索引取 class_cnt np.bincount(targets, minlengthnum_classes) class_w 1.0 / (class_cnt 1e-6) sample_w [class_w[t] for t in targets] sampler WeightedRandomSampler(sample_w, num_sampleslen(sample_w), replacementTrue) train_loader DataLoader(train_set, batch_size32, samplersampler, num_workers4)参数说明replacementTrue表示有放回采样小类会被反复抽到。num_samples设成训练集大小保证每个 epoch 看到的样本总量不变。4.5 推理时单张图预测结果和批量预测不一致现象单张图预测是苹果凑成 batch 预测变成梨。原因推理时忘了切model.eval()BatchNorm 还在用 batch 统计量。解决推理前务必model.eval()并用torch.no_grad()包住这两步是标配。5. 把 4,200 张果蔬图榨到极致难样本挖掘与置信度校准基线跑通、坑也排完接下来是怎么在有限数据上再抠几个点。我的习惯是回到数据本身做难样本挖掘而不是继续调网络结构——4,200 张的规模结构红利早就吃完了。具体做法用训练好的模型对训练集做一次推理把预测置信度高但预测错误的样本挑出来。这类样本是真正的难样本要么标注错了要么特征确实模糊。我会把它们单独存一个列表人工复核一遍确认标注无误后在下一轮训练里对这些样本做重复采样或者用更强的增强。这一步通常能带来 1~2 个点的提升代价只是半小时的人工。另一个技巧是置信度校准。果蔬分类落地时经常需要输出这张图是苹果的概率但 softmax 输出的概率往往偏乐观。我会在验证集上做温度缩放temperature scaling用一个标量 T 去平滑 logits让预测概率更接近真实正确率。# 温度缩放在验证集上搜索最优 T import torch.nn.functional as F logits_list, labels_list [], [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: logits_list.append(model(imgs.to(device)).cpu()) labels_list.append(labels) logits torch.cat(logits_list) labels torch.cat(labels_list) best_T, best_nll 1.0, float(inf) for T in np.arange(0.5, 3.0, 0.05): nll F.cross_entropy(logits / T, labels).item() if nll best_nll: best_nll, best_T nll, T print(f最优温度 T {best_T:.2f}, NLL {best_nll:.4f})逻辑说明温度 T 大于 1 会让概率分布更平缓降低过度自信。F.cross_entropy内部已经做了 log_softmax所以直接传logits / T即可。找到最优 T 后推理时把 logits 除以 T 再 softmax输出的概率就校准过了。这个技巧在需要设置置信度阈值的场景比如低于阈值转人工复核里特别有用。最后说个我自己的习惯每次在这个果蔬数据集上试完新想法我都会把配置、随机种子、验证准确率记到一张表里。果蔬分类的随机性不小同一个配置跑两次差 1 个点是常事不记录的话很容易把噪声当成改进。这套流程走下来4,200 张图在单卡上一天内就能从零到可用模型剩下的时间应该花在数据质量和难样本上而不是无止境地换 backbone。希望帮到你。本文还有配套的精品资源点击获取