習(xí)的農(nóng)作物病蟲(chóng)害識(shí)別畢設(shè)實(shí)戰(zhàn):從數(shù)據(jù)到部署)
簡(jiǎn)介這份資源是面向計(jì)算機(jī)相關(guān)專(zhuān)業(yè)畢業(yè)設(shè)計(jì)學(xué)生與深度學(xué)習(xí)實(shí)戰(zhàn)學(xué)習(xí)者的農(nóng)作物病蟲(chóng)害識(shí)別項(xiàng)目基于Python與人工智能技術(shù)實(shí)現(xiàn)難度適中適合作為畢設(shè)選題參考或項(xiàng)目練手。壓縮包共281個(gè)文件約522.67MB包含14個(gè)py源碼文件、8個(gè)vue與7個(gè)js前端文件、107個(gè)txt與3個(gè)md說(shuō)明文檔、83張jpg與34張png圖像樣本以及6個(gè)pdf、3個(gè)docx論文與技術(shù)路線文檔、3個(gè)caj參考文獻(xiàn)和json、html等配置資源覆蓋從模型訓(xùn)練到系統(tǒng)展示的完整鏈路。項(xiàng)目經(jīng)導(dǎo)師指導(dǎo)并通過(guò)評(píng)審源碼均本地編譯調(diào)試可運(yùn)行已有236人學(xué)習(xí)下載。讀者可獲得可復(fù)現(xiàn)的深度學(xué)習(xí)識(shí)別方案、論文寫(xiě)作素材、前后端工程結(jié)構(gòu)與調(diào)試排錯(cuò)思路便于快速搭建實(shí)驗(yàn)環(huán)境并完成畢設(shè)或課程實(shí)踐。1. 從一份高分畢設(shè)看農(nóng)作物病蟲(chóng)害識(shí)別到底難在哪很多同學(xué)做畢設(shè)時(shí)第一反應(yīng)是找一份Python人工智能基于深度學(xué)習(xí)的農(nóng)作物病蟲(chóng)害識(shí)別項(xiàng)目源碼論文直接跑通交差。但真正動(dòng)手后會(huì)發(fā)現(xiàn)公開(kāi)數(shù)據(jù)集里葉片背景干凈、光照均勻模型輕松跑到 98% 準(zhǔn)確率一旦換成自己用手機(jī)在田間拍的圖準(zhǔn)確率直接掉到六成以下。這不是模型不行而是數(shù)據(jù)分布、標(biāo)注質(zhì)量和推理部署三個(gè)環(huán)節(jié)里至少有一個(gè)沒(méi)對(duì)齊。這個(gè)方向適合兩類(lèi)人一類(lèi)是需要一份能講清楚原理、又能復(fù)現(xiàn)的畢設(shè)或大作業(yè)另一類(lèi)是想把識(shí)別能力真正落到田間地頭、做病蟲(chóng)害預(yù)警的工程同學(xué)。接下來(lái)我按自己帶過(guò)幾屆畢設(shè)的實(shí)操路徑把數(shù)據(jù)、模型、訓(xùn)練、部署和論文寫(xiě)作串成一條能走通的線重點(diǎn)講清楚每一步的參數(shù)怎么設(shè)、坑在哪。2. 數(shù)據(jù)集怎么選、怎么標(biāo)、怎么擴(kuò)從 PlantVillage 到田間實(shí)拍2.1 公開(kāi)數(shù)據(jù)集與自建數(shù)據(jù)集的取舍做農(nóng)作物病蟲(chóng)害識(shí)別繞不開(kāi) PlantVillage 這個(gè)數(shù)據(jù)集。它包含 14 種作物、26 種病害共 5 萬(wàn)多張葉片圖像背景基本是純色標(biāo)注質(zhì)量高適合作為 baseline 快速驗(yàn)證模型結(jié)構(gòu)。但它的致命問(wèn)題是太干凈——真實(shí)田間有土壤、雜草、陰影、蟲(chóng)糞、水滴還有多片葉子重疊。如果論文只跑 PlantVillage答辯時(shí)老師一問(wèn)田間泛化能力如何就容易卡住。我的做法是用 PlantVillage 做預(yù)訓(xùn)練和結(jié)構(gòu)驗(yàn)證再自建一個(gè)小規(guī)模田間數(shù)據(jù)集做微調(diào)與測(cè)試。自建數(shù)據(jù)不需要多每種病害 200300 張就夠關(guān)鍵是覆蓋不同光照順光、逆光、陰天、不同角度俯拍、側(cè)拍、不同生育期苗期、成株期。采集時(shí)用手機(jī)即可但要注意關(guān)閉美顏和 HDR避免顏色失真。提示自建數(shù)據(jù)集的劃分要按田塊而不是按圖片隨機(jī)分。同一片葉子的多張照片如果被分到訓(xùn)練集和測(cè)試集測(cè)試準(zhǔn)確率會(huì)虛高這是畢設(shè)里最常見(jiàn)的翻車(chē)點(diǎn)。2.2 標(biāo)注規(guī)范與格式轉(zhuǎn)換標(biāo)注工具推薦 LabelImg 或 Labelme。分類(lèi)任務(wù)只需按文件夾分好類(lèi)別如果要做病害區(qū)域定位就用邊界框標(biāo)注。標(biāo)注時(shí)統(tǒng)一用英文類(lèi)別名避免中文路徑在后續(xù)訓(xùn)練腳本里出現(xiàn)編碼問(wèn)題。類(lèi)別命名建議采用作物_病害格式例如tomato_early_blight、corn_rust這樣多作物混合訓(xùn)練時(shí)不會(huì)混淆。如果拿到的是 VOC 格式的 XML 標(biāo)注轉(zhuǎn)成 YOLO 需要的 txt 格式可以用下面這段腳本。它遍歷Annotations目錄讀取每個(gè) XML 里的目標(biāo)框歸一化后寫(xiě)入對(duì)應(yīng)的labels文件。import os import xml.etree.ElementTree as ET # 類(lèi)別映射順序必須與訓(xùn)練時(shí)的 data.yaml 一致 classes [tomato_early_blight, tomato_late_blight, corn_rust] def convert_voc_to_yolo(anno_dir, label_dir, img_w, img_h): os.makedirs(label_dir, exist_okTrue) for xml_file in os.listdir(anno_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(anno_dir, xml_file)) root tree.getroot() # 用文件名不含擴(kuò)展名作為標(biāo)簽文件名與圖片一一對(duì)應(yīng) txt_name os.path.splitext(xml_file)[0] .txt with open(os.path.join(label_dir, txt_name), w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # YOLO 格式中心點(diǎn) x,y 和寬高全部歸一化到 0-1 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) # 假設(shè)圖片統(tǒng)一為 640x640實(shí)際使用時(shí)應(yīng)讀取每張圖的真實(shí)尺寸 convert_voc_to_yolo(Annotations, labels, 640, 640)這段腳本的關(guān)鍵參數(shù)是classes列表它的順序決定了標(biāo)簽文件里cls_id的含義必須和后續(xù)訓(xùn)練配置文件里的names完全一致否則模型會(huì)把早疫病學(xué)成銹病。另外img_w和img_h如果所有圖片尺寸不統(tǒng)一需要先用 PIL 讀取每張圖的真實(shí)寬高不能寫(xiě)死。2.3 數(shù)據(jù)增強(qiáng)別只會(huì)旋轉(zhuǎn)翻轉(zhuǎn)數(shù)據(jù)增強(qiáng)是提升田間泛化最便宜的手段。除了常規(guī)的隨機(jī)旋轉(zhuǎn)、水平翻轉(zhuǎn)、亮度對(duì)比度擾動(dòng)針對(duì)農(nóng)業(yè)場(chǎng)景我還會(huì)加三類(lèi)增強(qiáng)隨機(jī)遮擋模擬葉片重疊、隨機(jī)霧化模擬清晨露水或藥?kù)F、色彩抖動(dòng)模擬不同手機(jī)白平衡。用 Albumentations 庫(kù)實(shí)現(xiàn)比較順手。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.3), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.6), A.HueSaturationValue(hue_shift_limit15, sat_shift_limit25, val_shift_limit20, p0.5), A.CoarseDropout(max_holes8, max_height32, max_width32, p0.4), # 模擬遮擋 A.GaussNoise(var_limit(10.0, 50.0), p0.3), A.Resize(224, 224), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])CoarseDropout的max_holes控制遮擋塊數(shù)量田間葉片重疊嚴(yán)重時(shí)可以調(diào)到 12HueSaturationValue的hue_shift_limit不要超過(guò) 20否則會(huì)把健康葉片的綠色偏成黃色引入錯(cuò)誤標(biāo)簽。歸一化參數(shù)用的是 ImageNet 的均值和方差因?yàn)楹竺嬗妙A(yù)訓(xùn)練權(quán)重這一步必須對(duì)齊。3. 模型選型與訓(xùn)練從 ResNet 到輕量級(jí) MobileNet 的取舍3.1 分類(lèi)還是檢測(cè)先想清楚論文要回答什么問(wèn)題如果論文題目是農(nóng)作物病蟲(chóng)害識(shí)別通常默認(rèn)是分類(lèi)任務(wù)——輸入一張葉片圖輸出病害類(lèi)別。但如果要定位病斑位置、統(tǒng)計(jì)病斑面積就需要目標(biāo)檢測(cè)。分類(lèi)任務(wù)用 ResNet50、EfficientNet-B0 這類(lèi)骨干網(wǎng)絡(luò)即可檢測(cè)任務(wù)則常用 YOLOv5/v8 或 Faster R-CNN。我的建議是畢設(shè)優(yōu)先做分類(lèi)因?yàn)橛?xùn)練快、調(diào)參少、論文好寫(xiě)。如果要做檢測(cè)YOLOv8n 在 2000 張圖上訓(xùn)練 100 輪就能出結(jié)果但標(biāo)注成本高答辯時(shí)容易被追問(wèn)檢測(cè)框的標(biāo)注一致性如何保證。分類(lèi)任務(wù)則可以把重點(diǎn)放在數(shù)據(jù)增強(qiáng)和泛化分析上更容易講出深度。3.2 用遷移學(xué)習(xí)快速跑通 baseline從零訓(xùn)練一個(gè) ResNet 在 5 萬(wàn)張圖上需要很久而且小數(shù)據(jù)集容易過(guò)擬合。遷移學(xué)習(xí)是標(biāo)準(zhǔn)做法加載 ImageNet 預(yù)訓(xùn)練權(quán)重替換最后一層全連接層先凍結(jié)骨干只訓(xùn)練分類(lèi)頭再解凍全部微調(diào)。import torch import torch.nn as nn from torchvision import models def build_model(num_classes10, freeze_backboneTrue): # 加載 ResNet50 預(yù)訓(xùn)練權(quán)重不下載則用 weightsNone model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) if freeze_backbone: # 凍結(jié)除 fc 層外的所有參數(shù) for name, param in model.named_parameters(): if fc not in name: param.requires_grad False # 替換分類(lèi)頭輸入維度 2048 是 ResNet50 最后一層特征維度 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.4), nn.Linear(in_features, num_classes) ) return model model build_model(num_classes10, freeze_backboneTrue) criterion nn.CrossEntropyLoss() # 只優(yōu)化 fc 層參數(shù)學(xué)習(xí)率可以設(shè)大一點(diǎn) optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)freeze_backboneTrue時(shí)只訓(xùn)練分類(lèi)頭通常 10 輪就能收斂到不錯(cuò)的效果之后把freeze_backbone設(shè)為False用更小的學(xué)習(xí)率如 1e-4微調(diào)全部參數(shù)。Dropout(0.4)是為了防止小數(shù)據(jù)集過(guò)擬合如果數(shù)據(jù)量超過(guò) 2 萬(wàn)張可以降到 0.2。3.3 學(xué)習(xí)率調(diào)度與早停訓(xùn)練過(guò)程中最容易翻車(chē)的是學(xué)習(xí)率設(shè)太大導(dǎo)致 loss 震蕩或者設(shè)太小導(dǎo)致收斂慢。我一般用余弦退火配合 warmup前 5 輪線性升溫到初始學(xué)習(xí)率之后按余弦曲線下降到 0。from torch.optim.lr_scheduler import CosineAnnealingLR, LinearLR, SequentialLR # 前 5 個(gè) epoch 從 0.1 倍學(xué)習(xí)率升到 1 倍之后余弦退火 warmup LinearLR(optimizer, start_factor0.1, total_iters5) cosine CosineAnnealingLR(optimizer, T_max95, eta_min1e-6) scheduler SequentialLR(optimizer, schedulers[warmup, cosine], milestones[5])T_max設(shè)為總 epoch 數(shù)減去 warmup 輪數(shù)eta_min是最小學(xué)習(xí)率一般設(shè) 1e-6 即可。早停策略看驗(yàn)證集 loss連續(xù) 10 輪不下降就停避免過(guò)擬合。驗(yàn)證集準(zhǔn)確率波動(dòng)是正常的不要因?yàn)槟骋惠喌袅?2% 就立刻調(diào)參先看 loss 曲線是否平穩(wěn)。4. 避坑與排查病蟲(chóng)害識(shí)別項(xiàng)目里最容易翻車(chē)的 5 件事4.1 準(zhǔn)確率虛高數(shù)據(jù)泄漏與背景捷徑現(xiàn)象訓(xùn)練集和驗(yàn)證集準(zhǔn)確率都到 99%但換一批新圖測(cè)試就崩。原因最常見(jiàn)的是數(shù)據(jù)泄漏——同一片葉子的多張照片被分到了訓(xùn)練集和驗(yàn)證集。另一個(gè)隱蔽原因是背景捷徑PlantVillage 里不同病害的背景顏色略有差異模型學(xué)會(huì)了看背景猜類(lèi)別而不是看葉片病斑。解決按田塊或按采集批次劃分?jǐn)?shù)據(jù)集用 Grad-CAM 可視化模型關(guān)注區(qū)域如果熱力圖集中在背景而非病斑上說(shuō)明模型走了捷徑需要增加背景隨機(jī)化增強(qiáng)或裁剪葉片區(qū)域。4.2 類(lèi)別不平衡導(dǎo)致少數(shù)類(lèi)全錯(cuò)現(xiàn)象健康葉片識(shí)別很準(zhǔn)但某種罕見(jiàn)病害的召回率接近 0。原因數(shù)據(jù)集中健康樣本遠(yuǎn)多于病害樣本交叉熵?fù)p失被多數(shù)類(lèi)主導(dǎo)。解決用加權(quán)交叉熵權(quán)重按類(lèi)別頻率的倒數(shù)設(shè)置或者用 Focal Loss 降低易分類(lèi)樣本的權(quán)重。采樣層面可以用 WeightedRandomSampler 讓每個(gè) batch 里各類(lèi)別比例均衡。from torch.utils.data import WeightedRandomSampler import numpy as np # labels 是訓(xùn)練集所有樣本的類(lèi)別標(biāo)簽列表 class_counts np.bincount(labels) class_weights 1.0 / class_counts sample_weights [class_weights[label] for label in labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue)4.3 圖像尺寸與歸一化不一致現(xiàn)象訓(xùn)練時(shí)準(zhǔn)確率正常推理時(shí)結(jié)果完全亂套。原因訓(xùn)練用了Resize(224)和 ImageNet 歸一化推理時(shí)忘了做同樣的預(yù)處理或者用了 OpenCV 默認(rèn)的 BGR 通道順序而訓(xùn)練用的是 RGB。解決把預(yù)處理封裝成一個(gè)函數(shù)訓(xùn)練和推理共用。用 PIL 讀圖默認(rèn)是 RGB用 cv2 讀圖要手動(dòng)cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。歸一化的均值和方差必須和預(yù)訓(xùn)練權(quán)重一致。4.4 顯存不足與 batch size 設(shè)置現(xiàn)象訓(xùn)練時(shí)報(bào) CUDA out of memory。原因ResNet50 在 224 尺寸下 batch size 設(shè)到 64 就可能爆顯存尤其是用了數(shù)據(jù)增強(qiáng)后中間變量更多。解決先把 batch size 降到 16 或 8配合梯度累積模擬大 batch。如果還不行換 MobileNetV3 或 EfficientNet-B0 這類(lèi)輕量骨干?;旌暇扔?xùn)練也能省不少顯存。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for images, labels in dataloader: optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()4.5 論文里的實(shí)驗(yàn)對(duì)比不充分現(xiàn)象論文只跑了一個(gè)模型沒(méi)有對(duì)比實(shí)驗(yàn)答辯時(shí)被問(wèn)為什么選 ResNet 不選 VGG。原因畢設(shè)時(shí)間緊跑通一個(gè)模型就寫(xiě)論文了。解決至少對(duì)比 3 個(gè)骨干網(wǎng)絡(luò)如 ResNet50、MobileNetV3、EfficientNet-B0在同一數(shù)據(jù)劃分下報(bào)告準(zhǔn)確率、參數(shù)量、推理時(shí)間。再加一組消融實(shí)驗(yàn)有無(wú)數(shù)據(jù)增強(qiáng)、有無(wú)遷移學(xué)習(xí)、不同學(xué)習(xí)率。這些表格是論文的核心支撐也是高分的關(guān)鍵。5. 從訓(xùn)練到落地模型導(dǎo)出、推理加速與論文寫(xiě)作的收尾技巧訓(xùn)練完模型只是第一步畢設(shè)要拿高分還得把推理鏈路跑通并在論文里講清楚工程價(jià)值。我一般會(huì)把 PyTorch 模型導(dǎo)出成 ONNX再用 ONNX Runtime 做推理這樣在普通筆記本上也能跑到實(shí)時(shí)。import torch import onnx import onnxruntime as ort import numpy as np # 導(dǎo)出 ONNX輸入尺寸固定為 1x3x224x224 dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, crop_disease.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11 ) # ONNX Runtime 推理 session ort.InferenceSession(crop_disease.onnx, providers[CPUExecutionProvider]) def predict(img_array): # img_array 需為 float32、NCHW、已歸一化 inputs {session.get_inputs()[0].name: img_array.astype(np.float32)} outputs session.run(None, inputs) return np.argmax(outputs[0], axis1)導(dǎo)出時(shí)opset_version建議用 11兼容性好dynamic_axes讓 batch 維度可變方便部署時(shí)調(diào)整。推理前記得把圖像轉(zhuǎn)成 float32 并做同樣的歸一化否則結(jié)果會(huì)偏。論文寫(xiě)作上我習(xí)慣把數(shù)據(jù)采集與標(biāo)注單獨(dú)成一章放上田間實(shí)拍圖和標(biāo)注示例這比堆公式更能體現(xiàn)工作量。實(shí)驗(yàn)部分用表格對(duì)比不同模型再用混淆矩陣分析錯(cuò)分案例比如早疫病和晚疫病容易混就單獨(dú)討論兩者病斑形狀差異。最后加一節(jié)田間測(cè)試放幾段手機(jī)拍攝的識(shí)別視頻截圖說(shuō)明模型在實(shí)際場(chǎng)景下的表現(xiàn)和局限。這樣整篇論文從數(shù)據(jù)到模型到落地形成閉環(huán)答辯時(shí)老師問(wèn)什么都有據(jù)可依。我自己帶畢設(shè)這些年最大的教訓(xùn)是別等到最后兩周才跑模型。數(shù)據(jù)標(biāo)注和增強(qiáng)至少留出一個(gè)月訓(xùn)練和調(diào)參留兩周論文寫(xiě)作留兩周中間還要留出返工的時(shí)間。另外代碼一定要用 Git 管理每次調(diào)參記錄清楚改了什么、結(jié)果如何否則跑了幾十組實(shí)驗(yàn)后根本記不住哪組對(duì)應(yīng)哪個(gè)配置。希望這些經(jīng)驗(yàn)?zāi)軒湍闵僮唿c(diǎn)彎路順利把這份畢設(shè)做成能拿得出手的作品。本文還有配套的精品資源點(diǎn)擊獲取