
简介这份资源是面向深度学习入门与图像分类实践者的水果图像数据集覆盖苹果、香蕉、樱桃、火龙果、芒果、橘子、菠萝、木瓜共8个类别可直接用于模型训练与验证省去自行采集与清洗的环节。压缩包内共约2000个文件以jpeg图片为主另含少量webp与png样本并附带1个json类别字典和1个可视化py脚本整体约636.77MB。解压后分为data-train训练集2220张与data-test测试集550张均按类别子文件夹组织文件夹名即分类标签便于直接接入PyTorch、TensorFlow等框架的ImageFolder读取。资源还提供classes字典文件与可视化代码方便核对类别映射、抽样查看图像分布。目前已有314人学习下载适合课程作业、分类模型对比实验或迁移学习练手能快速搭建可复现的8分类基线。1. 水果图像分类数据集8 分类任务从数据到落地的第一道坎很多人做深度学习图像识别第一个真正卡住的环节不是模型结构而是数据集。你拿到一个水果图像分类数据集8分类兴冲冲地写了个 CNN结果训练准确率 99%换一批自己拍的照片就崩了。这不是模型的问题是数据的问题。水果图像分类数据集通常包含苹果、香蕉、橙子、葡萄、芒果、草莓、菠萝、西瓜这 8 个常见类别每类几百到上千张不等。它看起来简单但恰恰因为“简单”很多人忽略了背景干扰、光照差异、类内方差这些真实存在的坑。这个数据集适合谁适合刚入门图像分类的工程师练手也适合需要快速验证一个分类 pipeline 是否跑通的团队。但如果你把它当成一个“随便训训就能上线”的数据集后面一定会翻车。这一章先把 8 分类水果数据集到底能做什么、不能做什么讲清楚后面再一步步拆解怎么用、怎么调、怎么避坑。2. 水果图像分类数据集的结构拆解与预处理决策2.1 8 分类水果数据集的典型目录结构与标签映射常见的水果图像分类数据集目录结构一般长这样根目录下按类别名建文件夹每个文件夹里放对应类别的图片。这种结构对torchvision.datasets.ImageFolder和tf.keras.utils.image_dataset_from_directory都是开箱即用的。但你要注意文件夹名称的顺序不一定等于标签顺序不同框架的映射规则不一样。我一般会先跑一段脚本把类别名和索引固定下来存成 JSON后面训练、推理、部署都用同一份映射避免“训练时苹果是 0推理时苹果变成 3”这种低级错误。import os import json from pathlib import Path data_root Path(fruits8) class_names sorted([d.name for d in data_root.iterdir() if d.is_dir()]) class_to_idx {name: idx for idx, name in enumerate(class_names)} with open(class_mapping.json, w, encodingutf-8) as f: json.dump(class_to_idx, f, ensure_asciiFalse, indent2) print(class_to_idx) # 输出示例{apple: 0, banana: 1, grape: 2, mango: 3, orange: 4, pineapple: 5, strawberry: 6, watermelon: 7}这段代码的逻辑很简单扫描根目录下的子文件夹按字母序排序后生成索引映射。参数上唯一需要注意的是sorted的排序规则它决定了最终的标签顺序。如果你希望自定义顺序比如把西瓜放在第一个那就不要用sorted直接写死一个列表。存成 JSON 的好处是推理端可以直接读取不用再依赖文件夹结构。2.2 图像尺寸、归一化与数据增强的参数选择水果图像分类数据集的原始图片尺寸往往不统一有的 256×256有的 1024×768。直接 resize 到 224×224 是最常见的做法因为大多数预训练模型ResNet、EfficientNet、MobileNet的输入都是 224。但这里有个细节水果的轮廓和纹理对分类很重要粗暴 resize 会丢失细节。我一般会先用Resize(256)再CenterCrop(224)这样能保留更多原始信息。归一化参数用 ImageNet 的均值方差[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]因为后面大概率要用预训练权重。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), 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]) ])训练增强里RandomResizedCrop的scale参数我设成(0.7, 1.0)意思是随机裁剪原图 70% 到 100% 的区域。这个范围对水果数据集比较合适再小就容易把整个水果裁掉一半导致标签和内容对不上。RandomRotation(15)是 ±15 度旋转水果在真实场景里不会倒过来放所以不要用 90 度或 180 度旋转。ColorJitter的强度也别太大否则青苹果可能被调成红苹果模型学到的就是颜色而不是形状。2.3 训练集、验证集、测试集的划分比例与分层采样8 分类水果数据集如果每类数量不均衡比如苹果 1200 张、西瓜只有 300 张直接按 8:1:1 随机划分会导致验证集里西瓜样本太少评估结果波动大。我一般用分层采样保证每个类别在训练集、验证集、测试集里的比例一致。sklearn.model_selection.train_test_split的stratify参数就是干这个的。from sklearn.model_selection import train_test_split import numpy as np all_paths [] all_labels [] for cls_name, cls_idx in class_to_idx.items(): cls_dir data_root / cls_name for img_path in cls_dir.glob(*.jpg): all_paths.append(str(img_path)) all_labels.append(cls_idx) all_paths np.array(all_paths) all_labels np.array(all_labels) # 先分训练验证 和 测试 X_trainval, X_test, y_trainval, y_test train_test_split( all_paths, all_labels, test_size0.15, stratifyall_labels, random_state42 ) # 再从训练验证里分训练和验证 X_train, X_val, y_train, y_val train_test_split( X_trainval, y_trainval, test_size0.176, stratifyy_trainval, random_state42 ) print(f训练集: {len(X_train)}, 验证集: {len(X_val)}, 测试集: {len(X_test)})这里test_size0.15表示测试集占 15%剩下的 85% 再按0.176的比例分验证集最终训练集大约占 70%验证集 15%测试集 15%。stratify参数保证每个类别的比例在划分后保持一致。random_state42是为了可复现你换成别的数字也行但一旦定了就不要改否则每次跑出来的结果都不一样调参时根本分不清是模型变了还是数据变了。3. 用迁移学习在水果 8 分类数据集上跑通第一个基线3.1 为什么选 ResNet18 而不是从零训练一个小 CNN水果图像分类数据集只有 8 个类别每类几百到一千张总样本量通常在 5000 到 10000 之间。这个量级从零训练一个 CNN 也能跑但准确率通常比迁移学习低 5 到 10 个百分点。原因很简单预训练模型在 ImageNet 上见过 120 万张图学到的边缘、纹理、颜色特征对水果分类同样有效。ResNet18 参数量 1100 万左右在 CPU 上也能推理适合快速验证。如果你用 MobileNetV3 或 EfficientNet-B0参数量更小精度也不差但 ResNet18 的代码示例最多踩坑最少。import torch 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_features model.fc.in_features model.fc nn.Linear(num_features, 8) # 8 分类 model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20)weightsmodels.ResNet18_Weights.IMAGENET1K_V1是加载 ImageNet 预训练权重。model.fc是 ResNet 最后的全连接层原来输出 1000 类改成 8 类。优化器用 AdamW学习率 1e-3权重衰减 1e-4。学习率调度器用余弦退火T_max20表示 20 个 epoch 后学习率降到接近 0。这些参数不是玄学是经过大量实验验证的默认值你可以在自己的数据集上微调但不要一上来就改成 0.1 或 0.0001。3.2 训练循环里的三个关键监控指标训练循环本身不难写难的是知道该看什么。我一般会监控三个指标训练损失、验证损失、验证准确率。训练损失下降但验证损失上升说明过拟合两个都下降但验证准确率不涨说明学习率可能太小训练损失震荡说明学习率太大或 batch size 太小。from torch.utils.data import DataLoader, Dataset from PIL import Image class FruitDataset(Dataset): def __init__(self, paths, labels, transform): self.paths paths self.labels labels self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) img self.transform(img) return img, self.labels[idx] train_dataset FruitDataset(X_train, y_train, train_transform) val_dataset FruitDataset(X_val, y_val, val_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) for epoch in range(20): model.train() train_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() * imgs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) scheduler.step() print(fEpoch {epoch1:02d} | fTrain Loss: {train_loss/len(train_dataset):.4f} | fVal Loss: {val_loss/len(val_dataset):.4f} | fVal Acc: {correct/total:.4f})batch_size32是常见起点显存不够就降到 16 或 8。num_workers4在 Linux 上没问题Windows 上如果报错就改成 0。训练循环里optimizer.zero_grad()必须在loss.backward()之前否则梯度会累积。scheduler.step()放在 epoch 末尾不是 batch 末尾。这些顺序错了训练结果会莫名其妙地差。3.3 用混淆矩阵定位 8 分类里最容易混的类别训练完模型不要只看准确率。8 分类水果数据集里橙子和橘子、苹果和梨、葡萄和蓝莓这些类别在低分辨率下很容易混。混淆矩阵能告诉你具体是哪两类在互相误判。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds [] all_labels [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix - Fruits8) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) print(classification_report(all_labels, all_preds, target_namesclass_names))classification_report会输出每个类别的精确率、召回率、F1 分数。如果某个类别的召回率明显低于其他类别说明这个类别的样本可能太少或者和其他类别太像。我遇到过橙子和橘子混得最厉害后来发现数据集里这两个类别的图片背景几乎一样模型只能靠颜色区分而颜色在归一化之后差异被缩小了。解决办法是增加这两个类别的样本多样性或者用更强的数据增强。4. 水果图像分类数据集训练中的避坑与排查清单4.1 现象训练准确率 99%测试准确率 60%原因数据泄露。训练集和测试集里有同一张图片的不同副本或者同一批次拍摄的图片被分到了不同集合。水果数据集经常从同一个视频里抽帧相邻帧几乎一样随机划分就会导致泄露。解决按拍摄批次或视频来源划分不要按图片随机划分。如果数据集没有批次信息至少用感知哈希去重把相似度极高的图片只保留一张。4.2 现象验证损失突然变成 NaN原因学习率太大或者某张图片损坏导致输入包含 NaN。水果数据集里偶尔会有灰度图、CMYK 图或截断的 JPEGImage.open能打开但转 RGB 后可能出问题。解决在 Dataset 的__getitem__里加 try-except跳过损坏图片同时把学习率降到 1e-4 再试。如果还是 NaN检查归一化参数是否写错比如把std写成了mean。4.3 现象模型把西瓜预测成苹果置信度还很高原因背景干扰。西瓜图片大多在绿色背景或户外拍摄苹果图片可能在红色桌布上。模型学的是背景颜色不是水果本身。解决用 Grad-CAM 可视化模型关注区域如果热力图集中在背景说明模型没学到正确特征。增加随机裁剪和颜色抖动或者用分割模型把水果抠出来再训练。4.4 现象推理时单张图片预测结果和验证集不一致原因预处理不一致。训练时用了RandomResizedCrop推理时用了CenterCrop但忘了同步Resize的尺寸或者归一化参数写错了。解决把验证集和推理端的预处理封装成同一个函数训练和推理都调用它。不要在两处分别写transforms.Compose迟早会不一致。4.5 现象GPU 显存够但训练速度很慢原因num_workers设成 0数据加载成了瓶颈或者图片尺寸太大没有提前 resize 缓存。解决把num_workers设成 CPU 核心数的一半左右如果数据集不大可以先把所有图片 resize 到 256×256 存成新文件训练时直接读取能快 2 到 3 倍。5. 从 8 分类基线到可复现实验的进阶技巧5.1 用学习率预热和分层学习率再涨两个点迁移学习有个常见问题新加的全连接层是随机初始化的如果一开始就用 1e-3 的学习率反向传播的梯度会很大把预训练权重带偏。我一般会先用 3 个 epoch 做学习率预热从 1e-6 线性升到 1e-3然后再用余弦退火。另外主干网络和分类头可以用不同的学习率主干用 1e-4分类头用 1e-3这样预训练特征不会被破坏得太快。def get_param_groups(model, backbone_lr1e-4, head_lr1e-3): backbone_params [] head_params [] for name, param in model.named_parameters(): if fc in name: head_params.append(param) else: backbone_params.append(param) return [ {params: backbone_params, lr: backbone_lr}, {params: head_params, lr: head_lr} ] optimizer torch.optim.AdamW(get_param_groups(model), weight_decay1e-4)这段代码把fc层和其他层的参数分开分别设置学习率。backbone_lr1e-4比head_lr1e-3小一个数量级目的是让预训练特征微调得慢一点新分类头学得快一点。这个技巧在样本量少于 10000 张时特别有效通常能涨 1 到 2 个百分点。5.2 用 TTA 和模型集成做最终验证测试时增强TTA是推理阶段最划算的技巧。对同一张测试图片做多次增强水平翻转、不同裁剪把预测概率平均通常能涨 0.5 到 1 个点。如果再训练 3 个不同随机种子的模型做集成还能再涨 1 个点左右。代价是推理时间变成 3 倍但对于水果分类这种轻量任务完全值得。def predict_with_tta(model, img_path, transform_list, class_names): img Image.open(img_path).convert(RGB) probs torch.zeros(len(class_names)).to(device) with torch.no_grad(): for t in transform_list: tensor t(img).unsqueeze(0).to(device) output model(tensor) probs torch.softmax(output, dim1).squeeze(0) probs / len(transform_list) pred_idx torch.argmax(probs).item() return class_names[pred_idx], probs.cpu().numpy() tta_transforms [ val_transform, transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p1.0), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) ]tta_transforms里第一个是原始验证变换第二个是强制水平翻转。predict_with_tta把两次预测的概率平均后取最大值。注意 TTA 只适合推理阶段训练时不要用否则会拖慢训练速度且没有收益。5.3 一个我踩过的坑类别顺序在部署时被重新排序最后说一个血泪教训。有一次我把训练好的模型导出成 ONNX部署到服务端结果所有预测都错位了。排查了半天才发现训练时class_to_idx是按字母序排的但服务端加载模型后自己又按文件夹读取顺序排了一遍两个顺序不一致。后来我强制要求训练、导出、推理三个阶段都必须读取同一份class_mapping.json谁都不许自己排序。这个习惯帮我省了至少两次线上事故。希望帮到你。本文还有配套的精品资源点击获取