
简介一份面向图像分类任务学习与算法验证的常见果蔬多类别数据集覆盖香蕉、苹果、梨、葡萄、橙子、胡萝卜、辣椒、洋葱、土豆等36个类别共约4200张已标注图像。数据经过统一预处理可直接作为分类网络输入并已划分训练集、测试集与验证集同类图片集中存放便于加载与评估。压缩包内共2000个文件其中1998张jpg图片构成图像主体1个json文件提供类别映射1个Python脚本用于数据集可视化整体约364.87MB。目前已有119人学习下载。借助资源中的show脚本可快速浏览各类别图像json文件可查看完整类别清单适合正在做图像分类毕设或复现CNN模型的开发者使用能省去标注和整理数据的繁琐环节。1. 先看清4200张果蔬图像分类数据集到底能解决什么问题手头接到一个果蔬识别需求时最尴尬的不是模型选型而是数据从哪来。自己拍几百张不够用去爬网图又带一堆水印和无关背景标注更是耗掉两三天。如果你也卡在这那么这份“常见果蔬多类别图像分类数据集已标注约4,200张数据”就值得花十分钟认真盘一盘。它解决的正是“从零起步做图像分类”的第一道坎数据集已经完成类别标注你不需要自己打开标注工具逐张框选拿过来就能拆训练集、跑模型、看指标。适合的人群很明确正在做课程设计、竞赛原型验证、农业App MVP或者想先评估“果蔬分类到底能做到什么精度”的工程师。它不会让你一步登天但能把从数据到模型的时间压缩到一两天以内。2. 果蔬分类数据集的长什么样标签分布、图像规格与三类常见组织方式2.1 标注格式ImageNet风格文件夹 vs JSON/CSV vs COCO拿到一份“已标注”的图像分类数据集第一件事不是打开图片看美不美而是确认它的标注格式。常见做法中约4,200张规模的数据集最爱用两种组织方式一种是每个类别一个文件夹图片直接躺在类名文件夹里PyTorch的ImageFolder、Keras的flow_from_directory都能直接读另一种是带一个labels.csv或annotations.json里面记录每张图片的文件名和类别ID。前者叫“暗标注”因为类别信息写在路径里后者叫“明标注”因为你需要自己把映射关系读出来。少数数据集会提供COCO格式的JSON里面除了类别还有边界框那是给目标检测任务用的。做纯图像分类时COCO格式反而要多写一步“把框裁出来当分类图”的转换逻辑。拿到数据集后我一般会先打印目录树确认是不是“train/类别A/xxx.jpg”这种结构。如果是一堆散图加CSV就写个三行脚本把文件移动成文件夹结构后面所有训练代码都能省掉自定义Dataset的麻烦。另外一定要看图像规格。有的数据集是统一缩放到224x224的有的是原始高清图。前者可以直接开训后者需要你在预处理里加Resize和CenterCrop。4200张图如果每张都是6000x4000的原始照片那连加载都慢到怀疑人生最好先离线批量缩到短边256。检查图像尺寸不需要一张张看用PIL读一张判断就能大致知道风格是否统一但更稳妥的是用脚本扫一个子集。2.2 看一眼数据标签类别与数量分布检查脚本很多人拿到数据集直接扔进训练脚本等loss曲线乱飞才回头怀疑数据。正确姿势是先跑一个分布统计。下面这个脚本用pathlib扫描类文件夹输出每个类别的图片数和总张数顺便检查是否有图片文件损坏。from pathlib import Path from PIL import Image data_root Path(fruits_veg_dataset/train) # 改成你的数据集路径 total 0 for cls_dir in sorted(data_root.iterdir()): if not cls_dir.is_dir(): continue files list(cls_dir.glob(*.*)) total len(files) print(f{cls_dir.name}: {len(files)} 张) print(f总张数: {total}) # 抽检前50张图片是否可正常打开 bad 0 for img_path in list(data_root.glob(*/*))[:50]: try: with Image.open(img_path) as im: im.verify() except Exception: bad 1 print(f损坏图片: {img_path}) print(f损坏数: {bad}/50)这段代码的逻辑分两步先对类别文件夹计数让你看到每个类有多少张再抽检50张做verify()排除半截下载或转码损坏的图片。命令行里跑完你会得到一个关键信息——类别分布是否均匀。如果“香蕉”有800张而“火龙果”只有60张后期就需要重点做类别平衡处理否则模型会直接把少见类忽略掉。参数说明data_root支持Path对象比字符串拼接更安全glob(*.*)能匹配常见的.jpg、.jpeg、.png如果你有.bmp或.webp就改成glob(*)再加后缀过滤。抽检50张是经验值能暴露批量坏图问题又不会太慢。如果抽检就发现好几张损坏建议写个全量检查脚本把坏图挪到broken/目录免得训练时读到一半报错。2.3 按需切分训练/验证/测试集划分的常用比例与随机种子4200张图不算海量但足够按8:1:1拆成训练、验证、测试。很多开源数据集直接给好了train/和val/目录但如果你只拿到一个总目录或者想自己重新划分以做交叉验证就需要自己动手。拆分的核心是保证随机且可复现不然下次跑同一脚本结果又变了。常见做法是用split_folders库或者用标准库自己写。我不太建议依赖额外库下面这段脚本用Python自带的random.shuffle完成划分逻辑可控import random import shutil from pathlib import Path src Path(fruits_veg_dataset) # 所有类别文件夹的根目录 out Path(fruits_veg_split) train_ratio, val_ratio, test_ratio 0.8, 0.1, 0.1 random.seed(42) # 固定随机种子 for cls_dir in src.iterdir(): if not cls_dir.is_dir(): continue images list(cls_dir.glob(*.jpg)) list(cls_dir.glob(*.png)) random.shuffle(images) n_train int(len(images) * train_ratio) n_val int(len(images) * val_ratio) splits { train: images[:n_train], val: images[n_train:n_train n_val], test: images[n_train n_val:], } for split_name, paths in splits.items(): dest out / split_name / cls_dir.name dest.mkdir(parentsTrue, exist_okTrue) for p in paths: shutil.copy2(p, dest / p.name)脚本按类别分别shuffle避免全部混在一起后某些类别只出现在训练集或测试集。这里我直接copy2而不是move保留原始数据方便回滚。random.seed(42)是必须的否则每次划分结果不同模型对比就没有意义。参数说明比例是硬编码的如果你数据少比如某些类只有50张那训练集只有40张测试10张后期评估会抖得厉害。这种情况建议改成stratified k-fold或者把测试集比例降到5%并把val比例提上去。我一般会先按8:1:1跑一版看测试集每个类别是否至少有20张少了就降低比例或干脆不用测试集把val集当测试用。对于4200张的中等规模数据8:1:1通常够用但如果你要发论文或做严谨对比建议用5折交叉验证。3. 用ResNet/FastAI在本地跑通果蔬分类最小训练管线3.1 环境准备与依赖选择训练一个42类假设果蔬分类模型主干网络推荐从ResNet50或ResNet18起步。ResNet18训练快内存占用低适合先验证数据没问题ResNet50精度更高但4200张图容易过拟合需要更强的正则化。PyTorch是首选框架因为生态里torchvision.models自带预训练权重迁移学习只需改最后一层全连接。环境安装不要图新稳定版本更重要。我常用的组合是Python 3.10、PyTorch 2.x、torchvision 0.17CUDA版本根据你的显卡驱动定。训练前先用CPU跑一个极小的批次验证代码逻辑通顺再切回GPU否则排查bug时还得和显存溢出纠缠。pip install torch torchvision timm scikit-learn matplotlibtimm不是必需品但它提供了比torchvision更新更多的骨干网络比如EfficientNet、ConvNeXt。如果只看重稳定只用torchvision也够。这里装timm是为后面第6章的进阶对比做准备。3.2 数据加载与预处理增强参数数据加载是训练管线最容易出毛病的环节。用ImageFolder读取我们已经按train/类别A/xxx.jpg排好的目录DataLoader会自动打标签。预处理要严格对齐预训练模型的输入要求ResNet系列用ImageNet的均值和标准差做归一化同时先把短边缩放到256再中心裁剪到224。数据增强方面4200张数据不算富裕我建议训练集用随机水平翻转、随机旋转±15度、随机亮度/对比度抖动。不必一上来就上CutMix或AutoAugment先把基础增强跑通。验证集和测试集只做缩放和中心裁剪不做随机翻转否则指标会虚高。from torchvision import datasets, transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform 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]) ]) train_dataset datasets.ImageFolder(fruits_veg_split/train, transformtrain_transform) val_dataset datasets.ImageFolder(fruits_veg_split/val, transformval_transform)这段代码里ImageFolder会按文件夹名排序生成类别索引比如“apple”对应0“banana”对应1。训练和验证的预处理唯一差别就是RandomCrop和CenterCrop——后者是为了可复现的评估。Resize(256)再RandomCrop(224)比直接Resize(224)多了一点随机裁剪的区域相当于免费的数据增强。ColorJitter的三个参数都控制在0.2太大会让果蔬颜色失真影响像“青苹果和梨”这类靠颜色区分的类别。3.3 训练脚本模型选择、学习率与epoch设置训练脚本的骨架可以复用关键是几个超参数。迁移学习场景下我习惯冻结骨干网络的前几层只微调最后一层和分类头。4200张数据不必从零训练直接用torchvision的ResNet18预训练权重把最后的fc层输出改成类别数。学习率是最大的玄学之一。微调分类头时用1e-3没问题如果解冻骨干网络要把学习率降到1e-5到5e-5否则预训练特征很快被破坏。Epoch数量建议30起步配合ReduceLROnPlateau验证loss连续3个epoch不降就把学习率乘0.5。下面是一个最小可跑的训练循环import torch import torch.nn as nn from torchvision import models from torch.utils.data import DataLoader model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(train_dataset.classes) model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience3, factor0.5) for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) train_loss running_loss / len(train_dataset) model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_loss val_loss / len(val_dataset) val_acc correct / total scheduler.step(val_loss) print(fEpoch {epoch1:02d} | Train Loss {train_loss:.4f} | Val Loss {val_loss:.4f} | Val Acc {val_acc:.4f})这段代码里ReduceLROnPlateau接收val_loss作为监控指标patience3意味着连续三个epoch验证loss不降低才降学习率。weight_decay1e-4是L2正则对防止小数据集过拟合很关键。batch_size32在ResNet18下大约占用3GB显存如果显存不够就降到16。参数调整优先级先确认验证集没有过拟合训练loss很低但val loss不降再去动学习率如果验证集一开始就高优先检查数据预处理而不是调模型结构。30个epoch在RTX 3060上大约10分钟能跑完非常快。如果发现val acc一直卡在某个值可以考虑换ResNet50或者回到第2章检查类别分布。4. 果蔬分类的落地评估混淆矩阵、Top-1/Top-5与单类召回4.1 评估指标怎么选很多教程只看整体准确率但在果蔬分类场景里整体准确率会骗人。假设数据集里苹果占40%模型把所有图都预测成苹果准确率就有40%可这显然是个废物模型。对于多类别分类我至少会看三个指标Top-1准确率、Top-5准确率和每个类别的召回率。果蔬类别之间存在天然的视觉相似性比如“富士苹果”和“红蛇果”、“青柠”和“柠檬”Top-5能反映模型是否把正确答案排进了前五这对实际应用中的“推荐式”交互更有参考价值。计算这些指标不需要自己写复杂逻辑scikit-learn的classification_report和confusion_matrix就够。关键是先保存测试集上每个样本的真实标签和预测结果再离线分析。from sklearn.metrics import classification_report, confusion_matrix import numpy as np all_labels [] all_preds [] model.eval() with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_labels.extend(labels.cpu().numpy()) all_preds.extend(preds.cpu().numpy()) print(classification_report(all_labels, all_preds, target_namestrain_dataset.classes, digits3)) cm confusion_matrix(all_labels, all_preds)这里classification_report每行输出一个类别的精确率、召回率、F1和样本数。我一般先看每类召回率低于0.7的类别就是要优先处理的。confusion_matrix会生成一个num_classes x num_classes的矩阵横轴是预测纵轴是真实标签。矩阵里数值最大的非对角线位置就是最容易混淆的类别对。4.2 用混淆矩阵定位易混类别拿到混淆矩阵后不要只看数字要画图并回看原始图片。有的混淆是标注本身造成的比如数据集中“青枣”和“绿苹果”的图像标签可能就有误模型反而比标注更“正确”。这种情况下不需要强行调模型而是先清洗错误标注。画热力图用matplotlib的imshow就能满足但要注意把类别名显示出来否则你数格子数到眼花。下面是一个简单可视化脚本import matplotlib.pyplot as plt plt.figure(figsize(12, 10)) plt.imshow(cm, interpolationnearest, cmapBlues) plt.colorbar() tick_marks np.arange(len(train_dataset.classes)) plt.xticks(tick_marks, train_dataset.classes, rotation90) plt.yticks(tick_marks, train_dataset.classes) plt.xlabel(Predicted) plt.ylabel(True) for i in range(len(train_dataset.classes)): for j in range(len(train_dataset.classes)): if cm[i, j] 10: # 只标注数量大于10的格子防止文字墙 plt.text(j, i, str(cm[i, j]), hacenter, vacenter, fontsize8) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi200)阈值设为10是因为4200张数据切出的测试集每类大约只有10张左右小于10的格子基本都是噪声。看图时重点找“横向亮、对角线也亮”的类别——说明模型经常把真实类别A预测成B而B本身预测得还行。这时候我会把那几对类别汇总成一个列表到训练集里翻几十张原图看它们到底像不像。如果是真像那就考虑增加这类样本或做针对性的增强比如色调抖动如果是标注错了就直接修正标签。4.3 推理部署时的预处理一致性模型在测试集上跑出95%的准确率一上线就掉到80%最常见原因就是部署时的预处理和训练不一致。训练时用的是Resize(256) CenterCrop(224) Normalize(ImageNet均值方差)部署时如果直接把原始图Resize((224,224))再丢进模型虽然输入尺寸一样但图像内容和分布已经变了。特别是果蔬图片经常带着叶子、标签贴纸、包装盒训练时靠中心裁剪把背景裁掉一部分部署时全图缩放就会把干扰物一起放大。解决方案是把预处理逻辑封装成和训练完全相同的函数并在部署环境里跑单张测试图做对比。我一般会在测试集里挑三张图分别用训练管线和部署管线跑一遍比较输出的logits是否接近。如果差异大于1e-3就说明哪一步没对齐。常见的坑是ToTensor的归一化放错了位置或者用了cv2.resize的默认插值而训练用的是PIL的双线性插值这两者输出有细微差别累积起来就能影响置信度。def preprocess_for_inference(image_path): from PIL import Image image Image.open(image_path).convert(RGB) image transforms.Resize(256)(image) image transforms.CenterCrop(224)(image) image transforms.ToTensor()(image) image transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])(image) return image.unsqueeze(0).to(device)这段代码直接复用训练时的val_transform里前三步保证一致性。注意Image.open后有convert(RGB)因为有些图片是RGBA或灰度模式如果不转换模型会报通道数错误。部署时不要在这里加任何随机增强推理必须确定性。5. 果蔬图像分类避坑与常见问题排查从标注错误到过拟合5.1 标注错误现象、原因与清洗训练loss很难降到0.2以下但验证集准确率一直在80%左右徘徊打开预测错误的样本一看发现某些图压根就是错标签——这就是典型的标注错误。原因很直接约4,200张数据的人工标注很难保证100%正确尤其像“杏鲍菇”和“白蘑菇”、“柠檬”和“青柠”这类相似品类标注员也可能混淆。解决方法是把训练集里每个类别的图按“预测置信度从高到低”排序人工抽查置信度最低的一批。我习惯写个脚本批量输出“模型预测为X但标签为Y”的样例图拼成一张大图快速扫一遍。如果发现某个类别有超过5%的错误标签就该考虑修正或删除这些样本。很多情况下删掉噪音样本比增加几千张新图更能提升精度。5.2 类别不平衡小类别被吃掉如果统计后发现有类别只有40张而其他类别有150张那么模型大概率会在训练中忽略这个小类别。现象是训练loss正常下降但小类别的召回率接近0混淆矩阵里那一行几乎全部分散到其他类别。原因是交叉熵损失在小样本类别上贡献的梯度太小被大类别淹没。解决方法有三个层次先做上采样——在DataLoader里用WeightedRandomSampler给小类别更高采样概率再不行就做数据增强的强化版比如对罕见类额外做RandomResizedCrop最后考虑用类别平衡损失函数比如FocalLoss。WeightedRandomSampler的权重一般按类别样本数的倒数设置还要注意replacementTrue否则采样不到足够多样本。修完代码后再看小类别的召回率有没有上来如果还是不行就需要补充数据而不是硬调。5.3 背景干扰与拍摄环境差异同一类果蔬在菜市场灯光下、在超市包装盒里、在自家厨房砧板上的表现差异巨大。训练集里如果大量图片都是同一种背景模型会把背景特征学进去。现象是测试集里“带红色塑料袋的西红柿”预测为“苹果”因为训练集里的苹果图很多也配了红色塑料袋。排查方法随机抽一个类别用matplotlib画一个网格图一眼扫过去看背景是否单一。解决这类问题靠数据层面最有效对训练集做随机背景替换增强或者加更强的RandomCrop让模型更关注果蔬主体。如果要求不高在预处理里把图片随机裁掉边缘10%的区域也能逼迫模型放弃背景信息。另外采集真实场景图时要有意识地覆盖不同光源角度不要都在同一天同一地点拍。5.4 模型过拟合与欠拟合的调参顺序小数据集上最典型的过拟合现象是训练loss稳步降到0.01验证loss从第10个epoch开始反弹验证准确率停滞。原因是模型容量太大4200张图不够ResNet50记住所有真实特征。我踩过最深的坑是一上来就换大模型结果过拟合更严重。正确调参顺序应该是先确认数据分布没问题再调数据增强强度然后调weight_decay最后才是换模型架构。对于这个规模的数据ResNet18搭配强增强已经能打如果还过拟合就把weight_decay从1e-4提到5e-4同时把Dropout层加在全连接前面。欠拟合的表现为训练loss和验证loss都高模型容量不够或学习率太低。这时候先调学习率用1e-3跑10个epoch看训练loss有没有下降趋势没有就升到3e-3但注意大学习率配合SGD容易震荡。还有一种情况是batch size太小导致收敛慢可以先加到64试一下。记住调参不能一次改多个变量否则翻车了都不知道是哪一步改坏的。6. 把4200张用出12000张的效果数据增强、迁移学习与半自动标注6.1 迁移学习骨干网络选型4200张图想训练一个高精度模型最划算的方案是使用更强的预训练骨干。ResNet18适合第一版跑通但如果最终精度要冲95%以上我会换EfficientNet-B3或ConvNeXt-Tiny。timm库里这些模型的预训练权重都是在ImageNet-21k上训过的特征提取能力比ResNet18强一截。选型时注意输入分辨率EfficientNet-B3默认是320x320比224x224更吃显存但小模型结构在果蔬纹理识别上反而占优势。换模型只需改一行代码import timm model timm.create_model(efficientnet_b3, pretrainedTrue, num_classesnum_classes)num_classes会自动替换分类头。但要注意timm的预处理统计量不一定和torchvision一样需要从timm.data.create_transform获取正确的均值方差。我一般直接用timm.data.create_model配合timm.data.create_transform省去手动对齐的麻烦。6.2 强增强策略与CutMix基础增强只能缓解过拟合想真正把数据“变多”就要用强增强。CutMix就是把两张训练图按块拼在一起标签也跟着变成两块区域的混合比例。它天然适合果蔬分类因为果蔬大多是圆形或椭圆裁剪块不会割裂语义主体。torchvision从1.13开始内置了CutMix配合MixUp通过torchvision.transforms.v2.MixUp和CutMix实现。引用时需要把数据集输出的(image, label)变成(image, target)因为CutMix要求标签是One-Hot或软标签。如果不想升级到v2接口也可以用timm里的mixup函数它直接接收batch输出。我建议只上CutMix不上MixUp因为果蔬图像混合后会产生“半苹果半香蕉”的怪异图模型虽然能学到鲁棒特征但调试时很难解释预测结果。CutMix的alpha参数设成1.0mixup的alpha设成0.2两个一起用容易让训练loss下降变慢需要更长epoch才能收敛。6.3 用预训练模型做伪标注扩展数据集当4200张确实不够用时不要盲目去爬图先用现有模型做一轮伪标注。具体做法是用训练好的模型对一批无标签果蔬图片做预测只保留置信度大于0.95的样本然后人工抽检后加入训练集。这里的关键是“置信度阈值”和“类别均衡”。如果你有1000张无标签图模型可能对其中500张都给出很高的置信度但可能全部集中在苹果和香蕉上导致新增样本加剧不平衡。因此伪标注时要按类别分别设置阈值对原本样本少的类别把阈值降到0.9对样本多的类别提到0.97。pred_probs torch.softmax(outputs, dim1) max_probs, pred_cls torch.max(pred_probs, dim1) keep_mask max_probs 0.95 # 再把保留的样本按类别去重避免单一类别刷屏 selected_paths [paths[i] for i in torch.nonzero(keep_mask).flatten()]这段逻辑里softmax得到的概率才是有效的置信度直接用outputs的最大值会被logits尺度影响。人工抽检时我会每类随机抽20张新图看看伪标签是否靠谱。如果某类别的误标率超过3%就把该类别的阈值上调或者直接放弃该类的伪标注。用这种方式4200张基础数据通常能扩展到6000到7000张精度提升肉眼可见。最后说一个我自己的教训最早拿到这类数据集时我图省事跳过分布检查直接开训结果因为类别目录里混入了一个损坏的文件夹训练Loss在某个epoch直接变成NaN排查了一个小时才发现是某张图片是0字节。后来学乖了不管数据来源多可靠第一件事永远是统计和清洗。图像分类项目的成败八成在数据准备模型只是放大镜。希望这个数据集能让你少走我这些弯路也祝你一版就跑到90%以上的准确率。本文还有配套的精品资源点击获取