简介:面向毕业设计、课程大作业及机器学习入门人群,这是一套基于Python完成水稻病虫害自动识别的完整项目,涵盖数据整理、模型训练与Web交互展示,能直接帮助解决病虫害识别中样本筛选、特征提取和结果可视化等环节难题。资源定位清晰,适合计算机、人工智能及相关专业学生和从业者参考二次开发。压缩包共311个文件、2.56MB,文件类型兼顾Java后端与Vue前端工程(xml、vue、js、css等),并包含图片样本及Python相关资源配置,整体呈现前后端分离的系统架构。项目已有294人学习下载,代码经过调试测试,具备较高完成度。基础较好者可通过41个Java类、33个Vue组件和10个JS脚本快速理解业务和交互逻辑,或替换数据、调整模型参数以实现不同作物的病虫害识别;从数据处理到界面展示都有完整脉络,学习与扩展价值兼备。
1. 水稻病虫害自动识别系统到底在做什么:别被98%的验证集准确率骗了
A同学用公共数据训练水稻病虫害识别模型,实验室验证集准确率98%,拿到田间一测,手机拍回来的照片一半识别错——这是这类项目最常见的翻车现场。标题里的“基于python机器学习的水稻病虫害自动识别系统源码+数据”,本质是把一套完整图像分类工程拆成可复现的两部分:整理好的水稻病虫害图片数据(稻瘟病、纹枯病、白叶枯病、稻曲病等),以及能直接训练的Python源码,最终输出一个对单张图片自动判断病害种类并给出置信度的系统。适合正在做课程设计、毕业设计,或者想在小范围试点农业图像识别的开发者。决定它能不能拿高分的关键,不在模型选得多新,而在数据组织、验证设计和阈值策略这三件事上。
2. 先把数据盘清楚:水稻病虫害数据集的类别构成与预处理脚本
先说结论:这类项目最容易翻车的环节不在模型,在数据。公开的水稻病虫害数据集普遍是每类一两百张到几百张的不均衡状态,有些图片还自带采集水印。直接把图丢给CNN训练,验证集分数好看,一换环境就露馅。所以第一步不是写网络,而是把数据整理成一份可复现、可追溯的目录结构。下面这套流程我用了很多次,基本可以原样照搬。
2.1 数据集该有哪几类:类别约定与数量底线
常见做法是把类别定成六类左右:稻瘟病、纹枯病、白叶枯病、稻曲病、稻飞虱危害、健康叶片。稻飞虱这类虫害在图像上表现为大面积枯黄,和叶部病害的斑点状形态差异明显,混在一起训练不会互相干扰。需要注意的坑是:不要把“叶瘟”和“穗颈瘟”并成一类却不做说明,同一类里形态差异过大,模型会学得很纠结。按下标和中文名的绑定关系,我习惯在数据根目录放一份class_names.json:
{ "0": "稻瘟病", "1": "纹枯病", "2": "白叶枯病", "3": "稻曲病", "4": "稻飞虱危害", "5": "健康叶片" }注意这里的键顺序要和后续用ImageFolder加载数据时文件夹名排出的下标完全一致,否则训练和推理阶段同一个数字对应不同病害,后面全乱。各类别样本量我按下面的底线来定:
| 类别 | 建议图片数 | 说明 |
|---|---|---|
| 稻瘟病 | 400~600 | 样本最易收集,注意叶瘟与穗颈瘟形态差异 |
| 纹枯病 | 300~500 | 病斑呈云纹状,田间背景干扰多 |
| 白叶枯病 | 300~500 | 病斑从叶缘开始,易与生理性枯黄混淆 |
| 稻曲病 | 200~400 | 主要在穗部,纯叶片图很难识别 |
| 稻飞虱危害 | 200~400 | 多为远景整株枯黄,与病害图差异大 |
| 健康叶片 | 500以上 | 负样本,越多越好 |
判据:最高类和最低类数量差超过3倍,就必须处理不平衡,具体做法在第5章;单类少于200张,建议合并成“其他”或者直接删除,硬凑只会拉低整体精度。
2.2 数据清洗、去重与统一尺寸:预处理脚本
我一般把原始图片按类别建子文件夹,然后跑下面的脚本。它做四件事:跳过损坏图片、剔除重复图、统一转RGB和224×224尺寸、按8:1:1划分train/val/test。这个版本适合文件来源比较杂的情况;如果你的数据是按田块一批批拍的,先看5.1再回来改划分逻辑。
import os import random from pathlib import Path from PIL import Image random.seed(42) # 固定随机种子,保证划分可复现 def clean_and_split(src_dir, dst_dir, img_size=(224, 224), ratios=(0.8, 0.1, 0.1)): """清洗图片并划分 train/val/test。 src_dir: 原始图片目录,每个子目录一个类别 dst_dir: 输出目录,自动生成 train/val/test/类别 结构 """ for class_name in os.listdir(src_dir): class_path = Path(src_dir) / class_name if not class_path.is_dir(): continue images = [p for p in class_path.iterdir() if p.suffix.lower() in ('.jpg', '.jpeg', '.png')] images.sort() random.shuffle(images) # 适合来源杂的数据;按批次采集的数据见5.1 n = len(images) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) for split, part in zip(('train', 'val', 'test'), (images[:n_train], images[n_train:n_train + n_val], images[n_train + n_val:])): out_dir = Path(dst_dir) / split / class_name out_dir.mkdir(parents=True, exist_ok=True) for img_path in part: try: img = Image.open(img_path).convert('RGB') img = img.resize(img_size, Image.BILINEAR) out_path = out_dir / f"{class_name}_{img_path.stem}.jpg" img.save(out_path, quality=95) except Exception as e: print(f"跳过损坏图片: {img_path} -> {e}") clean_and_split('raw_images', 'rice_disease_data')逻辑说明:脚本按类别子目录逐个处理,每个类内部先全局shuffle再切三段,保证三个集合里类别比例和原始数据一致。损坏图片用Image.open的异常捕获跳过,不会中断整个流程。resize用BILINEAR是因为224×224对识别病斑够用,LANCZOS更锐但慢,对最终精度影响可以忽略。JPEG质量设95,太高文件大、太低丢纹理。
参数说明:img_size=224要和后面模型的输入保持一致,改模型输入尺寸时要同步改这里。ratios=(0.8,0.1,0.1)适合单类300张以上;如果单类只有100~200张,改成(0.7,0.2,0.1),给验证集多留样本,否则每类十几张验证图,算出来的precision和recall全是毛刺。random.seed(42)固定随机种子,保证重跑脚本时划分结果完全一样,答辩时演示可复现性很有用。
提示:清洗和划分阶段不要做数据增强。增强放到训练时的transform里做,效果一样,还省得把增强后的图片写回磁盘占空间。
关于去重多说一句:很多公开数据集右下角带水印,模型会学到“有水印就是稻瘟病”这种假特征。最简单的防线是用感知哈希对全库图片算指纹,相似度超过0.95的只保留一张;带水印又裁不掉的直接删。
2.3 划分比例怎么定:不同数据量下的参数建议
划分比例不是拍脑袋。数据总量在3000张以上,8:1:1没问题;总量掉到1500张以下,建议7:2:1;再少就不适合做单次划分,先用5折交叉验证看稳定性,最终模型再拿全量训练集训练。还有一个常被忽略的点:验证集每类至少要有30~50张,否则分类报告里的recall波动很大,你会分不清是模型问题还是样本太少。所以划分之前先数一数最少的类有多少张,不够就去补数据、做采集,而不是压缩验证集比例。
3. 模型选型与训练参数:迁移学习让水稻病虫害识别少走三个月弯路
标题写的是“机器学习”,但到图像识别这一步,真正能落地的实现路径已经收敛到深度学习里的迁移学习。传统做法——HOG/SIFT特征加SVM——不是不行,而是在叶片病害这种纹理差异大、背景又复杂的场景上,调特征的时间成本远高于微调一个预训练CNN。对课程设计和毕业设计来说,迁移学习是性价比最高、代码量最少、也最好解释的路线。
3.1 为什么默认选迁移学习而不是从零训练CNN
三个理由。第一,数据量撑不起从零训练:ImageNet预训练模型已经学到了叶片级纹理的通用特征——边缘、颜色、形状——水稻病斑本质上是这些特征的局部组合,微调比重新学快得多。第二,训练成本低一个量级:从零训练ResNet50单卡要跑几十个epoch才收敛,微调通常10~20个epoch就够用。第三,从评分角度看,能讲清楚“冻结哪层、为什么分学习率”,比“我自己改了个新模块”更能体现工程理解深度。模型选择的经验值如下:
| 模型 | 参数量 | 单卡训练显存(bs=32) | 推理速度 | 适用场景 |
|---|---|---|---|---|
| ResNet18 | 11M | 约2GB | 快 | 数据量小、CPU可推理 |
| ResNet50 | 25M | 约4GB | 中 | 精度优先,最稳妥 |
| EfficientNet-B0 | 5M | 约2.5GB | 快 | 算力受限,精度不输ResNet50 |
| MobileNetV3-Large | 5.5M | 约2GB | 最快 | 后续要部署到移动端 |
数据量在几千张级别时,ResNet18、ResNet50和EfficientNet-B0的最终精度差距通常不超过2~3个点,优先选自己机器跑得动的,别为了好看硬上大模型。
3.2 训练主脚本:PyTorch下的数据加载、优化器与学习率设置
下面这段是训练的核心配置,我用ResNet18做例子,换成ResNet50只需要改一行模型名。预处理部分请仔细看,训练和推理必须共用同一套,这是5.5的坑的根源。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR # 训练/验证用同一套预处理管线,推理脚本必须保持一致 train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = 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_ds = datasets.ImageFolder('rice_disease_data/train', train_tf) val_ds = datasets.ImageFolder('rice_disease_data/val', val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 加载ImageNet预训练权重,替换分类头 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, len(train_ds.classes)) # 分层学习率:新分类头大步长,backbone小步长 optimizer = AdamW([ {'params': model.fc.parameters(), 'lr': 1e-3}, {'params': model.layer4.parameters(), 'lr': 1e-4}, {'params': model.layer3.parameters(), 'lr': 1e-5}, ], weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=25, eta_min=1e-6) criterion = nn.CrossEntropyLoss()逻辑说明:ImageFolder按文件夹名自动生成类别下标,train_ds.classes就是类别名列表,可以直接当分类头输出维度和后面checkpoint里存的class_names。RandomResizedCrop的scale=(0.8,1.0)是刻意控制的:病斑区域小,裁太狠会丢掉关键病斑。RandomRotation限制在15度,水稻叶片有朝向约束,转180度是违背农业常识的样本。Normalize用的是ImageNet统计量,和预训练权重配套。
参数说明:优化器用AdamW而不是SGD,因为微调阶段AdamW收敛更稳、对学习率不那么敏感;weight_decay=1e-4是通用值。分层学习率是这段代码的精华——新加的fc层从零开始,给1e-3;layer4离输出近,给1e-4;layer3更远更通用,给1e-5。这样既让分类头快速适配,又不至于把预训练特征冲掉。batch_size=32是单卡安全线,显存不够降到16,同时把学习率按0.7倍缩放。
然后是训练循环,连同早停一起写:
best_val_acc = 0.0 patience = 5 bad_epochs = 0 for epoch in range(25): model.train() train_loss = 0.0 for x, y in train_loader: x, y = x.cuda(), y.cuda() optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() train_loss += loss.item() * x.size(0) model.eval() correct = total = 0 with torch.no_grad(): for x, y in val_loader: x, y = x.cuda(), y.cuda() pred = model(x).argmax(dim=1) correct += (pred == y).sum().item() total += y.size(0) val_acc = correct / total print(f"epoch {epoch:02d} | train_loss {train_loss / len(train_ds):.4f} | val_acc {val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc torch.save({ 'model': model.state_dict(), 'class_names': train_ds.classes, 'val_acc': val_acc, }, 'best_model.pth') bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= patience: print("早停:连续5个epoch验证集没有提升") break scheduler.step()逻辑说明:每个epoch先train再eval,eval阶段必须包在torch.no_grad()里,否则会额外算梯度、吃显存还拖慢速度。val_acc记录的是验证集整体准确率,但它只能给出粗判断,逐类指标在下一章做。scheduler在每轮结束后step,配合CosineAnnealingLR让学习率从初始值平滑降到eta_min,最后几轮收敛更稳。
参数说明:patience=5的意思是验证集连续5个epoch没有刷新最好成绩就停,几千张数据量下这是比较稳的经验值;数据更少可以放宽到7。torch.save保存的不只是模型权重,还有class_names和val_acc,checkpoint自带上下文,推理脚本不需要另外读标签文件。
4. 评估与可视化:准确率之外,高分项目还要过这三关
很多项目做到“验证集98%”就停了。但无论是答辩还是实际使用,第一个问题往往是:这个98%是怎么来的?每一类分别多少?所以评估这一章才是高分项目和普通项目的分水岭。评估至少要做三件事:逐类指标看短板、混淆矩阵看错位、置信度阈值拦低质量预测。
4.1 混淆矩阵与逐类精确率/召回率:定位哪一类在拖后腿
直接给脚本,跑完会打印类别映射、混淆矩阵和完整分类报告。输出建议存成文本文件放进项目报告里,答辩很好用。
import torch import numpy as np from sklearn.metrics import confusion_matrix, classification_report from torchvision import datasets, transforms, models ckpt = torch.load('best_model.pth', map_location='cpu') class_names = ckpt['class_names'] model = models.resnet18(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, len(class_names)) model.load_state_dict(ckpt['model']) model.eval().cuda() val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_ds = datasets.ImageFolder('rice_disease_data/val', val_tf) val_loader = torch.utils.data.DataLoader(val_ds, batch_size=32, shuffle=False) all_preds, all_labels = [], [] with torch.no_grad(): for x, y in val_loader: out = model(x.cuda()) all_preds.extend(out.argmax(dim=1).cpu().numpy()) all_labels.extend(y.numpy()) cm = confusion_matrix(all_labels, all_preds) print('类别映射:', {i: n for i, n in enumerate(class_names)}) print(cm) print(classification_report(all_labels, all_preds, target_names=class_names, digits=3))逻辑说明:加载checkpoint时map_location='cpu'是保险做法,避免换机器后因为CUDA不匹配加载失败。模型的fc输出维度必须和保存时的类别数一致,这里从len(class_names)取,不会出错。推理阶段复用训练时的val_tf,一个像素都不许改。
参数说明:digits=3保留三位小数,报告里precision/recall/F1的差异才看得出来。看报告的顺序:先找recall最低的类,再去混淆矩阵那一行看它错到了哪个类。比如稻曲病老被误判成稻瘟病,说明这两类在224×224下形态太像,要么增加穗部特写样本,要么考虑单独加一个二分类阶段。
可视化部分我习惯用matplotlib画归一化混淆矩阵,x轴和y轴都标中文类名,对角线颜色越深越好。这张图放PPT里比任何文字都有说服力,代码就是简单的imshow加annotate,不展开了。另外我习惯把classification_report的输出重定向到report.txt,和混淆矩阵图一起放进项目的results目录。这样每次调参留底,答辩时能直接讲“第几版模型在哪一类上提升了多少”,比空口说准确率可信得多。
4.2 置信度阈值与拒绝识别:让系统在田间少出洋相
softmax输出的概率不是校准过的真实置信度,直接在田里把top-1当结果用会翻车。一种简单有效的工程手段是设阈值:低于阈值就输出“无法识别”,让用户人工复核。这个设计的出发点很朴素——病害误判的代价比识别失败高得多,喷错药比不识别更糟。
import torch import torch.nn.functional as F def predict_with_threshold(model, img_tensor, class_names, threshold=0.7): """低于阈值返回'无法识别',避免在田间硬给结论。""" model.eval() with torch.no_grad(): logits = model(img_tensor.unsqueeze(0)) prob = F.softmax(logits, dim=1) conf, idx = prob.max(dim=1) if conf.item() < threshold: return '无法识别', conf.item() return class_names[idx.item()], conf.item()参数说明:threshold=0.7是经验起点。判断标准:如果验证集上“无法识别”的占比超过15%,说明阈值太高或者类别可分性差,降到0.6再试。这里明确一点:阈值不是越高越好,它是漏报和误报之间的旋钮,田间光线杂、背景乱的场景,0.6比0.7更实用。img_tensor.unsqueeze(0)是把单张图扩成batch维度,模型必须吃四维张量。
还有一个小技巧:阈值可以按类别分开设。少数类如稻曲病,样本本身难,把它的阈值降到0.6,多数类健康叶片提到0.8,整体误报率能降不少。实现就是把predict_with_threshold里的threshold参数从float改成dict,按类别名取值。
5. 水稻病虫害识别项目避坑清单:五条血泪踩坑记录
下面五条都是我在模拟项目X上反复踩过的坑,每条按“现象、原因、解决”写,照着排查能省好几天。
5.1 验证集96%、田间不到六成:数据集同源泄露
现象:训练时验证集准确率96%,把手机拍的田间照片丢进去,识别率只有58%,而且错得很自信——置信度普遍在0.8以上。这一点很重要,因为它说明模型不是犹豫,而是把背景特征当成了病害特征。
原因:原始数据是按田块批量拍摄的,同一个田块拍出的照片背景、光照高度相似。全文件随机划分时,同一批连拍的图被同时分进训练集和验证集,模型实际是在背答案,验证集里全是“见过的画面”。
解决:改成按拍摄批次划分。给文件名加batch前缀,或者维护一个batch_id列表,先按batch分组,再把整组随机分配到train/val/test,保证一个批次的图片只落在一个集合里。按批次划分后验证集准确率会明显下降,那才是真实水平。我后来把批次划分写成了独立函数,输入是batch_id列表,输出的train/val/test路径会单独打印出来,核对完再喂给训练脚本。
5.2 稻曲病recall只有0.41:类别不平衡要先用加权采样
现象:整体准确率92%,一看分类报告,稻曲病recall只有0.41,大部分被误判成稻瘟病。整体准确率看着不错,但单类表现完全不合格。
原因:稻瘟病样本600张,稻曲病只有180张,CrossEntropyLoss在多数类上天然占优,少数类学不到判别特征。
解决:两步一起用。第一步在DataLoader里挂WeightedRandomSampler,按类别样本数的倒数加权,让每个epoch里少数类被抽到更多次;第二步给少数类单独放宽ColorJitter幅度,轻度增广。注意先后顺序:先做加权采样,还不够再加增强,别一上来就把少数类复制粘贴,那是给自己挖过拟合的坑。我实测加权采样后稻曲病recall从0.41涨到0.67,整体准确率只掉了0.8个点,这个交换非常划算。
5.3 增强过度,模型学的全是背景:控制增强幅度
现象:加了RandomRotation(180)和RandomResizedCrop(scale=(0.3,1.0))之后,验证集不升反降,盲测更差。
原因:水稻叶片的朝向和病斑位置是有物理约束的。旋转180度让叶片“头朝下”,裁掉中心病斑让感染区域消失,等于凭空造了一大批违背农业常识的样本。模型为了拟合这些样本,开始去学背景纹理。
解决:把rotation限制在±15度,RandomResizedCrop的scale下限提到0.8,ColorJitter只动brightness和contrast,不要动hue。增强的目的是模拟光照变化和轻微拍摄角度差异,不是把图片变成另一张图。判断增强是否过度的土办法:把增强后的样本随机抽9张拼一张图肉眼看,如果人类都认不出是水稻叶片,模型更认不出。
5.4 训练loss一直降、验证loss反弹:过拟合的排查顺序
现象:train_loss稳步降到0.1以下,val_loss先降后弹,val_acc跟着震荡,最后停在比顶峰低五六个点的位置。
原因:模型在几千张图上把训练集特征背下来了,典型过拟合,而且这段往往出现在epoch 10~15之后。
解决:按顺序做三件事,别跳步。第一,加早停,patience=5能拦住绝大部分反弹;第二,把backbone学习率降一个数量级,比如1e-4改成1e-5,让分类头先收敛;第三,在fc层前加Dropout(0.3)。三件事都做完了还在弹,回头查数据划分和类别平衡,大概率是5.1或5.2的问题。如果Dropout加了还弹,把ResNet18换成ResNet50通常也没有帮助,问题几乎都在数据侧。
5.5 推理脚本随手resize,置信度全乱:预处理必须复用训练代码
现象:训练时验证集95%,导出模型后写了个调用脚本,结果几乎每张图都报“无法识别”,置信度普遍低于0.5。
原因:调用脚本里用的cv2.resize直接缩放,没做CenterCrop也没做Normalize。模型输入分布和训练时完全不一致,输出概率自然乱掉。
解决:推理脚本里把val_tf原样复制,一行都不许改。我的习惯是预处理函数只写一遍,放到preprocess.py里,训练和推理都import它,从根上杜绝两份代码漂移。顺带一提,从文件直接读图的脚本要记得.convert('RGB'),灰度图和带Alpha通道的PNG会让ToTensor后的形状对不上,报错很隐蔽。这条看起来最基础,实际是翻车率最高的地方。
这五条有一个共同点:没有一条是模型结构问题,全是数据组织和工程细节。我见过太多项目栽在5.1和5.5上,最后归因于“模型不够好”,其实是流程不够稳。把这五条当成检查清单,每次跑项目前过一遍,比换更深的网络有效得多。
6. 收尾:把模型封装成一条命令跑通的单图识别脚本
模型训完、评估做完,最后要有一个能给别人演示的入口。我把推理封装成一个独立脚本,命令行跑一张图就出结果,带上阈值控制。下面是完整版本,直接存成inference.py就能用。
# inference.py —— 单张图片识别入口 # 用法: python inference.py --image field_photo.jpg [--threshold 0.7] import argparse import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms, models def main(): parser = argparse.ArgumentParser() parser.add_argument('--image', required=True, help='待识别图片路径') parser.add_argument('--ckpt', default='best_model.pth') parser.add_argument('--threshold', type=float, default=0.7) args = parser.parse_args() ckpt = torch.load(args.ckpt, map_location='cpu') model = models.resnet18(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, len(ckpt['class_names'])) model.load_state_dict(ckpt['model']) model.eval() # 必须与训练时val_tf完全一致 val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) img = val_tf(Image.open(args.image).convert('RGB')).unsqueeze(0) with torch.no_grad(): prob = F.softmax(model(img), dim=1) conf, idx = prob.max(dim=1) conf = conf.item() if conf < args.threshold: print(f"无法识别(置信度 {conf:.2f} 低于阈值 {args.threshold})") else: print(f"识别结果: {ckpt['class_names'][idx.item()]} | 置信度 {conf:.2f}") if __name__ == '__main__': main()这段代码把前面所有约定收拢到一处:checkpoint里读出class_names,预处理和训练完全一致,阈值默认0.7。用法是python inference.py --image field_photo.jpg,想调阈值就加--threshold 0.6。如果输出“无法识别”的比例偏高,先别急着改模型,看看输入图片是不是有曝光过度、反光或者叶子被水珠糊住的情况,这类图片人类专家也容易看走眼。
我现在的习惯是每次训练完,单独留一个“盲测文件夹”,放20~30张模型从未见过的田间实拍图,有病害的、有健康叶片的、有杂草背景的,用这个脚本一张张跑,通过率低于80%就不算系统完成。这个习惯帮我挡掉了至少三次验证集好看、实际不能用的尴尬。如果目标是拿高分,数据组织、迁移学习、分层评估、阈值拒绝这一套闭环已经足够撑起一份逻辑完整的项目展示;要往真实部署走,下一步是把这个脚本包成HTTP接口或者导出ONNX,但那是另一个故事了。希望帮到你。
本文还有配套的精品资源,点击获取