ARTICLE DETAIL

资讯详情

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

机器学习实战:农作物病虫害识别系统从特征工程到模型部署

机器学习实战:农作物病虫害识别系统从特征工程到模型部署 简介这是一套面向高校毕业设计、期末大作业及课程设计场景的农作物病虫害识别系统完整项目基于Python与机器学习实现。项目完整覆盖图像数据收集与预处理、病虫害标签标注、颜色纹理形状特征提取、CNN/SVM模型训练与评估调优、实时识别页面部署等环节并配有可直接运行的Web前端界面代码含详细注释非常适合希望短时间产出高分开题或答辩演示的初学者。压缩包共477个文件大小约81.98MB核心包含Python源码、训练好的模型权重、SQLite数据库、HTML/JS/CSS前端页面以及示例图片和说明文档目录结构清晰简单配置即可本地部署。目前已有662人学习下载。借助这份项目可从零理解农作物病虫害识别系统的完整链路同时获得可复用的项目框架、数据集组织方式与调优思路有效支撑毕业设计、期末大作业等场景的高质量输出。1. 从“这也能识别”到“毕业设计拿 98 分”一个朴素的机器学习实现思路我拆过不少带“人工智能”名头的课设项目大部分是套个 Flask 壳、调一遍 EfficientNet 的预训练权重就交差。但这个基于机器学习实现的农作物病虫害识别系统不太一样——它没有一上来就上深度学习那一套而是老老实实走完“数据标注→特征提取→分类器训练”的完整链路所以拿到导师的 98 分并不意外。整份资源包含源码、数据集和注释文档适合两类人一是期末大作业、课程设计需要从头到尾讲清楚原理的在校生二是想把手头图像分类任务从“调库”升级为“能调参、能排错”的工程师。下面我从数据处理、特征工程、模型训练到部署排查把每一步怎么落地说清楚。2. 数据预处理与特征提取决定识别上限的 80% 工作很多人在交作业时才意识到模型准确率低不是因为网络不够深而是喂进去的数据太“脏”。这个系统里最花时间的部分恰恰不在模型而在数据。2.1 图像读取、尺寸归一化与噪声抑制原始数据集大多来自田间拍摄和联网爬取分辨率从几百像素到几千像素不等直接用原图训练会导致 batch 内张量形状不一致。常见做法是先统一到一个固定尺寸我用的是 224×224兼顾了细节保留和训练速度。OpenCV 读图后先转 RGB再执行归一化再缩放import cv2 import numpy as np def load_and_preprocess(image_path, target_size(224, 224)): # 读取时直接转 RGB避免 BGR 通道顺序导致模型学错颜色特征 img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 去噪使用高斯滤波核大小 3x3防止把叶片表面的灰尘当作病斑特征 img cv2.GaussianBlur(img, (3, 3), 0) # 缩放INTER_AREA 适合缩小比默认的线性插值保留更多边缘纹理 img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) # 像素值归一化到 [0,1]SVM 等分类器对特征尺度非常敏感 img img.astype(np.float32) / 255.0 return img这里有个关键参数容易被忽略INTER_AREA和cv2.INTER_LINEAR的选择。INTER_AREA在缩小时会做区域像素平均相当于一种低通滤波能抑制高频噪声而线性插值会保留更多纹理细节。对于叶片这类纹理密集的图像前者更不容易让模型过拟合到噪点上。另外归一化必须用float32如果保持uint8后续传入分类器时梯度计算会溢出。2.2 颜色矩、纹理特征与形状因子的联合表达第 2 章说清楚特征提取层面的设计这个系统能在“机器学习”语境下拿高分关键不是用了多深的网络而是用了多合适的特征。项目里同时提取了三类特征组合成特征向量后才交给分类器颜色矩每个通道的一阶矩均值、二阶矩标准差、三阶矩偏度共 9 维。病斑区域的颜色分布和健康叶片有明显差异。纹理特征基于灰度共生矩阵GLCM的对比度、能量、相关性、同质性。skimage.feature.graycomatrix可以直接算但要注意distances参数经验值是取 1 和 3 两个距离的平均。形状特征病斑轮廓的面积、周长、圆形度、凸包面积比。用cv2.findContours提取轮廓后再计算。from skimage.feature import graycomatrix, graycoprops import numpy as np def extract_features(img_rgb): # 灰度化后计算 GLCM距离 1、角度 0/45/90/135 四个方向 gray cv2.cvtColor((img_rgb * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY) glcm graycomatrix(gray, distances[1, 3], angles[0, np.pi/4, np.pi/2, 3*np.pi/4], levels256, symmetricTrue, normedTrue) contrast graycoprops(glcm, contrast).mean() energy graycoprops(glcm, energy).mean() correlation graycoprops(glcm, correlation).mean() homogeneity graycoprops(glcm, homogeneity).mean() # 颜色矩 features [] for channel in range(3): ch img_rgb[:, :, channel] features.append(np.mean(ch)) features.append(np.std(ch)) features.append(np.mean(((ch - np.mean(ch)) ** 3))) features.extend([contrast, energy, correlation, homogeneity]) return np.array(features)值得说的是levels256这个参数。如果图像是 8bit 灰度图256 是完整动态范围但如果你的数据集在预处理阶段做了对比度拉伸纹理计算会集中在部分灰度级上这时可以降为 64 或 128不仅能提速还能减少稀疏矩阵带来的噪声。特征维度本身不高但组合后 SVM 在 3000 张图上训练也就几分钟这在答辩时可以现场演示比直接跑 CNN 更有说服力。3. 模型选型与训练CNN、SVM 和朴素 Baseline 的取舍这章是整个系统的核心也是我和身边做类似项目的人反复争论过的地方。识别系统不是非得上深度学习关键在于你的指标要求和部署环境。3.1 为什么用迁移学习而不是从零训练 CNN数据集的规模通常只有几千张从零训练 ResNet 这类深层网络几乎必然过拟合。常见做法是加载在 ImageNet 上预训练过的权重冻结前面若干层只训练最后几层全连接。这种方式工程上叫“特征提取器 微调分类头”。我在这个项目里用 ResNet50 作为 backbone去掉最后的 1000 类输出层替换成 3 个全连接层2048 → 512 → 类别数。这里有一个决定性的训练策略——先冻结所有卷积层只训练新加的全连接层等 loss 不再下降后再解冻最后两个 res_block 的所有层用更低的学习率做微调。import torch import torch.nn as nn from torchvision import models class CropDiseaseClassifier(nn.Module): def __init__(self, num_classes10): super().__init__() # 使用 ResNet50 预训练权重作为特征提取器 self.backbone models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 替换最后一层全连接适配自己的类别数 self.backbone.fc nn.Sequential( nn.Linear(2048, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), # 防止全连接层过拟合 nn.Linear(512, num_classes) ) def forward(self, x): return self.backbone(x) # 冻结 backbone 所有参数 model CropDiseaseClassifier(num_classes10) for name, param in model.named_parameters(): if backbone in name: param.requires_grad False这段代码里最值得留意的是冻结策略requires_gradFalse意味着反向传播时这些层不会更新梯度不仅省显存还保留了 ImageNet 学到的通用边缘/纹理特征。Dropout 放在 512 维全连接之前0.5 的失活率对几千张图的数据集来说刚好太高会欠拟合太低防不住全连接层过拟合。3.2 支持向量机做对照实验为什么它也能打答辩时导师一定会问“你为什么不用传统方法做一个对比”这个项目的聪明之处在于同时训练了一个 RBF 核 SVM用的正是第 2 章提取的手工特征。这不只是凑字数而是证明你对分类器选择有自己的判断。from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # X_features 是每张图提取的 13 维特征向量y_labels 是类别标签 X_train, X_test, y_train, y_test train_test_split( X_features, y_labels, test_size0.2, stratifyy_labels, random_state42 ) # C 是误分类惩罚系数gamma 控制 RBF 核的影响半径 svm_model SVC(kernelrbf, C10.0, gamma0.01, probabilityTrue) svm_model.fit(X_train, y_train) y_pred svm_model.predict(X_test) print(classification_report(y_test, y_pred))gamma0.01这个值不是随便拍的。gamma 越小决策边界越平滑不容易过拟合如果默认用scale在特征维度差异大的时候会出现某些特征主导核函数计算的情况。C 取 10 是为了配合小数据集——C 太大比如 100时对训练集噪声点过于敏感太小则欠拟合。SVM 在这种低维度特征上往往能达到 85% 以上的准确率和 ResNet 微调结果差距不大但训练速度快两个数量级而且模型文件只有几百 KB非常适合部署到没有 GPU 的机器上。3.3 数据增强防止“同一株叶片换个角度就认不出来”真实场景里用户拍的照片不可能每次都居中、正对、光线均匀。系统里我做了在线数据增强而不是离线扩充因为离线扩充会让每个 epoch 见到的都是同一批变换后的静态图片模型容易记住变换模式。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop的scale(0.7, 1.0)保证了裁剪面积不会太小避免模型学到“只看局部纹理”就下结论的捷径。ColorJitter三个参数的扰动幅度也是有讲究的叶片病害的判别依据之一是病斑颜色如果一个样本的色相被大幅度改变反而会误导模型。所以 saturation 的扰动我刻意控制在 0.2 以内。归一化用的 mean 和 std 是 ImageNet 的统计值千万不要改成(0.5, 0.5, 0.5)那种简易版本否则预训练模型的权重在特征分布上会对不上。4. 模型评估与调优不只看 accuracy还要知道它为什么错一个常见的误区是只看测试集整体准确率。农业场景下漏掉一种病害的代价远高于误报另一种病害所以这个项目的评估环节用到了混淆矩阵、精确率/召回率曲线和分错误样本的可视化分析。4.1 混淆矩阵定位“易混淆对”代码里通过sklearn.metrics.confusion_matrix输出混淆矩阵并正则化成按行归一化的比例形式方便看出模型把哪两类病害搞混。比如“稻瘟病”和“稻胡麻斑病”两者的病斑都是褐色小点如果混淆矩阵里这两类的交叉项特别高说明特征提取阶段丢失了纹理细节——GLCM 的对比度可能不够敏感需要增大对比度拉伸或者增加 Gabor 滤波器组。4.2 学习率与早停策略让训练过程不折腾训练时的最常见错误是 loss 震荡不收敛或者 val_loss 在某个 epoch 后开始回升。项目里我设置了余弦退火学习率调度器并配了早停机制连续 8 个 epoch val_loss 不下降就停止训练同时保存最优权重而非最后一轮权重。# 训练命令示例指定 backbone 解冻层数、初始学习率、批次大小 python train.py --backbone resnet50 --unfreeze_layers 2 --lr 1e-4 --batch_size 32 --epochs 50 --early_stop 8这里解释两个关键参数--unfreeze_layers 2表示解冻最后两个 res_block也就是layer4和layer3它们学到的语义特征和病害类别更相关--lr 1e-4是微调阶段的上限如果解冻层数更多学习率需要降到 5e-5否则预训练权重会被破坏。早停的 patience 设为 8 是因为图像增强带来的随机性会让 val_loss 有小幅震荡设得太小比如 3会提前终止训练。4.3 类别不平衡的权重修正真实数据集中“健康叶片”类别的样本数量往往是“某某病害”的数倍直接训练会让模型倾向于把不确定的叶子判为健康。常用的处理方式是在损失函数里按类别频率的倒数加权import torch.nn.functional as F def weighted_cross_entropy(logits, targets, class_counts): # 权重与样本数成反比防止多数类主导梯度 total sum(class_counts.values()) weights torch.tensor([total / class_counts[i] for i in range(len(class_counts))], dtypetorch.float32) weights weights / weights.mean() # 归一化到均值 1保持 loss 尺度稳定 return F.cross_entropy(logits, targets, weightweights.to(logits.device))在实践中有个细节权重归一化到均值 1 很重要。如果不做这一步整体 loss 会被放大数倍导致原本合适的学习率失效。另外total / class_counts[i]是反频率加权的经典写法没有用sqrt或者其他平滑形式因为这里的类别数量差距通常在 3 倍以内不需要过度平滑。5. 部署为新模型上线做一个带日志的批次推理脚本最后一步不是把所有图像塞进模型就完事。实际使用时可能是上传到 Web 系统也可能是本地跑批处理。这个项目里我把推理部分封装成了一个独立模块并且加上了耗时统计和结果缓存方便后续扩展成 Flask/Gradio 服务。import torch import time import json class InferenceEngine: def __init__(self, weights_path, class_names, devicecuda): self.device torch.device(device if torch.cuda.is_available() else cpu) self.class_names class_names self.model CropDiseaseClassifier(num_classeslen(class_names)) self.model.load_state_dict(torch.load(weights_path, map_locationself.device)) self.model.to(self.device) self.model.eval() # 关键一步切换成推理模式关闭Dropout等 self.cache {} # 简单结果缓存避免同一张图重复推理 def predict_batch(self, image_paths): results [] start time.time() for path in image_paths: if path in self.cache: results.append(self.cache[path]) continue img load_and_preprocess(path) img_tensor torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).to(self.device) with torch.no_grad(): logits self.model(img_tensor) prob torch.softmax(logits, dim1).squeeze(0) pred_idx torch.argmax(prob).item() conf prob[pred_idx].item() res {file: path, pred: self.class_names[pred_idx], conf: round(conf, 4)} self.cache[path] res results.append(res) elapsed time.time() - start print(fTotal {len(image_paths)} images in {elapsed:.2f}s, avg {elapsed/len(image_paths)*1000:.1f}ms/img) return results # 使用示例 engine InferenceEngine(model_best.pth, [健康, 稻瘟病, 胡麻斑病]) engine.predict_batch([leaf1.jpg, leaf2.jpg])关于推理优化有几个非常实际的做法设torch.no_grad()是必须的否则每个样本都会构建计算图、浪费显存和 CPUmap_location参数必须在load_state_dict里写清楚否则在只有 CPU 的机器上加载 GPU 权重会直接崩缓存字典对大文件夹批处理很有用配合后续接入 Web 接口同一张图重复提交时能省一次推理。如果想进一步提速可以导出为 TorchScript 或者 ONNX这时候推理速度从每张 50ms 降到 20ms 不是问题而且不需要依赖 PyTorch 环境。一旦发现某张图的置信度低于 0.6把原图和预测结果单独抽取出来按病害类别归档作为后续增量训练的候选数据——这就是一个能持续改良的识别系统。本文还有配套的精品资源点击获取
返回列表