練十大坑:從loss震蕩到mAP 92%的實(shí)戰(zhàn)避雷指南)
第一次用YOLO系列模型跑自己的數(shù)據(jù)集我最初的體驗(yàn)是這樣的loss曲線慢吞吞往下走看著像在收斂可驗(yàn)證集mAP六個(gè)epoch都紋絲不動(dòng)穩(wěn)定停在8%左右。我以為是網(wǎng)絡(luò)不夠強(qiáng)換了更大的骨干網(wǎng)絡(luò)結(jié)果顯存直接爆掉。那段時(shí)間我翻遍了網(wǎng)上關(guān)于訓(xùn)不收斂的討論得到的答案翻來覆去就兩句——調(diào)小學(xué)習(xí)率、換更強(qiáng)模型——對(duì)新手來說等于什么都沒說。前前后后折騰了快兩個(gè)月反復(fù)試錯(cuò)總算把模型調(diào)到了mAP 92%。復(fù)盤時(shí)我數(shù)了一下整個(gè)過程中踩過的坑正好十個(gè)而且絕大多數(shù)都不是模型結(jié)構(gòu)的問題而是配置、數(shù)據(jù)和評(píng)估方式里的細(xì)節(jié)。這篇就把它們一個(gè)個(gè)拆開講清楚每個(gè)坑都給癥狀、定位方法和修復(fù)動(dòng)作。標(biāo)題說90%的新手會(huì)中招以我?guī)н^的人來看這數(shù)字還保守了。如果你正準(zhǔn)備用YOLO訓(xùn)練自己的數(shù)據(jù)集建議先收藏踩到哪個(gè)坑直接跳到對(duì)應(yīng)小節(jié)對(duì)照。1. 先把答卷亮出來mAP 92%背后的配置與十大坑速覽1.1 我手里的任務(wù)和最終成績(jī)單復(fù)盤之前先把我當(dāng)時(shí)的任務(wù)環(huán)境和最終結(jié)果交代清楚后面講坑的時(shí)候你才知道我踩在什么基線上。任務(wù)本身不算復(fù)雜一個(gè)工業(yè)場(chǎng)景的目標(biāo)檢測(cè)需要檢測(cè)10個(gè)類別的目標(biāo)訓(xùn)練集2萬多張圖驗(yàn)證集2000張。比較棘手的是目標(biāo)尺度差異非常大既有占畫面5%的大件也有只有30x30像素的小件這直接影響了后面坑7和坑8的走向。機(jī)器是單卡顯存比較緊張這一點(diǎn)在坑8里也起了關(guān)鍵作用。模型用的是YOLO系列主流開源實(shí)現(xiàn)初始輸入分辨率就按最常見的640。第一次訓(xùn)練跑完300個(gè)epoch成績(jī)慘不忍睹指標(biāo)初版結(jié)果修坑后結(jié)果mAP0.56.8%92.3%mAP0.5:0.95約2.1%68.7%訓(xùn)練最終loss3.2附近震蕩明顯0.8附近穩(wěn)定收斂收斂狀態(tài)勉強(qiáng)算降但驗(yàn)證集完全不漲穩(wěn)定收斂驗(yàn)證集同步提升這組數(shù)字說明一個(gè)事實(shí)同樣的模型結(jié)構(gòu)同樣的數(shù)據(jù)量結(jié)果天差地別差距全在十個(gè)看似很小的坑里。我全程沒有換更大模型也沒有增加任何額外數(shù)據(jù)純粹是修數(shù)據(jù)和調(diào)參修出來的。1.2 十個(gè)坑的速覽癥狀與解法一句話版先把十個(gè)坑做成一張速查表后面每章展開細(xì)講。這張表我自己在調(diào)參過程中反復(fù)看遇到問題先對(duì)癥狀再翻對(duì)應(yīng)章節(jié)。坑號(hào)坑名典型癥狀解決思路一句話坑1初始學(xué)習(xí)率亂設(shè)loss震蕩、訓(xùn)不收斂用經(jīng)驗(yàn)法則隨batch縮放lr先用小步實(shí)驗(yàn)摸底坑2動(dòng)量和學(xué)習(xí)率脫節(jié)收斂極慢、調(diào)參越調(diào)越亂一次只改一個(gè)變量動(dòng)量保持常見默認(rèn)值坑3損壞圖片混入訓(xùn)練集loss突然變nan、訓(xùn)練中斷訓(xùn)練前全量校驗(yàn)圖像可解碼性和尺寸坑4標(biāo)注坐標(biāo)格式算錯(cuò)loss下降但mAP極低、框全歪格式轉(zhuǎn)換后必須抽樣可視化還原坑5類別ID不連續(xù)訓(xùn)練無報(bào)錯(cuò)但類別錯(cuò)位掃描全部標(biāo)簽ID重映射成0到C-1坑6驗(yàn)證集劃分隨意val虛高或劇烈波動(dòng)、上線崩盤按采集批次/時(shí)間段分層劃分杜絕幀重疊坑7anchor沿用默認(rèn)值小目標(biāo)漏檢、召回率卡住對(duì)GT寬高做k-means聚類用IoU距離坑8batch size與分辨率打架OOM或精度莫名下降由最小目標(biāo)尺寸反推分辨率再用梯度累積坑9數(shù)據(jù)增強(qiáng)一刀切要么過擬合要么訓(xùn)不動(dòng)分階段引入強(qiáng)增強(qiáng)mosaic概率別拉滿坑10評(píng)估閾值沒固定mAP數(shù)值忽高忽低、不可復(fù)現(xiàn)統(tǒng)一conf0.001、NMS IoU0.6的口徑排錯(cuò)順序我建議這樣走先看第2章的訓(xùn)不收斂排查因?yàn)槟P筒皇諗康臅r(shí)候調(diào)anchor和增強(qiáng)都是白費(fèi)力氣收斂之后再按第3章檢查數(shù)據(jù)、第4章做精度優(yōu)化最后按第5章統(tǒng)一評(píng)估口徑保證你看到的92%是真實(shí)的不是自己騙自己。這個(gè)順序本身也是我踩完坑之后回頭看才總結(jié)出來的最佳路徑。2. 訓(xùn)不收斂的元兇排查從loss震蕩到nan我按這個(gè)順序定位2.1 坑1初始學(xué)習(xí)率靠感覺拍板loss曲線直接起飛這個(gè)坑我第一次訓(xùn)練就踩了。初始學(xué)習(xí)率我完全是照抄默認(rèn)配置里的0.01但忽略了那套參數(shù)是按batch size16調(diào)的。我把batch size調(diào)到64想加快訓(xùn)練顯存也裝得下可學(xué)習(xí)率沒同步放大。結(jié)果第一個(gè)epoch的loss就開始跳前幾步直接沖到幾十后面再怎么調(diào)也沒能回到正常軌道說白了就是訓(xùn)不收斂。原理上其實(shí)很好理解。SGD類優(yōu)化器每一步的更新幅度大致正比于學(xué)習(xí)率乘以梯度而梯度是累加了一個(gè)batch的樣本得到的。batch變大一倍等效于梯度累計(jì)的樣本數(shù)翻倍更新步長(zhǎng)在數(shù)學(xué)上也會(huì)成倍放大。所以有個(gè)經(jīng)典經(jīng)驗(yàn)法則batch size翻倍學(xué)習(xí)率大致也要翻倍batch size減半學(xué)習(xí)率也要減半。這只是起步參考精確值還是要看loss曲線的實(shí)際形態(tài)。我的做法是先做小步實(shí)驗(yàn)摸底拿幾百張圖跑十幾個(gè)step分別用1e-2、3e-3、1e-3、1e-4試一輪看哪個(gè)學(xué)習(xí)率下loss前幾十步能穩(wěn)定下降。最后選中的初始lr是2e-2配合前3個(gè)epoch的warmup慢慢爬上去loss立刻就變得平滑。warmup的作用是讓優(yōu)化器在前幾步熟悉數(shù)據(jù)分布避免一開始就走偏但它不是用來掩蓋lr錯(cuò)誤的——如果你lr本身設(shè)得離譜warmup拖再久也救不回來。2.2 坑2動(dòng)量和學(xué)習(xí)率脫節(jié)收斂慢得像蝸牛第二個(gè)坑其實(shí)和第一個(gè)是配套的。我把lr調(diào)小之后發(fā)現(xiàn)loss下降變得非常慢500步只降了0.1一度懷疑是網(wǎng)絡(luò)結(jié)構(gòu)壞了。后來才意識(shí)到是我動(dòng)得太亂了那一次同時(shí)改了學(xué)習(xí)率、動(dòng)量和weight decay三個(gè)東西變量互相抵消根本定位不到是誰導(dǎo)致的。動(dòng)量momentum這個(gè)參數(shù)的作用是累積前幾步的更新方向讓梯度的方向更平滑減少震蕩。YOLO系常用的配置是0.937配合余弦退火使用這套組合在大量數(shù)據(jù)集上都被驗(yàn)證過對(duì)新手來說是最穩(wěn)的起點(diǎn)。我當(dāng)時(shí)自作聰明把momentum從0.937改成0.5以為能更穩(wěn)定結(jié)果反而讓優(yōu)化器失去了歷史梯度方向的平滑作用訓(xùn)練過程變得拖沓又遲鈍。這里最值得說的不是動(dòng)量本身怎么調(diào)而是調(diào)參的基本紀(jì)律一次只動(dòng)一個(gè)變量。我后來每次實(shí)驗(yàn)只改一個(gè)超參數(shù)并且記錄改了什么、結(jié)果變了多少很快就定位到了問題。如果你同時(shí)改了三四個(gè)東西漲了不知道怎么漲的跌了也不知道是誰拖后腿后面所有優(yōu)化都建立在糊涂賬上。2.3 坑3loss變nan先查圖像再查標(biāo)簽第三個(gè)坑是loss在第40輪左右突然變成nan訓(xùn)練直接中斷。剛開始我以為是lr太大調(diào)小之后重跑結(jié)果第30輪又nan。這次我老老實(shí)實(shí)開始排查把訓(xùn)練數(shù)據(jù)逐張過了一遍圖像解碼和標(biāo)簽讀取最后發(fā)現(xiàn)訓(xùn)練集里有幾十張損壞的圖片——文件擴(kuò)展名是.jpg但解碼出來是空的或者只有幾行數(shù)據(jù)。這種圖片在訓(xùn)練時(shí)產(chǎn)生異常特征梯度直接爆炸。排查腳本核心邏輯并不復(fù)雜from PIL import Image import os bad_images [] for f in all_images: try: img Image.open(f) img.verify() # 校驗(yàn)文件完整性 img Image.open(f) img.load() # 加載像素?cái)?shù)據(jù) except Exception as e: bad_images.append((f, str(e)))還有一個(gè)很容易被忽略的分支即使圖像能正常解碼部分增強(qiáng)操作在圖片尺寸異常時(shí)也可能產(chǎn)生nan比如mosaic拼接遇到空?qǐng)D像。所以校驗(yàn)時(shí)順便把每張圖的尺寸分布打印出來把極端尺寸的樣本一并清掉能省掉后續(xù)大量排查時(shí)間。損壞樣本剔除以后nan問題再?zèng)]出現(xiàn)過。2.4 訓(xùn)不收斂的完整排查鏈路把前面三步串成一套可復(fù)現(xiàn)的排查順序基本能覆蓋90%的訓(xùn)不收斂場(chǎng)景先看loss曲線的形態(tài)。如果是突然變nan直接跳到數(shù)據(jù)校驗(yàn)不要?jiǎng)訉W(xué)習(xí)率如果是震蕩幅度大、整體不降查學(xué)習(xí)率和batch縮放關(guān)系如果是一直橫盤或降得極慢查動(dòng)量和退火策略。數(shù)據(jù)校驗(yàn)優(yōu)先于超參調(diào)整。圖像解碼失敗、標(biāo)簽坐標(biāo)越界、類別ID越界這些錯(cuò)誤會(huì)被模型學(xué)進(jìn)去表面上看起來像超參問題實(shí)際換什么lr都沒用。超參調(diào)整堅(jiān)持一次只動(dòng)一個(gè)并且每次修改都記錄基線值。這一步最重要它能保證你所有結(jié)論都是可追溯的。這個(gè)順序千萬不要反。我見過太多人在loss nan的時(shí)候先去調(diào)lr調(diào)了一整天發(fā)現(xiàn)還是nan最后才發(fā)現(xiàn)是數(shù)據(jù)里有幾張壞圖白白浪費(fèi)十幾個(gè)小時(shí)的訓(xùn)練時(shí)間。3. 數(shù)據(jù)和標(biāo)注的暗坑比模型結(jié)構(gòu)更致命3.1 坑4標(biāo)注坐標(biāo)格式算錯(cuò)模型學(xué)得越快越離譜訓(xùn)不收斂解決之后模型終于能跑了但mAP一直上不去徘徊在30%左右。我開始懷疑是標(biāo)注有問題于是做了一次標(biāo)簽可視化——把txt里的歸一化坐標(biāo)乘回圖像寬高畫框出來和原圖對(duì)照。結(jié)果發(fā)現(xiàn)框全都偏了有些往上跑有些往右飛。最后檢查轉(zhuǎn)換腳本發(fā)現(xiàn)我把中心點(diǎn)x的歸一化寫成了x_center除以圖像高度中心點(diǎn)y寫成了y_center除以圖像寬度寬高歸一化也搞混了。這類錯(cuò)誤在手工寫格式轉(zhuǎn)換代碼的時(shí)候非常常見尤其是從其他標(biāo)注格式往YOLO格式轉(zhuǎn)的時(shí)候。為什么模型還能訓(xùn)練因?yàn)閘oss照常在下降模型確實(shí)學(xué)到了一些統(tǒng)計(jì)模式但坐標(biāo)本身就是錯(cuò)的學(xué)得再多也學(xué)不對(duì)驗(yàn)證集上預(yù)測(cè)框的位置總是歪的mAP自然上不去而且越是擬合得好錯(cuò)誤越被放大。從那次以后我定下一條規(guī)矩任何標(biāo)注格式轉(zhuǎn)換腳本轉(zhuǎn)換完成后必須抽樣可視化還原絕對(duì)不要信任一次性腳本。具體做法是取幾十張圖像把轉(zhuǎn)換后的txt坐標(biāo)乘回圖像寬高畫出框和原標(biāo)注疊加對(duì)比肉眼確認(rèn)位置、大小、類別都對(duì)得上再進(jìn)訓(xùn)練流程。這個(gè)習(xí)慣幫我躲過了后面好幾次數(shù)據(jù)遷移的大坑。3.2 坑5類別ID不連續(xù)訓(xùn)練沒報(bào)錯(cuò)但mAP崩了另一個(gè)數(shù)據(jù)暗坑是類別ID不連續(xù)。我手里的數(shù)據(jù)是從老系統(tǒng)導(dǎo)出的類別列表里帶著一個(gè)無效的背景占位導(dǎo)出時(shí)沒有過濾導(dǎo)致有效類別ID從0、1、3、4這樣跳著走中間缺了2。模型配置的類別數(shù)是10但部分標(biāo)簽里出現(xiàn)了越界ID訓(xùn)練過程中框架沒有直接報(bào)錯(cuò)模型靜默地把這些樣本學(xué)成了錯(cuò)位類別預(yù)測(cè)結(jié)果里類別混淆嚴(yán)重。這種問題是最陰間的因?yàn)閘oss曲線完全正常mAP也有一點(diǎn)但按類別看單個(gè)AP就會(huì)發(fā)現(xiàn)某些類別特別低或者混亂。排查方式是在訓(xùn)練前掃描全部標(biāo)簽文件的類別IDfrom collections import Counter label_ids [] for f in all_labels: with open(f) as fp: for line in fp: label_ids.append(int(line.split()[0])) stats Counter(label_ids) print(sorted(stats.items())) print(min:, min(label_ids), max:, max(label_ids))只要發(fā)現(xiàn)min、max和配置文件里的類別數(shù)對(duì)不上或者set集合里有空缺就要立刻重映射成0到C-1的連續(xù)序列。我第一次跑這個(gè)腳本就看到ID跳號(hào)修正之后mAP直接漲了一截類別混淆問題也消失了。這個(gè)腳本后來被我固化成標(biāo)準(zhǔn)流程每接手一個(gè)新數(shù)據(jù)集都先跑一遍成本一分鐘能躲掉幾天的瞎調(diào)試。3.3 坑6驗(yàn)證集劃分隨意mAP高不代表你有實(shí)力驗(yàn)證集劃分的問題最容易被忽視因?yàn)樗粓?bào)錯(cuò)、不崩loss只會(huì)讓你的評(píng)估結(jié)果失真。我的數(shù)據(jù)集是從幾段監(jiān)控視頻抽幀得到的最初我按隨機(jī)打亂劃分訓(xùn)練集和驗(yàn)證集結(jié)果驗(yàn)證mAP虛高到85%換一批沒見過的視頻測(cè)試直接跌到40%。原因就是隨機(jī)劃分把同一段視頻的連續(xù)幀同時(shí)放進(jìn)了訓(xùn)練集和驗(yàn)證集模型記住了幾乎重復(fù)的幀而不是學(xué)到了泛化能力。正確做法是按采集批次、時(shí)間段或場(chǎng)景做分層劃分保證驗(yàn)證集里的樣本和訓(xùn)練集完全沒有重疊。比如抽幀數(shù)據(jù)按時(shí)間順序切分前90%的視頻幀做訓(xùn)練后10%做驗(yàn)證兩者來自不同的時(shí)間段互不影響。這樣得到的mAP才勉強(qiáng)算可信。另外還要注意驗(yàn)證集本身的體量。如果驗(yàn)證集只有幾十張圖mAP波動(dòng)會(huì)非常劇烈改動(dòng)一個(gè)隨機(jī)種子結(jié)果就相差好幾個(gè)點(diǎn)你根本判斷不了調(diào)參是有效還是撞了大運(yùn)。我的經(jīng)驗(yàn)是驗(yàn)證集每個(gè)類別至少要有50到100個(gè)標(biāo)注框太少就只能換分層劃分策略或者增加驗(yàn)證集規(guī)模。4. anchor、batch size與數(shù)據(jù)增強(qiáng)把精度從70%磨到92%4.1 坑7anchor沿用默認(rèn)值小目標(biāo)被系統(tǒng)性漏檢數(shù)據(jù)問題和收斂問題都解決之后mAP到了60%左右的平臺(tái)期怎么調(diào)都上不去。此時(shí)我開始懷疑anchor配置。YOLO系列默認(rèn)的anchor是在COCO數(shù)據(jù)集上聚類出來的COCO場(chǎng)景以自然物體、行人和車輛為主而我的工業(yè)場(chǎng)景目標(biāo)長(zhǎng)寬比有明顯差異默認(rèn)的好幾組anchor和我數(shù)據(jù)里的GT完全匹配不上導(dǎo)致正樣本匹配數(shù)量偏少小目標(biāo)特別容易被忽略。做法其實(shí)不復(fù)雜對(duì)訓(xùn)練集所有GT框的寬高做k-means聚類聚類數(shù)按模型結(jié)構(gòu)來常見的就是9個(gè)分3組給3個(gè)不同尺度的檢測(cè)層。關(guān)鍵點(diǎn)是聚類距離一定要用IoU距離不要用歐氏距離。因?yàn)椴煌叨鹊哪繕?biāo)寬高差異大歐氏距離會(huì)把相似像素尺寸但長(zhǎng)寬比不同的框混在一起得不到有區(qū)分度的候選尺寸。聚類完成后把9組anchor按面積從小到大排序填入配置文件。怎么驗(yàn)證anchor是不是合適觀察訓(xùn)練日志里的正樣本匹配數(shù)量和召回率。聚類后的配置如果明顯優(yōu)于默認(rèn)配置能直觀看到這兩個(gè)指標(biāo)在漲。這一步做完后我的mAP從60%跳到了78%小目標(biāo)漏檢也明顯改善。4.2 坑8batch size與分辨率相互拖后腿batch size和輸入分辨率是互相牽扯的。當(dāng)時(shí)顯存有限想跑640分辨率又想開64的batch直接OOM。為了保住batch我把分辨率降到416顯存是夠用了但小目標(biāo)檢測(cè)效果肉眼可見地下降。原因很簡(jiǎn)單原圖上30x30像素的小目標(biāo)縮到416分辨率后變成了約20x20特征信息流失嚴(yán)重降分辨率等效于把小目標(biāo)往更小的方向推anchor匹配更難小目標(biāo)幾乎學(xué)不到判別特征。我的處理順序是先從小目標(biāo)標(biāo)注框的尺寸分布反推最小可接受分辨率。經(jīng)驗(yàn)值是最小目標(biāo)在訓(xùn)練圖像上不要小于10x10像素否則在特征圖層面只剩一兩個(gè)像素網(wǎng)絡(luò)學(xué)不到有效信息。確定分辨率之后再看顯存能裝多大的batch。如果batch不夠大用梯度累積gradient accumulation來湊等效batch size。比如實(shí)際batch8累積4次等效于batch32雖然BN層統(tǒng)計(jì)還是按實(shí)際的來但優(yōu)化器更新步長(zhǎng)更穩(wěn)。還有一個(gè)隱藏點(diǎn)batch size太小會(huì)讓BN層的統(tǒng)計(jì)量嚴(yán)重漂移。實(shí)測(cè)batch4和batch16相比精度能差好幾個(gè)點(diǎn)而且越訓(xùn)越不穩(wěn)定。所以不要把batch壓到8以下除非你用的是沒有BN的模型。4.3 坑9數(shù)據(jù)增強(qiáng)一刀切要么過擬合要么訓(xùn)不動(dòng)數(shù)據(jù)增強(qiáng)這個(gè)坑我踩了兩輪。第一次用最強(qiáng)增強(qiáng)配置——mosaic、mixup、旋轉(zhuǎn)、色彩抖動(dòng)全部打開小模型根本扛不住loss始終下不到低位。原因是增強(qiáng)強(qiáng)度太大模型剛學(xué)到一部分特征下一輪數(shù)據(jù)語義又變了學(xué)不動(dòng)。第二次我把增強(qiáng)全部關(guān)掉訓(xùn)練loss很快降下去但驗(yàn)證集mAP到80%就再也上不去了典型的過擬合。正確姿勢(shì)是分階段、控制強(qiáng)度。訓(xùn)練初期用基礎(chǔ)增強(qiáng)翻轉(zhuǎn)、輕微縮放、色彩擾動(dòng)讓網(wǎng)絡(luò)先穩(wěn)定收斂等loss掉到低位、模型框架穩(wěn)定了再逐