尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

农作物病虫害识别:PyTorch迁移学习图像分类实战

农作物病虫害识别:PyTorch迁移学习图像分类实战 简介一份基于Python的农作物病虫害识别分类项目完整资源适合机器学习初学者、农业院校学生或有图像识别需求的农业技术人员参考。项目围绕AI Challenger植物病害识别赛题展开覆盖数据收集与预处理、特征工程、模型选择、训练与评估完整流程可帮助读者理解如何用支持向量机、决策树、随机森林等算法对农作物病害图像进行分类。压缩包内含11个文件以Python脚本、JSON测试结果、PNG可视化图表和docx使用说明为主整体仅626KB轻量易用。其中包含训练集/验证集样本图、模型结构图与训练曲线图便于直观对比不同设置下的效果源码脚本展示了数据划分、交叉验证及参数调优等关键操作使用说明文档则梳理了运行步骤与实现思路。已有595人学习下载适合作为课程设计、毕业设计或算法竞赛复现的基础参考。1. 一整套能跑通农作物病虫害识别的 Python 分类项目一个农技站的朋友给我看过他手机里的相册几百张水稻叶片照片有的是稻瘟病有的是纹枯病有的只是晒伤。照片他拍了一年但一直堆在那里没标、没整理、更谈不上训练模型。这个标题对应的正是这类场景——拿到一套有标签的叶片图像用 Python 从数据整理、模型训练到预测脚本把「识别出这是什么病」这件事完整落地。它不是什么前沿研究而是一条被反复验证过的工程路径用迁移学习在预训练模型基础上做微调几十行代码就能达到可以用的精度。适合做课程设计、农业信息化课题以及刚接触图像分类、想拿真实数据集练手的人。2. 数据集怎么组织目录结构、预处理与数据增强拿到源码包和数据包之后第一件事不是打开训练脚本而是先看数据集的目录长什么样。图像分类项目最常见的组织方式就是「按类别建文件夹」每个子文件夹的名字就是类别标签文件夹里放对应类别的图片。torchvision 的ImageFolder能直接消费这种结构省掉手写 Dataset 的工作。2.1 用 ImageFolder 约定目录结构避免手写 Dataset一个标准的数据集目录通常长这样data/ train/ rice_blast/ # 稻瘟病 img_001.jpg img_002.jpg rice_sheath_blight/ # 纹枯病 img_101.jpg healthy/ # 健康叶片 img_201.jpg val/ rice_blast/ img_501.jpg rice_sheath_blight/ img_601.jpg healthy/ img_701.jpgtrain 和 val 下各有相同的类别文件夹验证集不要和训练集混在一起。ImageFolder会按文件夹名的字母序自动分配类别索引比如healthy是 0、rice_blast是 1、rice_sheath_blight是 2。用下面的代码加载from torchvision import datasets, transforms transform 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_data datasets.ImageFolder(data/train, transformtransform) val_data datasets.ImageFolder(data/val, transformtransform) print(类别映射:, train_data.class_to_idx) print(训练集样本数:, len(train_data)) print(类别数:, len(train_data.classes))class_to_idx会打印出类似{healthy: 0, rice_blast: 1, rice_sheath_blight: 2}的映射后面预测结果转回中文名称时要用到。还可以确认每个类别的样本数有没有严重的数量差异这决定了后面要不要做类别均衡处理。2.2 训练集/验证集怎么切避免同源图像数据泄漏如果数据包里没有现成的 train/val 目录需要自己划分。这里容易踩一个隐蔽的坑同一株作物上拍的多张病斑照片如果被同时分进训练集和验证集验证集精度会虚高。因为验证集里出现了和训练集高度相似的图像模型在验证集上表现的「好」并没有泛化到新场景的能力。划分的常见做法是按图像来源分组后再切而不是直接train_test_split随机打散文件。用scikit-learn做分层划分的代码样板import os import shutil from sklearn.model_selection import train_test_split all_images [] all_labels [] for class_name in os.listdir(raw_data): class_dir os.path.join(raw_data, class_name) for img in os.listdir(class_dir): all_images.append(os.path.join(class_dir, img)) all_labels.append(class_name) # stratify 保证每个类别在 train/val 中所占比例一致 train_files, val_files, train_labels, val_labels train_test_split( all_images, all_labels, test_size0.2, stratifyall_labels, random_state42 )参数stratifyall_labels是这里的关键。农作物数据集的类别数量往往不均衡比如稻瘟病的样本可能只有健康叶片的一半如果不分层抽样验证集里某个类别可能一个样本都没有报错时很难排查。random_state42固定随机种子保证每次跑出来的划分一致方便复现。2.3 农业图像的数据增强参数表与实现训练时用的 transform 和验证时不能一样。验证集只做 Resize、ToTensor、Normalize 这三步训练集要多加随机增强目的是让模型看到更多「变异」叶片角度不同、光线明暗不同、病斑位置不同。农业图像有个特殊性——病斑是细纹理特征增强强度太大反而会把纹理破坏掉模型学不到真正区分病害的关键信息。这一条经验在调参时要特别注意。训练集增强常用配置train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])增强操作参数建议作用注意事项RandomResizedCropscale(0.7, 1.0)模拟不同拍摄距离和多尺度病斑下界别低于 0.5会裁掉关键病斑RandomHorizontalFlipp0.5模拟叶片朝向翻转病害类别不依赖方向可以开RandomRotation15度模拟叶片倾斜旋转角度别太大超过 30 度会裁出无效区域ColorJitterbrightness/contrast 0.2模拟田间光照变化hue 只给 0.05色调偏移过大会改变病斑颜色增强策略的核心原则是「模拟真实拍摄差异不制造现实中不存在的图像」。使用说明文档里如果提到过拟合、训练集精度高但验证集低第一个要调的就是增强强度。3. 模型选择与训练配置从 ResNet 到轻量网络的迁移学习数据集就几千张图片的情况下从零训练一个深度卷积网络基本不会收敛到可用精度。ImageNet 上预训练过的模型把「边缘、纹理、形状」这些通用视觉特征已经学好了我们只需要在它基础上微调分类头这就是迁移学习。这个方案在农业病害识别里是绝对的主流源码包里大概率也是这个路线。3.1 为什么先选预训练 ResNet 而不是从零训练ResNet 的残差结构解决了深层网络退化问题18 层的 ResNet 在几万张规模的数据集上已经能取得够用的效果训练速度也快。比它更大的 ResNet50 在语义区分更细的任务上表现更好但训练时间和显存占用也会上升。常见做法是先用 ResNet18 跑通全流程确认数据没问题后再换 ResNet50 刷精度。如果部署端是嵌入式设备这个阶段也要一并考虑。用torchvision加载预训练权重import torchvision.models as models # ResNet18IMAGENET1K_V1 是官方在 ImageNet-1K 上预训练过的权重 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1注意旧代码里常见的写法models.resnet18(pretrainedTrue)在新版 torchvision 里已经标记为废弃用weights参数是当前推荐方式。IMAGENET1K_V1表示 ImageNet-1K 的第一版权重这是最常用的预训练权重。3.2 换掉分类头按类别数重建全连接层预训练模型的最后一层全连接层输出是 1000对应 ImageNet 的 1000 个类别必须换成自己的类别数。做法是冻结特征提取层替换最后的 fc 层num_classes len(train_data.classes) # 将最后一层全连接替换为新的分类器 model.fc torch.nn.Sequential( torch.nn.Dropout(0.2), # 防止过拟合 torch.nn.Linear(model.fc.in_features, num_classes) )这里用model.fc.in_features动态读取原层的输入维度不需要硬编码 512 或 2048换 ResNet 变体时就不用改这行代码。Dropout 放在全连接层前训练时随机丢弃 20% 的神经元在几千张样本量下是有效的正则化手段。微调策略上可以有两种选择。一种是把整个网络都设置为可训练用较小的学习率微调所有层另一种是先冻结 backbone 只训练分类头跑几个 epoch 后再解冻。对于农业病害这种「通用特征相关、新类别特征不明显」的任务直接全量微调的收敛速度通常最快但要调低学习率。通常的做法是先跑 2-3 个 epoch 看看 Loss 的变化趋势再决定是否需要做两阶段训练。3.3 训练超参数参考表训练超参数是复现效果的关键。给一组经过验证的基线参数可以按这个为起点调整超参数推荐值说明batch_size32显存不够就降到 16太低会导致梯度不稳定optimizerAdamW相比 Adam 权重衰减更规范效果略好base_lr1e-4微调 backbone 时用 1e-4只训练 fc 层可以到 1e-3weight_decay1e-4防止过拟合数据量大时可以减小schedulerCosineAnnealingLR配合 warmup 使用收敛更平稳epochs30-50迁移学习在这个区间可以收敛增加 epoch 未必更好label_smoothing0.1减轻模型对训练标签的过度自信损失函数用交叉熵PyTorch 里CrossEntropyLoss直接支持label_smoothing参数criterion torch.nn.CrossEntropyLoss(label_smoothing0.1)label_smoothing0.1的含义是真实的 one-hot 标签不再是[0, 1, 0]而是被平滑为[0.033, 0.933, 0.033]。效果是让模型在训练时不要对某一类过度自信通常能带来 1-2 个百分点的验证集精度提升。3.4 训练循环里值得盯的 3 个指标训练脚本的骨架是标准的 PyTorch 训练循环但有几个指标值得单独盯住for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_data) train_acc 100.0 * correct / total val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1}/{epochs} | Loss {epoch_loss:.4f} | Train Acc {train_acc:.2f}% | Val Acc {val_acc:.2f}%)第一个要盯的是训练 Loss如果 Loss 不降反而升高学习率可能太大如果 Loss 降到 0.01 以下而验证集精度没有提升大概率过拟合了需要提高增强强度或者加大 Dropout。第二个是验证集精度曲线如果验证集精度在前几个 epoch 快速上升然后横盘不动说明模型的容量已经用尽换更大的骨干网络比继续训练更有效。第三个是训练集和验证集精度的差差值超过 10 个百分点说明过拟合严重优先考虑加增强和正则化而不是换模型。4. 评估脚本与预测脚本从 checkpoint 到结果训练结束后模型权重保存在.pth文件里。但「模型训练完」不等于「项目做完」还需要两个脚本一个评估脚本在验证集上算各类别的详细指标一个预测脚本输入单张图片输出病害类别和置信度。这也是使用说明文档里通常会用大篇幅写的部分。4.1 多类分类不能只看 accuracy混淆矩阵才是结论验证集 accuracy 高不代表每个病害类别识别得都好。典型的例子健康叶片样本是其他类别的一倍模型只要把大部分图片都分类成健康accuracy 也能上 90%。在农业病害场景里漏诊一个病害比多报一次健康更严重所以要看每个类别的精确率和召回率。精确率意思是「模型认为是稻瘟病的图里有多少真的是稻瘟病」召回率意思是「真实是稻瘟病的图里模型找回来了多少」。在病害筛查场景召回率的优先级通常比精确率高因为宁可多复查不能漏掉带病样本。4.2 写一个能出各类别 precision/recall 的评估脚本用torchmetrics或者直接手写都可以。手写更透明逻辑如下from collections import defaultdict import torch def evaluate_model(model, val_loader, device, num_classes): model.eval() # 记录每个类别下 TP / FP / FN tp defaultdict(int) fp defaultdict(int) fn defaultdict(int) with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) for true_label, pred_label in zip(labels.cpu().numpy(), preds.cpu().numpy()): if true_label pred_label: tp[true_label] 1 else: fp[pred_label] 1 # 模型预测了这个类别但实际不是 fn[true_label] 1 # 实际是这个类别但模型没预测对 print(f{类别:20} {Precision:10} {Recall:10} {样本数:6}) for cls in range(num_classes): precision tp[cls] / (tp[cls] fp[cls]) if (tp[cls] fp[cls]) 0 else 0 recall tp[cls] / (tp[cls] fn[cls]) if (tp[cls] fn[cls]) 0 else 0 print(f{val_data.classes[cls]:20} {precision:.4f} {recall:.4f} {tp[cls]fn[cls]:6}) return tp, fp, fn从结果里能看出模型在哪些类别上容易混淆。比如稻瘟病和稻曲病的 Precision 都很低说明模型经常把别的病判成这两种那就需要去检查这两个类别的训练图像是不是标注有误或者病害早期症状确实太像。混淆矩阵可以配合sklearn.metrics.confusion_matrix画出来normalizetrue按行归一化之后每一行代表「真实类别下各预测类别的占比」对角线越接近 1 越好。注意行归一化后的混淆矩阵比不归一化的更直观因为不归一化时样本多的类别颜色总是很深会掩盖样本少类别的表现。4.3 单张图片预测脚本的预处理与模型加载预测脚本要复现验证集的预处理流程这是最常出错的地方。很多人训练时用RandomResizedCrop预测时也照搬结果每跑一张图结果都不一样。预测阶段的 transform 必须和验证集一致只做 Resize 和 Normalize。from PIL import Image import torchvision.transforms as transforms def predict_image(model, image_path, device): transform 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(image_path).convert(RGB) img_tensor transform(img).unsqueeze(0).to(device) # 加 batch 维度 model.eval() with torch.no_grad(): outputs model(img_tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, 1) return train_data.classes[pred.item()], conf.item() # 使用 class_name, confidence predict_image(model, test_leaf.jpg, device) print(f识别结果: {class_name}, 置信度: {confidence:.4f})unsqueeze(0)的作用是把单张图片的维度从(3, 224, 224)变成(1, 3, 224, 224)这对应 PyTorch 的 batch 维度。torch.softmax把网络的原始输出变成总和为 1 的概率分布torch.max取概率最大的类别作为预测结果。5. 部署时的量化、导出与常见坑训练好的模型最终要跑在别人的机器上可能是农技站的普通电脑也可能是边缘设备。PyTorch 模型直接部署需要安装全套 PyTorch 环境最常见的替代方案是导出成 ONNX 格式。ONNX 是通用模型交换格式不需要 PyTorch 也能运行而且可以配合推理引擎获得更快的速度。5.1 ONNX 导出与动态尺寸处理import torch.onnx model.eval() dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, crop_disease_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} ) print(ONNX 导出完成)dynamic_axes指定batch_size维度是动态的这样导出的模型可以接收任意 batch 大小的输入部署时灵活性更高。如果固定 batch_size 为 1可以省略这个参数部分推理引擎的优化效果会更好。5.2 训练与部署最容易踩的 3 个坑第一个坑是类别不均衡。农业数据集经常出现「健康叶片几百张、发病叶片几十张」的情况。解决思路不是简单地删除多数类样本而是优先收集少数类样本或者在采样器上用WeightedRandomSampler让每个 epoch 里少数类被抽到的概率更大。修改采样器权重的方式可以保持所有样本都被使用比过采样更不容易过拟合。第二个坑是验证集和部署场景的「分布漂移」。训练照片是实验室白背景下拍的病斑特写部署时用户拿手机拍的是田间自然光照、有泥土有水滴的整株叶片。这类场景差异导致的精度下降比任何超参数问题都严重。缓解方式是在数据集中掺入更多拍摄环境多样的图像或者对训练图做更强的背景扰动增强。第三个坑是预训练权重的加载失败或缺失。刚接触迁移学习的时候经常遇到torchvision下载权重时网络超时的情况然后误以为代码写错了。解决思路是提前手动下载权重文件通过torch.hub.load_state_dict_from_url的本地路径加载或者检查torch.hub的缓存目录~/.cache/torch/hub/checkpoints。源码包的使用说明里如果提到了环境搭建大概率也会专门说明这一点。验证整个项目是否真正完成有一个简单的检验方法把训练时没见过的、带标签的图片随机挑 20 张逐张跑预测脚本记录每一张结果的置信度。如果置信度普遍高于 0.9而人工查看图片却发现明显误判说明模型过拟合到了数据集的特征上如果置信度普遍在 0.6-0.8 之间但结果是正确的说明模型表达的是合理的不确定这在实际部署中反而是正常表现。真正可用的模型不是追求置信度都接近 1而是在置信度低时知道该输出「不确定建议人工复核」。本文还有配套的精品资源点击获取
返回列表