
简介这是一套面向农作物病虫害识别与检测任务的深度学习完整项目适合高校学生、毕设开发者以及从事智慧农业算法研究的入门到中级学习者。项目以卷积神经网络为核心覆盖数据预处理、特征提取、模型训练、评估和部署全流程内置基于VGG、ResNet、DenseNet等经典结构的实现方案并提供TensorFlow、PyTorch、Keras、Fastai等多种框架的Notebook示例。压缩包共61个文件包含Python代码、Jupyter Notebook、Markdown说明文档、图像样本及模型权重文件整体大小约88.41MB。目录结构清晰既有可直接运行的训练与预测脚本也包含本地Flask部署方案及AWS、GCP云端部署指南便于从原理理解到工程落地逐步推进。目前已有336人在CSDN学习使用能够在数据集处理、模型选型、训练调参、系统部署等环节提供完整参照。1. 当CNN遇上农作物病虫害这份资源能替你省掉多少事做农作物病虫害识别检测的毕设或小型落地项目很多人不是卡在算法原理上而是倒在环境配置、数据预处理和模型选型这三件事上——数据集只有几百张图、不知道TensorFlow和PyTorch该选哪个、训练完也不知道怎么部署给用户点开就用。这份基于深度学习卷积神经网络的农作物病虫害识别检测系统源码包把数据收集、预处理、CNN特征提取、模型训练评估到Flask网页部署的整条链路都跑通了。仓库里九个Notebook覆盖四种深度学习框架的完整实现VGG16、VGG19、ResNet50、DenseNet121四种骨干网络的横向对比也替你做好了还附带数据集和本地/云端部署指导。适合正在找毕设项目、或者想在农业图像分类场景里快速验证CNN真实效果的开发者。2. 拆开SYS.zip从文件结构看清CNN落地的完整链路2.1 九个Notebook里的框架矩阵选哪个当主力解压之后先别急着跑花十分钟把仓库根目录的Notebook文件过一遍你会发现这套资源最有价值的地方不在某一个模型而在它的对比设计。Plant_Disease_Detection_TensorFlow.ipynb、Plant_Detect_PyTorch.ipynb、Plant_Disease_Detection_Keras.ipynb、Plant_Disease_Detection_Fastai.ipynb分别是四个框架下的主流程实现Plant_Disease_VGG16.ipynb、Plant_Disease_VGG19.ipynb、Plant_Disease_RESNET50.ipynb、Plant_Disease_DenseNet121.ipynb则是同一份数据集上四种骨干网络的消融实验plant_disease_detector.ipynb是入门版的基础检测流程。Notebook框架骨干网络适合场景Plant_Disease_Detection_TensorFlowTensorFlow可替换生产部署、TF ServingPlant_Detect_PyTorchPyTorch可替换研究迭代、论文复现Plant_Disease_Detection_KerasKeras可替换快速验证、新手入门Plant_Disease_Detection_FastaiFastaiResNet系列少代码调参、迁移学习如果你做毕设我的建议是主力跑TensorFlow或PyTorch版本把VGG16和ResNet50的对比结果直接写进论文的实验章节——这两组数据本身就是很好的“为什么选这个网络”的论据。如果只是验证想法Keras版本代码量最少跑通最快。Fastai的优势在迁移学习封装几行就能拿到不错的效果但答辩时不好解释内部细节适合做辅助验证。提示仓库里notebook目录和根目录各有一批同名文件根目录的是最终整理版notebook下的多是中间版本跑代码优先看根目录。2.2 Flask服务端与模板目录训练之外的另一半链路模型训练完只能算是完成了一半这套资源的另一半亮点在app目录下的Flask应用。.github目录和根目录的文档可以先放一边重点看app下面四个东西server.py、models、views、static。server.py是服务端入口用Flask起一个HTTP服务接收前端上传的农作物叶片图片调用已训练好的模型做推理最后返回识别结果和置信度。views目录放页面渲染逻辑static放前端静态资源models目录放训练产出的权重文件。我第一次看这个结构时愣了一下——它把训练和推理分得清清楚楚训练在Notebook里做推理在Web应用里做中间通过保存的模型文件衔接。这对毕设的意义是你可以在论文里明确区分“离线训练阶段”和“在线推理阶段”答辩时逻辑很顺。local_flask目录是本地启动脚本和说明aws_deployment.md和gcp_deployment.md则给了云服务器部署的踩坑预演。这套结构把“训练-评估-部署”三件事解耦了你完全可以把Flask部分单独拎出来换成自己的模型权重改改类别标签就能复用到其他图像分类场景。2.3 requirements.txt 与部署配置这些文件才是复现的地基很多人拿到源码包第一件事就是装依赖装完直接报错然后骂资源不行。其实根目录的requirements.txt已经把坑标出来了——里面锁定了TensorFlow、Flask、NumPy、Pillow这些核心库的版本。复现跑不通的九成情况是版本不对TensorFlow 2.x和1.x的API差异很大Keras被合并进TensorFlow之后独立调用方式也变了。仓库里还带了Dockerfile和app.yaml。Dockerfile的意义在于把你的环境整个打包别人拉下来跑的时候不会因为Python版本、CUDA版本不一致而翻车。app.yaml类似地用于云平台部署时的资源配置。如果你在本地已经能跑通部署这块可以延后看如果你要交到别人手里跑我建议至少把Dockerfile用起来这是让人“拿到就能跑”的最省心路径。3. 数据预处理与迁移学习特征提取模型的胃口是你喂出来的3.1 从图像目录到网络输入尺寸、归一化与增强的标准三步农作物病虫害数据集通常是按类别分目录存放的比如train/Apple_Scab、train/Apple_Black_Rot这样。Keras的ImageDataGenerator可以直接从这种目录结构生成带标签的数据流省去手写数据加载器的麻烦。VGG和ResNet系列的标准输入尺寸是224×224这是预训练权重对输入图像的要求不能随意改小。from tensorflow.keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator( rescale1. / 255, # 像素值从0-255归一化到0-1 rotation_range20, # 随机旋转20度增加叶片朝向多样性 width_shift_range0.2, # 水平平移比例 height_shift_range0.2, # 垂直平移比例 shear_range0.2, # 剪切变换模拟叶片形变 zoom_range0.2, # 随机缩放模拟远近拍摄 horizontal_flipTrue, # 水平翻转 validation_split0.2 # 从训练集切出20%做验证集 ) train_generator train_datagen.flow_from_directory( dataset/train, target_size(224, 224), batch_size32, class_modecategorical, subsettraining ) val_generator train_datagen.flow_from_directory( dataset/train, target_size(224, 224), batch_size32, class_modecategorical, subsetvalidation )rescale1./255是把原始图像像素从0-255压到0-1区间这一步不做预训练网络的权重对输入分布会很敏感loss可能一开始就很大然后震荡。rotation_range、width_shift_range这些增强参数是为了让模型见到更多变的叶片姿态——田间拍摄的叶片不会像数据集里摆放得那么整齐。flow_from_directory会自动扫描子目录名生成类别标签class_modecategorical对应多分类任务的one-hot编码。注意训练集和验证集共用同一个ImageDataGenerator实例但验证集只做rescale不做增强。代码里train_datagen同时配了增强和验证集分割flow_from_directory里通过subset参数区分这是Keras的标准做法。3.2 用预训练CNN做特征提取为什么ResNet50/VGG16是作物病害首选病虫害识别本质上是一个细粒度图像分类任务——不同病害在叶片上的病斑颜色、纹理差异很细微从头训练一个CNN需要几十万张图才可能收敛。这套资源里所有骨干网络都走了迁移学习路线加载ImageNet预训练权重冻结卷积基只训练新增的全连接分类头。卷积基负责提取通用的边缘、纹理、形状特征这些特征在自然图像和叶片图像上是通用的分类头负责学习“什么样的特征组合对应什么病害”。from tensorflow.keras.applications import ResNet50 from tensorflow.keras.models import Model from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout base_model ResNet50( weightsimagenet, include_topFalse, # 去掉ImageNet自己的分类头 input_shape(224, 224, 3) ) base_model.trainable False # 冻结卷积基只训练分类头 x base_model.output x GlobalAveragePooling2D()(x) # 把7x7特征图压成2048维向量 x Dropout(0.5)(x) # 抑制全连接层过拟合 x Dense(256, activationrelu)(x) x Dropout(0.3)(x) predictions Dense(num_classes, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )include_topFalse是关键它把预训练网络最后的1000类分类器去掉只保留卷积基。GlobalAveragePooling2D比直接Flatten参数少得多——ResNet50的最后一层特征图是7×7×2048Flatten会得到10万个参数全局平均池化直接压成2048维过拟合风险小一个量级。两个Dropout层配合0.5和0.3的失活率是应对小数据集过拟合的常用手段。为什么优先推ResNet50而不是VGG16VGG16结构简单直观答辩时好讲但参数量大、训练慢ResNet50靠残差连接在深度和效率之间取得平衡实际收敛更快。两者在仓库里都有现成Notebook建议都跑一遍再决定主模型。3.3 数据集的多样性陷阱类平衡与光照背景摘要里强调“数据集的多样性和代表性”这句话放到实操里就是两个具体问题。第一是类平衡如果苹果黑腐病图片有2000张、健康叶片只有200张模型会倾向于把模糊图片都判成黑腐病因为这样整体准确率更高。处理办法是给少数类增加增强强度或对多数类做欠采样。第二是背景干扰早期训练集里叶片全是黑背景棚拍拿田间复杂背景的图去测准确率掉得一塌糊涂。这是模型学到了“黑背景→某种病害”的伪相关不是真的在学病斑纹理。数据收集阶段尽量包含不同光照、不同拍摄角度、不同生育期的叶片训练时增强里的zoom_range和rotation_range就是在模拟这些变化。如果数据实在不平衡可以在flow_from_directory里通过class_weight参数给少数类更高的损失权重Keras和TensorFlow的训练接口都支持。4. 模型训练与评估从损失曲线到指标矩阵4.1 冻结卷积基只训练分类头TensorFlow训练主循环迁移学习训练一般分两个阶段。第一阶段冻结卷积基只训练全连接分类头学习率用1e-4量级的Adam优化器第二阶段解冻部分深层卷积层做微调学习率降到1e-5。这套资源的Notebook里跑的是第一阶段为主适合数据集在几千张量级的情况。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau callbacks [ EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), ModelCheckpoint( models/best_plant_disease.h5, monitorval_accuracy, save_best_onlyTrue ), ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-6 ) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // 32, epochs50, validation_dataval_generator, validation_stepsval_generator.samples // 32, callbackscallbacks )EarlyStopping监听验证集loss连续5个epoch没改善就停restore_best_weightsTrue会把权重回滚到验证集表现最好的状态——这是防止过拟合的最后一道防线。ReduceLROnPlateau在loss停滞时自动把学习率减半比手动调参省心得多。ModelCheckpoint只保存验证集准确率最高的权重训练结束之后直接用这个文件做部署不需要保留最后一次epoch的权重。steps_per_epoch用samples // 32算的是每个epoch的批次数32对应batch_size显存够的话调到64能加快训练但小数据集上32更稳。第二阶段微调的常见做法是解冻ResNet50的后几十层把base_model.trainable设为True然后用1e-5的学习率重新编译训练。这一步对小数据集是双刃剑——调得好能涨三五个点调不好直接过拟合。我一般只在第一阶段结果满意、验证集准确率到达平台期时才解冻并且依然开着EarlyStopping。4.2 评估不止准确率精确率、召回率与混淆矩阵准确率在多分类且类别不平衡的场景里会骗人。比如某个病害类别占数据集40%模型全部预测成这个类准确率也能有40%。病虫害识别的实际诉求是得病的叶片不能被漏掉——漏掉一片染病叶片意味着整片果园可能被传染这对应召回率报错病害类型会浪费农药这对应精确率。from sklearn.metrics import classification_report, confusion_matrix import numpy as np val_generator.reset() pred_probs model.predict( val_generator, stepsval_generator.samples // val_generator.batch_size ) pred_labels np.argmax(pred_probs, axis1) true_labels val_generator.classes[:len(pred_labels)] print(classification_report( true_labels, pred_labels, target_nameslist(val_generator.class_indices.keys()) ))classification_report会输出每个类别的精确率、召回率和F1分数。用这份资源做毕设这张表可以直接进论文的实验部分。val_generator.reset()必须在预测前调用否则数据流指针不在起始位置预测的标签顺序会和真实标签对不上代码里很容易忽略这一点。如果某个病害类别的召回率明显低于其他类大概率是这个类样本太少回头用class_weight重训。混淆矩阵比准确率直观得多——它能看到模型具体把哪两类病害搞混了。苹果黑腐病和苹果疮痂病在早期症状上很像混淆矩阵里这两个类互相错分是正常现象但如果叶斑病和健康叶片大量混淆说明模型在学的特征不对需要回头看预处理或增强参数。4.3 训练曲线检查清单什么信号该停手把history.history里的loss和val_loss画出来能读出训练的全部秘密。训练loss持续下降、验证loss先降后升这是过拟合的经典信号说明模型开始在死记训练集了。训练loss和验证loss同时高位震荡不下降说明学习率太大或预处理有问题。两个loss同步下降后进入平台期这是正常状态此时要么等ReduceLROnPlateau触发要么进入微调阶段。5. 训练与部署的避坑清单六条血泪经验5.1 训练loss接近0验证集准确率却只有60%现象训练集准确率能到99%损失函数值降到0.1以下但验证集准确率始终在60%上下徘徊每个epoch的验证结果波动很大。原因这是典型过拟合。模型用几万甚至几百万参数去记几千张训练图把每张图的噪声和背景细节都背下来了压根没学到病害的通用纹理特征。数据增强强度不够、Dropout偏低是主因。解决把rotation_range调到30以上加入zoom_range0.3Dropout从0.3提到0.5。如果还不行换更小的骨干网络比如VGG16参数量少本身就带正则效果。最狠的一招是解冻后只微调最后10层前面的层保持冻结限制模型的拟合能力这套资源里ResNet50的Notebook就是按这个思路调的。5.2 换了机器No module named tensorflow现象在自己电脑上跑得好好的换到同学或服务器上第一行import tensorflow直接报错或者报了AttributeError: module tensorflow has no attribute keras。原因对方环境里TensorFlow没装或版本不对。TensorFlow 1.x和2.x的API差异很大手动pip install tensorflow默认装的是当前最新版跟源码包的依赖对不上。这个源码包里keras和tensorflow.keras混用了版本错一个就出问题。解决直接用仓库根目录里的requirements.txt装装之前先看第一行锁的版本号。如果对方机器没有GPU就把tensorflow换成tensorflow-cpu装对应版本内存小也不用怕这个小数据集CPU训练也只是慢不会崩。我之前遇到一个更隐蔽的坑——本机装了tensorflow和keras两个库代码里有的地方写import keras有的写from tensorflow.keras这两个是不同的包权重保存和加载会串务必统一成tensorflow.keras。5.3 部署后预测结果全是噪点准确率比训练时掉20个点现象训练阶段验证集准确率85%Flask服务跑起来后上传单张图片预测结果和训练时完全不符输出概率分布近乎均匀或者每次都是同一个类。原因推理时的图像预处理和训练时不一致。训练时光用了rescale1./255归一化到0-1区间部署代码里如果直接用img_to_array加载原始0-255像素喂给模型输入分布直接偏移卷积特征全部乱套。也可能忘了把输入图缩放到224×224。解决部署代码里必须复刻训练时的预处理管线——load_img时指定target_size(224, 224)img_to_array后手动除以255.0。如果训练时用了preprocessing_function做均值归一化部署代码也得把均值减掉。最简单的办法是把预处理封装成一个函数训练和部署两端共用同一份代码彻底消除偏差。5.4 模型把所有图片都预测成同一个类别现象混淆矩阵里某一类的预测数量异常多其他类几乎为零classification_report里那个类的精确率很高但其他类全是0。原因训练集分类别数量差距过大模型学到的决策边界整体偏向样本量大的类。这在植物病害数据集里非常普遍——某种常见病害好收集几十个标签全是它稀有病害只有一两百张图。解决两招一起用。第一用class_weight给少数类加权{0: 1.0, 1: 2.5}这样让少数类每个样本的loss贡献更大。第二增强对少数类的数据增强强度——单独为那个类目录配置更强的rotation_range和zoom_range相当于把少量样本复制出多种姿态。跑完看混淆矩阵的每行召回率确认每个类都被拉到正常水平。5.5 微调把ResNet50解冻后loss震荡到原来的三倍现象第一阶段的分类头训练已经拿到85%验证准确率解冻卷积基开始微调第一个epoch的loss直接涨到之前的三倍训练曲线剧烈震荡最后停在比冻结时还低的水平。原因把整个卷积基解冻后分类头之前学到的特征分布被底层参数的剧烈更新冲垮了。Adam的默认学习率1e-4对全网络微调来说太大底层卷积层的梯度虽然小但叠加起来足以破坏预训练权重。解决解冻时把编译参数里的学习率从1e-4降到1e-5并且只解冻靠后的深层块——ResNet50解冻从conv5_block开始即可浅层保留预训练权重。先以1e-5跑5个epoch观察验证loss没下降再考虑逐层扩大解冻范围。这个坑我从第一次微调就踩过后来形成的习惯是凡是要动预训练权重学习率一律先打三折起步。5.6 Flask并发请求直接卡死现象本地单用户上传图片预测一切正常多人同时访问网页服务时请求排队时间越来越长最后直接超时。原因Flask自带的开发服务器是单线程的app.run()默认只能串行处理请求。TensorFlow模型推理本身就占CPU或GPU资源一个推理请求耗时几百毫秒到几秒并发一来就全卡住。解决本地演示不用管真要给多人用时切到gunicorn或waitress这类生产级WSGI服务器多worker进程分担请求量。模型加载放在模块顶层而不是预测函数内部避免每次请求都重新读一次几百MB的权重文件。我见过有人在predict函数里用load_model每点一次上传就加载一遍模型卡到怀疑人生——加载一次模型的时间可能比推理还长。注意以上几条覆盖了训练、复现、部署三类场景。如果你跑的是PyTorch版本第5.2条换成torch版本不一致的排查思路其他几条逻辑完全一致因为问题本质不在框架而在数据集和算法流程。6. 部署验证与进阶技巧用Grad-CAM确认模型在学病斑而不是背景把训练好的模型封装成Flask推理接口代码并不复杂核心是把图像预处理和推理逻辑对齐。下面这段是我从这套资源的server.py思路里提取的最小实现能直接跑通单张图片的上传-预测-返回JSON链路。from flask import Flask, request, jsonify from tensorflow.keras.models import load_model from tensorflow.keras.preprocessing.image import img_to_array, load_img import numpy as np app Flask(__name__) model load_model(models/best_plant_disease.h5) # 类别顺序来自训练时的 flow_from_directory 目录名 class_names [Apple_Black_Rot, Apple_Healthy, Apple_Scab] def preprocess(image_path): img load_img(image_path, target_size(224, 224)) arr img_to_array(img) / 255.0 return np.expand_dims(arr, axis0) app.route(/predict, methods[POST]) def predict(): f request.files[image] f.save(tmp_upload.jpg) pred model.predict(preprocess(tmp_upload.jpg)) idx int(np.argmax(pred[0])) return jsonify({ label: class_names[idx], confidence: round(float(pred[0][idx]), 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)model.load_model加载的是训练时ModelCheckpoint保存的h5文件必须保证训练代码里保存的模型结构含自定义层定义。class_names的顺序必须和训练时flow_from_directory扫描到的目录名顺序一致否则预测结果标签会错位。debugFalse在生产模式下必须关开着会让服务器暴露给客户端交互式调试界面这是安全大忌。模型部署完我强烈建议做一次Grad-CAM可视化验证——这是毕设答辩时最有说服力的一张图。Grad-CAM通过计算最后一层卷积特征图对目标类别分数的梯度生成一个热力图叠加在原图上能直观看到模型依据什么区域作出判断。import tensorflow as tf def grad_cam(model, img_array, last_conv_layer_name): grad_model tf.keras.models.Model( inputsmodel.input, outputs[model.get_layer(last_conv_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_output, preds grad_model(img_array) class_idx tf.argmax(preds[0]) loss preds[:, class_idx] grads tape.gradient(loss, conv_output) pooled_grads tf.reduce_mean(grads, axis(0, 1, 2)) heatmap tf.reduce_sum(tf.multiply(pooled_grads, conv_output[0]), axis-1) heatmap tf.maximum(heatmap, 0) / tf.reduce_max(heatmap) return heatmap.numpy()运行后把热力图和原图叠加如果高亮区域集中在叶片病斑上说明模型学对了如果高亮区域在叶片边缘或背景土块上说明前面数据预处理出了问题——模型在走捷径。我做过一个番茄病害项目Grad-CAM一照热力图集中在叶片反光点模型实际学的不是病斑而是光泽特征后来加了光照增强和灰度扰动才纠正过来。从那以后我每次训练完都会强制走一遍Grad-CAM验证这个环节分类准确率再高也不跳过——希望这个习惯对你也有用。本文还有配套的精品资源点击获取