ARTICLE DETAIL

资讯详情

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

智慧农业实战:Python深度学习叶片病害识别与ONNX部署

智慧农业实战:Python深度学习叶片病害识别与ONNX部署 简介这份资源面向智慧农业与植物表型识别方向的学习者提供一套基于Python深度学习的叶片病害分类完整项目覆盖数据预处理、模型搭建、训练调参、单张与批量预测以及pth转onnx供Java后端调用的部署链路任务难度以入门为主、部分环节中等适合想打通深度学习模型到工程落地全流程的开发者。压缩包共2000个文件以1914张jpg病害图像构成数据集辅以19个py脚本、23个yaml与23个json配置、2个pth权重、1个onnx模型及若干txt说明整体约277.29MB目录结构清晰便于按模块检索。目前已有851人学习下载。项目对训练与预测脚本均附超详细注释读者可据此复现三类叶片病害分类实验理解数据增强、网络结构设计与调参技巧并掌握模型导出与跨语言调用时保持预测结果一致的排错思路。1. 从一堆番茄苹果叶片图说起这套 Python 深度学习病害识别源码到底能跑出什么如果你手头正好有一批番茄叶斑病、苹果黑星病的田间照片想快速搭一个能分类、能预测、还能被 Java 后端调用的模型那这套「智慧农业基于 Python 深度学习的叶片病害识别分类模型开发源码」就是冲着这个场景来的。它把数据预处理、模型搭建、训练、预测、部署五个环节全串起来了属于 ImageClassification 任务入门难度部分环节中等。数据集里番茄叶斑病和苹果黑星病两类样本混在一起文件名带类别和序号典型的小规模农业图像分类场景。适合谁刚接触深度学习想找一个完整闭环练手的 Python 开发者或者做智慧农业项目需要快速验证模型可行性的后端工程师。源码带超详细注释项目说明也在包里拿到手不用从零猜结构。2. 数据预处理从文件名到 DataLoader 的完整链路2.1 数据集结构分析与划分策略拿到手的图片命名格式是「类别_序号.jpg」比如番茄叶斑病_166.jpg、苹果黑星病_59.jpg。这种命名方式本身就携带了标签信息解析起来比读 CSV 标注文件省事。但要注意文件名里的中文类别名在跨平台传输时容易出编码问题Windows 打包、Linux 解压后可能变成乱码。常见做法是先用脚本把中文类别映射成英文或数字索引再生成一份label_map.json后续训练和推理都读这份映射避免路径里带中文。划分训练集、验证集、测试集时我一般按 7:2:1 切且必须做分层采样stratified split保证每个类别在三份里的比例一致。小规模数据集最怕某一类在验证集里一张都没有训练曲线看着挺好一评估就翻车。用sklearn.model_selection.train_test_split的stratify参数就能解决。import os import json import shutil from sklearn.model_selection import train_test_split # 解析文件名构建 (路径, 标签) 列表 data_dir dataset/raw samples [] for fname in os.listdir(data_dir): if not fname.lower().endswith((.jpg, .png)): continue # 文件名格式类别_序号.jpg label_name fname.rsplit(_, 1)[0] samples.append((os.path.join(data_dir, fname), label_name)) # 类别映射中文转数字索引 classes sorted(set(s[1] for s in samples)) label_map {name: idx for idx, name in enumerate(classes)} with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2) paths [s[0] for s in samples] labels [label_map[s[1]] for s in samples] # 分层划分70% 训练30% 临时集 X_train, X_tmp, y_train, y_tmp train_test_split( paths, labels, test_size0.3, stratifylabels, random_state42 ) # 临时集再对半分为验证和测试 X_val, X_test, y_val, y_test train_test_split( X_tmp, y_tmp, test_size0.5, stratifyy_tmp, random_state42 ) print(f训练集 {len(X_train)} 张验证集 {len(X_val)} 张测试集 {len(X_test)} 张)这段代码做了三件事遍历原始目录提取路径和标签、生成类别到索引的映射文件、按分层策略切分数据。random_state42保证每次运行划分结果一致方便复现。stratify参数是关键不加的话小类别可能被随机分没。输出会打印三份数据的数量如果某一类总数少于 10 张建议直接合并到其他类或做数据增强补足否则分层也救不了。2.2 数据增强与 Dataset 封装农业图像有个特点田间拍摄角度、光照、遮挡差异大但病害特征集中在叶片局部。增强策略不能太激进否则把病斑裁掉了模型学不到东西。我一般用torchvision.transforms组合随机水平翻转、小角度旋转±15°、颜色抖动亮度/对比度微调不做随机裁剪因为裁剪可能把关键病斑区域切掉。归一化用 ImageNet 的均值和标准差就行预训练权重直接兼容。from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class LeafDataset(Dataset): def __init__(self, paths, labels, transformNone): self.paths paths self.labels labels self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) label self.labels[idx] if self.transform: img self.transform(img) return img, label # 训练集增强翻转 旋转 颜色抖动 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证/测试集只做 Resize 和归一化 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]) ]) train_ds LeafDataset(X_train, y_train, transformtrain_tf) val_ds LeafDataset(X_val, y_val, transformval_tf) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size16, shuffleFalse, num_workers2)Resize((224, 224))是因为后面用的骨干网络输入尺寸是 224如果你换模型要同步改。batch_size16是小数据集的稳妥选择太大容易过拟合太小训练不稳定。num_workers2在 Windows 上如果报错就改成 0这是 PyTorch 在 Windows 下的老毛病。增强只加在训练集上验证和测试必须用干净数据否则评估指标虚高上线就露馅。3. 模型搭建与训练骨干网络选型、Tricks 与调参3.1 骨干网络选型与迁移学习策略这个任务类别少、数据量小从零训一个 CNN 基本没戏。常见做法是用预训练模型做迁移学习把 ImageNet 上学到的通用特征拿过来只微调最后的分类头。骨干网络选 ResNet18 或 EfficientNet-B0 都行ResNet18 参数少、推理快适合后续转 ONNX 给 Java 调用EfficientNet-B0 精度略高但结构复杂一些。源码里如果没指定我建议先用 ResNet18 跑通全流程再换骨干对比。关键操作是冻结前面的卷积层只训练fc层。等分类头收敛后再解冻最后几个 block 做小学习率微调。这样既省时间又不容易过拟合。代码里用requires_grad控制哪些层参与梯度更新。import torch import torch.nn as nn from torchvision import models def build_model(num_classes2, backboneresnet18, freezeTrue): if backbone resnet18: model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) elif backbone efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) in_features model.classifier[1].in_features model.classifier[1] nn.Linear(in_features, num_classes) else: raise ValueError(f不支持的骨干网络: {backbone}) if freeze: # 冻结除分类头外的所有参数 for name, param in model.named_parameters(): if fc not in name and classifier not in name: param.requires_grad False return model model build_model(num_classes2, backboneresnet18, freezeTrue) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device)num_classes2对应番茄叶斑病和苹果黑星病两类如果你数据集里实际类别数不同改这个参数。freezeTrue时只有分类头参与训练显存占用小、收敛快。解冻微调时把freeze设为False同时把学习率调低一个数量级否则预训练权重会被大梯度冲垮。weights参数指定预训练权重版本不写的话 PyTorch 会警告但也能跑只是精度差一截。3.2 训练循环、学习率调度与可视化训练脚本train.py的核心是一个 epoch 循环前向传播、算损失、反向传播、更新参数每个 epoch 结束后在验证集上评估。损失函数用交叉熵优化器用 AdamW学习率初始 1e-3配合余弦退火调度。小数据集训练轮数不用太多20 到 30 个 epoch 足够观察收敛趋势。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR from tqdm import tqdm criterion nn.CrossEntropyLoss() optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 history {train_loss: [], val_loss: [], val_acc: []} for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in tqdm(train_loader, descfEpoch {epoch1}): 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() * imgs.size(0) scheduler.step() train_loss running_loss / len(train_ds) # 验证阶段 model.eval() val_loss, correct, total 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 history[train_loss].append(train_loss) history[val_loss].append(val_loss) history[val_acc].append(val_acc) # 保存最优权重 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fEpoch {epoch1}: train_loss{train_loss:.4f}, fval_loss{val_loss:.4f}, val_acc{val_acc:.4f}) print(f最佳验证准确率: {best_acc:.4f})filter(lambda p: p.requires_grad, ...)确保优化器只更新解冻的参数冻结层不会被误更新。CosineAnnealingLR让学习率按余弦曲线下降前期大步走、后期小步调比固定学习率稳。best_model.pth保存验证集上表现最好的权重而不是最后一个 epoch 的避免过拟合后权重变差。history字典记录了每轮指标后面画曲线用。训练过程中如果val_loss连续多轮不降反升说明过拟合了该早停或加正则。3.3 训练过程可视化与调参判断光看数字不直观把 loss 和 accuracy 曲线画出来一眼就能判断模型是欠拟合还是过拟合。用 matplotlib 画双轴图训练和验证曲线放一起对比。import matplotlib.pyplot as plt epochs range(1, len(history[train_loss]) 1) fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) ax1.plot(epochs, history[train_loss], labelTrain Loss) ax1.plot(epochs, history[val_loss], labelVal Loss) ax1.set_xlabel(Epoch) ax1.set_ylabel(Loss) ax1.legend() ax1.set_title(Loss Curve) ax2.plot(epochs, history[val_acc], labelVal Accuracy, colorgreen) ax2.set_xlabel(Epoch) ax2.set_ylabel(Accuracy) ax2.legend() ax2.set_title(Validation Accuracy) plt.tight_layout() plt.savefig(training_curve.png, dpi150) plt.show()判断标准很直接训练 loss 降、验证 loss 也降说明还在学习可以继续训训练 loss 降、验证 loss 开始升过拟合了该停两条曲线都平且都高欠拟合换更大模型或解冻更多层。验证准确率震荡大把 batch_size 调大或学习率调小。这些判断不需要玄学看曲线形态就够了。4. 预测与 ONNX 导出让 Java 后端拿到一致的输出4.1 单张与批量预测脚本predict.py的逻辑是加载权重、预处理输入、前向推理、输出类别和置信度。单张预测和批量预测的差别只在数据组织方式模型部分完全一样。源码里让用户自行注释切换我建议把两种模式写成函数用命令行参数控制比注释来注释去清爽。import torch import json from PIL import Image from torchvision import transforms def load_model(weight_path, num_classes2): model build_model(num_classesnum_classes, freezeFalse) model.load_state_dict(torch.load(weight_path, map_locationcpu)) model.eval() return model def predict_single(model, img_path, label_map, devicecpu): 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(img_path).convert(RGB) tensor tf(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) conf, pred probs.max(dim1) idx2label {v: k for k, v in label_map.items()} return idx2label[pred.item()], conf.item() # 使用示例 with open(label_map.json, r, encodingutf-8) as f: label_map json.load(f) model load_model(best_model.pth) label, confidence predict_single(model, test.jpg, label_map) print(f预测类别: {label}, 置信度: {confidence:.4f})unsqueeze(0)是把单张图变成 batch 维度为 1 的张量模型要求输入是 4 维。torch.no_grad()关闭梯度计算推理时省显存。softmax把 logits 转成概率max同时拿到置信度和类别索引。idx2label做反查把数字索引还原成中文类别名。批量预测就是把多张图堆成一个 batch 一次前向逻辑一样只是输入组织不同。4.2 pth 转 ONNX 与 Java 调用一致性验证后端是 Java 的话直接加载 pth 不现实标准做法是导出 ONNX。PyTorch 自带torch.onnx.export指定输入输出名称和动态 batch 维度。导出后必须验证 ONNX 模型和原模型输出一致否则 Java 端预测结果对不上排查起来很痛苦。import torch import onnx import onnxruntime as ort import numpy as np # 导出 ONNX model load_model(best_model.pth) dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, leaf_disease.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 ) # 验证一致性 onnx_model onnx.load(leaf_disease.onnx) onnx.checker.check_model(onnx_model) session ort.InferenceSession(leaf_disease.onnx) test_input np.random.randn(1, 3, 224, 224).astype(np.float32) onnx_out session.run(None, {input: test_input})[0] with torch.no_grad(): torch_out model(torch.from_numpy(test_input)).numpy() diff np.abs(onnx_out - torch_out).max() print(fONNX 与 PyTorch 最大输出差异: {diff:.6f}) assert diff 1e-4, 输出差异过大检查导出配置dynamic_axes让 batch 维度可变Java 端传单张或多张都能处理。opset_version11兼容性较好太新的版本某些 Java ONNX 库不支持。onnx.checker.check_model做结构校验能提前发现导出错误。最后用 onnxruntime 跑一遍和 PyTorch 对比差异小于 1e-4 才算通过。这个验证步骤千万别省我见过导出后 softmax 层丢失导致输出全是 logits 的情况Java 端拿到一堆负数直接懵了。5. 避坑与排查那些让模型跑不起来的细节5.1 中文路径与编码问题现象训练脚本报FileNotFoundError但文件明明存在。原因Windows 下中文路径在某些 Python 版本或库里有编码兼容问题尤其是 DataLoader 多进程读取时。解决把数据集目录改成纯英文路径或者用pathlib.Path处理路径并在open时显式指定encodingutf-8。最稳妥的做法是项目根目录全英文中文只出现在 label_map 的值里。5.2 验证集准确率虚高现象验证准确率 99%测试集一跑只有 70%。原因数据划分时没做分层或者同一张图的不同增强版本同时出现在训练和验证集里。解决划分前先按文件名去重确保原始图片不跨集用stratify保证类别比例增强只在训练时做。如果数据集本身是视频抽帧来的相邻帧高度相似还要按视频来源分组划分否则就是自己骗自己。5.3 ONNX 导出后 Java 端结果不一致现象Python 端预测置信度 0.95Java 端同一个模型输出完全不同的类别。原因预处理不一致Python 用了 ImageNet 归一化Java 端可能只做了 resize 没归一化或者输入通道顺序 RGB/BGR 搞反了。解决把预处理参数均值、标准差、尺寸写进配置文件Java 端严格按同一份配置执行导出前用固定输入验证 ONNX 和 PyTorch 输出一致差异超过 1e-4 就查预处理。5.4 显存不足与 batch_size 设置现象训练到一半报CUDA out of memory。原因batch_size 太大或者没冻结骨干网络导致梯度占显存。解决先把 batch_size 降到 8 或 4 试确认freezeTrue时只有分类头参与训练用torch.cuda.empty_cache()清理缓存。如果还是不够把输入尺寸从 224 降到 128精度会掉一点但能跑起来。5.5 类别不平衡导致模型偏向多数类现象番茄叶斑病样本远多于苹果黑星病模型把所有输入都预测成番茄叶斑病准确率看着还行但没意义。原因交叉熵损失对多数类天然友好。解决给CrossEntropyLoss传weight参数按类别频率倒数加权或者对少数类做过采样。加权后准确率可能下降但少数类的召回率会明显提升实际部署更有价值。6. 进阶技巧用混淆矩阵和置信度阈值把模型管起来训练完看一个总准确率就收工是很多入门项目的通病。真正要上线给 Java 后端调用得知道模型在每一类上的表现以及预测不确定时怎么处理。混淆矩阵能告诉你模型把哪些类别搞混了置信度阈值能过滤掉低质量预测。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_nameslist(label_map.keys()))) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelslabel_map.keys(), yticklabelslabel_map.keys()) plt.xlabel(Predicted) plt.ylabel(True) plt.title(Confusion Matrix) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)classification_report输出每一类的 precision、recall、f1-score比总准确率信息量大得多。如果苹果黑星病的 recall 明显低于番茄叶斑病说明模型对少数类学得不好回去检查类别权重或增强策略。混淆矩阵热力图更直观对角线越深越好非对角线上的数字就是误判。置信度阈值是另一个实用技巧。推理时如果最高置信度低于某个阈值比如 0.6不直接输出类别而是返回「不确定」让上层人工复核。农业场景里误判病害类型可能导致用错药宁可让模型说不知道也别硬猜。def predict_with_threshold(model, img_path, label_map, threshold0.6): label, conf predict_single(model, img_path, label_map) if conf threshold: return 不确定, conf return label, conf阈值定多少要看业务容忍度0.5 到 0.7 之间调用验证集上的 precision-recall 曲线找平衡点。我一般会先在验证集上跑一遍不同阈值下的表现选一个误判率可接受的。还有个容易忽略的点模型上线后数据分布会漂移。田间新拍的照片光照、角度和训练集不一样准确率会下降。定期拿新数据跑一遍混淆矩阵发现某一类 recall 持续下降就重新标注、增量训练。别指望一次训练管一辈子。从那以后我每次导出 ONNX 都强制走一遍「PyTorch 输出 vs ONNX 输出」的数值对比差异超过 1e-4 绝不往下走。这个习惯帮我拦下过好几次预处理不一致的翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表