習(xí)的農(nóng)作物病蟲害識別系統(tǒng):源碼、教程與論文參考)
簡介這份資源是面向高校學(xué)生與深度學(xué)習(xí)入門者的常見農(nóng)作物病蟲害識別系統(tǒng)完整項目包對應(yīng)高分畢業(yè)設(shè)計涵蓋從數(shù)據(jù)收集、視覺顯著性處理到卷積神經(jīng)網(wǎng)絡(luò)云訓(xùn)練的完整鏈路并涉及Inception-V3、MobileNet-V2等經(jīng)典模型適合需要完成課程設(shè)計、畢業(yè)設(shè)計或想系統(tǒng)實踐圖像分類的讀者。壓縮包共281個文件約522.63MB包含14個Python源碼、8個Vue與7個JavaScript前端文件、34張png圖表、107個txt說明及多份pdf、docx論文與技術(shù)路線圖文檔源碼、教程與論文配套齊全。目前已有2898人學(xué)習(xí)下載。讀者可據(jù)此獲得一套經(jīng)導(dǎo)師指導(dǎo)并通過的完整方案包括數(shù)據(jù)處理流程、模型訓(xùn)練代碼、前端展示界面與論文寫作參考便于快速復(fù)現(xiàn)實驗、理解網(wǎng)絡(luò)結(jié)構(gòu)與超參數(shù)設(shè)置并在此基礎(chǔ)上完成二次開發(fā)或撰寫自己的畢業(yè)設(shè)計。1. 從一份能跑通的病蟲害識別源碼說起去年幫一個學(xué)弟看他的畢業(yè)設(shè)計打開壓縮包我就愣住了三個版本的代碼混在一起requirements.txt 里寫著一堆互相沖突的版本號README 只有一行“運行 main.py”。他跟我說網(wǎng)上找的“深度學(xué)習(xí)病蟲害識別”資源十個里有八個跑不起來剩下兩個跑起來準(zhǔn)確率只有 40%。這不是個例。農(nóng)作物病蟲害識別這個方向因為數(shù)據(jù)集相對規(guī)整、模型結(jié)構(gòu)成熟成了畢業(yè)設(shè)計的熱門選題但真正能跑通、能改、能寫進(jìn)論文的完整資源并不多。這份基于深度學(xué)習(xí)的常見農(nóng)作物病蟲害識別系統(tǒng)包含源碼、教程和論文參考覆蓋了從數(shù)據(jù)預(yù)處理到模型訓(xùn)練再到界面演示的完整鏈路適合正在做相關(guān)畢業(yè)設(shè)計、課程設(shè)計或者想快速搭一個圖像分類原型的開發(fā)者。它解決的核心問題是讓你不用從零攢數(shù)據(jù)集、調(diào)網(wǎng)絡(luò)結(jié)構(gòu)、寫界面直接在一個可復(fù)現(xiàn)的基線上做二次開發(fā)。2. 先搞清楚識別系統(tǒng)的技術(shù)底座CNN 怎么把葉片照片變成病蟲害標(biāo)簽?zāi)玫揭环萆疃葘W(xué)習(xí)項目最怕的就是把它當(dāng)黑匣子跑一遍就完事。病蟲害識別本質(zhì)上是一個細(xì)粒度圖像分類問題——不同病害在葉片上的表現(xiàn)差異可能只是幾個斑點的大小和顏色這對模型的特征提取能力要求比普通貓狗分類高得多。這份資源用的是卷積神經(jīng)網(wǎng)絡(luò)路線常見做法是 ResNet 或 MobileNet 做骨干配合數(shù)據(jù)增強(qiáng)和遷移學(xué)習(xí)。下面把關(guān)鍵環(huán)節(jié)拆開講。2.1 數(shù)據(jù)集結(jié)構(gòu)與類別不平衡的處理邏輯病蟲害數(shù)據(jù)集通常按“作物-病害”兩級目錄組織比如data/train/玉米-銹病/、data/train/玉米-健康/。這份資源的目錄結(jié)構(gòu)大概率也是這個套路因為 PyTorch 的ImageFolder和 TensorFlow 的image_dataset_from_directory都默認(rèn)這種格式。但真實場景里有個繞不開的問題健康樣本往往比病害樣本多某些罕見病害可能只有幾十張圖。直接訓(xùn)練會導(dǎo)致模型偏向多數(shù)類在驗證集上看著準(zhǔn)確率不錯實際對少數(shù)類幾乎沒識別能力。常見做法是在 DataLoader 里加WeightedRandomSampler給每個類別按樣本數(shù)反比賦權(quán)重。代碼大概長這樣import os from torch.utils.data import WeightedRandomSampler from torchvision import datasets # 加載訓(xùn)練集ImageFolder 自動按目錄名映射標(biāo)簽 train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) # 統(tǒng)計每個類別的樣本數(shù) class_counts [0] * len(train_dataset.classes) for _, label in train_dataset.samples: class_counts[label] 1 # 按樣本數(shù)反比計算權(quán)重樣本越少權(quán)重越高 class_weights [1.0 / c for c in class_counts] sample_weights [class_weights[label] for _, label in train_dataset.samples] # 用 WeightedRandomSampler 替換默認(rèn)的隨機(jī)采樣 sampler WeightedRandomSampler( weightssample_weights, num_sampleslen(sample_weights), replacementTrue # 允許重復(fù)采樣少數(shù)類 )這段代碼的邏輯是先遍歷train_dataset.samples拿到每張圖的標(biāo)簽統(tǒng)計出每個類別的總數(shù)然后對每個類別取倒數(shù)作為權(quán)重樣本越少權(quán)重越大最后用WeightedRandomSampler讓 DataLoader 在每次 epoch 中按權(quán)重采樣少數(shù)類會被更頻繁地抽到。參數(shù)replacementTrue表示有放回采樣這是必須的否則少數(shù)類樣本抽完就沒了。num_samples一般設(shè)成和訓(xùn)練集總數(shù)一致保證每個 epoch 看到的圖片數(shù)量和正常訓(xùn)練差不多。注意如果驗證集也做同樣的采樣評估結(jié)果會失真。驗證集必須用原始分布才能反映模型在真實場景下的表現(xiàn)。2.2 遷移學(xué)習(xí)與凍結(jié)層的選擇從零訓(xùn)練一個 ResNet 在幾千張圖上很容易過擬合所以這份資源大概率用了預(yù)訓(xùn)練權(quán)重。關(guān)鍵決策是凍結(jié)多少層。常見做法是只替換最后的全連接層然后分階段解凍先凍結(jié)骨干只訓(xùn)練分類頭等 loss 穩(wěn)定后再解凍最后幾個 block 做微調(diào)。代碼結(jié)構(gòu)如下import torchvision.models as models import torch.nn as nn # 加載預(yù)訓(xùn)練 ResNet18weights 參數(shù)指定使用 ImageNet 預(yù)訓(xùn)練權(quán)重 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 凍結(jié)骨干網(wǎng)絡(luò)的所有參數(shù) for param in model.parameters(): param.requires_grad False # 替換最后的全連接層輸出維度改為病蟲害類別數(shù) num_classes len(train_dataset.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 此時只有 model.fc 的參數(shù)需要訓(xùn)練 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)第一階段的lr可以設(shè)大一點比如1e-3因為只訓(xùn)練分類頭收斂快。等驗證集準(zhǔn)確率不再上升后解凍model.layer4把學(xué)習(xí)率降到1e-4繼續(xù)訓(xùn)練。這個策略比一上來就全網(wǎng)絡(luò)微調(diào)更穩(wěn)血淚經(jīng)驗是小數(shù)據(jù)集上直接全量微調(diào)前幾個 epoch 驗證集準(zhǔn)確率會劇烈震蕩甚至比只訓(xùn)練分類頭還差。2.3 訓(xùn)練循環(huán)里的關(guān)鍵參數(shù)與日志監(jiān)控訓(xùn)練腳本里最容易被忽視的是日志。很多人跑完只看最后一行準(zhǔn)確率中間 loss 怎么變的完全不知道。建議在訓(xùn)練循環(huán)里每個 epoch 記錄 train_loss、val_loss、val_acc畫成曲線一眼就能看出過擬合還是欠擬合。核心代碼片段for epoch in range(num_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 驗證階段 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() train_loss running_loss / len(train_dataset) val_acc val_correct / val_total print(fEpoch {epoch1}/{num_epochs} | Loss: {train_loss:.4f} | Val Acc: {val_acc:.4f})criterion一般用CrossEntropyLoss如果類別不平衡嚴(yán)重可以加weight參數(shù)傳入類別權(quán)重。optimizer.zero_grad()的位置必須在反向傳播之前這是 PyTorch 的固定套路但新手經(jīng)常寫成loss.backward()之后才清零導(dǎo)致梯度累積loss 曲線像心電圖。3. 把源碼跑起來環(huán)境配置、數(shù)據(jù)準(zhǔn)備與訓(xùn)練啟動理論清楚了接下來是動手環(huán)節(jié)。這份資源能不能在你的機(jī)器上跑通取決于三個東西Python 環(huán)境、數(shù)據(jù)集路徑、以及有沒有 GPU。下面按順序走一遍。3.1 環(huán)境依賴的版本對齊深度學(xué)習(xí)項目最玄學(xué)的地方就是版本兼容。PyTorch、CUDA、torchvision 三者版本必須匹配否則 import 就報錯。常見做法是先用conda建一個干凈環(huán)境然后按官方對應(yīng)關(guān)系裝。假設(shè)資源里用的是 PyTorch 1.12 CUDA 11.3命令如下# 創(chuàng)建虛擬環(huán)境指定 Python 版本 conda create -n pest_recognition python3.8 -y conda activate pest_recognition # 安裝 PyTorch 和 torchvision注意 cudatoolkit 版本要和驅(qū)動匹配 conda install pytorch1.12.1 torchvision0.13.1 cudatoolkit11.3 -c pytorch -y # 安裝其他依賴 pip install opencv-python pillow matplotlib scikit-learn tqdm如果你沒有 GPU把cudatoolkit去掉裝 CPU 版本的 PyTorch 也能跑只是訓(xùn)練慢。驗證安裝是否成功import torch print(torch.__version__) print(torch.cuda.is_available()) # 有 GPU 返回 Truetorch.cuda.is_available()返回 False 的話檢查顯卡驅(qū)動版本是否支持當(dāng)前 CUDA。常見坑是驅(qū)動太舊裝完 CUDA 11.3 但驅(qū)動只支持到 11.1這時候要么升級驅(qū)動要么降 CUDA 版本。3.2 數(shù)據(jù)集劃分與路徑配置資源里一般會附帶數(shù)據(jù)集或者給出下載方式。拿到數(shù)據(jù)后按train/val/test劃分比例常見是 7:2:1 或 8:1:1。如果原始數(shù)據(jù)沒有劃分用腳本切import os import shutil import random def split_dataset(src_dir, dst_dir, split_ratio(0.7, 0.2, 0.1)): 按比例劃分?jǐn)?shù)據(jù)集保持類別目錄結(jié)構(gòu) classes os.listdir(src_dir) for cls in classes: cls_path os.path.join(src_dir, cls) images os.listdir(cls_path) random.shuffle(images) n_train int(len(images) * split_ratio[0]) n_val int(len(images) * split_ratio[1]) for i, img in enumerate(images): if i n_train: subset train elif i n_train n_val: subset val else: subset test # 創(chuàng)建目標(biāo)目錄并復(fù)制圖片 dst_path os.path.join(dst_dir, subset, cls) os.makedirs(dst_path, exist_okTrue) shutil.copy(os.path.join(cls_path, img), os.path.join(dst_path, img)) split_dataset(raw_data, data)split_ratio三個數(shù)分別對應(yīng)訓(xùn)練、驗證、測試的比例加起來必須等于 1。random.shuffle保證每個類別的圖片被隨機(jī)分配避免按文件名順序切導(dǎo)致某個子集全是同一批拍攝的圖片。劃分完之后訓(xùn)練腳本里的data/train、data/val路徑就能直接用了。3.3 啟動訓(xùn)練與斷點續(xù)訓(xùn)配置好路徑后直接運行訓(xùn)練腳本python train.py --data_dir data --epochs 50 --batch_size 32 --lr 1e-3 --gpu 0參數(shù)說明--epochs是總訓(xùn)練輪數(shù)小數(shù)據(jù)集一般 30 到 50 輪就夠--batch_size受顯存限制8G 顯存跑 ResNet18 可以設(shè) 32跑 ResNet50 建議降到 16--lr初始學(xué)習(xí)率配合余弦退火調(diào)度器效果更好。如果訓(xùn)練中斷了資源里如果有--resume參數(shù)可以加載上次保存的 checkpoint 繼續(xù)python train.py --resume checkpoints/model_epoch_30.pth斷點續(xù)訓(xùn)的關(guān)鍵是 checkpoint 里要保存 optimizer 的 state_dict否則優(yōu)化器動量信息丟失loss 會突然跳一下。常見做法是每個 epoch 存一次文件名帶 epoch 數(shù)方便回滾。4. 避坑與排查那些讓準(zhǔn)確率卡在 60% 的細(xì)節(jié)跑通不等于跑好。下面這幾條是實際調(diào)試中最常翻車的地方每條按現(xiàn)象、原因、解決來寫。4.1 驗證集準(zhǔn)確率遠(yuǎn)高于測試集現(xiàn)象訓(xùn)練時驗證集準(zhǔn)確率 95%換測試集一跑只有 70%。原因通常是數(shù)據(jù)泄漏——同一張葉片的多角度照片被分到了訓(xùn)練集和驗證集模型記住了背景而不是病害特征。解決劃分?jǐn)?shù)據(jù)集時按“拍攝批次”或“原始圖片 ID”分組確保同一株植物的照片只出現(xiàn)在一個子集里。如果數(shù)據(jù)沒有批次信息至少用ImageFolder之前先做一次文件級去重。4.2 Loss 不下降且輸出全為同一類現(xiàn)象訓(xùn)練幾個 epoch 后 loss 幾乎不變預(yù)測結(jié)果全是樣本最多的那個類別。原因可能是學(xué)習(xí)率太大導(dǎo)致梯度爆炸或者數(shù)據(jù)標(biāo)簽沒對上。解決先把學(xué)習(xí)率降到1e-4試一輪然后檢查ImageFolder的class_to_idx映射確認(rèn)標(biāo)簽和目錄名一致。常見坑是目錄名里有中文或空格ImageFolder按字典序排序后標(biāo)簽順序和你以為的不一樣。4.3 GPU 顯存溢出但 batch_size 已經(jīng)很小現(xiàn)象CUDA out of memory但 batch_size 已經(jīng)降到 8 了。原因可能是圖片分辨率太高比如原始圖片是 4000x3000直接 resize 到 224 之前先被加載成了張量。解決在 Dataset 的__getitem__里用 PIL 打開圖片后立即 resize不要先轉(zhuǎn)成 numpy 數(shù)組再處理。另外檢查是否有殘留的 tensor 沒釋放在驗證循環(huán)里加torch.cuda.empty_cache()。4.4 數(shù)據(jù)增強(qiáng)把病害特征增沒了現(xiàn)象加了隨機(jī)裁剪和顏色抖動后驗證集準(zhǔn)確率反而下降。原因病蟲害識別的關(guān)鍵特征就是病斑的顏色和形狀過度顏色抖動會讓模型學(xué)不到有效特征。解決把ColorJitter的強(qiáng)度調(diào)低或者只用隨機(jī)水平翻轉(zhuǎn)和輕微旋轉(zhuǎn)。常見做法是訓(xùn)練集用RandomResizedCropRandomHorizontalFlip驗證集只做ResizeCenterCrop不要加任何隨機(jī)變換。4.5 保存的模型加載后預(yù)測結(jié)果亂跳現(xiàn)象訓(xùn)練完保存模型重新加載做推理同一張圖兩次預(yù)測結(jié)果不一樣。原因保存時沒設(shè)model.eval()Dropout 和 BatchNorm 還在訓(xùn)練模式。解決推理前必須調(diào)model.eval()并且用with torch.no_grad():包住前向傳播。另外保存 checkpoint 時推薦保存state_dict而不是整個模型對象避免 pickle 反序列化時的版本兼容問題。5. 從能跑到好用模型導(dǎo)出、界面集成與論文圖表生成把模型訓(xùn)練到 90% 準(zhǔn)確率只是第一步畢業(yè)設(shè)計還得有演示界面和論文素材。這一章講兩個進(jìn)階操作把 PyTorch 模型導(dǎo)出成 ONNX 方便部署以及用 Grad-CAM 生成熱力圖放進(jìn)論文。5.1 導(dǎo)出 ONNX 并驗證推理一致性O(shè)NNX 的好處是跨框架部署比如用 OpenCV 的 DNN 模塊直接加載不依賴 PyTorch 環(huán)境。導(dǎo)出代碼如下import torch import torch.onnx # 加載訓(xùn)練好的模型 model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(checkpoints/best_model.pth)) model.eval() # 構(gòu)造一個示例輸入batch_size13 通道224x224 dummy_input torch.randn(1, 3, 224, 224) # 導(dǎo)出 ONNX 模型 torch.onnx.export( model, dummy_input, pest_recognition.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version11 )dynamic_axes參數(shù)讓導(dǎo)出的模型支持動態(tài) batch_size推理時可以一次傳多張圖。opset_version11是兼容性比較好的版本太低不支持某些算子太高有些推理引擎不認(rèn)。導(dǎo)出后用onnxruntime驗證一下輸出是否和 PyTorch 一致import onnxruntime as ort import numpy as np sess ort.InferenceSession(pest_recognition.onnx) input_name sess.get_inputs()[0].name # 用同樣的 dummy_input 做推理 onnx_output sess.run(None, {input_name: dummy_input.numpy()}) # 對比 PyTorch 輸出 with torch.no_grad(): torch_output model(dummy_input).numpy() print(np.allclose(onnx_output[0], torch_output, atol1e-4))atol1e-4是允許的數(shù)值誤差浮點運算在不同框架下會有微小差異只要在 1e-4 以內(nèi)就算一致。如果差得遠(yuǎn)檢查導(dǎo)出時是否漏了model.eval()。5.2 用 Grad-CAM 生成熱力圖放進(jìn)論文論文里如果只有準(zhǔn)確率表格會顯得單薄加幾張熱力圖能直觀說明模型關(guān)注的是病斑區(qū)域而不是背景。Grad-CAM 的實現(xiàn)不復(fù)雜核心是拿到目標(biāo)層的梯度并做全局平均池化。這里給一個精簡版import cv2 import numpy as np import torch import torch.nn.functional as F class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None # 注冊鉤子前向傳播時保存特征圖反向傳播時保存梯度 target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor, class_idxNone): self.model.eval() output self.model(input_tensor) if class_idx is None: class_idx output.argmax(dim1).item() # 反向傳播求目標(biāo)類別的梯度 self.model.zero_grad() output[0, class_idx].backward() # 梯度做全局平均池化得到權(quán)重 weights self.gradients.mean(dim(2, 3), keepdimTrue) # 特征圖加權(quán)求和 cam (weights * self.activations).sum(dim1, keepdimTrue) cam F.relu(cam) # 歸一化到 0-1 cam cam - cam.min() cam cam / (cam.max() 1e-8) return cam.squeeze().cpu().numpy() # 使用示例target_layer 一般選 model.layer4 的最后一個 block grad_cam GradCAM(model, model.layer4[-1]) heatmap grad_cam.generate(input_tensor) # 把熱力圖疊加到原圖上 img cv2.imread(test_leaf.jpg) img cv2.resize(img, (224, 224)) heatmap cv2.resize(heatmap, (224, 224)) heatmap np.uint8(255 * heatmap) heatmap cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) overlay cv2.addWeighted(img, 0.6, heatmap, 0.4, 0) cv2.imwrite(gradcam_result.jpg, overlay)target_layer選model.layer4[-1]是因為這一層感受野足夠大能覆蓋整個葉片區(qū)域同時保留了空間信息。register_full_backward_hook在 PyTorch 1.8 之后替代了舊的register_backward_hook行為更穩(wěn)定。生成的熱力圖紅色區(qū)域就是模型判斷為病害的關(guān)鍵區(qū)域如果紅色集中在葉片邊緣而不是病斑上說明模型可能學(xué)到了錯誤的特征需要回頭檢查數(shù)據(jù)增強(qiáng)或重新標(biāo)注。從那以后我每次拿到一個新的分類項目都會先跑一遍 Grad-CAM 看模型到底在“看”哪里再決定要不要調(diào)數(shù)據(jù)增強(qiáng)策略。這個習(xí)慣幫我省了很多盲目調(diào)參的時間。希望這份資源能幫你把畢業(yè)設(shè)計順利推下去少走幾個我踩過的坑。本文還有配套的精品資源點擊獲取