马铃薯缺陷图像分类实战:7类病害数据集与CNN迁移学习
发布时间:2026/9/28 1:45:06来源:尧图网络
简介面向图像分类实践的马铃薯缺陷数据集已按7个类别完成标注涵盖Dry Rot、Blackleg、Pink Rot等常见病害类型。约450张图像以jpg/jpeg格式存储并附json标注文件与show可视化脚本可快速查验标签分布与类别定义数据按训练集与测试集分开存放同类图片归入对应目录无需额外整理即可直接用于CNN分类模型训练、验证与结果对比。包内共454个文件除主要图像数据外还包含配套Python脚本与标注信息压缩包整体约12.58MB下载与迁移成本低。已有150人学习使用既适合初学者入门图像分类流程也可作为研究人员验证网络改进效果的基准数据。结合作者提供的CNN分类网络改进系列内容可围绕马铃薯缺陷场景完成数据加载、模型搭建、训练评估等完整闭环。1. 马铃薯缺陷图像分类七类病害数据集的第一手拆解做农业视觉落地的同行应该都有同感模型结构从来不是瓶颈数据集才是。一个标注干净、类别齐全的马铃薯缺陷数据集比调三天网络结构都值钱。这个资源是约 450 张已标注的马铃薯缺陷图覆盖 Dry Rot、Blackleg、Pink Rot 等 7 个分类训练集和测试集已经按类别分好还附带一个 show 脚本可以直接可视化。对于正在做图像分类入门、食品质检项目原型验证或者需要一份真实农业病害数据来跑 CNN 改进实验的工程师来说这东西拿到手就能用不用自己蹲在分拣线上拍照片。我需要先说清楚一个判断450 张图对深度学习来说不算大但这个数据量恰好卡在「能训练」和「需要认真防过拟合」之间用来做迁移学习、数据增强实验、以及分类头改进的对比基线非常合适。接下来我会从数据目录结构、标注维度、训练配置、踩坑记录到迁移复用的边界完整拆一遍。2. 数据集的真实结构从文件名到目录划分的潜台词2.1 解压之后的目录应该长什么样拿到压缩包之后我习惯先不急着训练而是把目录树完整列出来。这个数据集虽然数量不大但作者已经预设了训练集和测试集分开存放的结构目录组织方式对自定义训练脚本非常友好。常见的解压后目录结构是这样的potato_defect_dataset/ ├── train/ │ ├── DryRot/ │ │ ├── 18.jpeg │ │ ├── 35.jpeg │ │ └── ... │ ├── Blackleg/ │ ├── PinkRot/ │ └── ... 其余类别 ├── test/ │ ├── DryRot/ │ └── ... ├── json/ │ └── label_config.json └── show.py这个结构最大的好处是直接兼容 PyTorch 的ImageFolder和 TensorFlow 的flow_from_directory不需要额外写数据映射逻辑。每个类别的文件夹名就是标签名训练时按目录顺序自动编码。注意18.jpeg、42.jpeg这种文件名是作者原始采集时的编号不代表任何类别信息千万别尝试从文件名推断标签。2.2 JSON 里藏着类别映射和标注信息项目描述里特别提到「具体查看 json 文件」这说明类别定义和标注规则都在这份配置里。用文本编辑器打开 JSON 后通常能看到类似这样的内容{ num_classes: 7, classes: [ DryRot, Blackleg, PinkRot, SoftRot, CommonScab, BlackScurf, Healthy ], train_samples: 350, test_samples: 100, image_size_hint: [224, 224] }num_classes决定了分类头的输出维度classes列表的顺序直接对应ImageFolder编码后的索引顺序。这一点非常关键如果自己写数据加载器而不是用ImageFolder类别索引错一位整个训练就白跑。image_size_hint是给输入尺寸做参考的建议按 224×224 处理。2.3 show 脚本的用法与可视化验证项目里提供 show 脚本我一般会先跑一遍确认标注和图像能正确对应。运行方式通常是python show.py --data_dir ./potato_defect_dataset --num_samples 8脚本的逻辑一般是随机从每个类别抽样若干张图用 matplotlib 拼接展示并在每张图上方标注类别名称。这一步本质是做数据完整性校验。我通常会先在训练集上跑一次再去测试集上跑一次重点确认测试集里没有混入训练集的同名文件。如果脚本默认只展示一个目录可以自己改成接收--split参数分别传入train和test。提示图像的 EXIF 信息和文件原始分辨率不会影响模型训练但会影响预处理时的 resize 策略。马铃薯缺陷图通常来源复杂建议统一走中心裁剪而不是直接拉伸避免缺陷纹理变形。3. 七类缺陷的视觉特征模型要学的东西到底是什么3.1 内部腐烂型缺陷的判别维度Dry Rot干腐病在视觉上表现为薯皮表面出现凹陷、干缩的褐色区域边缘不规则切面能看到空洞和褐色坏死组织。Blackleg黑胫病在块茎层面更多表现为维管束环变黑外部可能不明显切开才能看到。这两类的共同点是「内部状态」比「表面状态」更有判别力但数据集里是整薯照片而非切面图所以模型实际上在学的是外部皮层的颜色纹理异常和轻微形变。对分类任务来说这意味着模型必须从表面特征推断内部状态有一定的不可靠性训练时要注意类别间的颜色分布差异。Pink Rot粉红腐病的典型特征是薯肉变粉红色、组织松软外部常伴随表皮开裂或水渍状斑点。从图像分类的角度看这类缺陷的颜色特征是最明确的粉色区域与正常黄白色薯肉在 RGB 空间的距离很大。这也意味着模型对这类缺陷的识别准确率通常会高于干腐病这类颜色对比度低的类别。3.2 表面附着型缺陷与状态混淆Common Scab疮痂病在表皮形成褐色至深褐色的粗糙疮痂斑块凸起或凹陷都有可能。Black Scurf黑痣病则是表面附着黑色菌核形状不规则但边界清晰。这两类的视觉差异比内部腐烂型更直观但也更容易和土壤残留、机械损伤混淆。实际训练时如果发现验证集精度停滞大概率是模型把 Common Scab 和 Black Scurf 当成同一类了——它们都是深色斑块区别在于疮痂是嵌入表皮的黑痣是附着的平面图像上这个深度信息会丢失。Healthy健康类是整个数据集的锚点。不管做二分类有病/无病还是七分类健康样本决定了决策边界的位置。我见过不少项目在缺陷类别上精度很高但在健康样本上误报率失控原因就是健康样本数量不足或者背景干扰比如泥土、光线变化没有被数据增强覆盖。这个数据集既然包含 Healthy 这一类别做误报评估会比只有缺陷类别的数据集正规得多。3.3 类别数对模型设计的影响7 个类别意味着分类头输出是 7 维向量这对所有主流 CNN 来说都没有压力。关键不是类别数本身而是类别间特征重叠度。从上面分析来看这个数据集的判别难点集中在「内部腐烂型」和「表面附著型」两个组内而不是组间。这会直接影响损失函数的选择——如果直接用CrossEntropyLoss类别不均衡某些缺陷样本少于 50 张会导致小类欠拟合改用LabelSmoothing或Focal Loss之后效果通常会改善。4. 训练一个 CNN 基线模型选择、超参数与完整流程4.1 选 ResNet18 还是 VGG16对于 450 张图、7 类别的规模模型的容量控制比结构先进性重要得多。ResNet18 的参数量约为 1120 万VGG16 约为 1.38 亿后者在这个数据规模和硬件条件下不仅训练慢而且过拟合风险极高。我一般会选 ResNet18 做基线理由很简单残差结构在梯度传播上的稳定性对小数据集友好而且从 ImageNet 加载预训练权重后做迁移学习收敛速度明显快于随机初始化。另一个可以考虑的选项是 EfficientNet-B0参数量只有 530 万左右精度略高于 ResNet18。但代价是输入分辨率一般要调到 240×240对预处理流程的依赖更高。对于这个数据集来说先用 ResNet18 跑通流程再去尝试 EfficientNet 做精度提升是性价比最高的路径。4.2 完整训练脚本数据增强、迁移学习与学习率策略这里我给出一个可复用的 PyTorch 训练脚本假设数据集目录结构符合前文描述import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 数据增强小数据集必须做否则必过拟合 train_transforms transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(20), transforms.ColorJitter(brightness0.3, contrast0.3), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) test_transforms 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_dataset datasets.ImageFolder(./potato_defect_dataset/train, train_transforms) test_dataset datasets.ImageFolder(./potato_defect_dataset/test, test_transforms) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers4) test_loader DataLoader(test_dataset, batch_size16, shuffleFalse, num_workers4) # 迁移学习加载 ImageNet 预训练权重替换最后一层 model models.resnet18(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, 7) # 冻结除最后两层外的所有参数降低过拟合风险 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD( [p for p in model.parameters() if p.requires_grad], lr0.001, momentum0.9, weight_decay1e-4 ) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, verboseTrue ) # 训练 30 个 epoch动态调整学习率 for epoch in range(30): model.train() running_loss 0.0 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() running_loss loss.item() model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in test_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc 100.0 * correct / total avg_val_loss val_loss / len(test_loader) print(fEpoch {epoch1}: Train Loss{running_loss/len(train_loader):.4f}, fVal Loss{avg_val_loss:.4f}, Val Acc{val_acc:.2f}%) scheduler.step(avg_val_loss)这段脚本的核心逻辑是「预训练权重 部分冻结 低学习率微调」。冻结前面层的原因很直接450 张图不足以从头学习底层边缘、纹理特征ImageNet 预训练的底层特征对马铃薯表面纹理同样有效只需要让layer4和全连接层去适应病害级别的高层语义。ReduceLROnPlateau的patience5意味着验证集损失连续 5 个 epoch 不下降就把学习率减半这个小数据集的训练后期基本都会触发。4.3 超参数的调优方向数据增强的强度是关键变量。RandomResizedCrop(scale(0.8, 1.0))控制了裁剪缩放范围如果后续验证集精度和训练集精度差距大于 15%可以继续调低scale的下界到 0.5并增加RandomErasing或MixUp。batch_size16在这种小数据集上合适如果显存允许可以调到 32但要同步提高weight_decay到1e-3来压制过拟合。ColorJitter的亮度扰动范围我设了 0.3这是针对分拣线光照不稳定的场景。如果你的实际部署环境光照恒定这个值可以降到 0.1过强的颜色抖动反而会破坏 Pink Rot 的粉色判别特征。提示ReduceLROnPlateau的verboseTrue在 PyTorch 1.x 后已弃用如果报错直接删掉这个参数。4.4 训练结果能到什么水平在合理的数据增强和迁移学习配置下这个数据集跑出 85% 到 92% 的测试集准确率是正常区间。如果低于 80%先检查数据加载是否有类别顺序错误其次是数据增强是否过强导致缺陷特征被破坏。如果高于 95%先怀疑测试集里是否混入了训练集样本——这不是作者的问题而是自己在复制数据集时目录搞混。5. 训练土豆缺陷分类的避坑指南七条血泪经验5.1 类别不均衡导致的假精度现象测试集总精度接近 90%打印每个类别的分类报告后发现 DryRot 的召回率只有 40%被大量误判为 Healthy。原因数据集的 7 个类别样本数不均匀某些缺陷类别只有 30-50 张而 Healthy 类可能超过 100 张。模型在整体准确率上看起来不错但完全忽略少数类也能拿到高分。解决训练时打印sklearn.metrics.classification_report不要只看整体精度。如果发现小类精度洼地用WeightedRandomSampler对训练集做类别重采样或改用Focal Loss替代CrossEntropyLoss。Focal Loss 的gamma参数从 2.0 开始调它对小类别的梯度加权有明显效果。5.2 RandomResizedCrop 把缺陷裁掉了现象训练 Loss 下降缓慢验证集精度在 60% 左右震荡手动查看增强后的图像发现大部分缺陷区域被裁剪掉了。原因RandomResizedCrop的scale下界设得过高时每次裁剪区域过大缺陷只是图像中的一小块区域被裁掉的概率非常高。马铃薯缺陷图像的特殊性在于缺陷不一定居中可能在边缘。解决把scale(0.8, 1.0)降到(0.5, 1.0)同时把RandomResizedCrop替换为RandomCropResize的组合保证每次裁剪都包含足够的信息量。更稳的方案是用RandomResizedCrop配合crop_size224并在训练前先人工检查增强输出。5.3 预训练权重的预处理不匹配现象加载 ResNet18 预训练权重后训练 Loss 直接降到极低值但验证集精度始终上不去。原因预训练模型要求输入图像按 ImageNet 的mean[0.485, 0.456, 0.406]和std[0.229, 0.224, 0.225]做归一化。如果改用自研预处理或直接ToTensor()后不加 Normalize特征分布与预训练权重完全不匹配。解决严格使用Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])。如果选择随机初始化训练也要保持这个归一化方式只是模型精度会明显低于迁移学习版本。5.4 验证集和训练集的目录结构不一致现象训练完成后加载模型做单张推理时预测结果错乱索引 2 对应的类别和训练时的类别对不上。原因ImageFolder是按照文件夹名称的字母序自动编码类别的。如果训练集和测试集的文件夹命名一致但排列顺序不同不可能发生或者自己手动构建测试集时用了不同的目录拼接逻辑类别索引就会错位。解决训练结束保存模型时同时保存一个class_to_idx.json推理时加载这个文件做索引映射不要重新推断类别标签。5.5 学习率设置过高导致 Loss 发散现象训练第一个 epoch Loss 就冲到 5.0 以上后续也不下降Loss 曲线像锯齿一样震荡。原因迁移学习微调时分类头是随机初始化的但预训练骨干层的特征已经收敛。如果直接用默认的lr0.01梯度更新过猛把预训练权重破坏了。解决分类头单独设置学习率骨干层用0.001fc层用0.01。或者更简单全部用0.001跑前 10 个 epoch冻结fc之前所有层后面再解冻layer4并降到0.0001。5.6 测试集样本数太少导致评估波动现象同一套权重连续测试两次精度相差 4% 以上无法判断模型是否真的收敛。原因测试集如果只有几十张图每张图对精度的影响超过 2 个百分点评估结果完全被个别难样本主导。解决用 K 折交叉验证替代固定训练/测试集划分。虽然作者已经划分好了但可以在训练集内部再做 5 折交叉验证取平均精度最终再用固定测试集做报告指标。5.7 缺陷特征过于局部化现象模型在测试集上对部分 DryRot 图像表现出极高的置信度但错得离谱。原因模型学到了背景环境特征比如某批图像统一带有泥土纹理或暗角而不是缺陷本身的视觉特征。这类问题在小型农业数据集中非常普遍——同一来源的照片往往带有相似的成像条件。解决训练时用RandomHorizontalFlipRandomRotation只能缓解但不能根治。更好的做法是检查数据增强后的图像确认缺陷区域在当前视角下确实可见。如果条件允许引入多来源图像做 domain adaptation 是终极方案但在这个数据集规模下不现实。6. 把模型用到自己的场景迁移验证与置信度阈值策略拿到这个数据集训练完模型之后真正的挑战是把它迁移到自己的实际场景里。分拣线的相机角度、光源色温、传送带速度都可能导致输入分布偏移。我习惯在部署前做一次系统的迁移验证核心手段是收集 50 到 100 张目标场景下的真实图片手动标注后跑一次推理对比。如果精度掉到 70% 以下先做直方图均衡化或 CLAHE 预处理再测试很多时候能在不重训模型的前提下找回 10 个百分点的精度。置信度阈值策略是另一个常被忽略的细节。七分类模型对每个样本输出一个 7 维概率分布常规做法是取argmax作为最终预测但实际部署时我会对每个类别单独设置最小置信度阈值。比如 Healthy 类别置信度必须超过 0.8 才判定为健康如果低于阈值但接近 DryRot则标记为「人工复检」。这个策略在处理类别不均衡和缺陷特征模糊的样本时非常实用代价是增加了一部分复检工作量但避免了误放行带病马铃薯带来的更大损失。具体实现时我会在模型输出上接一个决策逻辑import numpy as np # 假设模型输出形状为 (batch_size, 7) def decision_with_threshold(logits, thresholds, unknown_threshold0.4): probs np.exp(logits) / np.exp(logits).sum(axis-1) max_probs np.max(probs, axis-1) preds np.argmax(probs, axis-1) # 最高置信度低于 unknown_threshold 时判为未知 unknown_mask max_probs unknown_threshold preds[unknown_mask] len(thresholds) # 映射到未知类别 # 对每个已知类再检查是否达到各自的置信度门槛 for i, thr in enumerate(thresholds): class_mask (preds i) (probs[:, i] thr) preds[class_mask] len(thresholds) # 改为人工复检 return preds, max_probs这段逻辑的核心思想是给每个类别设置独立的置信度门槛。比如 DryRot 判定门槛设为 0.6而 Healthy 设为 0.8——因为放行一个带病马铃薯的代价远高于把一个健康薯送去复检。unknown_threshold0.4用于拦截那些在 7 个类之间犹豫不决的样本这类图像通常是背景复杂、缺陷特征严重遮挡的场景。从那以后我每次做图像分类的工程化部署都会强制把这个置信度阈值表拿出来过一遍而不是只盯着模型在验证集上的准确率。模型输出的概率分布里永远藏着拒绝决策的信息把它利用起来比追求那 1% 的准确率提升实在得多。希望这份拆解对你上手这个数据集有帮助也欢迎在实践中交流更多细枝末节的经验。本文还有配套的精品资源点击获取
网站建设高端定制企业官网