:從數(shù)據(jù)轉(zhuǎn)換到模型訓練避坑指南)
簡介基于YOLOv5的道路交通標志識別項目以完整源碼與配套數(shù)據(jù)集打包專為畢業(yè)設計、期末大作業(yè)及課程設計打造。代碼注釋詳實從數(shù)據(jù)準備到模型訓練、推理部署均有清晰說明新手可快速上手項目曾獲導師高度認可的98分部署門檻低下載解壓即可運行。資源共266個文件包含YOLOv5模型配置yaml、訓練權(quán)重pt、Python腳本py、標注圖片jpg/png及數(shù)據(jù)集標簽等整體壓縮包423.32MB目錄結(jié)構(gòu)分明便于逐模塊學習與二次開發(fā)。目前已吸引439人學習下載適合計算機視覺方向的學生參考其算法實現(xiàn)與工程組織方式作為高分開題或答辯展示亦具說服力。1. 交通標志識別為什么是 yolov5 的主場畢業(yè)設計選型先把生態(tài)摸清楚道路交通標志識別是目標檢測方向里最常被拿來做畢業(yè)設計的真實場景之一原因很直白公開數(shù)據(jù)集好找、場景貼近實際生活、交付時能講出完整的故事。yolov5 也是我接到這類選題時默認推薦的第一方案。它未必每一項指標都比新模型強但它把“數(shù)據(jù)準備—訓練—驗證—導出部署”整條鏈路收得很干凈你搭出來的這套流程以后換到任何檢測項目都能復用。這篇筆記從零走一遍先跑通 yolov5 的最小推理再把交通標志數(shù)據(jù)集轉(zhuǎn)成訓練格式接著把訓練、調(diào)參、踩坑、驗收講透。不管手里是 40 系顯卡還是老款 1060這套流程都走得動。2. 跑通 yolov5 最小系統(tǒng)環(huán)境配置、權(quán)重下載與第一次推理2.1 conda 新建環(huán)境這一步?jīng)Q定后面幾天的心態(tài)第一次裝 yolov5 最容易翻車的就是環(huán)境。torch 裝錯 CUDA 版本或者直接往 base 環(huán)境里堆依賴后面 protobuf、dill、matplotlib 各種打架能把人折騰到想換題。我一般會先按題目建一個干凈的 conda 環(huán)境Python 版本按官方倉庫要求來3.8 到 3.10 之間的常見版本都可以然后按有沒有 NVIDIA 顯卡決定裝 GPU 版還是 CPU 版 torch。CPU 版訓練慢但把流程跑通沒問題后面再租云 GPU 也不遲。# 創(chuàng)建并激活獨立環(huán)境避免污染 base conda create -n yolo5 python3.8 -y conda activate yolo5 # 按顯卡情況安裝 torch/torchvision有 CUDA 就選對應版本沒有就裝 CPU 版 pip install torch torchvision # 拉取 yolov5 源碼并安裝依賴 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt這里真正要關(guān)注的是最后一行 requirements.txt它會一次性裝齊 numpy、opencv、pandas、tensorboard 這些訓練和可視化依賴。常見錯誤是跳過這步直接用自己的環(huán)境跑 detect.py結(jié)果報No module named torchvision或者 opencv 版本對不上。另外一個經(jīng)驗是裝完依賴后用一行命令檢查 CUDA 是否真的可用省掉后面訓練時才發(fā)現(xiàn)沒用上 GPU 的時間。python -c import torch; print(torch.__version__, torch.cuda.is_available())如果輸出末尾是 True說明 torch 正確識別到了顯卡如果是 False要么裝成了 CPU 版要么 CUDA 驅(qū)動不對。這一步排查成本很低但能避免你訓練一整晚后才發(fā)現(xiàn)全程在用 CPU 硬扛的尷尬。顯卡驅(qū)動和 torch 的 CUDA 版本對應關(guān)系不復雜但確實有點玄學我的建議是驅(qū)動別追最新去 torch 官方安裝頁看自己驅(qū)動支持到哪個 CUDA 版本再按那個索引裝 torch。2.2 第一次推理用官方 yolov5s 驗證鏈路通不通代碼和依賴都就位以后先別急著碰自己的數(shù)據(jù)集用官方預訓練權(quán)重做一次推理把整條鏈路驗證通。這一步的意義是區(qū)分“代碼環(huán)境問題”和“數(shù)據(jù)問題”如果官方權(quán)重跑不出框說明環(huán)境還有坑不值得帶著壞環(huán)境去訓練自己的模型。推理命令極其簡單第一次跑會慢一點因為要下載 yolov5s.pt 權(quán)重。python detect.py --weights yolov5s.pt --source data/images/street.jpg --conf 0.4這條命令里yolov5s.pt 是官方提供的最小可訓練權(quán)重文件會放到 weights 目錄--source可以是一張圖片、一個視頻文件、一個目錄實戰(zhàn)最常用的三種輸入都在這--conf 0.4是置信度閾值低于 0.4 的預測框被丟掉。跑完后結(jié)果默認寫在runs/detect/exp/里面那張帶框的圖就是我們想要的輸出??吹綀D里準確框出“人、車”這類 COCO 類別說明模型文件、圖像解碼、NMS 后處理全部正??梢赃M入數(shù)據(jù)階段。如果這一步卡住絕大多數(shù)是 opencv 讀取視頻幀出錯少部分是 matplotlib 在無顯示環(huán)境下保存圖片異常。我一般會順手加兩個參數(shù)再跑一次--save-txt保存每張圖的坐標文本--save-conf把置信度一起寫入文本。這兩個參數(shù)后面做指標分析時非常有用現(xiàn)在就養(yǎng)成習慣后面不慌。python detect.py --weights yolov5s.pt --source data/images/street.jpg --conf 0.4 --save-txt --save-conf2.3 交通標志數(shù)據(jù)集長什么樣VOC xml 與 yolov5 txt 的對應關(guān)系yolov5 訓練時讀取的不是 xml而是每個圖片對應的同名 txt 標注文件。第 3 章會寫完整轉(zhuǎn)換腳本這里先把格式差異講清楚因為后面所有翻車都從這里開始。項目VOC 格式xmlyolov5 格式txt坐標表達絕對像素坐標 xmin、ymin、xmax、ymax歸一化中心點 x_center、y_center、寬、高數(shù)值范圍0 到圖片寬/高之間0 到 1 之間的小數(shù)類別表達字符串名稱如 stop、pedestrian從 0 開始的整數(shù)編號存儲方式每個對象一個object節(jié)點每行一個對象類別 x y w h舉一個實際例子某個 VOC 標注片段長這樣object namestop/name bndbox xmin120/xmin ymin80/ymin xmax190/xmax ymax150/ymax /bndbox /object如果這張圖寬 640、高 480且 stop 在類別表里排第 2 位那么對應的 yolov5 標簽行是2 0.242187 0.239583 0.109375 0.145833。四個小數(shù)分別對應中心 x、中心 y、寬、高全部是歸一化結(jié)果。交通標志數(shù)據(jù)集的原始標注多半是 VOC 或 COCO 風格所以轉(zhuǎn)換時最容易錯的不是加減乘除而是忘了歸一化或把 xmax 減 xmin 的寬度算成高度。轉(zhuǎn)換完一定要抽幾張圖把框畫出來肉眼檢查這是所有后續(xù)訓練的后悔藥。3. 把交通標志數(shù)據(jù)集轉(zhuǎn)成 yolov5 格式voc2yolo 腳本與標簽校驗3.1 坐標換算邏輯為什么中心點格式和 VOC 差著一層除法VOC 的 bndbox 給的是左上角和右下角的絕對像素坐標yolov5 要的是歸一化中心點格式換算公式并不復雜。設圖片寬為 W、高為 H有x_center (xmin xmax) / 2 / Wy_center (ymin ymax) / 2 / Hbox_w (xmax - xmin) / Wbox_h (ymax - ymin) / H分母必須是圖片的實際寬高不是標注框的寬高也不是模型輸入尺寸的 640。這個細節(jié)看著不起眼實際踩的人很多有人把坐標換算成 0 到 1 之后又拿模型輸入尺寸 640 乘了一遍結(jié)果所有框都縮成了極小值也有人直接把 W 和 H 寫反訓練出的框全部偏斜。轉(zhuǎn)換腳本本身很簡單真正值錢的是轉(zhuǎn)換后的自動校驗因為這類錯誤不會報異常只會在訓練曲線上慢慢折磨你。3.2 轉(zhuǎn)換腳本一份能直接跑的 voc2yolo下面這份腳本面向最常見的數(shù)據(jù)集組織方式raw/annotations放 xmlraw/images放同名 jpg輸出寫到labels/目錄。類名單用固定順序的列表維護順序一旦定下來就不要改。# voc2yolo.py import glob import os import xml.etree.ElementTree as ET def convert_one(xml_path, txt_path, class_names): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) if img_w 0 or img_h 0: return False lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: # 遇到未知類別先跳過但外層要能統(tǒng)計到不能吞掉錯誤 continue cls_id class_names.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 裁剪到圖像邊界防止坐標越界導致訓練增強時報錯 xmin max(0.0, min(img_w - 1, xmin)) ymin max(0.0, min(img_h - 1, ymin)) xmax max(0.0, min(img_w - 1, xmax)) ymax max(0.0, min(img_h - 1, ymax)) if xmax xmin or ymax ymin: continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return True if __name__ __main__: # 順序必須和后面的 data.yaml 完全一致這是最容易踩的坑 class_names [speedlimit-20, speedlimit-30, stop, pedestrian, warning, no_entry, yield] os.makedirs(labels, exist_okTrue) total 0 empty 0 for xml_path in sorted(glob.glob(raw/annotations/*.xml)): base os.path.basename(xml_path).replace(.xml, ) img_path os.path.join(raw/images, base .jpg) if not os.path.exists(img_path): print(missing image:, img_path) continue txt_path os.path.join(labels, base .txt) ok convert_one(xml_path, txt_path, class_names) if not ok: print(bad annotation:, xml_path) if os.path.exists(txt_path) and os.path.getsize(txt_path) 0: empty 1 total 1 print(fprocessed {total}, empty {empty})這段代碼值得展開講幾個點。第一root.iter(object)用的是深層遍歷有些 xml 里嵌套了segmented或part節(jié)點用findall(object)只會取到第一層容易漏掉目標。第二裁剪邊界那四行不是多余的公開數(shù)據(jù)集的框經(jīng)常比圖像寬高多出幾個像素不裁會讓 yolov5 在 Mosaic 增強階段直接報錯。第三empty計數(shù)很關(guān)鍵如果某些類別不在class_names里被靜默跳過就會生成大量空 txt而空標注文件在訓練時等于告訴模型“這張圖沒有目標”輕則浪費樣本重則把背景學成負樣本。腳本跑完processed和empty兩個數(shù)字必須心里有數(shù)empty占比超過 1% 就要回頭排查類別命名。3.3 訓練集/驗證集劃分隨機種子、比例與文件搬運轉(zhuǎn)換完之后要把數(shù)據(jù)按比例拆成訓練集和驗證集。我的習慣是 8:1 到 9:1 之間交通標志樣本數(shù)量通常夠用驗證集至少留 100 張以上不然 mAP 曲線波動大得沒法判斷。劃分腳本不難難在“怎么分得科學”。# split_data.py import glob import os import random import shutil random.seed(2024) # 固定種子保證每次劃分結(jié)果可以復現(xiàn) imgs sorted(glob.glob(raw/images/*.jpg)) random.shuffle(imgs) val_ratio 0.2 val_num int(len(imgs) * val_ratio) os.makedirs(dataset/images/train, exist_okTrue) os.makedirs(dataset/images/val, exist_okTrue) os.makedirs(dataset/labels/train, exist_okTrue) os.makedirs(dataset/labels/val, exist_okTrue) for idx, img_path in enumerate(imgs): base os.path.basename(img_path)[:-4] src_label os.path.join(labels, base .txt) if idx val_num: shutil.copy(img_path, dataset/images/val/) if os.path.exists(src_label): shutil.copy(src_label, dataset/labels/val/) else: shutil.copy(img_path, dataset/images/train/) if os.path.exists(src_label): shutil.copy(src_label, dataset/labels/train/)劃分這里有個隱藏問題如果原始數(shù)據(jù)是視頻抽幀同一段路的連續(xù)幾幀高度相似隨機洗牌后可能同一場景的幀同時出現(xiàn)在訓練和驗證集里這叫“泄漏”。驗證集看起來指標很高但換一段新視頻就崩。交通標志公開數(shù)據(jù)集大多是不同地點單獨拍攝的圖片這個問題不嚴重但如果你是自采視頻抽幀必須先把連續(xù)幀按視頻片段分組整組劃分。舉個例子video01_0001.jpg到video01_0100.jpg是一組劃分時把組作為一個最小單位而不是把單幀打散。劃分完再打印訓練和驗證各自的圖片數(shù)、標簽數(shù)、每個類別的樣本數(shù)這一步能提前暴露漏拷 label 的問題。3.4 data.yaml類別名順序決定訓練結(jié)果能不能對齊yolov5 訓練時的數(shù)據(jù)配置集中在 data.yaml 里格式如下# dataset/traffic_sign.yaml path: ./ train: images/train val: images/val nc: 7 names: 0: speedlimit-20 1: speedlimit-30 2: stop 3: pedestrian 4: warning 5: no_entry 6: yieldpath是配置文件所在目錄的基準路徑train 和 val 都相對它寫不要寫絕對路徑否則換一臺機器訓練就要改一遍。nc是類別數(shù)必須和 names 的長度一致。這里的 names 順序必須和第 3.2 節(jié)轉(zhuǎn)換腳本里的class_names完全一致因為 txt 里只存整數(shù) id順序錯一個整個模型的語義就錯位了。比如 stop 排到第 0 位訓練出來模型記住的 0 號類別是 stop而你推理時顯示的第 0 位可能寫成了 speedlimit-20。這類錯誤不報任何異常只能靠推理階段手動對比一旦發(fā)現(xiàn)就要回爐重排標簽。驗證 data.yaml 最直接的方法是畫圖檢查用 opencv 把訓練集圖片和 label 框畫在一起隨機抽 20 張看類別名和框是否對得上。4. 訓練自己的交通標志識別模型預訓練權(quán)重、超參數(shù)與曲線判讀4.1 預訓練權(quán)重選 s 還是 m精度、顯存和訓練時間的三角yolov5 官方權(quán)重從 n 到 x 一溜排開參數(shù)量和精度依次上漲。做交通標志識別這種不算太難的檢測任務我一般不推薦一上來就選最大的實踐中最常用的是 s 和 m 兩檔。yolov5s 顯存占用小訓練速度快6G 顯存跑 batch 16 沒有壓力適合先通流程yolov5m 精度比 s 高一截但顯存和時間成本上漲明顯適合在 s 已經(jīng)把流程跑通、準備沖最終指標時換上去。交通標志本身是小目標居多模型容量稍微大一點確實有幫助但前提是數(shù)據(jù)沒有大問題否則就是拿顯卡電費買教訓。預訓練權(quán)重還有個容易被忽略的用法當你自己標注的數(shù)據(jù)量不夠時先從公開交通標志數(shù)據(jù)集訓一版保留這個權(quán)重再用自己的數(shù)據(jù)繼續(xù)微調(diào)。這比每次從 COCO 預訓練權(quán)重開始效果更直接因為 COCO 的 80 類里沒有專門的交通標志語義模型要花更多時間去理解什么是“限速牌”。遷移學習不是玄學它解決的是“數(shù)據(jù)量不夠時模型先從大模型偷學特征”的問題交通標志識別這種任務很適合這個套路。4.2 訓練命令與必調(diào)超參數(shù)batch、epoch、imgsz 和 hyp 文件一切就緒后訓練命令長這樣python train.py \ --data dataset/traffic_sign.yaml \ --weights yolov5s.pt \ --epochs 120 \ --batch-size 16 \ --imgsz 640 \ --workers 4 \ --device 0逐個拆開說。--data指向第 3 章寫的 data.yaml--weights是預訓練權(quán)重最好先下載到本地再填路徑避免訓練到一半聯(lián)網(wǎng)拉權(quán)重失敗--epochs交通標志這類數(shù)據(jù)集 100 到 150 輪足夠再多就有過擬合傾向--batch-size越大 loss 曲線越平滑但受顯存限制6G 顯存配 batch 16 比較中庸--imgsz是訓練輸入分辨率交通標志小目標多條件允許就上 640 甚至 1280顯存不夠再往下退--workers是數(shù)據(jù)加載進程數(shù)Windows 上設成 0 反而更穩(wěn)Linux 下 4 或 8 都沒問題。yolov5 的超參數(shù)默認值在data/hyps/hyp.scratch-low.yaml里做交通標志識別我一般會單獨建一個 hyp 文件調(diào)三處學習率、色域增強、mosaic 開關(guān)。一個常用的修改版長這樣# data/hyps/hyp.traffic.yaml lr0: 0.005 # 初始學習率默認 0.01數(shù)據(jù)規(guī)模小時調(diào)低更可靠 lrf: 0.01 # 最終學習率 lr0 * lrf余弦退火的終點 momentum: 0.937 weight_decay: 0.0005 hsv_h: 0.015 # 色調(diào)擾動標志顏色本身是語義不能擾得太狠 hsv_s: 0.7 # 飽和度擾動提高對褪色標志的適應 hsv_v: 0.4 # 亮度擾動模擬逆光和陰影 degrees: 0.0 # 旋轉(zhuǎn)設為 0交通標志不會倒著出現(xiàn) translate: 0.1 # 平移擾動讓目標出現(xiàn)在不同位置 scale: 0.5 # 縮放擾動模擬遠近變化 mosaic: 1.0 # mosaic 增強默認開對小目標訓練幫助明顯 mixup: 0.1 # mixup 比例太大會讓標志邊緣變糊把文件傳給訓練命令python train.py ... --hyp data/hyps/hyp.traffic.yaml。這里要強調(diào)兩個方向。一是交通標志的顏色是關(guān)鍵特征hsv_h不能設太大否則紅色禁令牌被擾動成藍色模型會學歪二是旋轉(zhuǎn)擾動對標志這類剛性物體應該關(guān)掉現(xiàn)實中標志不會橫著長。很多人拿默認 hyp 直接訓效果也能看但這類任務相關(guān)的細節(jié)是自己調(diào)優(yōu)時真正值錢的地方。還有一個參數(shù)值得單獨說--multi-scale它讓模型在訓練中隨機使用不同輸入尺寸對交通標志這種尺度變化大的任務很友好代價是訓練時間變長、曲線更抖適合數(shù)據(jù)量大的時候用。4.3 訓練曲線怎么讀loss、mAP 和過擬合信號訓練過程中yolov5 會實時把指標寫進runs/train/exp/results.csv和results.png。第一次訓練的人最容易犯的錯是只盯訓練集 loss看到 train/box_loss 一路下降就以為萬事大吉其實更要看的是 val 側(cè)指標以及最終訓練輸出目錄里weights/best.pt和last.pt的區(qū)別。正常曲線一般長這樣前 10 輪 loss 下降又快又陡20 輪后逐漸平緩mAP0.5 上升后出現(xiàn)平臺。如果 validation loss 先降后升而 train loss 還在降這是過擬合信號解決辦法不是繼續(xù)加數(shù)據(jù)增強而是提前停止、減小學習率或者減少 epoch。如果 mAP 從第一輪開始就在 0 附近抖動先別調(diào)參回頭檢查標簽十有八九是第 3 章的歸一化或類別順序問題。訓練結(jié)束時會自動選 mAP 最高的權(quán)重存為 best.pt后面所有推理、驗證、導出都用它不要順手用了 last.pt后者通常是最后一輪的權(quán)重過擬合程度更高。看曲線時我還習慣把results.csv用 pandas 讀出來按輪次畫精度的局部放大圖這樣比直接看 results.png 更細。import pandas as pd df pd.read_csv(runs/train/exp/results.csv) print(df.columns) # 列出所有指標列 print(df[[metrics/mAP_0.5, metrics/mAP_0.5:0.95, val/box_loss]].tail())這樣做的好處是能精確看到 mAP 從第幾輪開始不再上升方便你決定訓練輪數(shù)而不是盲目填一個 300 輪跑三天。4.4 顯存不夠怎么辦降分辨率、調(diào) nbs、用梯度累積顯存不夠是這臺課設最常遇到的事尤其老顯卡上跑交通標志的高分辨率大圖。最直接的辦法是把--imgsz從 640 降到 512 或 416代價是小目標更難學所以這只適合數(shù)據(jù)里目標本身不太小的場景。第二個辦法是把--batch-size調(diào)小到 8 甚至 4同時配合--nbs 64。yolov5 的 nbs 參數(shù)是“名義 batch size”它的機制是用梯度累積模擬大 batch實際 batch 小但每 N 步累加一次梯度再更新權(quán)重效果上等價于一個大 batch。我這個例子把 nbs 設成 64實際 batch 16 的話模型相當于每 4 步做一次更新既省顯存又不犧牲太多訓練穩(wěn)定性。注意 loss 的放縮也因此變了不要只看表面數(shù)值對比 mAP 曲線才是判斷依據(jù)。5. 避坑手冊交通標志識別里 5 個典型的翻車現(xiàn)場與解法5.1 坐標全部錯亂的坑xml 看起來沒問題txt 卻全亂現(xiàn)象轉(zhuǎn)換腳本跑完抽查某一兩張圖框的位置明顯對不上物體有的框跑到圖外有的把所有物體堆在左上角。訓練時 loss 一開始就很大驗證集 mAP 始終在個位數(shù)徘徊。原因常見兩類。一類是寬度寫成(xmax - xmin)但忘了除以圖像寬導致框?qū)捠菤w一化后的幾百倍另一類是圖像本身存在 EXIF 旋轉(zhuǎn)VOC xml 里的寬高是按旋轉(zhuǎn)前算的而訓練時讀圖和轉(zhuǎn)換腳本讀圖用到了不同方向的尺寸信息。交通標志數(shù)據(jù)集很多從行車記錄儀截取EXIF 里的 Orientation 字段經(jīng)常暗藏問題轉(zhuǎn)出來的框錯位一點都看不出來直到訓練階段 loss 爆炸。解決先用一個 40 行的小腳本把“原圖 預測框”畫出來隨機抽 20 張肉眼檢查。一旦發(fā)現(xiàn)錯位回到原始圖片用 PIL 讀 EXIF 方向統(tǒng)一按旋轉(zhuǎn)后的寬高重算。我的習慣是轉(zhuǎn)換前先把所有圖片統(tǒng)一轉(zhuǎn)成標準方向并覆蓋寫入再跑轉(zhuǎn)換腳本從根上避免這個黑匣子。5.2 小目標漏檢路牌明明在畫面上模型就是不報現(xiàn)象訓練完成后 mAP 看著不小但驗證集中距離較遠的限速牌、小型警告牌一個都檢不出來畫面上 30×30 像素的小標志直接穿過。原因交通標志數(shù)據(jù)集里源圖往往是 2048×2048 甚至更大的行車記錄儀截圖直接 resize 到 640 之后一個 30 像素的標志只剩 9 個像素特征基本消失。模型不是不會檢而是這個目標在輸入圖上已經(jīng)不存在了。這類小目標漏檢是交通標志識別的標配問題跟模型大小關(guān)系不大。解決最有效的手段是分塊訓練和分塊推理。常見做法是維護一個滑窗裁切步驟把大圖切成 640×640 且?guī)е丿B的若干塊只保留中央?yún)^(qū)域的標注最后匯總各塊檢測結(jié)果做去重。如果嫌麻煩就把--imgsz提到 1280 直接訓練顯存不夠就配合第 4.4 節(jié)降 batch。這兩個方向是交通標志小目標問題的兩條主線光靠調(diào) conf 閾值解決不了問題。另外訓練時保持默認的 Mosaic 增強對小目標有增益不要因為曲線波動就關(guān)掉先跑完再判斷。5.3 逆光與夜間場景數(shù)據(jù)增強讓燈牌從背景里顯出來現(xiàn)象白天測試效果挺好換成逆光路段或傍晚場景召回率直線下滑標志牌上反光、背光模型要么漏檢要么把路燈當目標框出來。原因數(shù)據(jù)集大多在白天晴天采集光照分布單一模型學到的其實是“白日光照下的標志長什么樣”而不是“標志的語義形狀”。逆光時標志牌的邊緣對比度下降YOLO 的深層特征提取不出來夜間標志靠反光材質(zhì)與背景區(qū)分白天訓練數(shù)據(jù)里沒有這種對比模式。解決在數(shù)據(jù)增強上對癥下藥。一是調(diào)大 hyp 文件里的hsv_v和translate并額外加對比度擾動讓模型見過更暗、更偏色的輸入二是如果數(shù)據(jù)集里完全沒有夜間樣本值得單獨采一小批夜間圖混進訓練集人工標注幾百張就夠。增強不是越多越好特別是把顏色擾得太狠會影響標志類別判斷這個度要在驗證集上實測。實戰(zhàn)中我會跑兩組實驗一組默認增強一組加了逆光擾動對比逆光測試集上的 recall數(shù)值說話。5.4 類別極度不均少樣本類別 mAP 被拖到腳面現(xiàn)象val 輸出的每個類別指標里stop、限速牌這類常見類別 mAP 很高少數(shù)類別如“禁止拖拉機”一類的 mAP 是 0整個項目的平均指標被拖低一大截。原因公開交通標志數(shù)據(jù)集天然存在長尾分布。常見標志出現(xiàn)幾千次少數(shù)標志只有幾十次yolov5 的 loss 是按 batch 平均計算的多數(shù)類別學得多、少數(shù)類別沒機會學。這不是模型壞了是統(tǒng)計規(guī)律。解決最可靠的是在數(shù)據(jù)層面做類別平衡不要只在網(wǎng)絡層面加損失權(quán)重。常見做法是把少數(shù)類別樣本重復復制進訓練集或者對包含少數(shù)類別的圖片做額外增強后擴充如果自建數(shù)據(jù)就注意采集時主動補采少數(shù)類別。另一個技巧是先在完整公開數(shù)據(jù)上預訓練再微調(diào)自己的少數(shù)類別。判斷依據(jù)仍然是驗證集上逐類打印的 P、R、mAP不要只看總平均。訓練完第一件事就是打印 per-class 指標哪類最低就去查那類的訓練樣本數(shù)是 30 張還是 300 張心里要有數(shù)。5.5 導出 onnx 后指標掉一截預處理不一致的坑現(xiàn)象PyTorch 下推理 mAP 0.93導出 onnx 后用 onnxruntime 重新跑同一張圖框的位置偏了幾個像素低置信度目標消失指標掉了 5 個點以上。原因yolov5 推理用 letterbox 保持長寬比并把圖填充到 640×640導出 onnx 后用 opencv 或 onnxruntime 加載時預處理做了兩套但填充值和歸一化方式不一致。常見三種錯位PyTorch 側(cè)用 BGR 而 onnx 側(cè)用 RGB歸一化一個用 0~255 另一個用 0~1letterbox 的填充位置左右算偏。每一步差一點點疊加起來精度就掉得明顯。解決把預處理統(tǒng)一成同一套函數(shù)最實用的是在 onnx 推理側(cè)復刻 letterbox 的完整邏輯包括填充值、位置、比例并把 imgsz 固定成訓練時的值不要用動態(tài)輸入。導出后第一件事是畫一張“PyTorch 結(jié)果 vs onnx 結(jié)果”的對比圖兩個框完全重合再談后續(xù)優(yōu)化。我見過不少同學在量化上折騰半天最后發(fā)現(xiàn)只是 BGR 和 RGB 的順序沒對齊。6. 驗證與交付把交通標志項目從能跑變成能答辯6.1 val.py 結(jié)果表怎么翻譯P、R、mAP 三類指標的解讀訓練結(jié)束后的驗證不是跑一遍 detect.py 看幾張圖就完事答辯時老師一定會問“你這個模型到底好在哪里”。標準做法是用驗證腳本在測試集上出指標表。python val.py \ --data dataset/traffic_sign.yaml \ --weights runs/train/exp/weights/best.pt \ --task test \ --conf 0.4 \ --iou 0.5輸出表里最重要的三列是 precision、recall 和 mAP0.5。precision 是檢出的框里有多少是對的recall 是所有真實標志里被檢出多少mAP0.5 是不同置信度下 P-R 曲線的面積。對交通標志識別我更看重 recall因為漏檢一個路牌比多畫一個框后果嚴重得多。關(guān)鍵指標不要只看平均數(shù)逐類打印后把最差的類別單獨存圖觀察。6.2 視頻 demo 與結(jié)果導出答辯現(xiàn)場演示的固定動作答辯可能不允許現(xiàn)場訓練但可以現(xiàn)場跑推理。把測試視頻放在項目根目錄下用最佳權(quán)重跑一次同時導出坐標文本這是演示的固定動作。python detect.py \ --source test_road.mp4 \ --weights runs/train/exp/weights/best.pt \ --conf 0.35 \ --save-txt \ --save-conf跑完之后runs/detect/exp/里會有一份帶框的視頻和每幀的 txt 坐標文件。如果時間充裕我一般會把其中連續(xù)十幾幀的檢測結(jié)果落成一張大圖標注出真值和預測框?qū)Ρ确胚M論文的實驗分析章節(jié)里。6.3 答辯前必查的三個檢查點第一個檢查點把隨機種子固定把測試集路徑固定連續(xù)跑兩遍 val.py 結(jié)果完全一致防止答辯現(xiàn)場復現(xiàn)時指標對不上。第二個檢查點把最低置信度設為 0.25 重新跑一遍 detect確認視頻 demo 不會因為閾值調(diào)太高經(jīng)常漏檢。第三個檢查點把每種失敗樣例單獨截圖想清楚“為什么漏檢”和改進方向哪怕答辯時回答不上來也能展示你做過驗證。我?guī)дn設時吃過最大的虧就是一陣猛訓對手里模型哪一類最差、為什么差完全沒有印象答辯時被老師問住只能支支吾吾。所以后來我養(yǎng)成一個習慣訓練結(jié)束先打印 per-class 指標表貼在代碼旁邊再開始寫論文所有的改進方向都從表格里的最低項入手。這個習慣幫你把項目從“能跑”變成“能講”希望幫到你。本文還有配套的精品資源點擊獲取