練YOLOv8作物雜草檢測:完整流程與避坑指南)
簡介面向YOLO系列算法目標(biāo)檢測的開發(fā)者與研究人員這份作物雜草數(shù)據(jù)集包含200張已標(biāo)注圖像可直接用于訓(xùn)練、驗(yàn)證與測試并附帶data.yaml配置文件適配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本。標(biāo)注提供YOLO格式txt與VOC格式xml兩套分別存放于不同文件夾便于切換訓(xùn)練框架txt記錄類別索引、歸一化中心點(diǎn)坐標(biāo)及寬高xml保留傳統(tǒng)檢測標(biāo)注信息適應(yīng)不同工作流。壓縮包共601個(gè)文件含200個(gè)jpg原圖、200個(gè)txt標(biāo)簽、200個(gè)xml標(biāo)簽及1個(gè)yaml配置整體約185.56MB數(shù)據(jù)劃分清晰可直接用于模型訓(xùn)練和測試。目前已有106人學(xué)習(xí)下載適合需要快速驗(yàn)證模型效果、開展農(nóng)業(yè)視覺識(shí)別實(shí)驗(yàn)或入門YOLO數(shù)據(jù)格式的讀者。1. 先回答最直接的問題200 張帶標(biāo)簽圖像做作物雜草檢測夠不夠用很多人拿到“yolo算法-作物雜草數(shù)據(jù)集-200張圖像帶標(biāo)簽-.zip”這種壓縮包第一反應(yīng)就是“200 張這能訓(xùn)出什么”但作物雜草檢測和你平時(shí)刷的 COCO 類通用目標(biāo)檢測完全是兩碼事——場景固定、目標(biāo)類別少、拍攝設(shè)備基本不變。200 張干凈、對準(zhǔn)標(biāo)簽、和真實(shí)田間狀況對得上的圖足夠你把 YOLO 的完整鏈路走一遍解包、核對標(biāo)簽、轉(zhuǎn)換格式、訓(xùn)練、驗(yàn)證、導(dǎo)出模型最后得到一個(gè)能下地試跑的最小可用版本。這篇文章就圍繞這個(gè)小數(shù)據(jù)集展開告訴你怎么一步步把它變成能用的檢測模型以及中途一定會(huì)踩到的幾個(gè)坑。2. 拆開 .zip 之后先別訓(xùn)練把標(biāo)簽格式、轉(zhuǎn)換腳本和目錄結(jié)構(gòu)一次理清2.1 200 張圖能撐起什么任務(wù)小數(shù)據(jù)集的邊界和可能性先給結(jié)論200 張圖能撐起一個(gè)“固定場景下的單類或多類雜草檢測模型”撐不起“任意地塊、任意光照、任意生長期的通用雜草識(shí)別”。這里面的差距不在算法而在數(shù)據(jù)分布。如果你手里的 200 張圖是在同一個(gè)地塊、同一種相機(jī)高度、同一時(shí)間段拍的那訓(xùn)練集和驗(yàn)證集之間的分布差距很小訓(xùn)練出來的模型在相似條件下表現(xiàn)會(huì)不錯(cuò)。但如果你指望它到了另一塊地、換個(gè)光照角度還能保持同樣的效果那基本是玄學(xué)不可能。所以拿到數(shù)據(jù)集的第一件事不是急著訓(xùn)練而是確認(rèn)這些圖的覆蓋范圍包含了哪些雜草類別、作物是什么、拍攝角度是俯視還是側(cè)視、圖像分辨率多大。這些信息決定了你后面怎么劃分驗(yàn)證集、怎么調(diào)參數(shù)、怎么設(shè)置盲測標(biāo)準(zhǔn)。200 張圖本身的邊界就在那里你要做的是在這個(gè)邊界內(nèi)把模型做扎實(shí)而不是幻想小數(shù)據(jù)創(chuàng)造奇跡。2.2 先核對壓縮包里是什么格式VOC/XML、COCO/JSON、YOLO/TXT 三選一解壓之前先用腳本看一眼壓縮包里的文件結(jié)構(gòu)。常見的數(shù)據(jù)集打包方式有三種每張圖對應(yīng)一個(gè) XML 的 VOC 格式、一個(gè) JSON 文件的 COCO 格式、或者每張圖對應(yīng)一個(gè) TXT 的 YOLO 格式。這三種格式的標(biāo)簽結(jié)構(gòu)完全不同直接決定了你接下來要寫什么樣的轉(zhuǎn)換腳本。import zipfile from pathlib import Path zip_path Path(作物雜草數(shù)據(jù)集-200張圖像帶標(biāo)簽.zip) with zipfile.ZipFile(zip_path) as zf: names zf.namelist() # 按擴(kuò)展名統(tǒng)計(jì)快速判斷是哪一種標(biāo)注格式 stats {} for n in names: ext Path(n).suffix.lower() stats[ext] stats.get(ext, 0) 1 for ext, count in sorted(stats.items(), keylambda x: -x[1]): if ext : print(f{目錄:8s} {count} 個(gè)) else: print(f{ext:8s} {count} 個(gè)文件) # 打印前5個(gè)非目錄文件的具體路徑確認(rèn)目錄層級 files [n for n in names if Path(n).suffix] for n in files[:5]: print(n)這段代碼的邏輯很簡單把壓縮包里的文件按擴(kuò)展名歸類統(tǒng)計(jì)每個(gè)擴(kuò)展名有多少個(gè)文件。看到.jpg配.xml那基本就是 VOC 格式看到.jpg配.txt可能是 YOLO 格式也可能只是沒標(biāo)簽的占位文件看到單個(gè).json加一堆.jpg大概率是 COCO 格式。打印前幾個(gè)文件的路徑是為了確認(rèn)壓縮包內(nèi)部有沒有嵌套目錄后面寫 data.yaml 時(shí)路徑需要對應(yīng)上。三種格式的核心差異如下轉(zhuǎn)換前心里要有數(shù)格式標(biāo)簽文件坐標(biāo)形式類別標(biāo)識(shí)VOC (XML)每圖一個(gè) .xmlbndbox 內(nèi) 4 個(gè)絕對像素坐標(biāo) xmin/ymin/xmax/ymaxobject 標(biāo)簽下的 name 文本COCO (JSON)單文件或每圖一個(gè) .json像素坐標(biāo)或歸一化坐標(biāo)annotation 內(nèi)數(shù)組category_id 數(shù)字YOLO (TXT)每圖一個(gè) .txt一行一個(gè)目標(biāo)x_center y_center w h 歸一化 0~1每行開頭的數(shù)字類別 ID其中 VOC 和 COCO 都要先轉(zhuǎn)換成 YOLO 的 txt 格式才能訓(xùn)練。別省這一步Y(jié)OLO 原生訓(xùn)練器只認(rèn) txt。2.3 把 VOC 轉(zhuǎn)成 YOLO 歸一化 txt轉(zhuǎn)換腳本與四個(gè)邊界坑作物雜草數(shù)據(jù)集里最常見的標(biāo)簽格式是 VOC XML因?yàn)楹芏鄻?biāo)注工具默認(rèn)導(dǎo)出的就是 XML。下面這段是轉(zhuǎn)換腳本能直接跑import xml.etree.ElementTree as ET from pathlib import Path # 類別順序必須和壓縮包內(nèi)標(biāo)簽的 name 字段一致 class_names [weed, crop] def voc_to_yolo(xml_path: Path, out_dir: Path, class_names: list[str]): tree ET.parse(xml_path) root tree.getroot() size root.find(size) if size is None: print(f跳過 {xml_path.name}: 缺少 size 信息) return img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: print(f{xml_path.name}: 忽略未知類別 {name}) continue cls_id class_names.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 邊界坑一XML 里偶爾出現(xiàn)越界坐標(biāo)先裁剪到圖像范圍內(nèi) x1 max(0, min(x1, img_w)) x2 max(0, min(x2, img_w)) y1 max(0, min(y1, img_h)) y2 max(0, min(y2, img_h)) w x2 - x1 h y2 - y1 # 邊界坑二過濾掉寬高為 0 的壞框 if w 0 or h 0: print(f{xml_path.name}: 跳過大小為 0 的框) continue # YOLO 需要的是中心點(diǎn)坐標(biāo)和寬高并且全部歸一化到 0~1 x_center (x1 x2) / 2 / img_w y_center (y1 y2) / 2 / img_h w_norm w / img_w h_norm h / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}) out_path out_dir / (xml_path.stem .txt) out_path.write_text(\n.join(lines), encodingutf-8)這段代碼把 XML 里的絕對像素坐標(biāo)轉(zhuǎn)成中心點(diǎn)加寬高的歸一化坐標(biāo)。為什么 YOLO 要中心點(diǎn)格式因?yàn)橥粡垐D在不同尺度下縮放歸一化坐標(biāo)不需要跟著變模型訓(xùn)練時(shí)做各種尺度增強(qiáng)就不用再回算坐標(biāo)了。實(shí)際轉(zhuǎn)換時(shí)會(huì)碰到幾個(gè)坑一是 XML 里的坐標(biāo)偶爾會(huì)超出圖像寬高不裁剪的話生成的 txt 里會(huì)出現(xiàn)大于 1 的坐標(biāo)值訓(xùn)練時(shí)輕則警告、重則 loss 異常二是標(biāo)注工具可能把某個(gè)目標(biāo)標(biāo)成 0 寬或 0 高這種框要直接過濾掉不然損失函數(shù)里會(huì)出現(xiàn) NaN三是 XML 里的類別 name 和你的 class_names 列表必須完全匹配差一個(gè)字母就會(huì)把目標(biāo)標(biāo)成“未識(shí)別”四是圖像是 JPG 但 XML 文件名后綴是 .jpeg匹配圖像和標(biāo)簽時(shí)要用 Path.stem 而不是完整文件名。2.4 劃分 train/val按類別分層不能隨機(jī)抽完事200 張圖最忌憚的就是隨便random.shuffle之后按 82 切一刀。作物雜草數(shù)據(jù)集的類別分布往往極不均衡可能雜草出現(xiàn) 180 張圖里作物只出現(xiàn) 30 張圖里。如果隨機(jī)切很有可能驗(yàn)證集里一種類別一張都沒有訓(xùn)練出來的模型在這類目標(biāo)上就是瞎猜。我一般用按類別分層的辦法來切分保證每種類別在驗(yàn)證集里至少出現(xiàn)一次import random from pathlib import Path random.seed(42) img_dir Path(images) label_dir Path(labels) imgs sorted(img_dir.glob(*.jpg)) # 統(tǒng)計(jì)每張圖里包含哪些類別 img_classes {} for img in imgs: txt label_dir / (img.stem .txt) if not txt.exists(): continue cls_ids set() for line in txt.read_text(encodingutf-8).strip().splitlines(): if line.strip(): cls_ids.add(int(line.split()[0])) img_classes[img] cls_ids # 先保證每個(gè)類別在 val 里至少有一張 val set() for cls_id in sorted({c for cs in img_classes.values() for c in cs}): candidates [i for i in imgs if cls_id in img_classes[i] and i not in val] if candidates: val.add(random.choice(candidates)) # 再補(bǔ)齊到 20% 總量 target max(1, int(len(imgs) * 0.2)) while len(val) target: c random.choice(imgs) if c not in val: val.add(c) train [i for i in imgs if i not in val] print(ftrain: {len(train)} 張, val: {len(val)} 張)這段代碼的核心邏輯是先做“保類別”再做“補(bǔ)數(shù)量”。先遍歷所有類別 ID從包含該類的圖片里抽一張進(jìn)驗(yàn)證集然后再隨機(jī)補(bǔ)到總張數(shù)的 20%。這樣即使某個(gè)類別只有兩張圖驗(yàn)證集里也一定能看到它。代價(jià)是訓(xùn)練集里這類只留一張訓(xùn)練效果會(huì)打折但至少驗(yàn)證指標(biāo)不會(huì)失真到騙你自己。目錄結(jié)構(gòu)也按 YOLO 慣例來組織這是 Ultralytics 訓(xùn)練器默認(rèn)認(rèn)可的布局weed_dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/圖片和標(biāo)簽分開存放train 和 val 各自對應(yīng)。注意 labels 里的 txt 文件名必須和 images 里的 jpg 文件名完全一致不包括擴(kuò)展名否則訓(xùn)練時(shí)找不到匹配標(biāo)簽。提示壓縮包里如果有中文文件名在 Linux 下解壓可能出現(xiàn)亂碼。用unzip -O gbk 文件名.zip可以指定編碼解壓避免后面路徑匹配翻車。3. 用 YOLOv8 訓(xùn)練雜草檢測模型選型理由、data.yaml 配置和訓(xùn)練參數(shù)設(shè)置3.1 為什么選 YOLOv8 而不是 YOLOv5/YOLO11小數(shù)據(jù)集的實(shí)際選型邏輯現(xiàn)在 YOLO 家族成員很多但做作物雜草檢測這個(gè)小方向我首推 YOLOv8理由有三個(gè)。第一YOLOv8 的預(yù)訓(xùn)練權(quán)重下載和加載做得最省心一條命令自動(dòng)拉取 COCO 預(yù)訓(xùn)練模型不需要手動(dòng)去翻各種鏡像站這對初學(xué)者最友好。第二Ultralytics 的訓(xùn)練器把數(shù)據(jù)增強(qiáng)、早停、日志可視化都集成好了200 張小數(shù)據(jù)集最需要的恰恰是這些內(nèi)置機(jī)制不需要你自己搭一堆外圍腳本。第三YOLOv8 的模型規(guī)模梯度很清晰從 n 到 x 都有小數(shù)據(jù)集先用 nano 級模型起步非常合適。YOLOv5 雖然生態(tài)成熟但對新用戶來說接口不如 v8 統(tǒng)一YOLO11 更偏向最新的算法特性社區(qū)里踩坑記錄還沒沉淀夠。做項(xiàng)目講究的是穩(wěn)定復(fù)現(xiàn)不是追新。提示用 YOLOv8 訓(xùn)練自己的數(shù)據(jù)集最省事的方式是從 COCO 預(yù)訓(xùn)練權(quán)重開始做遷移學(xué)習(xí)。200 張圖從頭訓(xùn)練基本不可能收斂但從預(yù)訓(xùn)練權(quán)重繼續(xù)微調(diào)效果會(huì)好得多。這就是熱詞里說的“yolo預(yù)訓(xùn)練模型下載”的意義所在。3.2 data.yaml 配置路徑、類別數(shù) nc、類別名 names 三個(gè)坑訓(xùn)練前先寫好 data.yaml這是 YOLO 讀取數(shù)據(jù)集配置的唯一入口# data.yaml path: /home/yourname/weed_dataset # 改成你解壓后的絕對路徑 train: images/train val: images/val nc: 2 # 類別數(shù)量 names: 0: weed 1: crop這里三個(gè)參數(shù)最容易出問題。path必須是絕對路徑雖然相對路徑也能寫但在不同機(jī)器上跑容易因?yàn)楫?dāng)前工作目錄不對而找不到數(shù)據(jù)浪費(fèi)半天排查時(shí)間。nc必須和類別實(shí)際數(shù)量嚴(yán)格一致寫多了會(huì)在訓(xùn)練日志里看到莫名其妙的空類寫少了類別 ID 直接越界。names的順序不是隨便寫的它決定了預(yù)測結(jié)果里“第 0 類叫什么”如果你在標(biāo)簽轉(zhuǎn)換腳本里把 weed 設(shè)為 0這里就必須讓 0 對應(yīng) weed。3.3 訓(xùn)練命令和參數(shù)batch、epochs、imgsz、patience 怎么定安裝 Ultralytics 然后跑訓(xùn)練一條命令就能開始pip install ultralytics yolo train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience30 \ device0參數(shù)解釋很直白但每個(gè)都有講究。modelyolov8n.pt表示基于 COCO 預(yù)訓(xùn)練的 nano 版繼續(xù)訓(xùn)練這次訓(xùn)練會(huì)自動(dòng)從官網(wǎng)下載權(quán)重文件到本地緩存。epochs100是最大訓(xùn)練輪數(shù)不是必然跑滿配合patience30做早停。因?yàn)?200 張圖很小模型很容易在 30 到 50 輪之間達(dá)到最優(yōu)patience 設(shè)成 30 表示連續(xù) 30 輪驗(yàn)證指標(biāo)不再提升就自動(dòng)停節(jié)省時(shí)間也避免過擬合。imgsz640是訓(xùn)練時(shí)縮放到 640x640。如果原圖分辨率在 1920 以上且雜草目標(biāo)很小可以考慮提到 768 或 1024但顯存占用會(huì)顯著增加。batch16在 200 張圖的情境下意味著每個(gè) epoch 只有 12 個(gè) batch梯度更新次數(shù)很少所以小數(shù)據(jù)集不要用特別大的 batch8 到 16 比較合理。如果顯存報(bào) OOM優(yōu)先把 batch 降到 8而不是改 imgsz。device0是使用第一塊 GPU純 CPU 訓(xùn)練時(shí)改成devicecpu但訓(xùn)練速度會(huì)慢一個(gè)數(shù)量級200 張圖也要等一陣子。YOLOv8 的損失函數(shù)由三部分組成box_loss 負(fù)責(zé)預(yù)測框和真實(shí)框的重合度cls_loss 負(fù)責(zé)分類是否正確dfl_loss 負(fù)責(zé)邊界框分布的細(xì)節(jié)擬合。訓(xùn)練日志里這三項(xiàng)都下降說明模型在正常學(xué)習(xí)。3.4 訓(xùn)練日志怎么看四個(gè)關(guān)鍵字段判斷模型真的在學(xué)訓(xùn)練開始后終端會(huì)輸出這樣的日志Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 30/100 3.1G 1.21 0.583 1.102 7 640 Class Images Instances P R mAP50 all 40 102 0.851 0.79 0.831這里要看四個(gè)關(guān)鍵字段。第一是box_loss它下降的速度代表模型定位能力在提升如果這個(gè)值一直橫盤不降大概率是標(biāo)簽坐標(biāo)有錯(cuò)。第二是cls_loss它代表分類能力如果它降了但 box_loss 不動(dòng)問題出在標(biāo)注框的位置不準(zhǔn)確。第三是驗(yàn)證集里的R召回率雜草檢測場景里漏檢比誤檢更致命召回率上不去說明模型漏掉了很多目標(biāo)。第四是mAP50這個(gè)指標(biāo)最容易騙人后面單獨(dú)說。訓(xùn)練產(chǎn)生的所有中間權(quán)重保存在runs/detect/train/下best.pt是驗(yàn)證集上表現(xiàn)最好的模型last.pt是最后一輪的模型。有些新手不看日志直接拿 last.pt 去用結(jié)果發(fā)現(xiàn)效果很差因?yàn)樽詈髱纵喛赡芤呀?jīng)過擬合了。記住只用 best.pt。4. 避坑用 200 張作物雜草圖訓(xùn) YOLO 的 5 個(gè)高頻翻車現(xiàn)場4.1 mAP 全程為 0類別編號和標(biāo)簽對不上現(xiàn)象訓(xùn)練 loss 正常下降但驗(yàn)證集 mAP 從第一個(gè) epoch 到最后一個(gè) epoch 始終是 0precision 和 recall 也接近 0。原因這是 200 張小數(shù)據(jù)集最經(jīng)典的翻車現(xiàn)場。壓縮包里的標(biāo)簽類別編號和你在 data.yaml 里定義的類別順序不一致。比如數(shù)據(jù)集的標(biāo)簽文件里 0 代表 crop、1 代表 weed但你的 data.yaml 里 0 寫了 weed、1 寫了 crop。模型在訓(xùn)練時(shí)看到的目標(biāo)類別和真實(shí)類別完全錯(cuò)位驗(yàn)證時(shí)自然一個(gè)都認(rèn)不出來。解決不要靠肉眼猜。訓(xùn)練前先寫一段 5 行的腳本把標(biāo)簽 txt 里實(shí)際出現(xiàn)的類別 ID 全部打印出來再和 data.yaml 的 names 一一對照from pathlib import Path label_dir Path(labels/train) all_cls set() for txt in label_dir.glob(*.txt): for line in txt.read_text(encodingutf-8).strip().splitlines(): if line.strip(): all_cls.add(int(line.split()[0])) print(標(biāo)簽中實(shí)際存在的類別 ID:, sorted(all_cls))如果打印結(jié)果是[0, 1]但你覺得 weed 應(yīng)該是第 0 類那就核對轉(zhuǎn)換腳本里的 class_names 順序。這個(gè)腳本應(yīng)該是你整個(gè)流程的第一步每次換新數(shù)據(jù)集都要跑一遍別嫌麻煩。4.2 驗(yàn)證 loss 第 30 輪抬頭過擬合比預(yù)想來得早現(xiàn)象訓(xùn)練集 loss 一直在降驗(yàn)證集 loss 在第 20 到 30 輪左右開始掉頭向上同時(shí)驗(yàn)證集 mAP 不再上漲甚至下滑。原因200 張圖本來就少模型在訓(xùn)練集上轉(zhuǎn)了兩三輪就能把標(biāo)注目標(biāo)背下來從第 20 輪開始就進(jìn)入死記硬背階段。這和模型大小也有關(guān)如果用 YOLOv8s 甚至更大的模型過擬合會(huì)來得更快。解決三個(gè)手段配合使用。第一模型先用 nano 級YOLOv8n 對 200 張小數(shù)據(jù)集是更安全的選擇參數(shù)少意味著記憶能力弱、泛化壓力大。第二patience30配合早??吹津?yàn)證 loss 掉頭后自動(dòng)停止不用手工盯。第三把驗(yàn)證集比例從 20% 提到 25%200 張里分出 50 張做驗(yàn)證能更早觀測到過擬合信號。4.3 雜草框老是偏大偏小標(biāo)注質(zhì)量和 imgsz 的鍋現(xiàn)象訓(xùn)練完看預(yù)測結(jié)果框住雜草的邊界框明顯偏大一個(gè)框里裝了土和相鄰的作物葉子或者框偏小只框住了雜草的局部。原因兩個(gè)來源。一是原始標(biāo)注就不準(zhǔn)——作物雜草這類密集場景標(biāo)注員很難精確框住每一株的外觀特別是葉片交叉部分很多人會(huì)隨手把整個(gè)葉片簇框進(jìn)去二是模型推理時(shí)用的 imgsz 和訓(xùn)練時(shí)差距太大導(dǎo)致目標(biāo)尺度失配訓(xùn)練 640 推理 1280 雖然能讓小目標(biāo)更清晰但邊界框更傾向于放大。解決先檢查標(biāo)注質(zhì)量。用yolo predict跑幾張驗(yàn)證集圖片把預(yù)測框和真實(shí)框疊加看如果普遍偏大回到標(biāo)注文件檢查。如果標(biāo)注本身沒問題只是推理時(shí)想提高小目標(biāo)檢測可以試試在訓(xùn)練階段就把 imgsz 提到 768而不是訓(xùn)練 640 推理 1280 這種割裂做法。4.4 綠草里找不到雜草對比度不足時(shí)調(diào)增強(qiáng)參數(shù)現(xiàn)象模型在驗(yàn)證集 mAP 還行但實(shí)際檢測時(shí)雜草檢出率很低特別是背陰處和土壤背景里的雜草幾乎全漏。原因這是作物雜草場景最典型的視覺難題——雜草和背景都是綠的對比度天然低。如果原圖是在陰天或逆光下拍的細(xì)節(jié)對比更差。模型在低對比度圖像上學(xué)到的特征不夠銳利推理時(shí)自然漏檢。解決不換算法先調(diào)數(shù)據(jù)增強(qiáng)。Ultralytics 訓(xùn)練器內(nèi)置了 HSV 增強(qiáng)參數(shù)在 data.yaml 同級可以建一個(gè) hyp.yaml 覆蓋默認(rèn)值# hyp.yaml hsv_h: 0.02 hsv_s: 0.8 hsv_v: 0.5把hsv_s從默認(rèn)的 0.7 提到 0.9、hsv_v從 0.4 提到 0.6讓模型在訓(xùn)練中看到更多色彩飽和度和明暗變化強(qiáng)迫它去學(xué)形狀和紋理特征而不是依賴顏色。如果你的圖像普遍偏暗可以在數(shù)據(jù)預(yù)處理階段加一個(gè)直方圖均衡化步驟但這需要在訓(xùn)練前把圖像統(tǒng)一處理不要混著來。4.5 指標(biāo)很好看但田間就廢分布偏移和盲測集現(xiàn)象驗(yàn)證集 mAP50 達(dá)到 0.9precision 和 recall 都過 0.8但拿到另一個(gè)地塊或不同天候下實(shí)拍的照片上跑檢測結(jié)果慘不忍睹。原因這就是機(jī)器學(xué)習(xí)里說的分布偏移。200 張訓(xùn)練圖如果都來自同一塊地和同一天的光照條件模型學(xué)到的其實(shí)是“這個(gè)地塊的雜草長這樣、這個(gè)光照下的顏色是這樣”而不是“雜草長什么樣”。驗(yàn)證集也是從同一個(gè)分布里切的指標(biāo)自然虛高。解決在訓(xùn)練之前就預(yù)留盲測集。從 200 張之外找 20 到 30 張完全不同地塊或不同時(shí)間拍攝的圖不打進(jìn)訓(xùn)練也不打進(jìn)驗(yàn)證等訓(xùn)練結(jié)束后再跑一遍。指標(biāo)以盲測結(jié)果為準(zhǔn)驗(yàn)證集 mAP 只是用來選模型的參考不是用來向別人證明效果的素材。5. 驗(yàn)證模型能不能下地混淆矩陣、PR 曲線和盲測腳本5.1 混淆矩陣讀法草被認(rèn)成作物和漏檢要分開看訓(xùn)練結(jié)束后的第一步不是急著部署而是先跑一遍驗(yàn)證yolo val modelruns/detect/train/weights/best.pt datadata.yaml這個(gè)命令會(huì)在runs/detect/val/下生成 confusion_matrix.png、PR_curve.png、F1_curve.png 等一系列圖表。很多人只看 mAP 就結(jié)束了但混淆矩陣才是判斷模型能否下地的關(guān)鍵。在作物雜草場景里混淆矩陣?yán)镒钗kU(xiǎn)的不是“把雜草認(rèn)成作物”而是“雜草那一行的召回率過低”。把雜草認(rèn)成作物后果是漏噴雜草繼續(xù)生長把作物認(rèn)成雜草后果是誤噴整片作物受損。兩種錯(cuò)誤的代價(jià)完全不同。所以看混淆矩陣時(shí)先看雜草類別的召回率再看作物類別的精確率最后才看整體準(zhǔn)確率。5.2 PR 曲線怎么用把置信度閾值調(diào)到實(shí)際場景的平衡點(diǎn)PR_curve.png 展示的是在不同置信度閾值下 precision 和 recall 的折中關(guān)系。默認(rèn)推理閾值是 0.25這個(gè)值對雜草檢測來說可能偏低或偏高取決于你的場景偏誤檢還是偏漏檢。如果下地后發(fā)現(xiàn)雜草漏檢多把置信度閾值降到 0.15 或 0.1多框出來一些無所謂后面可以人工篩選。如果發(fā)現(xiàn)誤檢太多把閾值提到 0.4減少誤報(bào)的代價(jià)是犧牲一部分召回率。調(diào)閾值不需要重新訓(xùn)練推理時(shí)加一個(gè)conf參數(shù)就行yolo predict modelruns/detect/train/weights/best.pt source../field_test/ conf0.15實(shí)際使用中我習(xí)慣先用 0.25 跑一遍盲測數(shù)一下漏檢和誤檢數(shù)量再根據(jù)代價(jià)方向決定閾值往哪個(gè)方向調(diào)。5.3 用沒見過的田間照片做 blind test一段能直接跑的腳本盲測集是驗(yàn)證模型能不能用的最后一道關(guān)口。從不同地塊或不同時(shí)間拍的圖里找 20 張左右手工標(biāo)好每張圖里雜草和作物的數(shù)量然后跑一段推理腳本統(tǒng)計(jì)檢測結(jié)果from ultralytics import YOLO from pathlib import Path model YOLO(runs/detect/train/weights/best.pt) test_dir Path(field_test) # 放盲測圖片的目錄 for img_path in sorted(test_dir.glob(*.jpg)): result model(img_path, conf0.25, verboseFalse)[0] boxes result.boxes if boxes is None or len(boxes) 0: print(f{img_path.name}: 無檢測) continue for box in boxes: cls_id int(box.cls[0]) conf float(box.conf[0]) # 輸出文件名、類別名和置信度 print(f{img_path.name}: {result.names[cls_id]} conf{conf:.2f}) # 打印每張圖檢測到的各類別數(shù)量 cls_count {} for box in boxes: name result.names[int(box.cls[0])] cls_count[name] cls_count.get(name, 0) 1 print(cls_count)這段腳本對每張盲測圖輸出檢測到的類別和數(shù)量拿這個(gè)和手工統(tǒng)計(jì)的“真值表”對比算一下實(shí)際召回率。判斷標(biāo)準(zhǔn)很簡單雜草漏檢率必須低于 5%因?yàn)槁z意味著漏噴作物誤檢率需要根據(jù)你的噴灑代價(jià)來決定如果誤噴直接造成經(jīng)濟(jì)損失那閾值還要再往上調(diào)。盲測時(shí)有一個(gè)很容易犯的錯(cuò)誤把置信度調(diào)得極低來追求召回率結(jié)果一張圖里輸出幾十個(gè)框。這種模型的精確率已經(jīng)崩了下地實(shí)際使用會(huì)被大量誤檢淹沒。所以盲測的結(jié)論要同時(shí)給出“在這個(gè)閾值下的漏檢數(shù)和誤檢數(shù)”而不是只看一個(gè)指標(biāo)。6. 從 200 張到能長期用的模型數(shù)據(jù)增強(qiáng)、偽標(biāo)簽和半監(jiān)督的落地順序200 張圖訓(xùn)練出的模型只能算一個(gè)能用的起點(diǎn)離“長期穩(wěn)定地服務(wù)于農(nóng)業(yè)生產(chǎn)”還有距離。接下來的路徑也很明確按順序做就可以。第一步是數(shù)據(jù)增強(qiáng)。不要一上來就拍腦袋加旋轉(zhuǎn)、翻轉(zhuǎn)先看模型在盲測集上的失敗模式。如果漏檢發(fā)生在光照暗的區(qū)域重點(diǎn)加 HSV 亮度擾動(dòng)如果漏檢發(fā)生在密集的雜草叢重點(diǎn)加 Mosaic 增強(qiáng)。Ultralytics 默認(rèn)開了 Mosaic200 張圖配 Mosaic 是夠用的但不要太激進(jìn)增強(qiáng)過頭會(huì)讓模型學(xué)到一堆不真實(shí)的紋理。第二步是偽標(biāo)簽。用 best.pt 在更多未標(biāo)注圖像上跑推理把置信度高于 0.7 的檢測結(jié)果轉(zhuǎn)成 YOLO 標(biāo)簽人工抽查其中一部分然后把這些偽標(biāo)簽數(shù)據(jù)混進(jìn)訓(xùn)練集。這個(gè)做法能快速擴(kuò)數(shù)據(jù)前提是人工抽查比例足夠高不能直接把模型自己的輸出喂回去訓(xùn)練否則錯(cuò)誤會(huì)在迭代中被不斷放大。第三步才是考慮更多標(biāo)注數(shù)據(jù)。優(yōu)先補(bǔ)充盲測里漏檢最多的那些場景而不是隨機(jī)拍一堆同樣地塊的照片。數(shù)據(jù)質(zhì)量比數(shù)量重要得多50 張覆蓋了不同光照、不同土壤背景的標(biāo)注圖效果可能好過 500 張同一地塊湊數(shù)的圖。我自己的習(xí)慣是200 張圖的模型永遠(yuǎn)只當(dāng)基線來看。每次換地塊或換設(shè)備都要重新收集一小批當(dāng)?shù)貓D像做盲測用失敗樣例決定下一步動(dòng)作。這套打法的核心不是追求一次訓(xùn)練直接達(dá)標(biāo)而是讓數(shù)據(jù)和模型迭代形成一個(gè)能收斂的閉環(huán)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取