简介:这是一份面向深度学习入门与图像分类实战的11种水果分类数据集,涵盖苹果、鳄梨、蓝莓、辣椒、樱桃、猕猴桃、芒果、橙子、岩瓜、草莓、小麦共11个类别。数据已按类别分文件夹存放,训练集含2562张图片,测试集含636张图片,无需额外标注即可直接用于卷积神经网络等模型的训练与评估。压缩包共2000个文件,以jpeg图片为主,同时包含png、webp及少量bmp格式,方便不同场景下的图像加载;另附classes.json类别字典和可视化脚本py文件,便于快速查看样本分布与验证分类效果。压缩包整体约855MB,目录层级清晰,适合正在学习深度学习图像分类、需要现成数据完成课程设计或模型验证的开发者使用。目前已有1667人学习下载,是一份结构规范、上手门槛低的水果图像分类训练资源。
1. 为什么 11 种水果分类是深度学习图像识别最合适的练手题
「深度学习图像识别数据集:11种水果分类数据集」这类资源在行业内流传很广,常见形态是一个包含 11 个类别子文件夹的图片集,苹果、香蕉、橙子、柠檬、猕猴桃、葡萄、桃、梨、西瓜、石榴、草莓各占一个目录,总量在几百到一万多张之间。它解决的问题很直接:让一个刚接触视觉任务的人,用最短路径跑通「数据加载 → 模型训练 → 评估 → 推理」全流程,而且 11 分类的难度恰好卡在「不能靠猜」和「不至于练不动」之间。适合三类人:准备做课程设计或毕设的学生、想验证自己 PyTorch 环境是否配对的初学者、需要快速建立图像分类 baseline 的工程师。别小看这个小数据集,它能把深度学习里最核心的过拟合、类别不均衡、数据增强、迁移学习这几个问题全部暴露一遍。
2. 打开数据集:目录结构、加载代码与张量形状的全套读法
拿到「11种水果分类数据集」之后,第一件事不是立刻训练,而是先把这个数据集「读对」。分类任务的数据集组织形式直接决定后续代码怎么写,也决定你踩不踩 label 错位的坑。
2.1 目录结构:为什么 train / val / test 三层文件夹就是标准答案
绝大多数水果分类数据集的压缩包解压之后,内部是这样一个树形结构:
fruits-11/ ├── train/ │ ├── apple/ │ │ ├── apple_001.jpg │ │ ├── apple_002.jpg │ │ └── ... │ ├── banana/ │ ├── orange/ │ ├── lemon/ │ ├── kiwi/ │ ├── grape/ │ ├── peach/ │ ├── pear/ │ ├── watermelon/ │ ├── pomegranate/ │ └── strawberry/ ├── val/ │ └── (同样按类别分子文件夹) └── test/ └── (有的版本没有 test,只有 val)这个结构之所以是标准答案,是因为 PyTorch 的torchvision.datasets.ImageFolder就是为它设计的。它按子目录名自动生成类别索引,文件夹名即标签,不需要额外的 CSV 标注文件。我一般会先跑一遍find命令确认每个类别的图片数量,防止某些类别图片被漏拷。
find fruits-11/train -type d -exec sh -c 'echo "$1: $(ls "$1" | wc -l)"' _ {} \; | sort上面这条命令会列出 train 下每个子文件夹的图片数量。正常情况下 11 个类别数量应大致均衡,如果发现某个类别只有几十张,后面就要做类别加权或者放弃该类别,这是后话。数量分布直接决定你后面用不用WeightedRandomSampler,这一步花 30 秒值得。
2.2 用 PyTorch 的 ImageFolder 一次性读入全部数据
确认目录没问题之后,加载数据只需一段十几行的代码。这里我直接给出一份「跑通版」的 PyTorch 数据加载片段,适用于 torchvision 0.13 及以上版本。
import torch from torchvision import datasets, transforms # 训练集与验证集使用不同的预处理策略 train_transform = transforms.Compose([ transforms.Resize((256, 256)), # 先缩放到统一尺寸 transforms.RandomResizedCrop(224), # 随机裁剪,数据增强 transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转 transforms.ToTensor(), # PIL Image -> Tensor,像素值归一化到 [0,1] transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), # 验证集不做随机增强,只中心裁剪 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_data = datasets.ImageFolder('fruits-11/train', transform=train_transform) val_data = datasets.ImageFolder('fruits-11/val', transform=val_transform) print(train_data.classes) # 按字母序排列的 11 个类名 print(train_data.class_to_idx) # {'apple': 0, 'banana': 1, ...} print(len(train_data), len(val_data))这段代码的逻辑是:ImageFolder在初始化时会遍历fruits-11/train下的所有子文件夹,按字母序生成class_to_idx映射,并把每张图片的路径和对应整数标签存进内部列表。训练集和验证集必须用同一个数据源目录结构,否则类别索引会错位。Normalize里的均值方差用的是 ImageNet 统计值,因为我们后面要做迁移学习,预训练模型就是在这些数值上训练的,输入分布不一致会让微调效果打折扣。
2.3 输入张量到底是什么:从 JPEG 像素到归一化数组
很多新手在这里犯迷糊:图片明明是.jpg文件,怎么ToTensor()之后就变成了三维数组?一张 224×224 的 RGB 彩色图片,经过ToTensor()后得到的 Tensor 形状是(3, 224, 224),三个维度分别是通道数、高度、宽度,数值范围从 0~255 缩放到 0~1。再经过Normalize,每个通道减去均值除以标准差,数值变成近似标准正态分布,这是为了让模型训练更稳定。
你可以在加载之后单独检查一下数据形状:
sample, label = train_data[0] print(sample.shape) # torch.Size([3, 224, 224]) print(label) # 整数,比如 7这里的7不是类别名字,而是class_to_idx里对应的整数。后面模型输出的也是 11 个概率值,取 argmax 后得到整数索引,再通过train_data.classes[idx]反查回水果名字。这个「整数索引 ↔ 文件夹名」的双向映射是整个训练过程中最容易错位的环节,建议在加载之后立即打印一遍确认。
3. 训练一个 11 分类水果识别模型:迁移学习与三个关键超参数
数据集读进来了,接下来进入核心环节:训练模型。11 分类水果识别的训练本质上是一个标准的图像分类问题,选对模型和超参数,每个人都能稳定跑到 94% 以上的验证准确率。
3.1 选型理由:为什么这个量级的数据不配用大模型
常见做法是用 ImageNet 预训练权重做迁移学习。水果图片和 ImageNet 里已有的苹果、香蕉、草莓等类别高度相关,预训练模型已经学会了纹理、边缘、颜色这些底层特征,我们只需要替换最后一层分类头,让它输出 11 个类别。这是个「站在巨人肩膀上」的策略,比从零开始训练省下大量时间,准确率还高得多。
基础版本我推荐 ResNet18,而不是 ResNet50 或 Vision Transformer。原因很现实:水果数据集通常只有几千到一万张图,ResNet18 参数量约 1100 万,足够拟合这个规模的数据;ResNet50 参数量翻了几倍,在小数据上更容易过拟合,训练时间也成倍增加。如果你打算部署到边缘设备或手机端,那mobilenet_v3_small更合适,精度比 ResNet18 低 1~2 个点,但体积小一个数量级。教学演示和 baseline 场景老老实实用 ResNet18 就行。
3.2 预处理与数据增强:让模型看见更多「角度的苹果」
数据增强是整个训练里性价比最高的一环。水果拍摄角度、光照、遮挡情况各异,想让模型在验证集和真实场景里都稳定工作,就得在训练时人为制造「多样性」。前面 2.2 节代码里已经写进了三个增强策略:随机裁剪、随机翻转、随机旋转。我再补一个完整的版本,包含颜色抖动:
import torchvision.transforms as T train_transform = T.Compose([ T.Resize(256), T.RandomResizedCrop(224, scale=(0.6, 1.0)), T.RandomHorizontalFlip(), T.RandomRotation(15), T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])注意RandomResizedCrop的scale=(0.6, 1.0),意思是每次随机裁剪原图 60%~100% 的区域再缩放到 224×224。这个参数决定了模型能看到「多局部的物体」:scale 下限设得太低,比如 0.08,模型会经常只看到水果的半个切面,训练难度骤增;设得太高又起不到增强作用。水果数据集里 0.6 起步比较稳。RandomRotation(15)控制在 15 度以内,超过 30 度会让真实世界里的「水果朝上摆放」这个语义失真。颜色抖动幅度也别太大,亮度对比度各 0.2 足够,因为在真实场景里过强的颜色扰动会让红苹果和红柿子难以区分。
3.3 训练循环:损失、优化器与学习率的三个关键旋钮
训练代码的核心只有三个旋钮:损失函数、优化器、学习率,外加一个 batch size。分类任务损失函数固定用交叉熵,优化器选 SGD 带动量,这是迁移学习里最稳的组合。学习率是重中之重:微调预训练模型时,新加的分类头需要较大的学习率,而前面几层卷积已经学会通用特征,学习率过大会把预训练权重冲坏。常见做法是给全连接层设 0.001,特征提取层设 0.0001。
import torch.nn as nn from torchvision import models # 加载 ImageNet 预训练权重(torchvision 0.13+ 推荐写法) model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层全连接,输出 11 类 num_features = model.fc.in_features # 512 model.fc = nn.Linear(num_features, 11) # 全连接层用较大学习率,特征层用较小学习率 fc_params = [p for name, p in model.named_parameters() if 'fc' in name] base_params = [p for name, p in model.named_parameters() if 'fc' not in name] optimizer = torch.optim.SGD([ {'params': base_params, 'lr': 1e-4}, {'params': fc_params, 'lr': 1e-3} ], momentum=0.9, weight_decay=5e-4) criterion = nn.CrossEntropyLoss()训练循环本身不复杂,每轮迭代做四件事:取一个 batch 的数据和标签、前向传播计算损失、反向传播计算梯度、优化器更新参数。如果数据集总量在 8000 张左右、batch size 为 64,一个 epoch 大约 125 个 step,ResNet18 在消费级显卡上跑一个 epoch 只要 30~60 秒。通常训练 15~25 个 epoch 就能收敛到 92%~96% 的验证准确率。设置一个学习率衰减策略lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1),每 7 个 epoch 学习率降为原来的十分之一,让收敛更平滑。整个训练过程的核心观察指标只有一个:验证集准确率。每跑完一个 epoch 打印一次 val acc,如果连续 5 个 epoch 不再上升,就提前终止训练,这就是早停策略。
4. 验证做扎实:混淆矩阵、分类报告与每类召回率的排查价值
训练完模型之后,准确率数字只说明「整体还行」。11 分类任务里,真正能暴露问题的是混淆矩阵和每一类的精确率、召回率。一个 98% 整体准确率的模型可能对石榴这类深色水果的召回率只有 60%,这种偏科在测试阶段必须揪出来。
4.1 混淆矩阵才是分类任务的照妖镜
写一个评估函数,跑完整个验证集后生成混淆矩阵:
import numpy as np import torch from sklearn.metrics import confusion_matrix, classification_report def evaluate(model, dataloader, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in dataloader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) # 取概率最大的类别索引 all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_names=train_data.classes)) return cm, np.array(all_labels), np.array(all_preds)这段代码的逻辑是:把验证集所有图片过一遍模型,收集预测结果和真实标签,然后用 scikit-learn 的confusion_matrix和classification_report输出详细指标。运行后会看到一张 11×11 的矩阵和一份包含每类 precision、recall、f1-score 的报告。我重点看两处:一是对角线上的数字是否明显大于非对角线,二是 recall 最低的那一两类是什么。
4.2 从 PyTorch 模型到 ONNX:推理脚本怎么写
验证通过后,模型还是要落地的。新手常犯的错误是训练完直接拿着.pth文件到处跑,但.pth只存了参数,没有模型结构定义。一个更实用的做法是把模型导出成 ONNX 格式,这样部署时不需要重新定义模型类,也方便后续用 ONNX Runtime 做推理加速。
dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, 'fruits11_model.onnx', input_names=['input'], output_names=['output'], opset_version=11, dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} )导出前必须把模型切到model.eval()模式,否则 BatchNorm 层的统计值会跟着改变。dynamic_axes参数设置了动态 batch,这样导出后的模型既支持单张图片推理,也支持批量预测。导出完成后用一行代码验证 ONNX 输出和 PyTorch 原模型是否一致:
import onnxruntime as ort sess = ort.InferenceSession('fruits11_model.onnx') onnx_out = sess.run(None, {'input': dummy_input.numpy()})两者的输出差异如果小于 1e-4,说明模型转换没有引入数值偏差。这个数值一致性检查是 ONNX 部署步骤里最容易省掉的,省掉之后大概率在某个环境里遇到玄学 bug。
5. 避坑指南:水果数据集训练里最常见的 5 个翻车现场
这个数据集看起来简单,实际训练起来坑并不少。下面五条全是真实训练中会反复遇到的现象,每条都按「现象 → 原因 → 解决」的顺序拆开讲。
5.1 训练集准确率 99%,验证集只有 71%
这是最典型的过拟合信号。11 分类水果数据集总量不大,而 ResNet18 有 1100 万参数,模型完全有能力把训练集里的每一张图都背下来。对比一下两个准确率的差距:如果训练集 99% 而验证集 71%,差距超过 20 个点,基本可以判定过拟合了,而不是验证集分布问题。
解决方案按性价比排序:第一,加大数据增强强度,把RandomRotation从 15 度提到 20 度,把ColorJitter的饱和度扰动调到 0.3,让模型看到更多变体;第二,把weight_decay从 5e-4 提到 1e-3,加强对大权重的惩罚;第三,在验证集准确率连续 3 个 epoch 不上升时触发早停。做了这三步之后,就算个别类别仍然过拟合,整体差距也会缩到 5 个点以内。
5.2 Label 与路径映射错位:ImageFolder 的类名顺序按字母排
有一次训练出来的模型把苹果全部识别成香蕉,准确率只有 9%,看混淆矩阵发现对角线全偏了一位。排查后发现问题出在类别索引:ImageFolder生成的class_to_idx是按文件夹名字母序排列的,我的数据集里apple排第 0 位,但之前某份 CSV 格式的数据是人工打标的,类别编号跟字母序对不上。两边一旦混用,标签就整体错位。
解决方法是每次加载数据后强制打印print(train_data.class_to_idx),并且用同一个ImageFolder实例去获取类名映射,绝不用手写的硬编码列表。训练前花 10 秒看一眼这行输出,能省掉一下午的排查。
5.3 单张图推理速度 200ms:预处理反而比模型更慢
训练完模型丢进推理脚本,发现单张图片要 200ms 才能出结果,其中模型推理只占 3ms。问题出在我的预处理里调用了 PIL 的resize到 256 再center_crop到 224,而resize传入的是普通 Python 函数,每张图都走一遍 PIL 全量插值。图片分辨率越大,这个瓶颈越明显。
解决思路是推理阶段用固定尺寸输入:把Resize(256)换成Resize((224, 224)),省掉CenterCrop,模型输入直接是 224×224。再配合 ONNX Runtime 的 CPU 推理,单张耗时能压到 20ms 以内。训练阶段保持随机裁剪没问题,但推理阶段追求的是确定性,一切多余的预处理都是浪费。
5.4 类别不均匀:某个类只有 30 张,另一个类有 400 张
水果数据集下载来源五花八门,有的版本石榴照片很少,草莓特别多。直接训练的结果是模型对石榴的召回率可能低到 40%。横纵坐标看一眼数据分布就能确认:数量少于中位数一半的类别,基本就是问题类别。
解决方法是两选一:要么在DataLoader里配WeightedRandomSampler,让每个类别每个 epoch 被抽到的概率大致相同;要么干脆把少量类别的图片做针对性增强,比如对石榴多做几次旋转和裁剪变体。我一般倾向后者,因为WeightedRandomSampler会让模型反复看同几张石榴图,对这类样本过拟合的风险更高。
5.5 验证 Loss 不降反升,梯度爆炸把准确率打到 10%
从头训练模型时最容易遇到:前几个 epoch loss 一直不降,突然一个 epoch 后准确率掉到 10% 以下,loss 数值变成几百上千。原因基本是初始学习率设太大,梯度一步跨越了最优点。我在调试时把学习率从 0.001 改成 0.1,结果第一个 epoch 之后整个模型的权重全乱了。
解决方法是给优化器加一个 warmup:前 5 个 epoch 让学习率从 0 线性上升到目标值,之后再做衰减。PyTorch 自带的torch.optim.lr_scheduler.LambdaLR可以实现:
warmup_epochs = 5 def lr_lambda(epoch): if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs return 0.1 ** ((epoch - warmup_epochs) // 7) scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_lambda)这个调度器在前 5 个 epoch 做线性预热,之后每 7 个 epoch 学习率降一个数量级。配上这个策略,即使初始学习率设到 0.01,训练也能稳定起步。
6. 上线前最后一公里:写一个人人可用的单张图片推理脚本
训练和评估都通过了,最后给一个能直接用的推理脚本,不依赖训练时的 DataLoader,单张图片输入、类别文字输出。这个脚本可以直接丢给同事用,不需要他们懂 PyTorch。
from PIL import Image import torch from torchvision import models, transforms device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = models.resnet18(weights=None) model.fc = torch.nn.Linear(512, 11) model.load_state_dict(torch.load('fruits11_best.pth', map_location=device)) model.to(device) model.eval() # 类名列表必须与训练时 class_to_idx 保持一致 classes = ['apple', 'banana', 'orange', 'lemon', 'kiwi', 'grape', 'peach', 'pear', 'watermelon', 'pomegranate', 'strawberry'] predict_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def predict_image(img_path, topk=3): img = Image.open(img_path).convert('RGB') # 统一转 RGB,防止灰度图翻车 tensor = predict_transform(img).unsqueeze(0).to(device) # (1, 3, 224, 224) with torch.no_grad(): probs = torch.softmax(model(tensor), dim=1)[0] topk_probs, topk_idx = torch.topk(probs, topk) for p, i in zip(topk_probs.tolist(), topk_idx.tolist()): print(f'{classes[i]}: {p:.4f}') if __name__ == '__main__': predict_image('test_apple.jpg')这个脚本的关键细节在最后几行:topk默认输出置信度最高的前三个类别,而不是只给一个答案。实际使用中这个设计很有用,因为模型对某些外观接近的水果(青苹果和绿色猕猴桃)本身就会混淆,输出前三个结果交给业务方判断比硬给一个答案靠谱得多。另外注意Image.open(...).convert('RGB'),有些手机拍的图片是 RGBA 模式或灰度模式,不转换的话会在ToTensor()阶段维度报错。
我的经验是:凡是给非算法同事用的推理脚本,一定要保证「输入一张图,输出一眼能看懂的文字」,不要输出整数索引,更不要把模型结构定义留在训练脚本里。这个脚本已经用在我的好几个图像分类项目里了,现在遇到类似需求我都是先把数据集目录结构确认好,再从这个推理脚本反向搭训练流程,反而比从头搭更少踩坑。希望帮到你。
本文还有配套的精品资源,点击获取