:22600張數(shù)據(jù)集訓(xùn)練調(diào)優(yōu)與邊緣部署)
駕駛員行為檢測這個方向我在過去兩年里陸續(xù)接觸過幾個落地項目從最初拿公開數(shù)據(jù)集跑通baseline到后來自己參與標(biāo)注和清洗兩萬多張實拍圖踩過的坑不算少。這次拿到的是一份22600張規(guī)模的YOLO格式駕駛員行為檢測數(shù)據(jù)集說實話這個體量在細(xì)分場景里已經(jīng)算相當(dāng)能打的了——市面上大多數(shù)公開的駕駛行為數(shù)據(jù)集要么只有幾千張要么類別定義模糊、標(biāo)注質(zhì)量參差真正能直接拿來訓(xùn)練并部署上車的并不多。這份數(shù)據(jù)集的核心價值在于它把駕駛員行為這個籠統(tǒng)的概念拆成了可檢測的具體動作類別并且用YOLO系列最順手的標(biāo)注格式組織好了省去了從零構(gòu)建標(biāo)注體系的大量時間。我打算圍繞這份數(shù)據(jù)集把從數(shù)據(jù)理解、格式校驗、訓(xùn)練配置、類別不均衡處理到最終部署時的一些真實經(jīng)驗完整講一遍。不管你是剛?cè)腴T目標(biāo)檢測想找個真實場景練手還是已經(jīng)在做智能座艙相關(guān)產(chǎn)品需要快速驗證方案這篇內(nèi)容應(yīng)該都能給你省下不少試錯成本。尤其是那些準(zhǔn)備把模型往邊緣設(shè)備上搬的朋友后面關(guān)于輸入分辨率和推理幀率的取舍部分值得仔細(xì)看看。1. 先搞清楚這22600張圖到底能檢測什么拿到任何一份數(shù)據(jù)集我的習(xí)慣是先別急著寫訓(xùn)練腳本而是花半天時間把數(shù)據(jù)摸一遍。很多人上來就model.train()結(jié)果訓(xùn)到一半發(fā)現(xiàn)類別定義和自己業(yè)務(wù)對不上或者某些類別的樣本少得可憐白白浪費算力。這份駕駛員行為檢測數(shù)據(jù)集從命名和常見同類數(shù)據(jù)集的組織方式推斷覆蓋的行為類別大概率包括打電話、抽煙、喝水、吃東西、單手駕駛、雙手離開方向盤、轉(zhuǎn)頭張望、低頭看手機(jī)、正常駕駛等。具體類別數(shù)以你拿到的data.yaml為準(zhǔn)但理解這些類別的劃分邏輯比記住數(shù)字更重要。1.1 行為類別的粒度決定了模型的上限這里有個很容易被忽略的問題行為檢測的類別粒度直接決定了你后面能不能把它用起來。舉個例子打電話和看手機(jī)在很多數(shù)據(jù)集里是分開的兩類但在實際業(yè)務(wù)中低頭看導(dǎo)航和低頭刷視頻在視覺上高度相似如果數(shù)據(jù)集沒有區(qū)分你的模型就永遠(yuǎn)分不出來。反過來如果數(shù)據(jù)集把右手打電話和左手打電話分成兩類那對檢測精度是災(zāi)難——樣本被稀釋模型還得學(xué)一個本質(zhì)上無關(guān)的左右手差異。我的建議是先列出你業(yè)務(wù)真正關(guān)心的行為清單再和數(shù)據(jù)集類別做映射。能合并的合并該拆分的如果數(shù)據(jù)集沒拆就得考慮自己補(bǔ)標(biāo)注。22600張的規(guī)模如果按8:1:1劃分訓(xùn)練驗證測試測試集也有兩千多張足夠做一次靠譜的類別分布統(tǒng)計。1.2 用幾行腳本把類別分布和標(biāo)注質(zhì)量摸清楚在動手訓(xùn)練前我強(qiáng)烈建議跑一遍數(shù)據(jù)體檢。下面這段腳本可以統(tǒng)計每個類別的實例數(shù)量、每張圖的標(biāo)注框數(shù)量分布以及框的寬高比分布這幾個指標(biāo)能幫你提前發(fā)現(xiàn)大部分問題。import os import glob from collections import Counter import numpy as np label_dir labels/train class_names [normal, phone, smoke, drink, eat, hand_off_wheel, look_around, look_down] cls_counter Counter() boxes_per_img [] aspect_ratios [] for txt in glob.glob(os.path.join(label_dir, *.txt)): with open(txt) as f: lines [l.strip() for l in f if l.strip()] boxes_per_img.append(len(lines)) for line in lines: parts line.split() cid int(parts[0]) w, h float(parts[3]), float(parts[4]) cls_counter[cid] 1 if h 0: aspect_ratios.append(w / h) print(類別實例數(shù):, {class_names[k]: v for k, v in cls_counter.items()}) print(每圖平均框數(shù):, np.mean(boxes_per_img)) print(寬高比中位數(shù):, np.median(aspect_ratios))跑完之后重點看兩件事。第一有沒有某個類別實例數(shù)特別少比如只有幾百個而最多的類別有幾萬個這種長尾分布會直接導(dǎo)致小類別召回率上不去。第二寬高比中位數(shù)如果偏離1太遠(yuǎn)說明默認(rèn)的anchor設(shè)置可能不合適需要考慮重新聚類anchor或者用anchor-free的檢測頭。提示如果發(fā)現(xiàn)某些類別實例數(shù)低于總實例數(shù)的2%先別急著上focal loss優(yōu)先考慮數(shù)據(jù)層面能不能補(bǔ)或者用過采樣把這類圖片在訓(xùn)練時多喂幾遍效果往往比調(diào)損失函數(shù)更直接。1.3 標(biāo)注框的臟數(shù)據(jù)長什么樣YOLO格式的標(biāo)注是歸一化的class x_center y_center width height理論上所有值都在0到1之間。但實際拿到的數(shù)據(jù)集里我見過坐標(biāo)超出1的、寬高為0的、甚至類別id超出類別總數(shù)的。這些臟標(biāo)注如果不清理訓(xùn)練時輕則loss異常重則直接報錯中斷。上面那段腳本稍微改一下就能做校驗把越界的行打印出來人工確認(rèn)是刪是改。22600張的規(guī)模臟數(shù)據(jù)比例通常在千分之幾花一兩個小時清理完全值得。2. YOLO格式數(shù)據(jù)的目錄組織與配置陷阱數(shù)據(jù)摸清楚之后接下來是把它組織成YOLO訓(xùn)練框架能直接吃的結(jié)構(gòu)。這一步看起來簡單但我在不同項目里見過太多因為路徑、緩存、配置文件寫錯導(dǎo)致訓(xùn)練跑不起來的案例。尤其是當(dāng)你在多臺機(jī)器之間遷移數(shù)據(jù)時絕對路徑和相對路徑的坑幾乎每次都會踩。2.1 標(biāo)準(zhǔn)目錄結(jié)構(gòu)與data.yaml的正確寫法YOLO系列無論是v5、v8還是更新的版本對目錄結(jié)構(gòu)有約定俗成的期望。推薦的組織方式是這樣dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml關(guān)鍵點是images和labels下的子目錄名必須嚴(yán)格對應(yīng)YOLO在找標(biāo)簽時是把圖片路徑里的images替換成labels再改后綴如果目錄名對不上它會靜默地認(rèn)為這張圖沒有標(biāo)簽直接跳過。這個坑特別隱蔽因為訓(xùn)練不會報錯只是你的有效樣本悄悄少了一批。data.yaml的寫法也有講究path: /abs/path/to/dataset train: images/train val: images/val test: images/test nc: 8 names: [normal, phone, smoke, drink, eat, hand_off_wheel, look_around, look_down]path建議寫絕對路徑train/val/test寫相對于path的路徑。我遇到過有人把train寫成絕對路徑、path又寫了另一個絕對路徑結(jié)果框架拼接出來的路徑完全不對。另外nc和names的長度必須一致少一個都會在訓(xùn)練啟動時報索引錯誤。2.2 緩存文件引發(fā)的改了數(shù)據(jù)沒生效YOLO在第一次訓(xùn)練時會生成*.cache文件把標(biāo)簽解析結(jié)果緩存起來加速后續(xù)訓(xùn)練。這本來是個優(yōu)化但如果你中途修改了標(biāo)簽文件卻沒刪緩存框架會繼續(xù)用舊緩存導(dǎo)致你改的東西完全不生效。我有個同事調(diào)了一下午類別映射怎么訓(xùn)結(jié)果都不對最后發(fā)現(xiàn)是緩存沒清。處理辦法很簡單每次改動標(biāo)簽后手動刪掉labels目錄下所有.cache文件或者干脆在訓(xùn)練腳本里加一句清理邏輯。這個細(xì)節(jié)在官方文檔里提得不多但實際項目中幾乎人人踩過。2.3 訓(xùn)練集驗證集劃分的隱藏偏差如果你的數(shù)據(jù)集是別人劃分好的最好自己再檢查一遍劃分是否合理。我見過按圖片文件名順序簡單切分的結(jié)果同一段視頻抽出來的連續(xù)幀被分到了訓(xùn)練集和驗證集兩邊導(dǎo)致驗證指標(biāo)虛高——模型其實在驗證集上見過幾乎一樣的畫面。駕駛員行為數(shù)據(jù)很多來自連續(xù)視頻抽幀這個問題尤其嚴(yán)重。正確的做法是按視頻源或按時間段劃分確保同一個駕駛員、同一段行程的幀只出現(xiàn)在一個集合里。如果數(shù)據(jù)集沒提供視頻源信息退而求其次可以按圖片的拍攝時間或文件修改時間做分組劃分。這一步多花點心思驗證指標(biāo)才有參考價值。3. 訓(xùn)練配置從anchor到學(xué)習(xí)率的實戰(zhàn)取舍數(shù)據(jù)準(zhǔn)備好了真正決定模型好不好用的就是訓(xùn)練配置。這一塊網(wǎng)上教程很多但大多是拿COCO或VOC的通用配置直接套放到駕駛員行為這種特定場景未必合適。我結(jié)合這份數(shù)據(jù)集的特點講幾個我認(rèn)為最關(guān)鍵的配置點。3.1 輸入分辨率與駕駛員行為的小目標(biāo)問題駕駛員行為檢測有個天然特點攝像頭通常裝在方向盤上方或A柱附近畫面里駕駛員占的比例不小但手部、手機(jī)、煙這些關(guān)鍵目標(biāo)相對整張圖來說可能偏小。如果你用默認(rèn)的640輸入手部動作的細(xì)節(jié)可能就糊掉了。我的經(jīng)驗是如果算力允許訓(xùn)練時用比推理時更大的分辨率。比如訓(xùn)練用960或1280推理再降到640這樣模型學(xué)到的是更清晰的特征降分辨率推理時精度損失相對可控。反過來如果訓(xùn)練就用640推理想提到1280精度提升非常有限因為模型沒見過高分辨率的細(xì)節(jié)。當(dāng)然分辨率翻倍顯存占用和訓(xùn)練時間大致是平方級增長。22600張圖1280分辨率下用單張24G顯存的卡batch size可能只能開到8到16訓(xùn)練輪次要多一些才能收斂。這個取舍要根據(jù)你手頭的硬件來定。3.2 anchor聚類別直接用COCO的默認(rèn)值YOLOv5和v8都支持自動anchor計算訓(xùn)練啟動時會根據(jù)你的數(shù)據(jù)集重新聚類anchor。這個功能一定要開。駕駛員行為數(shù)據(jù)集的框分布和COCO差異很大——COCO里各種尺度都有而駕駛行為框大多集中在中大尺度寬高比也偏向接近1的方形手部、手機(jī)、臉部區(qū)域。如果你用的是YOLOv5在訓(xùn)練命令里加上--noautoanchor的反面也就是保持自動anchor開啟默認(rèn)就是開的。如果用的是需要手動指定anchor的版本建議自己跑一遍k-means聚類from sklearn.cluster import KMeans import numpy as np # wh 是從所有標(biāo)簽里讀出來的 (width, height) 數(shù)組已歸一化 k 9 kmeans KMeans(n_clustersk, random_state42).fit(wh) anchors kmeans.cluster_centers_ print(聚類anchor:, anchors)聚類出來的anchor如果和默認(rèn)值差異超過20%就果斷換成新的。這個改動對召回率的提升在特定場景下往往比換backbone還明顯。3.3 學(xué)習(xí)率與warmup小數(shù)據(jù)集要更保守22600張在目標(biāo)檢測里算中等偏小。這種規(guī)模下我傾向于用比默認(rèn)更小的初始學(xué)習(xí)率和更長的warmup。YOLOv8默認(rèn)初始lr是0.01我一般會降到0.005甚至0.003warmup輪次從3提到5。原因是小數(shù)據(jù)集上梯度噪聲大學(xué)習(xí)率太高容易在早期就把特征帶偏后面很難拉回來。另外余弦退火cosine schedule在這個規(guī)模上表現(xiàn)通常比step schedule更穩(wěn)。如果你發(fā)現(xiàn)訓(xùn)練loss在前幾個epoch劇烈震蕩八成是學(xué)習(xí)率或warmup設(shè)置太激進(jìn)先降lr再看。3.4 數(shù)據(jù)增強(qiáng)的度駕駛場景不能亂增強(qiáng)YOLO默認(rèn)的增強(qiáng)包括mosaic、mixup、隨機(jī)縮放、色彩抖動等。這些在通用場景很有效但駕駛行為檢測要小心。mosaic把四張圖拼一起可能把駕駛員的手和另一個人的手機(jī)拼到一塊產(chǎn)生語義錯誤的樣本。mixup更激進(jìn)直接做圖像混合對行為識別這種依賴局部細(xì)節(jié)的任務(wù)可能有害。我的建議是mosaic可以開但把概率從默認(rèn)的1.0降到0.5左右mixup直接關(guān)掉色彩抖動保留因為車內(nèi)光照變化確實大隨機(jī)縮放保留但范圍別太大避免把關(guān)鍵的手部動作縮得看不清。翻轉(zhuǎn)要謹(jǐn)慎水平翻轉(zhuǎn)會讓左手打電話變成右手打電話如果你的類別區(qū)分了左右手翻轉(zhuǎn)就得關(guān)掉。4. 類別不均衡與難例讓模型真正學(xué)會危險行為駕駛員行為數(shù)據(jù)集幾乎必然存在類別不均衡——正常駕駛的樣本遠(yuǎn)多于抽煙、打電話這些異常行為。這是數(shù)據(jù)本身的分布決定的不是標(biāo)注問題。怎么處理這個不均衡直接決定了模型在真實場景下能不能及時報警。4.1 從損失函數(shù)入手的幾種方案對比處理不均衡最直接的是在損失函數(shù)上做文章。我把常用的幾種方案和適用場景整理成表方便你對照選擇。方案原理適用場景注意事項類別加權(quán)給少樣本類別更高權(quán)重中度不均衡權(quán)重別設(shè)太極端否則正常類誤報飆升Focal Loss降低易分樣本權(quán)重難例多、長尾明顯需調(diào)gamma默認(rèn)2不一定最優(yōu)過采樣重復(fù)少樣本圖片少樣本類別極少容易過擬合配合強(qiáng)增強(qiáng)使用復(fù)制粘貼增強(qiáng)把少樣本目標(biāo)貼到其他圖目標(biāo)可分離粘貼位置要合理避免懸空我個人的優(yōu)先級是先試類別加權(quán)簡單可控如果小類別召回還是上不去再上focal loss過采樣作為最后手段且必須配合較強(qiáng)的數(shù)據(jù)增強(qiáng)否則模型會把那幾張圖背下來。4.2 難例挖掘那些看起來像但其實不是的樣本駕駛員行為檢測里有一類特別討厭的難例駕駛員撓頭被誤判成打電話拿水杯被誤判成抽煙調(diào)整后視鏡被誤判成轉(zhuǎn)頭張望。這些樣本在訓(xùn)練集里往往被標(biāo)成正常類但模型就是學(xué)不會區(qū)分。處理這類問題的有效辦法是難例挖掘。先用訓(xùn)練好的模型在驗證集上跑一遍把置信度在0.3到0.7之間的預(yù)測框挑出來人工復(fù)核。這些模型拿不準(zhǔn)的樣本恰恰是提升邊界能力的關(guān)鍵。把它們加入訓(xùn)練集重新訓(xùn)練往往能帶來幾個點的精度提升。22600張的規(guī)模挖出幾百個難例補(bǔ)充進(jìn)去性價比很高。4.3 評估指標(biāo)不能只看mAP在行為檢測這種安全相關(guān)場景mAP高不代表能用。你更該關(guān)注的是每個類別的召回率和誤報率。抽煙這種類別漏檢召回低意味著沒報警誤報精度低意味著頻繁打擾駕駛員。兩者哪個更不能接受取決于你的產(chǎn)品定位。我通常會把每個類別的PR曲線單獨畫出來看而不是只看一個總mAP。如果某個危險行為的召回低于85%那這個模型基本不能上線得回去補(bǔ)數(shù)據(jù)或調(diào)閾值。另外混淆矩陣一定要看它能告訴你模型到底把A類錯分成了B類還是C類這對定位問題是決定性的。5. 部署落地分辨率和幀率的真實賬訓(xùn)練完模型真正的挑戰(zhàn)才開始。駕駛員行為檢測大多要跑在車機(jī)或邊緣盒子上算力有限還得保證實時性。這一塊我踩的坑最多也最有發(fā)言權(quán)。5.1 輸入分辨率、幀率與路數(shù)的三角關(guān)系經(jīng)常有人問某個算力平臺上YOLO能跑多少路。這個問題沒有標(biāo)準(zhǔn)答案因為它取決于分辨率、幀率、模型大小三者的組合。我拿一個常見的場景舉例說明這個賬怎么算。假設(shè)你用TensorRT加速模型是YOLOv8s級別輸入640x640在某個主流邊緣芯片上單幀推理耗時約8毫秒。那么理論最大幀率是125幀每秒。如果每路視頻需要25幀每秒的處理速度理論上能支持5路。但這是理想值實際要打七折左右因為還有視頻解碼、預(yù)處理、后處理、內(nèi)存拷貝的開銷。所以實際能穩(wěn)定跑3到4路。如果把輸入提到1280推理耗時大約變成原來的3到4倍也就是25到32毫秒一幀那25幀每秒就只能勉強(qiáng)跑1路甚至跑不滿。這就是為什么分辨率的選擇必須和你的路數(shù)需求一起考慮。輸入分辨率單幀耗時(相對)25fps下單路占用可支持路數(shù)(估算)6401x約40%3-4路960約2.2x約90%1-2路1280約3.5x超100%1路(需降幀)注意上表是相對估算具體數(shù)值必須在你自己的硬件上實測。不同芯片的TensorRT優(yōu)化程度、內(nèi)存帶寬差異很大別人的數(shù)據(jù)只能參考。5.2 模型剪枝與量化精度換速度的邊界在哪如果算力實在不夠就得考慮剪枝和量化。INT8量化通常能帶來1.5到2倍的速度提升精度損失在1到2個點以內(nèi)對行為檢測來說一般可以接受。但有個前提你的校準(zhǔn)集必須覆蓋真實場景的光照和角度分布否則量化后的模型在暗光或逆光下會崩得很厲害。剪枝要更謹(jǐn)慎。駕駛員行為檢測依賴手部、臉部這些細(xì)節(jié)特征剪枝剪過頭會直接把這些小目標(biāo)的特征通道剪沒。我的經(jīng)驗是剪枝率控制在20%以內(nèi)剪完必須重新微調(diào)至少10個epoch并且重點看小類別召回有沒有掉。5.3 后處理與報警邏輯模型之外的功夫模型輸出只是檢測框真正要變成產(chǎn)品還得有后處理邏輯。比如打電話這個行為單幀檢測到可能是誤報連續(xù)5幀都檢測到才觸發(fā)報警這樣能大幅降低誤報。但連續(xù)幀數(shù)設(shè)太多又會漏掉快速的動作。這個閾值需要在真實數(shù)據(jù)上反復(fù)調(diào)。另外檢測框的置信度閾值也不是越高越好。危險行為檢測我傾向于把閾值設(shè)低一點比如0.3寧可多報也別漏報然后用時序邏輯去過濾誤報。這和通用目標(biāo)檢測的思路是反的但符合安全場景的需求。6. 幾個我踩過的坑和對應(yīng)的解法最后這部分我想把幾個印象深刻的坑單獨拎出來講都是那種文檔里不會寫、但實際項目里一定會遇到的。6.1 訓(xùn)練中BN層崩潰有一次訓(xùn)練到第30個epoch左右loss突然變成NaN怎么都恢復(fù)不了。排查下來是某個batch里出現(xiàn)了全黑的圖片數(shù)據(jù)里有損壞文件導(dǎo)致BN層的方差計算出問題。解決辦法有兩個一是在數(shù)據(jù)加載時加校驗把全黑、全白、尺寸異常的圖片過濾掉二是把BN的eps調(diào)大一點增加數(shù)值穩(wěn)定性。前者治本后者治標(biāo)建議都做。6.2 混淆矩陣總和不等于樣本數(shù)這個現(xiàn)象很多人遇到過以為是框架bug。其實是因為YOLO的混淆矩陣統(tǒng)計的是預(yù)測框和真實框的匹配結(jié)果一個真實框可能匹配到多個預(yù)測框或者因為IoU閾值設(shè)置導(dǎo)致某些框沒被計入。如果你發(fā)現(xiàn)總和對不上先檢查IoU閾值和置信度閾值通常調(diào)一下就能對上。這不是數(shù)據(jù)問題不用慌。6.3 驗證集指標(biāo)很好但實車一塌糊涂這是最經(jīng)典的坑。原因通常是訓(xùn)練數(shù)據(jù)的分布和實車場景不一致——數(shù)據(jù)集里的駕駛員可能都是白天、正面、光線充足而實車會遇到夜間、側(cè)臉、逆光。解決辦法只有一個拿實車數(shù)據(jù)做測試把bad case挑出來補(bǔ)進(jìn)訓(xùn)練集。22600張是個很好的起點但要真正上車通常還需要再補(bǔ)幾千張真實場景的難例。數(shù)據(jù)集是起點不是終點。6.4 關(guān)于預(yù)訓(xùn)練權(quán)重的選擇很多人糾結(jié)用COCO預(yù)訓(xùn)練還是ImageNet預(yù)訓(xùn)練。我的經(jīng)驗是目標(biāo)檢測任務(wù)直接用COCO預(yù)訓(xùn)練的檢測權(quán)重收斂最快。如果找不到對應(yīng)版本的檢測權(quán)重用ImageNet的分類權(quán)重初始化backbone也比從頭訓(xùn)強(qiáng)。但要注意如果你改過backbone結(jié)構(gòu)預(yù)訓(xùn)練權(quán)重可能對不上這時候要么用strictFalse加載能對上的部分要么干脆從頭訓(xùn)但把學(xué)習(xí)率調(diào)更小、輪次拉更長。駕駛員行為檢測這個方向數(shù)據(jù)是根基配置是杠桿部署是試金石。22600張的數(shù)據(jù)集給了你一個不錯的起點但真正決定成敗的是你對業(yè)務(wù)場景的理解和對細(xì)節(jié)的把控。我在實際項目里最大的體會是與其花大量時間調(diào)模型結(jié)構(gòu)不如先把數(shù)據(jù)清洗和類別定義做扎實前者帶來的提升往往是后者的好幾倍。另外別迷信公開數(shù)據(jù)集上的漂亮指標(biāo)拿你自己的場景數(shù)據(jù)測一遍才知道模型到底行不行。