水果图像8分类数据集:从数据到ResNet迁移学习实战 简介面向深度学习图像识别初学者与快速验证分类模型的开发者这是一套开箱即用的水果图像分类数据集覆盖苹果、香蕉、樱桃、火龙果、芒果、橘子、菠萝、木瓜共8个常见类别。数据已按训练集2220张、测试集550张划分并分别置于data-train和data-test目录每个子文件夹名称即类别标签无需额外处理即可接入主流深度学习框架。压缩包内共2000个文件以1812张jpeg图像为主辅以少量webp、png图片同时提供classes.json类别字典和可视化Python脚本方便查看样本分布与验证模型效果。资源包整体约637MB目录结构直观适合作为图像分类入门练习、课程实验或算法对比测试的数据基础。目前已有314人学习下载值得需要现成训练数据的同学选用。1. 水果图像8分类数据集从2220张训练图到8分类模型的最小闭环我刚做图像分类的时候最头疼的从来不是模型选型而是数据整理。网上爬了几天水果图还得一张张删重复、修错标签前后折腾快一周准确率还是上不去。换成这份水果图像分类数据集之后整个流程一下子变得顺畅苹果、香蕉、樱桃、火龙果、芒果、橘子、菠萝、木瓜共8个类训练集2220张、测试集550张文件夹名就是类别名自带classes.json类别字典和可视化代码。对想快速跑通深度学习图像分类流程的开发者来说它省掉的正是最容易被低估的数据准备环节。适合谁准备做课程设计或面试Demo的同学需要一份干净数据去验证模型改进想法的算法工程师以及想从零完整走一遍“数据加载-训练-评估-可视化”链路的新手。数据集解压之后可以直接作为深度学习训练数据不需要额外清洗这也是它和那些网上爬下来还要手动标注的资源最大的差别。下面按实际开发顺序把这份资源从结构到二次开发逐层拆开。2. 数据集结构拆解目录命名、类别字典与可视化代码2.1 目录与类别映射训练集和测试集按类分子文件夹的设计巧思解压之后目录结构非常直观>data-train/ |-- 苹果/ | |-- Baidu_0288.jpeg | |-- Baidu_0290.jpeg | -- ... |-- 香蕉/ |-- 樱桃/ |-- 火龙果/ |-- 芒果/ |-- 橘子/ |-- 菠萝/ -- 木瓜/之所以强调“训练集和测试集都按类分子文件夹”是因为你不需要维护一份单独的标签文件torchvision.datasets.ImageFolder会自动把子文件夹名转成类别索引。它读取目录后生成两个重要属性classes按字母或拼音顺序排好的类别名列表和class_to_idx类别名到数字标签的字典。训练代码在数据加载这一层几乎不需要额外逻辑。对比一下另一种常见格式把所有训练图放在一个平铺目录里旁边配一份train.csv写“文件名,类别”。这种格式在检测任务里更常用但在分类任务中会多一道“合并两个文件”的工序而且一旦文件名和CSV对不上就开始出莫名奇妙的问题。这份数据集选择子文件夹结构等于把最麻烦的“文件名与标签对齐”这一步用文件系统本身解决了。2.2 数据规模分析2220550的体量对应什么训练策略训练集2220张、测试集550张按8个类别平均计算每个类大约277张训练图、68张测试图。这个规模在深度学习图像分类里属于中小型数据集。如果从零随机初始化训练一个ResNet277张/类的数据量很容易过拟合但用ImageNet预训练权重做迁移学习这个量级又刚好能支撑起不错的收敛效果。实际使用中还有一个前提8个类别不一定严格均匀多的类可能300多张少的可能只有200张左右这与拍摄成本和水果季节都有关系。所以拿到资源后的第一件事不是直接训练而是统计一下每个子文件夹下的图片数量。统计脚本放在2.3节的代码块里10秒钟就能跑完看清楚每类分布之后心里才有底后面调数据增强策略时才知道要不要给某些类单独加权重。另一个值得注意的点是文件总大小644MB平均到2770张图上每张图约230KB左右说明图片分辨率不算低直接缩放到224×224时细节保留会比较充分。这个体积在本地开发场景下很友好用GPU训练时整个数据集能一次读进内存不需要写复杂的流式加载逻辑。2.3 classes.json与可视化代码训练前先做的两个动作数据集的作者额外提供了两个东西classes.json和可视化代码。前者是类别字典用于训练结束后把模型输出的数字标签翻译回可读的类别名后者一般是一小段 matplotlib 脚本用来随机展示几张图片确认图片没有损坏、内容确实是目标类别。import json with open(classes.json, r, encodingutf-8) as f: classes json.load(f) # 常见有两种结构拿到后先打印看清楚再决定怎么用 print(classes) # 结构1: {0: 苹果, 1: 香蕉, ...} # 结构2: {苹果: 0, 香蕉: 1, ...}这段代码本身没有难度关键在“拿到后先打印”这个动作。如果你的训练代码用ImageFolder真正生效的类别顺序是文件夹在磁盘上的排序结果和classes.json里的顺序未必一致。所以我的习惯是把classes.json当作参考字典最终以train_set.classes为准而不是反过来。这个坑后面还会详细展开。可视化代码通常是下面这种形式遍历某个子文件夹然后画成3×3网格import os import matplotlib.pyplot as plt from PIL import Image # 取训练集第一个子文件夹预览前9张图 first_cls os.listdir(data-train)[0] cls_dir os.path.join(data-train, first_cls) paths os.listdir(cls_dir)[:9] fig, axes plt.subplots(3, 3, figsize(8, 8)) for ax, p in zip(axes.flatten(), paths): img Image.open(os.path.join(cls_dir, p)) ax.imshow(img) ax.axis(off) plt.show()这段脚本的用途有两个一是解压后快速确认图片能正常被 Pillow 打开二是扫一眼拍摄背景和物体姿态的多样性——如果图片大多是纯色背景下的标准俯视图那么训练出来的模型在复杂背景下大概率掉点后面要针对性做数据增强。可视化这一步花不了两分钟但能提前暴露很多数据层面的问题。3. 核心代码实现从ImageFolder到ResNet18微调的完整训练链路3.1 transforms与DataLoader训练集和测试集为什么用两套预处理训练图像分类模型时预处理策略直接决定了模型能学到什么。对于这份水果数据集我常用的 transform 配置如下from torchvision import datasets, transforms from torch.utils.data import DataLoader train_trans transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) test_trans transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_set datasets.ImageFolder(data-train, transformtrain_trans) test_set datasets.ImageFolder(data-test, transformtest_trans) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) test_loader DataLoader(test_set, batch_size32, shuffleFalse, num_workers4) print(train_set.classes) # 8个类别的排序结果 print(train_set.class_to_idx) # 类别名到数字标签的映射训练集里我用了RandomResizedCrop它会随机裁剪一部分再缩放到224×224相当于每轮训练模型看到的都是同一张图的不同局部这是一种低成本的数据增强。RandomHorizontalFlip对水果这种姿态不一定左右对称的物体其实有争议——苹果和橘子翻转影响不大但某些有明显朝向的品类不太适合稳妥起见也可以去掉。ColorJitter(0.2, 0.2, 0.2)对亮度、对比度、饱和度做随机扰动能缓解“训练图都在室内灯光下测试图在自然光下”这类问题。测试集必须保持“确定性”只做Resize(256)加CenterCrop(224)不加入任何随机操作。如果测试集也做翻转或裁剪扰动同一张图每次评估出来的结果会上下抖动没法判断模型真实水平。这一点在图像分类中是一条铁律。3.2 模型构建替换分类头实现8分类的迁移学习写法这个数据量下从零训练CNN不是明智选择。常见做法是加载ImageNet预训练的ResNet18把最后一层全连接换成8分类输出让模型在小数据集上快速收敛import torchvision.models as models import torch.nn as nn model models.resnet18(pretrainedTrue) num_ftrs model.fc.in_features model.fc nn.Linear(num_ftrs, 8) # 8分类pretrainedTrue会从 torchvision 缓存中加载在ImageNet上训好的权重。这里的原理是ImageNet上训练得到的浅层特征边缘、纹理、颜色块具有通用性水果分类同样用得上我们需要重新学习的只是“如何组合这些通用特征来区分8种水果”也就是最后一层全连接。代码里用model.fc.in_features动态读取上一层的输出维度而不是硬编码512或2048这样从ResNet18换成ResNet50时这一行不用改。fc层的选择也决定了冻结策略如果训练数据特别少可以先冻结前面所有层只训练fc等fc收敛后再解冻全部层微调但对277张/类的数据规模直接全量微调通常效果更好只要学习率别设太大。3.3 训练循环与超参数SGD、lr_scheduler与loss/acc的观察点训练循环本身不复杂但有些细节会决定最终效果import torch import torch.nn as nn criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) model.train() for epoch in range(10): running_loss 0.0 correct 0 total 0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) epoch_loss running_loss / total epoch_acc correct / total print(fepoch {epoch 1}, loss: {epoch_loss:.4f}, acc: {epoch_acc:.4f}) scheduler.step()有三个值得反复确认的细节。第一个optimizer.zero_grad()必须在loss.backward()之前执行如果放错位置梯度会在不同batch之间累积loss表现成周期性跳动这种问题PyTorch不会报错只能靠经验识别。第二个model.train()不能漏它决定BatchNorm层使用当前batch的统计量还是累积统计量漏掉它的话训练和测试结果都会失真。第三scheduler.step()放在每个epoch结束后step_size5, gamma0.1的含义是前5个epoch用0.001的学习率快速拟合第6个epoch开始学习率降为0.0001把权重磨稳定。优化器这里选SGDmomentum而不是Adam是因为这个数据量下SGD配合StepLR的收敛曲线更平滑最终准确率通常也更高。Adam的优点是前期下降快但后期容易在最优解附近震荡。如果你更追求省心用Adam的话学习率建议调到0.0001且可以去掉调度器。4. 避坑指南处理水果分类数据时最常见的4个翻车现场4.1 图片加载失败中文文件名与扩展名不统一现象ImageFolder加载时报“Found 0 images in subfolder”或者训练时某张图直接抛PIL.UnidentifiedImageError。原因这类从平台下载的数据集有两个隐患。一是文件名里的中文经过不同压缩工具解压后可能乱码尤其Windows下编码不一致时文件夹名乱码会导致整个类别文件夹对不上号二是图片扩展名不完全统一.jpeg和.jpg混用少数文件可能只是改了扩展名但内部格式仍是PNG。解决解压后的第一件事不是训练而是全量校验一遍图片是否能正常打开同时把扩展名统一from PIL import Image import os def check_images(root_dir): bad_files [] for cls in os.listdir(root_dir): cls_dir os.path.join(root_dir, cls) if not os.path.isdir(cls_dir): continue for f in os.listdir(cls_dir): p os.path.join(cls_dir, f) try: Image.open(p).verify() except Exception: bad_files.append(p) return bad_files print(train bad:, check_images(data-train)) print(test bad:, check_images(data-test))排到损坏文件后直接删除或替换即可。对中文文件名我一般提前用脚本重命名成纯拼音或数字编号顺手把扩展名规范成.jpg避免后续跨平台或部署到Linux服务器时再踩一遍。4.2 类别顺序错位ImageFolder排序与classes.json对不上现象训练过程loss正常下降但评估时“苹果”的预测整体偏向“香蕉”准确率直接崩到十几甚至更低。原因ImageFolder的类别索引是按文件夹在磁盘上的排序生成的这个排序在不同操作系统下可能是中文编码序、英文名称序或拼音序不一定是人类直觉里的“苹果、香蕉、樱桃……”顺序。如果你拿着classes.json里的顺序去解读模型的数字输出就会整体错位。这个问题最隐蔽的地方在于它不会报错loss曲线看起来一切正常只有结果解释时才露馅。解决用自己的代码直接打印train_set.classes和train_set.class_to_idx以它为准。模型预测出来的数字标签preds用train_set.classes[pred]来找含义不要拿外部JSON文件来映射。classes.json可以用于核对类别名总数是否为8但不要直接当成训练顺序。4.3 训练集与测试集分布不一致导致掉点现象训练集准确率95%以上测试集却只有60%出头而且每次epoch之间的测试波动很大。原因这不是代码bug而是数据本身的分布差异。最典型的是“同一种水果不同品种”训练集里全是红富士那种大红色苹果测试集里出现了青苹果或者训练集图片全是白背景商品图测试集里出现了手拿水果的场景图。水果数据集由于拍摄环境不稳定这类问题几乎必然存在。解决训练过程正常但测试偏低时先去看错误样本而不是急着调模型结构。把混淆矩阵打印出来确认哪些类互相混。如果集中在某两类先补充数据增强如果分散在各处说明模型容量或训练时长不够。另外可以考虑从训练集里切出10%当验证集监控验证曲线避免盲调。4.4 显存不足batch_size与图像尺寸的取舍现象训练到第一个epoch时直接OOMOut of Memory或者num_workers4导致内存直接吃满。原因ResNet18在224×224输入下batch_size32约需要3-4GB显存。如果在2GB左右的卡上训练OOM几乎是必然。num_workers过大则会让多个数据加载进程同时把预处理后的图片往内存里塞内存小的话也会爆。解决优先把batch_size降到16或8这是最直接的调整。但减小batch_size后学习率也要相应降一些比如从0.001降到0.0005因为小batch的梯度方差更大。还有一种做法是把输入改小transforms.Resize(192)CenterCrop(192)输入面积少了约28%显存占用明显下降准确率损失通常不到1%。如果这样还不够就换轻量模型如MobileNetV3-Small分类头的替换方式与ResNet完全一致。5. 模型评估与可视化用混淆矩阵定位那些“长得像”的误判5.1 测试集预测与分类指标计算训练10个epoch后在测试集上评估模型是必做的一步。这里我直接用全部550张测试图计算整体准确率、每个类的精确率和召回率再打印混淆矩阵from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds, all_labels [], [] with torch.no_grad(): for inputs, labels in test_loader: outputs model(inputs) _, 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_namestrain_set.classes)) print(cm)关键在model.eval()和torch.no_grad()两行。前者把BatchNorm和Dropout切到推理模式后者关闭梯度跟踪减少显存占用。注意preds.cpu().numpy()必须先把tensor搬到CPUGPU上的tensor不能直接转numpy。classification_report会输出每个类别的precision、recall和f1-score这比只看整体accuracy有价值得多。举例来说芒果和木瓜在颜色纹理上都有“黄”的共性如果这两个类互相误判报告里会看到两个类的recall同时偏低但整体的accuracy可能还维持在85%以上——只有拆开看每类指标才能发现。5.2 错误样本可视化反归一化与网格展示数字指标只能告诉你“有多少错”看不出“错在哪里”。把错误样本画出来才是定位问题的关键一步import matplotlib.pyplot as plt import numpy as np class_names train_set.classes mis_idx [i for i, (p, l) in enumerate(zip(all_preds, all_labels)) if p ! l] print(f错误样本数: {len(mis_idx)}) fig, axes plt.subplots(2, 3, figsize(12, 8)) for idx, ax in zip(mis_idx[:6], axes.flatten()): img, _ test_set[idx] img img.numpy().transpose(1, 2, 0) img img * np.array([0.229, 0.224, 0.225]) np.array([0.485, 0.456, 0.406]) img np.clip(img, 0, 1) ax.imshow(img) ax.set_title(ftrue: {class_names[all_labels[idx]]}\n fpred: {class_names[all_preds[idx]]}) plt.tight_layout() plt.show()这里最容易被忽略的是反归一化。训练时的Normalize把像素从[0,1]减去均值再除以方差所以直接拿tensor显示出来的图是偏色甚至接近灰噪点的。展示前必须乘回标准差再加回均值然后clip到[0,1]。另外注意test_set[idx]返回的图像是已经过预处理后的版本经过了Resize和CenterCrop所以画出来的图不是原始完整图而是一张中心裁剪后的正方形图。如果你想看原图需要自己从>import torch.onnx model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, fruit_classifier.onnx, input_names[input], output_names[output], opset_version11 )导出时有两个关键点model.eval()必须在导出前调用否则BatchNorm的参数会处于训练统计量的不确定状态dummy_input的尺寸要和训练时一致如果你的预处理改成192×192这里也要同步改。转换后用onnxruntime跑一遍推理和PyTorch的结果对比正常情况下误差在1e-4量级以内。我的习惯是从做第一个水果分类任务开始就强制自己走完整流程拿到数据集先统计类别分布再可视化抽检然后打印class_to_idx和classes确认标签对齐最后才上训练。这三步加起来不超过两分钟但能避开后面调一晚上模型的坑。希望帮到你。本文还有配套的精品资源点击获取