
简介面向农产品视觉质检与图像分类研究资源提供了一份标注完整的马铃薯食物缺陷图像数据集适用于有监督分类模型的训练与评估。样本量约450张覆盖Dry Rot、Blackleg、Pink Rot等7类典型采后缺陷并预先划分训练集与测试集可直接用于CNN等深度学习分类器的训练和验证。压缩包共454个文件主体为jpg/jpeg格式的样本图像另含json类别标签文件、Python编写的show可视化脚本以及一张说明图片整体仅12.58MB便于快速下载与本地复现。目前已有150人学习使用。下载后既可加载数据训练基线分类器也可运行show脚本检查各类样本的数量与分布为数据增强和网络调参提供直观依据若需继续优化可参考作者公开的CNN分类网络改进专题从残差结构、注意力机制或损失函数等方向入手提高马铃薯缺陷识别准确率。1. 只有约450张已标注图的马铃薯缺陷分类值不值得做先说结论像“马铃薯食物缺陷图像分类数据集【已标注约450张数据】”这种规模的项目恰恰是图像分类落地里最容易被低估、也最容易翻车的一类。450张听起来少但对于缺陷检测这种类别高度集中、背景受控的场景它完全够跑通一条“数据验证→模型训练→上线评估”的完整链路。你要解决的不是“训练一个大模型”而是“在有限标注下把缺陷类型分得稳、分得准”。很多人一看到几百张图就觉得必须上数据增强、必须上预训练大模型其实真正卡住你的往往不是模型而是标签定义和验证集划分。比如“缺陷”到底按腐烂、机械伤、发芽还是畸形来分每种缺陷有多少张有没有同一张图重复出现这些直接决定了训练出来的模型是可用工具还是黑匣子。这篇笔记就基于450张左右的已标注图像讲清楚怎么做数据检查、怎么做训练验证划分、怎么用ResNet18跑通一个高精度基线以及那些会让你白干一周的坑。2. 先搞清这450张图该怎么用缺陷类别定义与标注质量核对2.1 缺陷分类不是“有缺陷/无缺陷”二值问题马铃薯食物缺陷在真实产线上通常分几类腐烂软腐、干腐、机械损伤切削伤、擦伤、发芽、绿皮、畸形。约450张图听起来是“一个数据集”但打开标注文件之前必须先把类别清单摸清楚。常见做法是打开label文件CSV或JSON统计每个类别的样本数。import pandas as pd df pd.read_csv(labels.csv) counts df[defect_type].value_counts() print(counts)这段代码做的是类别分布统计。defect_type列是我假设的标注字段名实际可能是label或class先打印列名再说。这一步的意义在于如果某类只有20张训练时要么合并成“其他缺陷”要么接受它永远学不好。参数上需要注意分类别计数之后要把“样本数少于30张”的类别单独列出来这些类在后面的数据增强和类别权重里要特殊处理。不要直接删先看它和哪个大类在语义上相近。2.2 检查标注一致性同一张图反复出现是最大的隐患450张已标注图最常见的坑是“同一张原图被裁剪成多张”或者“同一缺陷被标了两次”。图像分类模型如果训练集和验证集里出现同一来源的图像验证分数会虚高线上表现直接打对折。import hashlib from pathlib import Path def img_hash(path): return hashlib.md5(path.read_bytes()).hexdigest() hashes {} for p in Path(images).glob(*.jpg): h img_hash(p) hashes.setdefault(h, []).append(p) dups {h: ps for h, ps in hashes.items() if len(ps) 1} print(f重复组数: {len(dups)})这里用的是MD5文件哈希计算整张图片的二进制指纹。只要两张图哪怕只有一个像素不同哈希就不同所以它查的是“完全重复”不是“相似”。查出重复组之后保留其中一张其余从训练集和验证集里一起剔除。提示要查“相似而不同”的图需要用感知哈希pHash但450张图的规模下完全重复才是主要矛盾先做MD5就够。2.3 标注文件与图像文件的数量交叉验证450张标注图经常出现图像文件夹里496张、标注文件里450条的情况。多出来的46张是废图还是漏标这个必须在对模型动手之前解决。直接做一次集合比对labels set(df[image_id]) files {p.stem for p in Path(images).glob(*.jpg)} print(有图无标注:, files - labels) print(有标注无图:, labels - files)set的差集运算一次把两边对不上号的都列出来。有图无标注的要么补标要么移出训练目录有标注无图的说明数据记录有误直接删掉对应行。做完这步数据集才算真正进入可用状态。3. 从450张到可靠实验划分训练集与验证集的标准流程3.1 按缺陷类别分层划分避免验证集“恰好缺了一类”450张图如果随机划分最惨的情况是验证集里完全没有“腐烂”这个类别训练时模型没见过这个类的验证样本最后报告的数字没有任何意义。正确做法是按类别比例分层抽样确保训练集和验证集里每个类别的占比与原数据集一致。from sklearn.model_selection import train_test_split df[split] train for cls in df[defect_type].unique(): cls_idx df[df[defect_type] cls].index train_idx, val_idx train_test_split( cls_idx, test_size0.2, random_state42, stratifydf.loc[cls_idx, defect_type] ) df.loc[val_idx, split] val print(df.groupby([defect_type, split]).size())这里对每个缺陷类别单独做了一次分层切分。test_size0.2意味着450张里大约90张进验证集。random_state42固定随机种子保证你每次跑出来的划分完全一致——这是实验可复现的前提。有个细节值得注意stratify的标签向量传的是defect_type但因为已经按类别过滤了所以每个子集内部其实是单类别这行参数更多是形式上的保险。真正起作用的是外层按类别循环这个结构。3.2 按来源分组划分的问题如果这批图像来自不同产地、不同批次按文件名或目录名分组会让验证更接近真实场景。做法是先提取文件名中的批次字段然后按批次划分而不是按单张图划分。df[batch] df[image_id].str.extract(rbatch_(\d)) batches df[batch].unique() val_batches batches[:2] # 取前两个批次做验证集 df[split] df[batch].apply(lambda b: val if b in val_batches else train)这种划分方式比随机划分更严格因为它验证的是模型“没见过这个批次的拍摄条件”时的表现。代价是如果两个批次的缺陷分布差异很大验证集会很难看但这恰恰是真实上线会遇到的情况。450张图如果可以按批次划分建议优先用这种方式。3.3 验证集规模的下限与置信度90张验证图能说明问题吗对于一个二分类模型90张验证图得到的准确率95%与91%之间的差异其实不一定是模型变好了可能只是换了一批验证图。所以在报告结果时要同时给出每类的样本数而不是只给一个总体准确率。最好把验证集的类别分布打印出来每个类别至少保留15到20张低于这个数就考虑把类别合并。通常在450张的规模下5类缺陷每类保留15张以上是能做到的。4. 用ResNet18在450张图上跑通基线迁移学习与参数设置4.1 为什么选ResNet18而不是ViT450张图训ViT几乎必然过拟合。ViT需要大量数据学出全局注意力模式几百张图根本不够。而ResNet18是ImageNet预训练模型里参数最少、最容易收敛的之一它的归纳偏置局部卷积、层级特征非常适合缺陷这种局部纹理起决定作用的图像分类任务。注意这里用的是“预训练权重微调”而不是“从零训练”。从零训练ResNet18在450张图上也能收敛但效果远不如微调而且训练时间更长。4.2 最小可跑通的训练脚本训练部分需要把图像整理成ImageFolder可读的目录结构即train/类别名/图片.jpg这种层级。然后做数据增强随机翻转、随机旋转、色彩抖动。色彩抖动对马铃薯缺陷尤其重要因为不同产地的薯皮颜色深浅差异很大。import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder transform_train transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), 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]), ]) transform_val 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_ds ImageFolder(data/train, transformtransform_train) val_ds ImageFolder(data/val, transformtransform_val) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size16, shuffleFalse, num_workers4) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, len(train_ds.classes)) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4)参数说明batch_size16在450张图规模下是合理选择显存不够可以降到8lr0.001是微调ResNet18的常用初始值比从头训练的0.01要低因为预训练权重已经在一个好的起点上SGDmomentum在这个任务上通常比Adam更稳尤其数据量小的时候。RandomResizedCrop(224, scale(0.8, 1.0))的scale范围设得窄是因为缺陷区域可能只占图像的30%裁剪范围太大容易切掉缺陷本身。这里宁缺毋滥。4.3 训练循环与早停450张图的训练不需要跑很多轮通常15到20个epoch就够。关键是监控验证集loss并在验证loss不再下降时停止否则模型会从“记住缺陷特征”滑向“背下训练图像”。best_val_acc 0 patience 5 bad_epochs 0 for epoch in range(20): model.train() for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() model.eval() correct, total 0, 0 val_loss 0.0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total print(fEpoch {epoch}: val_acc{val_acc:.4f}, val_loss{val_loss:.4f}) if val_acc best_val_acc: best_val_acc val_acc bad_epochs 0 torch.save(model.state_dict(), best_mulberry.pth) else: bad_epochs 1 if bad_epochs patience: print(早停触发) break逻辑说明每个epoch结束后比较验证集准确率如果连续5个epoch没有刷新记录就停。保存的是验证集表现最好的权重而不是最后一轮的这等于给你吃了颗后悔药——即使后面过拟合了你手里还有最优版本。4.4 类别不均衡的处理450张图天然存在类别不均衡比如机械伤150张、腐烂只有40张。两个办法一是给CrossEntropyLoss传weight参数让少数类的梯度更大二是在数据增强时对少数类做更强的增广。from sklearn.utils.class_weight import compute_class_weight import numpy as np class_weights compute_class_weight( class_weightbalanced, classesnp.array(train_ds.classes), ynp.array([train_ds.classes[i] for i in train_ds.targets]), ) loss_weight torch.tensor(class_weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightloss_weight)compute_class_weight的balanced模式会根据样本数自动计算权重样本越少的类权重越大。注意y参数传的是每个样本的类别索引而不是类别名。这个改动带来的提升通常在2到5个点之间值得做。5. 避坑与常见问题排查小数据集图像分类的五个典型翻车现场5.1 验证集准确率95%线上识别一塌糊涂现象离线实验报告验证集准确率95%部署到实际环境后准确率掉到70%以下。原因数据泄露。常见的泄露途径是同一颗马铃薯在不同角度、不同光照下拍了多张图这些图被同时分到了训练集和验证集。模型等于“见过”验证集的同源图像。解决回到第2章用MD5先查完全重复再用感知哈希查近似重复。按来源批次重新划分数据集再训练。这个动作能解释大量“离线好、线上差”的怪象。5.2 训练loss下降但验证loss反弹现象前10个epoch验证loss稳步下降第11轮开始验证loss持续上升训练loss还在降。原因过拟合开始。450张图的训练集容量有限模型从第10轮左右开始“记住”训练图的细节噪声而不是通用缺陷特征。解决触发早停取第10轮的权重。同时可以调低lr比如降到0.0003让模型更慢地逼近训练集或者把weight_decay从1e-4提到5e-4。5.3 某一个类别准确率特别低其他类都正常现象总体准确率90%但“绿皮”类只有50%。原因类别样本太少或标注边界不清。绿皮与其他类别在视觉上容易混淆或者标注时把“轻微绿皮”标成了“正常”。解决打开这类图像的标注逐一检查是否误标。再把训练集中该类的图像数量打出来——如果只有20张考虑收集更多数据或将其合并到“其他缺陷”类。5.4 增强后图像缺陷区域被裁掉现象加了RandomResizedCrop之后验证集效果反而下降。原因scale设得太低比如默认的(0.08, 1.0)会随机裁出一小块纯色背景把缺陷区域裁掉。解决把scale限制在(0.7, 1.0)或(0.8, 1.0)。缺陷检测任务里保留全局信息比追求尺度不变性更重要。还有一个替代做法是用Resize(256) RandomCrop(224)替代RandomResizedCrop虽然多样性略降但缺陷区域丢失的概率小很多。5.5 分类报告里某个类精确率很高、召回率很低现象打印classification_report发现“腐烂”类精确率95%、召回率40%。原因模型把大部分腐烂样本都判成了机械伤。二者在视觉上确实有重叠——腐烂初期表面发黑和机械伤形成的暗色区域很像。解决首先检查标注是否把“腐烂初期”和“机械伤”混标了。如果标注没问题考虑合并这两个类别为“表面损伤”先做粗粒度分类再看要不要细分。小数据集上粗粒度分类比硬拆细类的可持续性更强。提示五个问题里三个根源都是“数据本身有问题”不是模型问题。先用第2章的检查脚本过一遍数据再谈调参能省掉大量无效训练时间。6. 让450张图的模型更稳五折交叉验证与全量训练的两步走6.1 五折交叉验证估计真实水平前面训练时用了一次随机划分结果可能偏高或偏低。用五折交叉验证能更客观地估计这个数据集上模型的真实水平。把450张图分成5份轮流拿4份训练、1份验证最终取5次验证准确率的平均值和标准差。from sklearn.model_selection import StratifiedKFold import numpy as np skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) accs [] for fold, (train_idx, val_idx) in enumerate(skf.split(df[image_id], df[defect_type])): # 根据 train_idx / val_idx 生成 train.txt / val.txt # 训练脚本同上记录每个fold的最佳验证准确率 passStratifiedKFold保证每一折里各类别比例与原数据集一致。五折训练在450张图规模下单卡几分钟到十几分钟就能跑完成本完全可接受。如果5折准确率的标准差超过3个百分点说明数据划分对结果影响很大要回到数据检查环节找原因。6.2 全量训练与最终部署参数五折交叉验证确认模型水平之后最后一版模型用全部450张图训练。这个版本的上线准确率预计会比五折平均值略高因为模型见过更多训练样本。训练参数与第4章一致但可以适当增大RandomRotation到20度给模型多一点姿态鲁棒性。6.3 可视化与错误分析最后一步把验证集中预测错误的图像单独保存成文件夹人工过一遍。这个动作能直接告诉你“还有哪些缺陷形态没学到”。我一般会把错误图按“真实类别/预测类别”命名比如腐烂_机械伤_001.jpg然后用一个很小的图像查看器过一遍。这一步的花费是十分钟省下的却是上线后反复返工的时间。对一个450张的已标注数据集能做到“五折准确率稳定在90%以上、错误样本人工可解释”这个方案就达到了生产可用的起点。希望这些步骤能帮你把数据用透少走我当年走过的弯路。本文还有配套的精品资源点击获取