简介:苹果品种分类数据集是一份面向机器学习与计算机视觉研究者的图像资源,适合从事智能农业、食品质量检测及图像识别算法训练的开发者和学生使用。压缩包共收录1766个文件,整体约64.01MB,其中305个jpg与275个jpeg构成核心图像样本,580个xml文件对应目标检测标注信息,606个txt文件则承载品种名称、产地、口味特点等分类标签,便于主流框架直接读取与处理。数据集覆盖苹果全貌、切面、横截面等多角度图像,并兼顾不同背景环境,可用于训练卷积神经网络等深度学习模型,评估算法在复杂场景下的识别性能。目前已有206人学习下载,读者可借助其完整的图像与标注结构,快速搭建品种分类实验流程,为农业自动化识别、品质检验与电商应用提供可复用的数据基础。
1. 苹果品种分类数据集:580 张图能跑出什么名堂
拿到一份 580 张图的苹果品种分类数据集,第一反应往往是「这点量够训模型吗」。我拆过不少农业图像的小数据集,说实话,580 张在深度学习里属于「小样本」范畴,但它恰恰是练手和验证 pipeline 的好材料。这份数据集收录了 580 张高清苹果图片,覆盖多个品种,文件名以数字编号命名(如 203.jpeg、530.jpeg、18.jpeg),格式为 JPEG,适合直接喂给主流的图像分类框架。它能解决的核心问题是:让你在不需要海量爬虫、不需要标注团队的情况下,快速跑通一条从数据加载、增强、训练到评估的完整链路。适合谁?刚接触计算机视觉、想拿真实农业场景练手的开发者,以及需要快速验证某个 backbone 在细粒度分类任务上表现的算法工程师。别指望它刷 SOTA,但它能帮你把「数据怎么读、标签怎么对、增强怎么做」这些脏活累活先趟一遍。
2. 数据集的目录结构与标签映射:先搞清楚文件名到底代表什么
2.1 从文件名到类别标签的推断逻辑
这份数据集最直接的特征是文件名全是数字加.jpeg后缀,比如203.jpeg、530.jpeg、18.jpeg。这里有个关键问题:文件名本身不携带品种信息。也就是说,你不能像 ImageNet 那样从文件名直接解析出类别。常见做法是,数据集在打包时会把不同品种的图片放在不同子目录下,或者附带一个labels.csv/classes.txt映射文件。如果拿到手发现所有图片平铺在一个文件夹里,那大概率需要你手动按品种分目录,或者根据图片内容做聚类后再人工校验。
我一般会先跑一段脚本,把目录结构和文件数量摸清楚:
import os from pathlib import Path from collections import Counter data_root = Path("./apple_dataset") # 替换成你解压后的实际路径 # 统计所有图片文件 image_files = list(data_root.rglob("*.jpeg")) + list(data_root.rglob("*.jpg")) + list(data_root.rglob("*.png")) print(f"总图片数: {len(image_files)}") # 查看目录层级分布 dirs = [str(p.parent.relative_to(data_root)) for p in image_files] dir_counter = Counter(dirs) for d, count in dir_counter.most_common(): print(f"目录: {d} -> {count} 张")这段代码的逻辑很直白:递归扫描所有图片格式,统计总数,然后按父目录分组计数。参数上,rglob支持递归匹配,relative_to让输出路径更干净。如果输出显示所有图片都在根目录下,那你就得考虑用无监督方式先粗聚类,或者找找有没有隐藏的标注文件。注意,有些数据集会把标签藏在图片的 EXIF 信息里,虽然少见,但值得用exifread查一下。
2.2 划分训练集、验证集与测试集的比例选择
580 张图,如果按 7:2:1 划分,训练集 406 张,验证集 116 张,测试集 58 张。这个量级下,验证集和测试集的波动会比较大,一次评估的准确率可能上下浮动 5 个百分点。我的血泪经验是:小数据集不要只做一次划分,用 5 折交叉验证更稳。但如果你只是想快速看个趋势,固定划分也能用。
划分时要注意类别均衡。假设数据集有 5 个品种,每个品种大约 116 张,那按比例分层抽样(stratified split)是必须的。用sklearn的train_test_split可以指定stratify参数:
from sklearn.model_selection import train_test_split import numpy as np # 假设 labels 是长度为 580 的列表,每个元素是品种名称 labels = [...] # 从你的标注文件读取 indices = np.arange(len(labels)) # 先分训练+验证 和 测试 train_val_idx, test_idx = train_test_split( indices, test_size=0.1, stratify=labels, random_state=42 ) # 再从训练+验证中分训练和验证 train_idx, val_idx = train_test_split( train_val_idx, test_size=0.2, stratify=[labels[i] for i in train_val_idx], random_state=42 ) print(f"训练集: {len(train_idx)}, 验证集: {len(val_idx)}, 测试集: {len(test_idx)}")参数说明:test_size=0.1表示测试集占 10%,stratify保证划分后各类别比例一致,random_state固定随机种子方便复现。这里有个坑:如果某个品种只有 10 张图,分层抽样后训练集可能只剩 7 张,模型根本学不动。所以先统计每个类别的数量,少于 50 张的类别要考虑合并或过采样。
3. 用 PyTorch 搭一条训练流水线:从 DataLoader 到第一个 baseline
3.1 自定义 Dataset 与图像增强策略
PyTorch 的Dataset类是小数据集训练的标准入口。你需要实现__len__和__getitem__两个方法。对于苹果品种分类,图像增强是提升泛化能力的关键,因为 580 张图实在不多。常见的增强手段包括随机裁剪、水平翻转、颜色抖动、旋转等。但要注意:苹果的品种区分有时依赖颜色和纹理,过度颜色抖动可能把红富士和嘎啦果的特征搅乱。
我一般会这样写:
import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class AppleDataset(Dataset): def __init__(self, image_paths, labels, transform=None): self.image_paths = image_paths self.labels = labels self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img = Image.open(self.image_paths[idx]).convert("RGB") label = self.labels[idx] if self.transform: img = self.transform(img) return img, label # 训练集增强:适度激进 train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集和测试集:只做 resize 和归一化 val_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]) ])逻辑说明:RandomResizedCrop的scale=(0.7, 1.0)表示随机裁剪面积占原图的 70% 到 100%,再缩放到 224。ColorJitter的hue只给 0.05,就是怕把苹果颜色改得面目全非。归一化用的 ImageNet 均值方差,因为后面要加载预训练权重。参数怎么改?如果你的图片分辨率远高于 256,可以把Resize调大;如果显存不够,把224降到128或160。
3.2 加载预训练模型并替换分类头
580 张图从零训练 CNN 基本没戏,迁移学习是唯一靠谱的路子。用torchvision.models加载 ResNet18 或 EfficientNet-B0,把最后的全连接层换成你的类别数。常见做法是冻结前面的卷积层,只训分类头,等 loss 稳定后再解冻部分层做微调。
import torch.nn as nn from torchvision import models num_classes = 5 # 根据你的实际品种数修改 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 冻结所有参数 for param in model.parameters(): param.requires_grad = False # 替换最后的全连接层 model.fc = nn.Linear(model.fc.in_features, num_classes) # 只优化 fc 层 optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss()参数说明:weights=IMAGENET1K_V1加载 ImageNet 预训练权重,requires_grad=False冻结特征提取层,model.fc.in_features通常是 512(ResNet18)。优化器只传model.fc.parameters(),学习率给 1e-3。训练几个 epoch 后,如果验证集准确率不再上升,可以解冻layer4并调小学习率到 1e-4 继续微调。注意:解冻后要重新创建优化器,把解冻层的参数也加进去。
3.3 训练循环与验证集监控
训练循环本身不复杂,但小数据集上容易过拟合,所以每个 epoch 都要在验证集上跑一遍,记录 loss 和准确率。我习惯用tqdm看进度,用简单的早停策略防止过拟合。
from tqdm import tqdm device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) best_acc = 0.0 patience = 5 counter = 0 for epoch in range(30): model.train() running_loss = 0.0 for imgs, labels in tqdm(train_loader, desc=f"Epoch {epoch+1}"): imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() # 验证 model.eval() 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) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total print(f"Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_apple_model.pth") counter = 0 else: counter += 1 if counter >= patience: print("早停触发") break逻辑说明:每个 epoch 先训练再验证,保存验证集准确率最高的模型。patience=5表示连续 5 个 epoch 验证集准确率没提升就停。参数上,batch_size建议设 16 或 32,太小梯度不稳,太大显存吃紧。学习率如果解冻了部分层,记得调小。
4. 避坑与排查:小数据集训练中最容易翻车的五个地方
4.1 现象:训练准确率冲到 99%,验证集却卡在 60%
原因:典型过拟合。580 张图,模型参数量远大于样本量,加上增强不够或没做正则化,模型直接把训练集背下来了。 解决:先冻结 backbone 只训分类头,加 Dropout(nn.Dropout(0.5))和权重衰减(weight_decay=1e-4),增强里加上RandomErasing。如果还不行,换更小的模型,比如 MobileNetV3-Small。
4.2 现象:loss 变成 NaN,训练直接崩
原因:学习率太大,或者输入数据没归一化,或者某张图片损坏导致像素值异常。 解决:先把学习率降到 1e-4 试试。检查归一化是否漏了。用 PIL 打开每张图,捕获异常,把打不开的图片剔除。我遇到过一张 CMYK 模式的 JPEG,转 RGB 后就好了。
4.3 现象:验证集准确率波动巨大,每次跑结果都不一样
原因:验证集太小(116 张),随机种子没固定,或者数据划分时类别不均衡。 解决:固定torch.manual_seed(42)和np.random.seed(42)。用分层抽样划分数据。如果条件允许,做 5 折交叉验证,取平均准确率。
4.4 现象:模型把某个品种全预测成另一个品种
原因:两个品种外观极其相似,或者某个品种的图片背景单一,模型学到了背景而不是苹果本身。 解决:检查混淆矩阵,找出混淆的类别对。针对性地对这两类做更强的增强,比如随机裁剪不同区域、换背景。如果数据集里某个品种的图片全是白色背景,考虑用分割把背景去掉再训练。
4.5 现象:GPU 显存够,但训练速度慢得离谱
原因:DataLoader的num_workers设成了 0,或者图片没提前 resize,每次都在读大图。 解决:把num_workers设成 4 或 8(Windows 下可能报错,设 0 或 2)。提前把所有图片 resize 到 256x256 存一份缓存,训练时直接读缓存。另外,pin_memory=True能加速 GPU 传输。
5. 进阶技巧:用混淆矩阵和 Grad-CAM 把模型的黑匣子撬开
训练完一个模型,准确率只是个数字。想知道模型到底在看苹果的哪个部位,Grad-CAM 是最直观的工具。它能把模型关注区域热力图叠加到原图上,一眼看出模型是看苹果形状、颜色还是背景。对于农业图像任务,这个技巧能帮你判断模型是否学到了真正的品种特征。
先装pytorch-grad-cam:
pip install grad-cam然后写一段推理脚本:
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import numpy as np import cv2 # 加载训练好的模型 model.load_state_dict(torch.load("best_apple_model.pth")) model.eval() # 选择目标层,ResNet18 通常是 layer4 target_layers = [model.layer4[-1]] # 准备一张测试图片 img_path = "test_apple.jpg" rgb_img = cv2.imread(img_path, 1)[:, :, ::-1] # BGR 转 RGB rgb_img = cv2.resize(rgb_img, (224, 224)) input_tensor = val_transform(Image.open(img_path).convert("RGB")).unsqueeze(0).to(device) cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor=input_tensor)[0, :] visualization = show_cam_on_image(rgb_img / 255.0, grayscale_cam, use_rgb=True) cv2.imwrite("cam_output.jpg", visualization[:, :, ::-1])逻辑说明:target_layers指定要可视化的卷积层,越靠后感受野越大。grayscale_cam是归一化的热力图,show_cam_on_image把它叠加到原图。参数上,use_rgb=True保证颜色通道正确。跑完打开cam_output.jpg,如果热力图集中在苹果轮廓和颜色区域,说明模型学对了;如果集中在背景或角落,那你的数据增强或背景处理需要返工。
除了 Grad-CAM,混淆矩阵也是必看的。用sklearn.metrics.confusion_matrix跑一遍测试集,把矩阵打印出来。我一般会盯着对角线以外的数字,哪个格子大,就去翻对应的图片看看到底哪里像。有一次我发现模型把两个品种搞混,翻图后发现那两张图拍摄角度几乎一样,连梗的方向都一致,这种就是数据泄露的隐患。
最后说个习惯:每次拿到新数据集,我都会先跑一遍torchsummary看模型参数量,再跑一遍pandas统计类别分布,最后用matplotlib画 16 张随机样本的网格图。这三步走完,心里就有底了。从那以后我每次训小数据集都强制走一遍这个流程,省得训到一半才发现某个类别只有 3 张图。希望帮到你。
本文还有配套的精品资源,点击获取