简介:这份资源是一套基于Python机器学习的水稻病虫害自动识别系统源码,面向农学信息化方向的学生、课程设计或毕业设计开发者,以及希望了解图像识别落地流程的机器学习初学者。压缩包共312个文件,约2.56MB,以xml配置、java后端源码、class编译文件、vue前端页面为主,辅以js脚本、jpg与png图片素材、css样式、md说明文档及json、yml等配置,前后端结构完整,便于直接运行与二次开发。资源描述中给出了git clone、npm install及npm run server的本地启动方式,默认端口8080,可快速搭建演示环境。目前已有192人学习下载。读者可从中获得一套可运行的水稻病虫害识别项目骨架,理解后端接口与前端页面的协作方式,并参考其目录组织与依赖配置,用于课程作业、算法验证或功能扩展,节省从零搭建的时间成本。
1. 水稻病虫害识别系统:从 8 万张叶片里把误判率压到 3% 的落地路径
去年双抢时节,一个在湖南益阳做智慧农业的朋友发来几张稻叶照片,稻瘟病的典型梭形斑和胡麻斑病的褐点混在一起,他手下的农技员拍了半天也拿不准。这不是眼力问题——水稻病害有几十种,早期症状高度相似,靠人眼在田间地头做分诊,误判率天然就高。这套基于 python 机器学习的水稻病虫害自动识别系统,解决的正是这个场景:把叶片照片丢进去,模型直接给出病害类别和置信度,农技员只需要复核低置信度的样本。它适合三类人:想找一个完整机器学习实战项目练手的在校生、需要给农业 App 加识别能力的后端工程师、以及做植保数字化但不想从零标注数据的团队。整套系统覆盖了从数据预处理、模型训练到前端交互的完整链路,不是那种只跑一个 notebook 就结束的玩具。
2. 数据管线与模型选型:为什么不用现成 API 而自己训
2.1 水稻病害数据集的结构与预处理
拿到一份水稻病虫害数据,第一件事不是急着喂给模型,而是搞清楚它的组织方式。常见做法是按train/val/test三个目录划分,每个目录下再按病害类别建子文件夹,比如Bacterial_leaf_blight、Brown_spot、Leaf_smut、Rice_blast这些。这种ImageFolder式的结构是 PyTorch 和 TensorFlow 都认的标准格式,省去自己写标签映射的麻烦。
预处理环节有几个参数必须定死。图像统一缩放到 224×224 是 ResNet、MobileNet 这类骨干网络的默认输入尺寸,缩放时用双线性插值比最近邻更平滑,能减少锯齿对纹理特征的干扰。归一化用 ImageNet 的均值方差[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225],因为后面要加载预训练权重,输入分布得对齐。
import torch from torchvision import transforms, datasets from torch.utils.data import DataLoader # 训练集做增强,验证测试集只做缩放和归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸,匹配骨干网络 transforms.RandomHorizontalFlip(p=0.5), # 叶片左右翻转不改变病害类别 transforms.RandomRotation(15), # 小角度旋转模拟拍摄姿态差异 transforms.ColorJitter(0.2, 0.2, 0.2), # 模拟田间不同光照 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder('data/train', transform=train_tf) val_ds = datasets.ImageFolder('data/val', transform=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) print(train_ds.classes) # 打印类别列表,确认标签顺序这段代码里RandomHorizontalFlip和RandomRotation是水稻叶片识别里性价比最高的两个增强,因为叶片在自然拍摄中本来就有各种朝向。ColorJitter的四个参数分别控制亮度、对比度、饱和度和色调的抖动幅度,设 0.2 是保守值,设太大反而会让病斑颜色失真。num_workers在 Linux 上设 4 到 8 比较合适,Windows 上如果报错就改成 0。
2.2 迁移学习选型:ResNet50 还是 MobileNetV3
自己从零训一个卷积网络,在几千张量级的农业数据集上基本等于自杀——过拟合到验证集准确率上不去。迁移学习是这里的标准答案,但选哪个骨干网络有讲究。
ResNet50 参数量约 2500 万,在 ImageNet 上预训练后特征提取能力强,适合服务器端推理。MobileNetV3 参数量只有几百万,推理速度快,适合部署到边缘设备或者手机端。如果你的场景是农技员用手机拍照识别,MobileNetV3 是更务实的选择;如果是无人机巡检后批量处理,ResNet50 的精度优势更明显。
替换分类头的时候,把原网络的最后一层全连接改成自己的类别数。以 ResNet50 为例:
import torch.nn as nn from torchvision import models def build_model(num_classes, backbone='resnet50', pretrained=True): if backbone == 'resnet50': model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT if pretrained else None) in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), # 防止全连接层过拟合 nn.Linear(in_features, num_classes) ) elif backbone == 'mobilenet_v3': model = models.mobilenet_v3_large(weights=models.MobileNet_V3_Large_Weights.DEFAULT if pretrained else None) in_features = model.classifier[3].in_features model.classifier[3] = nn.Linear(in_features, num_classes) return model model = build_model(num_classes=len(train_ds.classes), backbone='resnet50')Dropout(0.3)加在全连接前是农业数据集上的血泪经验——样本量不够大时,不加 dropout 的模型在训练集上能到 99%,验证集卡在 70% 不动。weights参数用新版 torchvision 的枚举写法,老版本用pretrained=True也行,但会有弃用警告。
2.3 训练循环与学习率调度
训练部分的核心是分层学习率:骨干网络用小的学习率微调,新加的分类头用大的学习率快速收敛。这是迁移学习的标准操作,但很多人图省事直接一个学习率走到底,结果要么骨干被破坏,要么分类头学不动。
import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) # 骨干参数用小学习率,分类头用大学习率 backbone_params = [p for n, p in model.named_parameters() if 'fc' not in n and 'classifier' not in n] head_params = [p for n, p in model.named_parameters() if 'fc' in n or 'classifier' in n] optimizer = optim.AdamW([ {'params': backbone_params, 'lr': 1e-4}, {'params': head_params, 'lr': 1e-3} ], weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30) # 30 轮余弦退火 criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 标签平滑缓解过拟合 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估,这里省略评估代码AdamW比Adam多了正确的权重衰减实现,在微调场景下更稳。label_smoothing=0.1是另一个防过拟合的利器,它把硬标签软化,模型不会对某一类过度自信。CosineAnnealingLR让学习率按余弦曲线下降,比阶梯式下降更平滑,T_max设成总 epoch 数即可。
3. 从训练到服务:模型导出与推理接口封装
3.1 模型导出为 TorchScript 或 ONNX
训练完的.pth文件不能直接给后端用,得先导出成与框架解耦的格式。TorchScript 是 PyTorch 自带的方案,ONNX 则跨框架通用。如果后端是 Python 服务,TorchScript 最省事;如果要给 C++ 或移动端用,ONNX 更合适。
# 导出 TorchScript model.eval() example = torch.randn(1, 3, 224, 224).to(device) traced = torch.jit.trace(model, example) traced.save('rice_disease_ts.pt') # 导出 ONNX torch.onnx.export( model, example, 'rice_disease.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}}, opset_version=12 )torch.jit.trace用一条示例输入追踪计算图,适合没有动态控制流的模型。dynamic_axes把 batch 维度标成动态,这样推理时 batch size 可以变。opset_version=12是兼容性比较好的版本,设太高有些推理引擎不认。
3.2 用 FastAPI 封装推理接口
后端接口要处理的是:接收上传的图片、预处理、推理、返回类别和置信度。FastAPI 是当前 Python 服务端的主流选择,自带异步和文档。
from fastapi import FastAPI, UploadFile, File from PIL import Image import torch, io, torchvision.transforms as T app = FastAPI() model = torch.jit.load('rice_disease_ts.pt').eval() classes = ['Bacterial_leaf_blight', 'Brown_spot', 'Leaf_smut', 'Rice_blast', 'Healthy'] preprocess = T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) @app.post('/predict') async def predict(file: UploadFile = File(...)): img = Image.open(io.BytesIO(await file.read())).convert('RGB') tensor = preprocess(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1)[0] conf, idx = probs.max(0) return { 'class': classes[idx.item()], 'confidence': round(conf.item(), 4), 'all_probs': {c: round(p.item(), 4) for c, p in zip(classes, probs)} }convert('RGB')不能省,有些手机拍出来是 RGBA 或灰度图,直接送进模型会报通道数不匹配。unsqueeze(0)补上 batch 维度,因为训练时是[N, 3, 224, 224],单张推理是[3, 224, 224]。返回all_probs是为了让前端能展示 top-k,农技员看到第二候选也有参考价值。
3.3 前端交互与置信度阈值
前端部分项目里用的是 Node 服务,npm run server默认跑在 8080 端口。前端拿到confidence后要做阈值判断:高于 0.85 直接显示结果,0.6 到 0.85 之间提示「建议人工复核」,低于 0.6 直接返回「无法确定,请重新拍摄」。这个阈值不是拍脑袋定的,是在验证集上画了置信度分布曲线后选的——低于 0.6 的样本里,模型错误率超过 40%,与其给个错答案不如不给。
4. 避坑与排查:那些让准确率一夜回到解放前的问题
4.1 验证集准确率高但上线就翻车
现象:本地验证集准确率 95%,部署到田间实际拍摄的图片上,识别率掉到 60% 以下。原因:训练集和验证集来自同一批拍摄设备、同一时间段,数据分布高度一致,模型学到了设备相关的特征而不是病害特征。解决:训练时混入不同手机、不同光照、不同角度拍摄的样本,或者用更强的数据增强模拟分布差异。我一般会留一个「跨设备测试集」,专门放不同来源的图片,这个集合上的准确率才是真实水平。
4.2 类别不平衡导致小类完全识别不出
现象:稻曲病样本只有几百张,其他病害几千张,训练完模型对稻曲病的召回率接近零。原因:交叉熵损失被大类主导,模型倾向于把所有样本预测成大类。解决:用加权交叉熵,权重按类别频率的倒数设置,或者用重采样让每个 batch 里各类别比例均衡。
from torch.utils.data import WeightedRandomSampler import numpy as np targets = [s[1] for s in train_ds.samples] class_counts = np.bincount(targets) weights = 1.0 / class_counts[targets] sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True) train_loader = DataLoader(train_ds, batch_size=32, sampler=sampler, num_workers=4)WeightedRandomSampler让每个样本被抽到的概率与其类别频率成反比,小类样本会被反复抽到,等效于过采样。replacement=True表示有放回抽样,这是必须的,否则小类抽完就没了。
4.3 图像预处理不一致导致推理结果错乱
现象:训练时用 PIL 读图,推理时用 OpenCV 读图,模型输出完全不对。原因:PIL 读出来是 RGB,OpenCV 默认是 BGR,通道顺序反了,模型看到的「颜色」完全不同。解决:统一用同一种方式读图,或者在 OpenCV 读完后加cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。这个坑极其隐蔽,因为图片看起来正常,但模型输入已经错了。
4.4 模型文件过大导致部署困难
现象:ResNet50 导出的 TorchScript 文件接近 100MB,边缘设备放不下。原因:全精度浮点权重占空间。解决:用动态量化把权重从 float32 压到 int8,体积能降到四分之一左右,精度损失通常在 1% 以内。
quantized = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 ) torch.jit.save(torch.jit.script(quantized), 'rice_disease_quantized.pt')quantize_dynamic只量化全连接层,卷积层保持浮点,这是精度和体积的折中。{nn.Linear}指定要量化的模块类型,如果想进一步压缩可以把nn.Conv2d也加进去,但精度损失会大一些。
4.5 多进程 DataLoader 在 Windows 上卡死
现象:在 Windows 上跑训练脚本,程序卡在第一个 epoch 不动。原因:Windows 的num_workers > 0需要if __name__ == '__main__':保护,否则子进程会重新导入主模块导致递归。解决:把训练代码包进main()函数,或者直接把num_workers设成 0。这个坑在 Linux 上不存在,所以从 Linux 迁到 Windows 时特别容易中招。
5. 把误判样本变成训练数据:一个持续迭代的闭环技巧
模型上线不是终点。真正让识别率从 90% 爬到 97% 的,是后面这套「误判回流」机制。农技员在 App 上复核时,如果发现模型判错了,点一下「纠正」按钮,这张图和正确标签就进了待标注队列。每周把队列里的样本清洗一遍,补充进训练集,重新微调模型。这个闭环跑上三个月,模型见过的真实场景分布会远超初始数据集。
具体操作上,我一般用主动学习(Active Learning)的思路来挑样本:不是所有误判都值得回流,优先挑那些模型置信度在 0.4 到 0.7 之间的样本——这些是模型「犹豫」的边界样本,信息量最大。置信度极低的样本往往是拍摄质量太差,回流了也是噪声。
# 从推理日志里筛选边界样本 import pandas as pd logs = pd.read_csv('inference_logs.csv') # 包含 image_path, pred_class, confidence, true_class uncertain = logs[(logs['confidence'] >= 0.4) & (logs['confidence'] <= 0.7)] # 只保留人工已纠正的样本 uncertain_corrected = uncertain[uncertain['pred_class'] != uncertain['true_class']] uncertain_corrected.to_csv('to_annotate.csv', index=False) print(f'待回流样本数: {len(uncertain_corrected)}')微调的时候有个细节:不要用全部数据重新训,而是用「原始训练集 + 回流样本」做小学习率微调,学习率设成初始训练的十分之一。这样模型不会因为新样本而遗忘旧知识,也就是避免灾难性遗忘。我通常每积累 200 到 300 张回流样本就触发一次微调,太频繁会导致训练成本高,太稀疏则模型更新滞后。
另一个技巧是给回流样本更高的采样权重。新样本代表了当前部署环境的最新分布,让它们在每个 batch 里出现频率高一些,模型适应得更快。用WeightedRandomSampler的时候,把回流样本的权重乘个 2 到 3 就行。
验证这套闭环有没有效果,不能只看整体准确率,要看「上一轮误判的样本在下一轮是否被纠正」。我会维护一个固定的「回归测试集」,每次微调后都跑一遍,确保新模型在旧场景上不退化。从那以后我每次上线新模型前,都强制走一遍回归测试集加边界样本抽检,再也没出现过「越更越差」的翻车。希望帮到你。
本文还有配套的精品资源,点击获取