简介:一份基于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", transform=transform) val_data = datasets.ImageFolder("data/val", transform=transform) 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_size=0.2, stratify=all_labels, random_state=42 )参数stratify=all_labels是这里的关键。农作物数据集的类别数量往往不均衡,比如稻瘟病的样本可能只有健康叶片的一半,如果不分层抽样,验证集里某个类别可能一个样本都没有,报错时很难排查。random_state=42固定随机种子,保证每次跑出来的划分一致,方便复现。
2.3 农业图像的数据增强参数表与实现
训练时用的 transform 和验证时不能一样。验证集只做 Resize、ToTensor、Normalize 这三步,训练集要多加随机增强,目的是让模型看到更多「变异」:叶片角度不同、光线明暗不同、病斑位置不同。农业图像有个特殊性——病斑是细纹理特征,增强强度太大反而会把纹理破坏掉,模型学不到真正区分病害的关键信息。这一条经验在调参时要特别注意。
训练集增强常用配置:
train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])| 增强操作 | 参数建议 | 作用 | 注意事项 |
|---|---|---|---|
| RandomResizedCrop | scale=(0.7, 1.0) | 模拟不同拍摄距离和多尺度病斑 | 下界别低于 0.5,会裁掉关键病斑 |
| RandomHorizontalFlip | p=0.5 | 模拟叶片朝向翻转 | 病害类别不依赖方向,可以开 |
| RandomRotation | 15(度) | 模拟叶片倾斜 | 旋转角度别太大,超过 30 度会裁出无效区域 |
| ColorJitter | brightness/contrast 0.2 | 模拟田间光照变化 | hue 只给 0.05,色调偏移过大会改变病斑颜色 |
增强策略的核心原则是「模拟真实拍摄差异,不制造现实中不存在的图像」。使用说明文档里如果提到过拟合、训练集精度高但验证集低,第一个要调的就是增强强度。
3. 模型选择与训练配置:从 ResNet 到轻量网络的迁移学习
数据集就几千张图片的情况下,从零训练一个深度卷积网络基本不会收敛到可用精度。ImageNet 上预训练过的模型把「边缘、纹理、形状」这些通用视觉特征已经学好了,我们只需要在它基础上微调分类头,这就是迁移学习。这个方案在农业病害识别里是绝对的主流,源码包里大概率也是这个路线。
3.1 为什么先选预训练 ResNet 而不是从零训练
ResNet 的残差结构解决了深层网络退化问题,18 层的 ResNet 在几万张规模的数据集上已经能取得够用的效果,训练速度也快。比它更大的 ResNet50 在语义区分更细的任务上表现更好,但训练时间和显存占用也会上升。常见做法是先用 ResNet18 跑通全流程,确认数据没问题后,再换 ResNet50 刷精度。如果部署端是嵌入式设备,这个阶段也要一并考虑。
用torchvision加载预训练权重:
import torchvision.models as models # ResNet18,IMAGENET1K_V1 是官方在 ImageNet-1K 上预训练过的权重 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1注意旧代码里常见的写法models.resnet18(pretrained=True)在新版 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_size | 32 | 显存不够就降到 16,太低会导致梯度不稳定 |
| optimizer | AdamW | 相比 Adam 权重衰减更规范,效果略好 |
| base_lr | 1e-4 | 微调 backbone 时用 1e-4,只训练 fc 层可以到 1e-3 |
| weight_decay | 1e-4 | 防止过拟合,数据量大时可以减小 |
| scheduler | CosineAnnealingLR | 配合 warmup 使用,收敛更平稳 |
| epochs | 30-50 | 迁移学习在这个区间可以收敛,增加 epoch 未必更好 |
| label_smoothing | 0.1 | 减轻模型对训练标签的过度自信 |
损失函数用交叉熵,PyTorch 里CrossEntropyLoss直接支持label_smoothing参数:
criterion = torch.nn.CrossEntropyLoss(label_smoothing=0.1)label_smoothing=0.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(f"Epoch {epoch+1}/{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画出来,normalize='true'按行归一化之后,每一行代表「真实类别下各预测类别的占比」,对角线越接近 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, dim=1) 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,而是在置信度低时知道该输出「不确定,建议人工复核」。
本文还有配套的精品资源,点击获取