ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

深度学习实战:水稻叶部病害识别从数据到部署全攻略

深度学习实战:水稻叶部病害识别从数据到部署全攻略 简介图像识别技术正从通用场景走向细分农业领域其中卷积神经网络通过提取纹理、颜色等特征让计算机具备像植保专家一样的病害判断能力。水稻叶部病害识别作为典型落地场景依赖高质量图像数据与合理的迁移学习策略在PyTorch框架下完成模型训练与调优。实际工程中还需解决类别不均衡、数据泄露、边缘设备部署等问题。本文结合完整项目实践系统梳理水稻叶部病害识别的数据采集、增强、模型选型、训练技巧、评估指标与ONNX导出流程并借助Grad-CAM可视化验证模型关注区域为农业AI落地提供可复用的技术路径。1. 稻叶上那点斑深度学习模型到底能认出几分傍晚的田埂上一个人举着手机蹲在稻丛边对着叶片来回找角度按快门。这不是摄影爱好者在采风而是植保站的常规操作把带病叶片拍下来拿回电脑前让人一张张看。而“基于深度学习的水稻叶部病害识别方法研究”这个标题本质上就是把“人眼看病斑”这件事交给卷积神经网络去做——拍一张稻叶照片输入模型输出“稻瘟病/白叶枯病/胡麻斑病”这样的结论。它解决的是基层植保人手不足、老专家经验不可复制、靠肉眼巡检效率低的现实问题。适合三类人看想把识别模型从论文落到田间的算法工程师、要给农户做拍照识病小程序的农业物联网团队、以及被毕业论文选题按在键盘上的农学研究生。这篇笔记不聊论文怎么写只讲这套方法从头到尾怎么做通、参数怎么设、以及哪些环节会让你一晚上白干。2. 数据比模型更值钱水稻叶部病害图像从哪里来、怎么喂给网络2.1 病害数据集的三种来源公开集、田间自采、实验室补拍做“基于深度学习的水稻叶部病害识别方法”第一个绕不开的问题是图片从哪来。我见过不少人一上来就搭ResNet结果训练到第三天发现验证集准确率卡在78%上不去回头一查是数据只有八百张、其中三类病还是同一个角度拍的。数据来源常见有三种每种都有自己的脾气。公开数据集是起步最省事的做法。水稻病害方向比较常用的公开集有水稻病害识别数据集Rice Disease Dataset、IPM Images这类带标签的农业图像库里面稻瘟病、纹枯病、白叶枯病的图片基本都有。但公开集的毛病是“场景太干净”——很多图是近距离特写、单一背景、光线均匀模型在这种图上练出来一到水田实拍就露馅。田间自采是让模型“接地气”的关键。用手机在上午9点到11点、下午3点到5点这两个时段拍光线斜射叶片反光少。每片病叶至少拍3个角度同时把健康叶、虫咬叶、机械损伤叶也拍进去。这里有个从业者很容易忽略的点水稻叶部病害识别不只是“判断有没有病”还得“判断是不是这种病”而虫咬和机械伤的纹理在卷积网络眼里有时和病斑非常像。我在做数据采集时一般会专门建一个“other_damage”类把非病害叶片归进去宁可多分一类也不让模型把杂物硬塞进病害类别。实验室补拍是给数据做“兜底”。田间采不到某些病害的典型症状期就去种植温室里人工接菌等发病后用白纸板背景补拍光线不够就上两盏LED平板灯左右45度角打光避免叶片油亮反光形成高光斑。这块的成本不高但能让少数类样本的完备度明显提升。三种来源的比例上我一般控制在公开集:田间自采:实验室补拍约3:5:2田间自采要占大头否则模型很难适应真实场景。2.2 数据不平衡与标注一致性决定模型上限的两个前置问题数据量凑够了真正的坑在水面下。水稻叶部病害类别天然不平衡稻瘟病发生面积大、图片容易拍到往往占数据集的一半以上而稻曲病、胡麻斑病这类要么发生不规律、要么症状不明显样本量常常只有几十张。深度学习模型在极端不平衡下会把大头类学得很好对小类直接摆烂——表现为少数类的召回率不到40%而这种表现在混淆矩阵里一目了然。解决不平衡优先级是采集 重采样 损失函数。优先回田间补拍小类样本哪怕只多二十张真实图片也比用复制粘贴造出一百张强。补不到再考虑在训练时做类别加权采样让每个epoch里小类被抽到的概率更高。更进一步的可以换损失函数常见做法是把交叉熵换成Focal Loss让模型把注意力放到难分的少样本上。不过这招要在模型已经能正常收敛之后再上一上来就换损失函数反而经常把训练搞得不稳定。标注一致性是同样要命但更隐蔽的问题。水稻病害早期的病斑形态区分度很低稻瘟病急性型病斑是“灰绿色水渍状”白叶枯病是“枯黄色条斑”但在图片上如果只有手指肚大一块标注员A和标注员B的判断可能完全不同。我的做法是写一页纸的标注规范配上每个病害的典型图和非典型图各5张灌给每个标注的人看完再动手每标200张抽查30张发现分歧超过10%就停下来重新对齐标准。这个环节花半天时间省的是后面整个训练周期的返工成本。2.3 在PyTorch里搭一个带增强的Dataset代码与参数数据准备好了下一步是把图片文件夹变成PyTorch能吃的Dataset。这里我直接用torchvision的ImageFolder加上transforms来组织训练集和验证集的增强策略分开写这是一个关键取舍训练集需要足够强的扰动让模型见多识广验证集则要尽量保持原始形态增强太激进会污染评估指标。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader, WeightedRandomSampler # 训练集增强旋转、翻转、亮度扰动、透视变化一起上 train_transforms transforms.Compose([ transforms.RandomResizedCrop(size224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.3), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集只做缩放和中心裁剪不做任何随机扰动 val_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(./data/train, transformtrain_transforms) val_dataset datasets.ImageFolder(./data/val, transformval_transforms) # 类别加权采样样本少的病害类别每个epoch多抽几次 label_list [s[1] for s in train_dataset.samples] class_counts torch.bincount(torch.tensor(label_list)).float() class_weights 1.0 / class_counts sample_weights class_weights[torch.tensor(label_list)] sampler WeightedRandomSampler(weightssample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)这里有一个关键参数值得单独说RandomResizedCrop的scale范围。病害识别的病斑常以像素尺度出现在叶片上如果scale设得太低比如(0.2, 1.0)增强后病斑被放大到看不清纹理模型会把“模糊”当成特征来学。水稻叶片病害建议scale保持(0.6, 1.0)让裁剪后的图仍然保留足够清晰的病斑形态。ColorJitter的幅度也别太大叶片颜色是诊断的重要依据之一调得太狠会让稻瘟病的“灰绿色”变成“枯黄色”反而制造噪声。WeightedRandomSampler里的replacementTrue保证小类样本在采样时允许重复抽取否则类别权重就起不到增采样效果。2.4 训练集/验证集/测试集划分按植株不按图片数据切分是许多人最容易犯低级错误的地方。如果同一株稻叶的多个角度的照片被随机分到了训练集和验证集那么验证结果会虚高——模型在某张照片上学到的背景纹理会在另一张同株的照片上“作弊”。这种泄露在深度学习里叫数据泄露会让你的模型在实验室指标上好看得离谱下地一测就露馅。我一般会按“植株”划分而不是“图片”划分采集时每株水稻给一个编号文件名前缀带上比如plant_023_leaf_2_diseased.jpg切分时保证同一个植株编号的所有图片全部进同一个集合。这样验证集和测试集反映的是“没见过的新植株”而不是“同一个体换了个姿势”评估才有参考价值。划分比例上6:2:2是稳妥的底限数据量少于一千张时训练集比例可以提到7成测试集仍然留足2成不动。3. 模型选型与训练一个能跑通水稻叶部病害识别的PyTorch最小闭环3.1 病害识别选什么网络从ResNet到EfficientNet的取舍在基于深度学习做水稻叶部病害识别这个方向上模型选型有个很实际的约束既要在田里能用又别把实验室机器烧穿。病斑识别本质上是细粒度图像识别——类间差异小稻瘟病和胡麻斑病的斑点是颜色和形状的双重近似这对网络的纹理提取能力要求比普通分类高。ResNet50是这个任务里最稳的起手式。残差结构在数据量不大的时候不容易退化torchvision里带预训练权重加载方便参数量2500万在单张消费级GPU上能跑。比它更深的ResNet101收益有限病害识别的瓶颈往往在数据层面而非模型容量堆深度不如把数据搞干净。EfficientNet系列里B3到B4是性价比区间同样的准确率下参数量小一半适合后面要往树莓派、Jetson这类边缘设备上部署的场景。MobileNetV3则属于“牺牲一点准确率换部署顺畅”如果目标是做小程序端的拍照识病可以直接从它开始。VGG系列不建议碰——参数量大、训练慢在这个任务里没有任何优势。结构选型之外一个被低估的决定是输入分辨率。ImageNet标准的224x224在处理病斑时经常不够用稻叶上绿豆大小的初期病斑缩到224分辨率下只剩十几个像素。如果显存允许把输入调成320或384病斑纹理细节能明显保留。我在做这个方向时两个分辨率都跑过对比320输入相比224的top-1准确率普遍能高出2到3个百分点代价只是训练时间增加约40%。3.2 迁移学习用ImageNet预训练权重给病害模型“打地基”水稻叶部病害识别有个天然痛点专业数据量少哪怕全部收集齐也到不了ImageNet量级。从头训练一个ResNet50需要几百万张图才能收敛出有效的底层特征而病害数据撑死几千张唯一靠谱的路线是迁移学习——把模型在ImageNet上已经学好的边缘、纹理、颜色等底层特征“搬过来”只微调高层语义部分。加载预训练权重的姿势有两种。第一种是直接torchvision官方的weights接口省事。第二种是自己先在一个大而杂的植物病害数据集上预训练再在稻叶数据上微调效果通常更好因为中间域的图像分布更接近目标域但实现成本高前期要有足够数据。对大多数项目直接用ImageNet权重就已经能给出相当好的起点。关键是训练策略要与迁移学习匹配预训练模型已经具备了很强的底层特征提取能力如果从头就用大学习率会把预训练权重冲得稀碎那还不如不加载。后面3.3节的训练脚本里采用了冻结骨干、分段学习率的方案这是迁移学习落地时最常用的稳健选择。3.3 完整训练脚本与关键参数batch size、学习率与数据增强强度下面这份脚本是我做水稻叶部病害识别时最常用的一套配置ResNet50骨架加载ImageNet预训练权重先冻结骨干只训练分类头等分类头收敛后再解冻整个网络用小学习率微调。这套两阶段策略在数据量处于1000到5000张区间时比全程全参数训练稳定得多。import torch import torch.nn as nn import torch.optim as optim from torchvision import models from tqdm import tqdm # 加载预训练ResNet50把最后一层改成自己的类别数 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) num_classes 6 # 稻瘟病、白叶枯病、纹枯病、胡麻斑病、稻曲病、其他损伤 model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 阶段一冻结backbone只训练新加的fc层 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True criterion nn.CrossEntropyLoss() # fc层是随机初始化的学习率可以给大一点 optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) # 前10个epoch只训练分类头 for epoch in range(10): model.train() for inputs, labels in tqdm(train_loader): inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() print(fStage1 Epoch {epoch1}, Loss: {loss.item():.4f}) # 阶段二解冻backbone全参数微调学习率放低 for param in model.parameters(): param.requires_grad True # 全参数训练时学习率降到1e-4避免破坏预训练权重 optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.5) for epoch in range(30): model.train() for inputs, labels in tqdm(train_loader): inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() print(fStage2 Epoch {epoch1}, Loss: {loss.item():.4f}) torch.save(model.state_dict(), rice_disease_resnet50.pth)batch size的选择要同时看显存和数据规模。32在ResNet50224输入下是消费级显卡的安全值24G显存的卡可以提到64但batch size过大会让BatchNorm的统计量更平滑收敛虽稳但容易陷入尖锐极小值泛化反而下降。数据量只有一两千张时batch size 16到32足够不要盲目加大。学习率的设置原则是“迁移学习永远从保守开始”——阶段1给1e-3是因为fc层是全新的需要大步子快速找到方向阶段2降到1e-4是因为预训练权重已经足够好只需要微调步子太大就把之前学好的特征全部覆盖掉了。StepLR的step_size10和gamma0.5表示每10个epoch学习率减半这是图像分类任务里最简单也最不容易出错的调度方式换CosineAnnealing效果可能更好但参数更敏感。训练结束时保存的是state_dict后面转ONNX、做推理都要用到这个文件。3.4 训练过程的中间检查loss曲线与top-1准确率的正确看法训练跑起来后别急着等30个epoch跑完再看结果。每一两个epoch就要把验证集上的top-1准确率和loss打印出来我习惯在训练循环里再套一个验证循环每个epoch结束都算一次验证集指标。判断训练是否健康有三个信号要看。第一个信号是训练loss与验证loss的差。两者差距小说明模型还没过拟合差距拉大且训练loss继续下降说明模型开始死记硬背训练集了这时应该增强数据扰动、加大dropout或者提前停。第二个信号是阶段切换点的准确率跳变。从阶段1切到阶段2时因为分类头换成了全参数微调验证准确率通常会有一次抖动这是正常现象如果抖动超过10个百分点且回不来说明学习率给大了。第三个信号是每个类别的recall而不是整体的accuracy。水稻叶部病害数据不平衡整体accuracy会被大头类主导模型可能在稻瘟病上有95%的recall白叶枯病却只有30%。只看总体指标会让你误以为模型已经能用了。4. 评估与落地部署识别方法能不能用看这三个环节4.1 别只看accuracy用混淆矩阵揪出“稻瘟病 vs 胡麻斑病”这类高频混淆模型练完了第一件事不是欢呼准确率而是把混淆矩阵打出来看。水稻叶部病害的类间相似度非常高稻瘟病的慢性型病斑和胡麻斑病都是褐色带黄晕的椭圆斑区别只在病斑大小和边缘颜色饱和度这类组合在数据不足的情况下几乎必然互相“串味”。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay model.eval() all_preds [] all_labels [] with torch.no_grad(): for inputs, labels in val_loader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) # 类别顺序要和训练集的folder名一致 class_names [稻瘟病, 白叶枯病, 纹枯病, 胡麻斑病, 稻曲病, 其他损伤] cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(cm, display_labelsclass_names) plt.figure(figsize(8, 8)) disp.plot(cmapBlues, axplt.gca()) plt.savefig(confusion_matrix.png, dpi150)看混淆矩阵时有几个具体的观察点。第一主对角线上的数值是否每个类别都足够高如果有某一行几乎全部分散到其他列说明这个类别的特征没有被模型学会。第二非对角线上的最高值出现在哪两类之间这决定了你有没有必要增加针对性数据。第三背景类“其他损伤”是否频繁被预测成病害——如果频繁说明模型把“非病斑纹理”和“病斑”混淆了需要回去补充更多健康叶和虫咬叶样本。混淆矩阵分析完之后再决定是补充数据还是调整模型不要一上来就动网络结构。4.2 推理延迟与模型尺寸边缘设备上跑深度学习模型的选型依据识别方法要落地模型不能只在服务器上跑。农业场景里最常见的部署环境是农户拿手机拍照上传、服务端返回结果或者田间的摄像头节点做实时推理。前者延迟容忍度高一些后者对单帧推理时间有硬要求。这里有个参数选择上的现实逻辑——模型参数量和推理延迟不是一回事MobileNet参数量小但在某些边缘设备上没有针对性的算子和加速库支持实际延迟反而不如ResNet50配TensorRT优化后的表现。判断一个模型能不能上边缘设备看四个指标参数量、单帧推理时间、模型文件大小、内存占用。常见部署板上这个表格可以当参考具体数据会因为硬件平台不同而有出入但量级不会差太远模型参数量224x224推理延迟Jetson Nano, FP16是否适合边缘ResNet5025.6M约35ms适合MobileNetV3-Large5.4M约12ms适合EfficientNet-B419.3M约40ms一般需要量化VGG16138.4M约120ms不合适如果目标是在摄像头节点上做连续推理MobileNetV3经过量化后能跑到30fps基本满足实时监测。如果是“拍照上传”模式选准确率更高的模型更划算ResNet50的延迟完全在接受范围内。我一般会在项目立项时先问清楚部署形态再回来定模型顺序反了会导致返工。4.3 把PyTorch模型导出ONNX并离线校验不管是部署到服务端还是边缘设备把PyTorch训练好的模型转成ONNX都是必经之路。ONNX作为中间表示可以再转成TensorRT的engine、OpenVINO的IR格式或者直接用ONNX Runtime跑。这里有一个高频翻车点模型在PyTorch里推得好好的转成ONNX后输出对不上常见的原因是预处理不一致或动态维度没配置好。import torch import onnx import onnxruntime as ort from torchvision import models # 重新加载训练好的模型注意要加.eval() model models.resnet50(weightsNone) model.fc torch.nn.Linear(model.fc.in_features, 6) model.load_state_dict(torch.load(rice_disease_resnet50.pth)) model.eval() # 构造一个固定的输入尺寸转ONNX时这里定义输入维度 dummy_input torch.randn(1, 3, 320, 320) torch.onnx.export( model, dummy_input, rice_disease_model.onnx, opset_version12, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} ) # 用onnxruntime做离线校验对比两种框架的输出差异 ort_session ort.InferenceSession(rice_disease_model.onnx) test_tensor torch.randn(4, 3, 320, 320) with torch.no_grad(): torch_out model(test_tensor).numpy() ort_out ort_session.run([output], {input: test_tensor.numpy()})[0] diff (torch_out - ort_out).max() print(fPyTorch与ONNX输出最大差异: {diff:.6f})dynamic_axes这段配置很关键。如果你不声明batch维度为动态导出的模型就固定成batch size1部署时一旦凑不够刚好1张图就要做填充。实际推理时单张图最常见所以只把batch维度动态化就够了。opset_version选择12是保守选择新版ONNX Runtime都支持而太高的版本号反而可能在旧设备上缺失算子。跑完这段脚本后打印出的最大差异一般应该在1e-5这个量级如果差异达到1e-2先查预处理——训练时用Normalize、推理时忘了用同样的均值和标准差这是最典型的“训练好、部署歪”的原因。4.4 误判率与阈值调整识别方法进入实际应用的最后一公里模型输出的是每个类别的概率但“概率大于多少算确实属于这个病”是由阈值策略决定的这个决策直接影响实际使用体验。水稻叶部病害识别里漏诊和误诊的代价不一样漏诊稻瘟病意味着农户错过防治窗口期损失大误诊意味着农户喷了不该喷的药花冤枉钱。两种错误的代价不对称就不能简单把阈值定在0.5。我的习惯是针对每个类别单独定阈值——用测试集跑出每个类的概率分布然后逐一尝试阈值选一个让“重误代价最小”的点。如果重点是防漏诊就把该类的阈值压低比如0.35只要模型有35%的把握就报警如果重点是防误诊阈值拉到0.7以上宁可少报也不空报。类的阈值适合写进一个配置文件里部署时读入不用改代码。最后再留一个“不明确类别”的兜底输出概率都达不到阈值时返回“症状不明显建议上传高清图片或咨询当地植保站”这个交互设计比直接甩一个错误结论要讨喜得多。5. 水稻叶部病害识别常见问题与避坑从采集到部署的5个坑5.1 训练loss不降、验证集准确率长期纹丝不动现象训练跑了几百个steploss一直在2.0附近打转验证集准确率在40%以下纹丝不动。原因这种症状八成是学习率设置问题。水稻叶部病害识别用的是迁移学习很多人直接把整个模型的学习率设成1e-2甚至1e-1预训练权重被大步长冲击后迅速“失忆”。还有一种可能是模型输入尺寸不对比如预训练权重要求224输入你喂的是64x64缩略图网络在几乎无纹理可辨的图上自然学不到特征。解决把初始学习率降到1e-4量级阶段式的两段训练策略优先采用。检查输入分辨率是否在224以上病斑这种细粒度特征需要足够的像素支撑。如果还没改善打印几个batch的图片看一眼是不是预处理后图像发黑——Normalize的mean和std用错会导致输入分布整体偏移模型看到的图和训练时的截然不同。5.2 准确率虚高却下地失效现象实验室测试集上准确率95%拿到田里拍新图识别准确率掉到60%。原因最典型的成因是数据泄露。同一个植株的多角度照片被随机切分到了训练集和验证集等于考试时偷看了答案。另一个原因是训练数据“太干净”了都是中午顶光拍摄的典型病斑而实际农田里清晨的露水反光、正午的高光斑、傍晚的阴影睡在叶片上特征分布完全变了。解决按植株编号切分数据集而非按图片切分。训练图里刻意混入不同光照条件、不同拍摄距离、甚至有点虚焦的图让模型见过“脏场景”。如果条件允许用另一个完全独立采集的测试集做盲测这个测试集连你自己都不要反复去看。5.3 稻瘟病和胡麻斑病永远互相弄混现象混淆矩阵里稻瘟病的预测结果有30%落到了胡麻斑病上补充了数据也没有明显改善。原因这两种病害在早期症状确实高度相似但更底层的因素是标注本身的边界不清晰。不同标注员会把同一张病斑图一个标成稻瘟病、一个标成胡麻斑病模型学到的就是一套自相矛盾的标准怎么练都练不对。解决回到标注环节做“一致性复核”。把两类的边界案例单独挑出来请植保专家给出判定依据的明确描述落到标注规范里。如果专家也无法区分部分图片果断把这类图挪到“不确定/其他损伤”类不要硬塞进某一边。5.4 memory error显存不足导致训练中断现象训练进行到几个epoch后显存占用突然飙升报CUDA out of memory重启后重跑还是挂在同一个位置。原因最常见的是DataLoader的num_workers开太高每个worker多份数据预加载还有增强过程中有些操作会保存计算图导致显存峰值远高于单个batch的理论占用。训练中途跑到验证集阶段时显示存不足往往是验证集batch size没调、分辨率太大。解决先看模型本身占用的显存把batch size降到16并调低num_workers到2做测试性运行。检查图里是否有超大尺寸的原始图片没有resize就送入网络——ImageFolder默认不预处理尺寸如果某张图是4000x3000的原始照片那一张图就顶得上32张224图的内存。标准做法是给train和val的transforms都加上Resize上面2.3节的代码里已经体现。5.5 模型部署到手机上输出概率全是差不多的值现象ONNX模型集成到小程序后每次推理输出的概率都像0.2、0.3这样的平均分布没有哪个类别明显突出。原因这是在预处理环节把图片通道排列搞错了。PyTorch训练时用RGB顺序通过Normalize前的ToTensor决定而手机端用OpenCV读图默认是BGR顺序。BGR直接喂给训练时用RGB的模型颜色特征全乱了模型只能输出模糊的概率。解决在手机端或服务端做一次通道翻转cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。同时检查Normalize的mean和std是否和训练时一致有的部署框架要求输入是0到1的float格式和训练时的Tensor格式保持一致有的则是0到255的uint8这个不匹配也会让输出概率变得不可理喻。6. 用Grad-CAM验证深度学习模型到底在看哪一片叶子上面说了那么多参数和踩坑最后留一个我认为最值的验收入门动作用Grad-CAM把模型的注意力画出来亲眼看看它到底“看”的是病斑还是背景。这一步是“识别方法研究”和“识别方法可用”的分水岭——如果模型的注意力热力图集中在叶片边缘、水珠反光这些地方说明它学的不是病害特征而是场景伪迹。Grad-CAM的原理是利用最后一个卷积层的梯度算出特征图每个通道对最终类别判断的贡献权重再把加权后的特征图放大到输入图片尺寸。在PyTorch里实现一个最简版本只依赖torch自带的功能就能完成。import torch import cv2 import numpy as np # 假设model已加载输入一张tensor格式的测试图 def grad_cam(model, input_tensor, target_classNone): model.eval() # 注册hook拿最后一个卷积层的输出特征图 target_layer model.layer4[-1] feature_map None gradients None def forward_hook(module, input, output): nonlocal feature_map feature_map output.detach() def backward_hook(module, grad_input, grad_output): nonlocal gradients gradients grad_output[0].detach() forward_handle target_layer.register_forward_hook(forward_hook) backward_handle target_layer.register_full_backward_hook(backward_hook) output model(input_tensor.unsqueeze(0)) if target_class is None: target_class torch.argmax(output, dim1) model.zero_grad() one_hot torch.zeros_like(output) one_hot[0, target_class] 1 output.backward(gradientone_hot) # 对特征图按通道求权重并加权求和 weights torch.mean(gradients, dim(2, 3), keepdimTrue) cam torch.relu((weights * feature_map).sum(dim1, keepdimTrue)) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) forward_handle.remove() backward_handle.remove() return cam # 与原图做叠加可视化 img cv2.imread(test_leaf.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) cam grad_cam(model, test_tensor, target_classpred_class) cam_resized cv2.resize(cam, (img.shape[1], img.shape[0])) heatmap np.uint8(255 * cam_resized) heatmap cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) overlay cv2.addWeighted(heatmap, 0.5, cv2.cvtColor(img, cv2.COLOR_RGB2BGR), 0.5, 0) cv2.imwrite(gradcam_result.jpg, overlay)这个脚本的用法是找5到10张验证集里的典型病叶图每张都跑一遍Grad-CAM然后看图。我最在意三类图的结果。第一类正确预测的样本热力图应该落在病斑区域且只落在病斑区域。第二类错误预测的样本热力图一般会指向叶片边缘、叶尖枯黄或非病斑的褐色区域说明模型在靠“看起来像病”的整体颜色判断而不是靠病斑形态。第三类训练集中数量最多的类别它的热力图如果能集中在病灶说明类别不平衡还没把模型压垮如果热力图上颜色一片乱散说明模型学到的是统计规律而非诊断逻辑。做Grad-CAM这件事的成本很低每次训练完花半小时跑一遍能直接决定这个模型是调到部署环节还是回炉重造。我做这个方向有一个习惯每轮训练结束后第一件事不是看最终准确率而是选一张典型的稻瘟病图和一张错误的样本图把Grad-CAM热力图贴在训练日志第一页。有一次模型在测试集上看起来一切正常热力图却显示它把某张图的叶鞘高光区域当成了病斑——原始数据集里这类“高光病斑”组合图片太多模型学的其实是“亮斑”的特征。那次之后我把采集规范改成了“每个病斑必须在无直射光的条件下补拍一张”模型的假阳性明显回落。如果你正打算投入水稻叶部病害识别这个方向我建议你做完整套流程后认真留一份“模型注意力记录”——哪个类别热力图稳定、哪个类别热力图飘忽不定半年后回来翻会非常有用。另一种做法是把热力图作为答辩或项目验收的交付物效果比贴一张混淆矩阵更能说明模型质量。希望这个验证思路能帮到你也祝你的模型在下一茬稻子抽穗前顺利下地。本文还有配套的精品资源点击获取
返回列表