ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python的农作物病虫害识别:从数据集到推理的完整实战

基于Python的农作物病虫害识别:从数据集到推理的完整实战 简介这份资源是面向农业信息化、计算机视觉方向的开发者与高校学生的农作物病虫害识别分类完整项目包适合作为课程设计、毕业设计或入门深度学习实战的参考方案。包内共61个文件以ipynb实验笔记、png与jpg图像素材、md说明文档为主辅以py脚本、yml与yaml配置、html/css/js前端页面、pkl模型文件及Dockerfile等部署文件压缩包约88.66MB。项目覆盖数据收集与预处理、基于ResNet、VGG、Inception等预训练模型的特征提取、TensorFlow/Keras/PyTorch多框架模型训练与评估以及Flask后端与Web界面部署的完整链路并附有本地、AWS、GCP等部署指南。已有306人学习下载读者可据此快速复现病虫害分类流程理解从数据到上线的工程结构并借鉴多框架对比实验与排错思路。1. 农作物病虫害识别项目从数据集到推理一条能跑通的落地路径田里叶子刚出现褐色斑点时种植户往往分不清是早疫病还是叶霉病打错药不仅白花钱还耽误了最佳防治窗口。基于 Python 的农作物病虫害识别分类项目本质上就是拿卷积神经网络把叶片照片映射到具体病害类别输出一个可解释的置信度。这类项目通常打包成「源码 数据集 说明文档」的形式适合想入门深度学习图像分类的开发者、做农业信息化的技术团队以及需要快速验证识别效果的学生。它解决的不是「从零训练一个大模型」的问题而是让你在本地用一张显卡或 CPU 就能跑通数据加载、训练、评估、单张推理的完整闭环。下面按我实际复现这类项目的顺序把每一步的参数和坑讲清楚。2. 先看清数据集结构类别、划分与预处理决定上限2.1 农作物病害数据集的典型目录长什么样拿到一个病虫害识别项目第一件事不是急着跑 train.py而是把数据集目录打印出来。常见做法是按类别分文件夹每个文件夹里放对应病害的叶片图类似 PlantVillage 的组织方式。你需要确认三件事类别数是否和说明文档一致、每类样本量是否均衡、有没有混入非叶片图。我一般会先跑一段统计脚本把类别名、图片数量、图片尺寸分布列出来避免训练到一半才发现某个类别只有十几张图。import os from collections import Counter from PIL import Image data_dir dataset/train counter Counter() size_stat Counter() for cls in sorted(os.listdir(data_dir)): cls_dir os.path.join(data_dir, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): counter[cls] 1 with Image.open(os.path.join(cls_dir, fname)) as im: size_stat[im.size] 1 for cls, n in counter.items(): print(f{cls}: {n}) print(尺寸分布 top5:, size_stat.most_common(5))这段代码做的是类别计数和尺寸统计。data_dir指向训练集根目录counter记录每类图片数size_stat统计图片分辨率。逻辑说明先遍历类别文件夹再遍历图片文件用 Pillow 打开只读尺寸不加载全部像素速度快。参数说明如果你的数据集是train/val/test三段式把data_dir分别指向三个目录各跑一次。重点看两个信号——某类样本数低于 100 就要考虑过采样或数据增强尺寸分布如果集中在 256×256 附近说明数据集已经统一过可以省掉 resize 的争议。2.2 训练集、验证集、测试集该怎么切很多打包项目只给一个总目录需要你自己切分。常见做法是按 7:2:1 或 8:1:1 划分但病虫害数据有个特殊点同一片叶子可能被拍了多张如果随机切分同一片叶子的不同角度可能同时出现在训练和验证集里导致验证准确率虚高。我一般会先按「拍摄批次」或「原始文件名前缀」分组再按组切分。如果数据集没有批次信息至少要用固定随机种子保证每次切分结果一致。import random import shutil from pathlib import Path random.seed(42) src Path(dataset/raw) dst Path(dataset/split) for split in [train, val, test]: (dst / split).mkdir(parentsTrue, exist_okTrue) for cls_dir in sorted(src.iterdir()): if not cls_dir.is_dir(): continue imgs sorted([p for p in cls_dir.iterdir() if p.suffix.lower() in (.jpg, .jpeg, .png)]) random.shuffle(imgs) n len(imgs) n_train, n_val int(n * 0.7), int(n * 0.2) for split, subset in [ (train, imgs[:n_train]), (val, imgs[n_train:n_train n_val]), (test, imgs[n_train n_val:]), ]: out_dir dst / split / cls_dir.name out_dir.mkdir(parentsTrue, exist_okTrue) for p in subset: shutil.copy2(p, out_dir / p.name)逻辑说明random.seed(42)固定切分结果保证可复现。按类别分别切分避免某类全部落入训练集。shutil.copy2保留原文件元数据。参数说明0.7/0.2/0.1是通用比例如果某类样本少于 200建议改成 8:1:1把更多数据留给训练。注意切分后要再跑一次 2.1 的统计脚本确认三个子集的类别数一致否则说明某个类别样本太少切分后验证集里没有该类。2.3 数据增强哪些变换对叶片图有效哪些是帮倒忙叶片病害识别的增强策略和通用图像分类不完全一样。水平翻转、±15 度旋转、亮度对比度微调通常是安全的但垂直翻转要谨慎因为叶片正反面纹理差异大垂直翻转可能把背面病斑当成正面。颜色抖动ColorJitter的幅度也不宜过大病斑的颜色是重要特征过度抖动会让模型学不到颜色线索。我一般用 torchvision 的 transforms 组合训练集用增强验证和测试集只做 resize 和归一化。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逻辑说明Resize((224,224))是为了适配 ImageNet 预训练模型的输入。RandomHorizontalFlip和RandomRotation增加空间多样性。ColorJitter参数刻意调小brightness 和 contrast 只给 0.2saturation 给 0.1。Normalize用的是 ImageNet 的均值和标准差如果你从零训练不用预训练权重可以改成数据集自身的统计值。参数说明degrees15是经验值超过 30 度可能引入不真实的叶片形态。注意验证集和测试集绝对不要加随机增强否则每次评估结果都在变无法比较模型优劣。3. 模型选型与训练从 ResNet 到轻量网络的取舍3.1 为什么多数病虫害项目用迁移学习而不是从零训练农作物病害数据集通常几千到几万张类别几十种这个量级从零训练一个 ResNet50 很容易过拟合。常见做法是加载 ImageNet 预训练权重替换最后一层全连接只微调分类头或整个网络。迁移学习的好处是收敛快、对学习率不敏感、小数据集也能拿到不错的准确率。我一般会先冻结 backbone 训练几轮分类头再解冻全部层用更小的学习率微调。这样做的原因是随机初始化的分类头一开始梯度很大如果直接解冻全部层会把预训练好的特征也带偏。import torch import torch.nn as nn from torchvision import models num_classes 38 # 按你的数据集类别数修改 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, num_classes) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)逻辑说明weights...IMAGENET1K_V1加载预训练权重requires_gradFalse冻结所有层只训练新的fc层。CrossEntropyLoss是多分类标准损失。Adam学习率给 1e-3因为只训练分类头可以大一点。参数说明num_classes必须和数据集类别数一致改错会直接报维度不匹配。如果你的显卡显存小把resnet18换成resnet18或mobilenet_v3_small精度略降但速度快很多。注意冻结训练几轮后要用param.requires_grad True解冻并把学习率降到 1e-4 或更低否则容易破坏预训练特征。3.2 训练循环里必须记录的四个指标训练不是跑完 loss 就完事至少要记录训练损失、验证损失、验证准确率、学习率。这四个指标能帮你判断是过拟合、欠拟合还是学习率设错了。我习惯在每个 epoch 结束后打印一行并保存验证准确率最高的模型权重。下面是一个最小训练循环的骨架。import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_ds ImageFolder(dataset/split/train, transformtrain_tf) val_ds ImageFolder(dataset/split/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) best_acc 0.0 for epoch in range(20): model.train() train_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) train_loss / len(train_ds) model.eval() correct, total, val_loss 0, 0, 0.0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) val_loss loss.item() * imgs.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) val_loss / len(val_ds) val_acc correct / total print(fepoch {epoch1}: train_loss{train_loss:.4f} val_loss{val_loss:.4f} val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)逻辑说明ImageFolder自动按文件夹名映射标签要求目录结构是train/类别名/图片。训练时model.train()启用 BatchNorm 和 Dropout验证时model.eval()关闭。torch.no_grad()节省显存。best_acc保存验证集最优权重避免用最后一个 epoch 的过拟合模型。参数说明batch_size32是 8GB 显存下的安全值显存不够降到 16 或 8。num_workers4在 Windows 上如果报错就改成 0。epoch20是冻结训练阶段的常见轮数解冻微调再加 10 到 20 轮。注意如果val_loss持续上升而train_loss下降说明过拟合要加数据增强或提前停止。3.3 学习率调度与早停两个防止白跑的机制学习率调度和早停是训练里最容易被忽略、但最能省时间的两个机制。常见做法是用ReduceLROnPlateau验证损失连续几轮不降就把学习率乘以 0.1。早停则是验证准确率连续多轮不提升就终止训练。这两个机制配合使用能避免你在一个已经过拟合的模型上继续浪费算力。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.1, patience3 ) early_stop_patience 7 no_improve 0 # 在验证循环结束后 scheduler.step(val_loss) if val_acc best_acc: best_acc val_acc no_improve 0 torch.save(model.state_dict(), best_model.pth) else: no_improve 1 if no_improve early_stop_patience: print(early stop) break逻辑说明ReduceLROnPlateau监控val_losspatience3表示连续 3 轮不降才降学习率。early_stop_patience7比调度器的 patience 大给学习率下降后模型重新改善的机会。参数说明factor0.1是标准值降得太猛会导致模型无法跳出局部最优。注意早停的 patience 不要设得太小病虫害数据集本身噪声大验证损失波动正常设 5 到 7 比较稳。4. 推理与部署单张图片识别和批量测试怎么做4.1 加载模型做单张推理的完整代码训练完保存的best_model.pth只包含权重推理时要先重建模型结构再加载权重。这一步的坑在于类别顺序必须和训练时一致ImageFolder是按文件夹名排序生成类别索引的推理时也要用同样的排序。下面是一个单张图片推理的完整例子。import torch from PIL import Image from torchvision import models, transforms import torch.nn as nn class_names sorted(os.listdir(dataset/split/train)) num_classes len(class_names) model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() infer_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) img Image.open(test_leaf.jpg).convert(RGB) tensor infer_tf(img).unsqueeze(0) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) conf, pred probs.max(dim1) print(f预测类别: {class_names[pred.item()]}, 置信度: {conf.item():.4f})逻辑说明weightsNone因为我们要加载自己的权重不需要预训练。load_state_dict加载保存的参数字典。map_locationcpu保证在没有 GPU 的机器上也能加载。unsqueeze(0)增加 batch 维度。softmax把 logits 转成概率max取最高概率类别和置信度。参数说明class_names必须和训练时ImageFolder生成的顺序一致用sorted保证。注意如果推理结果置信度普遍低于 0.5说明模型没训练好或者测试图和训练图分布差异大不要硬信。4.2 批量测试与混淆矩阵看清模型到底错在哪单张推理只能看个例要评估模型真实水平得在测试集上跑批量推理并生成混淆矩阵。混淆矩阵能告诉你哪些类别容易被混淆比如早疫病和晚疫病可能互相误判。下面代码在测试集上计算准确率并输出混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report import numpy as np test_ds ImageFolder(dataset/split/test, transformval_tf) test_loader DataLoader(test_ds, batch_size32, shuffleFalse) all_preds, all_labels [], [] model.eval() with torch.no_grad(): for imgs, labels in test_loader: outputs model(imgs.to(device)) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names)) cm confusion_matrix(all_labels, all_preds) print(cm)逻辑说明classification_report输出每个类别的精确率、召回率、F1比总体准确率更有信息量。confusion_matrix是二维数组行是真实标签列是预测标签。参数说明target_names传入类别名列表顺序要和标签索引一致。注意如果某个类别召回率明显低说明该类样本太少或特征不明显优先补数据而不是调模型。4.3 导出 ONNX 做跨平台推理的注意点如果要把模型部署到没有 PyTorch 的环境常见做法是导出 ONNX。导出时要注意输入维度固定为(1, 3, 224, 224)或动态 batch以及 opset 版本不要太高否则某些推理引擎不支持。dummy torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11, )逻辑说明dummy是示例输入用来追踪计算图。dynamic_axes允许 batch 维度动态变化。opset_version11兼容性较好。参数说明如果部署到 TensorRTopset 可以升到 13 或更高如果部署到 OpenVINO11 比较稳。注意导出后要用onnxruntime跑一遍验证输出和 PyTorch 一致差异超过 1e-3 就要检查是否有不支持的操作。5. 避坑与排查复现病虫害识别项目时最容易翻车的五件事5.1 现象训练准确率 99%测试准确率 40%原因数据泄漏。同一片叶子的多张照片被随机切分到了训练集和测试集模型记住了叶子而不是病斑。解决按拍摄批次或原始文件名前缀分组切分确保同一片叶子的图片只出现在一个子集里。如果数据集没有批次信息至少用感知哈希去重把相似度极高的图片分到同一组。5.2 现象验证损失突然变成 NaN原因学习率太大或者数据里有损坏图片导致梯度爆炸。解决先把学习率降到 1e-4 试一轮再用 2.1 的脚本检查图片是否能正常打开损坏图片直接删除。如果用了混合精度训练NaN 更常见可以先关掉 AMP 跑一轮确认。5.3 现象推理时置信度全是 0.1 左右预测类别随机原因推理时的预处理和训练时不一致。最常见的是归一化参数用错或者 resize 的插值方式不同。解决把训练时的val_tf原封不动复制到推理脚本不要凭记忆重写。检查Normalize的 mean 和 std 是否和训练一致。5.4 现象Windows 上 DataLoader 报 BrokenPipeError原因num_workers大于 0 时Windows 的多进程机制和 Linux 不同容易在脚本没有if __name__ __main__:保护时出错。解决把训练代码包进if __name__ __main__:或者直接把num_workers设为 0。后者会慢一些但最稳。5.5 现象换了台机器加载模型报 KeyError原因保存模型时用了torch.save(model)保存整个对象换机器后类定义路径变了。解决始终用torch.save(model.state_dict(), ...)只保存权重加载时先重建模型结构再load_state_dict。如果已经保存了整个对象用pickle手动提取state_dict。6. 把准确率再往上推一档类别不均衡下的重采样与损失函数技巧类别不均衡是病虫害数据集里最隐蔽的精度杀手。假设 38 个类别里最常见的病害有 2000 张图最罕见的只有 80 张模型会倾向于把罕见类预测成常见类总体准确率看着不错但罕见类的召回率惨不忍睹。我一般先用classification_report确认哪些类召回率低于 0.6再针对性处理。第一种做法是重采样。对罕见类做过采样或者对常见类做欠采样。过采样简单但容易过拟合我通常配合更强的数据增强一起用。第二种做法是给CrossEntropyLoss传weight参数权重和类别频率成反比。下面是一个计算类别权重并传入损失的例子。import numpy as np from sklearn.utils.class_weight import compute_class_weight labels [label for _, label in train_ds.samples] class_weights compute_class_weight( class_weightbalanced, classesnp.unique(labels), ylabels, ) class_weights torch.tensor(class_weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)逻辑说明compute_class_weight的balanced模式自动按n_samples / (n_classes * np.bincount(y))计算权重罕见类权重高常见类权重低。criterion替换后训练时每个样本的损失会乘以对应类别权重。参数说明权重张量必须和类别索引顺序一致np.unique(labels)返回的是排序后的类别和ImageFolder的索引顺序一致。注意加权后训练损失的量级会变大学习率可能要相应调小。第三种做法是改用 Focal Loss它通过调制因子降低易分类样本的权重让模型聚焦难样本。Focal Loss 在病虫害识别里对病斑面积小、特征不明显的类别提升明显。实现上就是在交叉熵前乘一个(1 - p_t)^gammagamma通常取 1 到 2。class FocalLoss(nn.Module): def __init__(self, gamma2.0, weightNone): super().__init__() self.gamma gamma self.weight weight def forward(self, logits, targets): ce nn.functional.cross_entropy(logits, targets, weightself.weight, reductionnone) pt torch.exp(-ce) loss ((1 - pt) ** self.gamma) * ce return loss.mean()逻辑说明先算每个样本的交叉熵cept是模型对真实类别的预测概率(1 - pt)^gamma让高置信度样本的损失被压低。weight可以继续传类别权重两个机制叠加。参数说明gamma2.0是原论文推荐值gamma越大对难样本聚焦越强但太大可能导致训练不稳定。注意Focal Loss 收敛比普通交叉熵慢训练轮数要相应增加。验证这些技巧是否有效不能只看总体准确率要看罕见类的召回率和 F1。我习惯在测试集上跑一次classification_report对比处理前后的变化。如果罕见类召回率提升但常见类下降超过 3 个百分点说明权重给得太激进要回调。最后说个我自己的习惯每次改完损失函数或采样策略都把best_model.pth另存一个名字比如best_model_focal.pth不然跑了几轮之后分不清哪个权重对应哪组参数后悔药都没得吃。希望帮到你。本文还有配套的精品资源点击获取
返回列表