:各類別指標計算與避坑指南)
簡介在語義分割任務(wù)中總體像素精度容易被高頻類別主導逐類別的mIoU才是定位薄弱環(huán)節(jié)的關(guān)鍵指標。這套輕量腳本工具面向PyTorch實踐者聚焦上述評估需求。壓縮包內(nèi)僅含2個Python腳本整體約4KB結(jié)構(gòu)精簡無需額外依賴即可直接運行。兩段腳本分工明確一段將模型輸出轉(zhuǎn)化為8位預(yù)測圖另一段讀取對應(yīng)標注mask逐類別計算IoU并匯總整個測試集的平均mIoU輸出結(jié)果可用于分析類別不平衡等典型問題。腳本邏輯直觀便于按需修改既能用于快速驗證分割模型也能嵌入現(xiàn)有訓練評估流程。目前已有7160人學習下載小巧實用適合語義分割初學者掌握評估細節(jié)也方便研究者快速獲取各類別精度反饋。1. 語義分割里那個繞不開的mIoU它是精度表也是黑匣子做過語義分割的人十有八九都經(jīng)歷過這樣一幕跑完幾十個epoch的模型日志里總損失掉到了0.1以下看起來完美收斂可一算mIoU只有六十幾個點。換一個解碼頭、調(diào)一下輔助loss的權(quán)重mIoU可能突然又漲了三個點。你隱約覺得哪里不對勁但指標擺在那只能繼續(xù)調(diào)參。這個mIoU就是全類別的平均交并比是所有語義分割論文里必須出現(xiàn)的一個數(shù)字也是工程上判斷模型好不好用的硬指標。問題是它并不是一個單一數(shù)字那么簡單——mIoU背后藏著各類別的IoU、類別不均衡、混淆矩陣、忽略像素等一系列容易把人繞進去的細節(jié)。這篇文章只聊一件事怎么把各類別mIoU計算這件事徹底搞清楚從公式原理到代碼實現(xiàn)再到邊界情況和踩坑經(jīng)驗。不管你是剛開始跑FCN、DeepLab還是U-Net還是已經(jīng)在訓遙感影像分割模型但被驗證集指標搞到懷疑人生這篇文章想做的是讓那個黑匣子打開給你看。mIoU全稱是Mean Intersection over Union語義分割任務(wù)中最常見的評價指標衡量模型預(yù)測的每個像素類別和真實標注的重合程度。這個指標直接反映了模型對每個類別的分類精度尤其是小目標、邊緣區(qū)域和類別不平衡場景下的表現(xiàn)。這篇筆記適合所有做語義分割算法落地、模型評估和調(diào)優(yōu)的從業(yè)者從原理到代碼實現(xiàn)從參數(shù)設(shè)置到常見坑點一次講透。2. 從交并比到平均交并比mIoU公式里藏著的高頻細節(jié)2.1 先搞懂單個類別的IoU預(yù)測和標簽的交集/并集但分母不是你想的那樣單個類別的IoU定義很簡單對于類別$c$來說假設(shè)模型把所有像素預(yù)測成了兩類——是類別$c$或者不是類別$c$同時真實標注也把所有像素分成了是類別$c$或者不是類別$c$。于是就有了四個數(shù)字真正例TP預(yù)測是類別$c$且標注也是類別$c$的像素數(shù)、假正例FP預(yù)測是類別$c$但標注不是、假負例FN預(yù)測不是類別$c$但標注是、真負例TN預(yù)測不是且標注也不是。那IoU的數(shù)學形式直觀來看是交集像素數(shù)除以并集像素數(shù)即$$\text{IoU}_c \frac{TP_c}{TP_c FP_c FN_c}$$注意這里的分母并集是三類像素之和而不是TPFPTNFN。TN不在分母里也不在分子里。這個區(qū)分非常重要因為很多初寫代碼的人會很自然地拿整個圖像的像素總數(shù)當分母或者把TN算進去那樣算出來的數(shù)字非常虛高——背景類別占比大時IoU可以被沖到0.99看起來模型無敵了其實只是在碾壓背景。$$IoU_c \frac{TP_c}{TP_c FP_c FN_c}$$再往深想一步FP和FN的性質(zhì)完全不同。FP是模型把別的類別錯認成了類別$c$FN是模型漏檢了本屬于類別$c$的像素。這兩個數(shù)字一不平衡IoU就下來了。以遙感圖像語義分割為例建筑物邊緣通常只有1-2個像素寬一旦解碼器輸出邊緣偏移半個像素FP和FN同時爆發(fā)IoU直接掉到0.5以下而其他指標比如準確率還好端端地停在0.95以上。所以IoU對邊緣像素的敏感度遠比像素準確率高這也是它成為語義分割核心指標的原因。2.2 經(jīng)典的11點法和像素級累加兩種差之毫厘的實現(xiàn)路徑mIoU之所以叫平均顧名思義是把所有類別的IoU求平均。但具體怎么算業(yè)界存在兩套常見做法數(shù)學上等價但工程實現(xiàn)不同數(shù)值上可能有微小差異。第一種是PASCAL VOC最早推廣的辦法先按類別逐張圖像算IoU然后對同一類多張圖的結(jié)果做平均最后對所有類別再做一次平均。這種做法學名叫per-image平均。第二種是像素級累加把整個驗證集的所有像素對應(yīng)的TP、FP、FN先全局累加然后一次性計算每個類別的IoU最后求平均。第二種是現(xiàn)在的主流做法因為整個驗證集上類別分布更穩(wěn)定單張圖如果某個類別只出現(xiàn)幾個像素per-image平均會把這張圖的IoU拉得特別低產(chǎn)生劇烈波動。如果你跟論文對比mIoU數(shù)值盡量確認對方用的是哪種累加方式不然你復現(xiàn)出來的數(shù)字可能和論文差一兩個點。實現(xiàn)上有一個非常重要的細節(jié)各類別的IoU逐類計算完成后求的是算術(shù)平均不是加權(quán)平均。也就是說不管某個類別在數(shù)據(jù)集中只占0.1%的像素還是占50%的像素它對最終mIoU的貢獻是相同的。這就是為什么mIoU天然是類別不均衡場景下更嚴苛的指標——小類別的表現(xiàn)一旦差直接把整體mIoU拖下水。反過來這也意味著如果一個小類別的IoU為0整體mIoU可能會被拉低好幾個點即便模型在絕大多數(shù)像素上都預(yù)測正確。2.3 為什么要各類別單獨算混淆矩陣、weighted IoU和頻率平衡有些語義分割框架默認給你一個總的mIoU數(shù)字不看各類別明細。但工程上真正的排障信息全藏在各類別IoU列表里。舉例來說道路分割任務(wù)里路面的IoU可能已經(jīng)到0.92但人行道只有0.61這說明問題大概率出在路緣邊界附近或者是標注本身不一致。此時如果你只盯著mIoU0.76這個數(shù)字你根本不知道從何下手調(diào)模型。在實踐中我一般會同時打印三個東西各類別的IoU、各類別的TP/FN/FP像素數(shù)、以及類別頻率占比。這三個放在一起才能判斷IoU低到底是標注太差、類別太稀有還是模型根本沒有學會這個類別的特征。順便說一句有個常見的操作是把懲罰因子加進loss或評估里也就是weighted IoU——給罕見類別更高的權(quán)重讓模型優(yōu)先學它。這種做法的前提是你能拿到可靠的各類別IoU來反向設(shè)計權(quán)重系數(shù)而各類別IoU的計算能力正是這一步的地基。類別不均衡對mIoU的影響非常微妙一個做無人駕駛的朋友跟我說過他的血淚經(jīng)驗馬路上行人這一類別只占全部像素的不到2%模型每幀圖像都能錯過分岔路口的那幾個行人像素整個人行類別IoU就只剩0.3上下而總mIoU還能維持在0.72。這個0.72其實掩蓋了安全隱患。所以如果做自動駕駛或遙感影像語義分割建議把每類IoU最低閾值寫進驗收標準而不是只看最終mIoU。3. 動手實現(xiàn)各類別mIoU計算PyTorch代碼、參數(shù)與邊界坑3.1 最小可用的mIoU實現(xiàn)預(yù)測輸出如何對齊標注形狀寫代碼之前先明確輸入輸出。訓練好的模型輸出是一張?zhí)卣鲌D形狀通常是[B, C, H, W]其中C是類別數(shù)。要算mIoU首先要把這張?zhí)卣鲌D轉(zhuǎn)成每個像素的預(yù)測類別也就是在通道維度上做argmax得到[B, H, W]的索引圖。同時真實標注gt的形狀一般是[B, H, W]每個位置存放類別編號邊界往往是255或某個特殊值表示忽略區(qū)域。下面是我在PyTorch里常用的一個基礎(chǔ)實現(xiàn)思路非常直白先算混淆矩陣再逐類取IoUimport torch def compute_miou_per_class(pred, label, num_classes21, ignore_index255): pred: [B, H, W] 已經(jīng)做過argmax的預(yù)測類別索引 label: [B, H, W] 真實標注 num_classes: 類別總數(shù)包含背景 ignore_index: 需要忽略的標注像素值通常為255或-1 返回: 每個類別的IoU列表和整體的mIoU # 先忽略ignore_index的像素置為一個不影響后續(xù)統(tǒng)計的臨時類別 pred pred.clone() label label.clone() ignore_mask (label ignore_index) label_copy label.clone() label_copy[ignore_mask] num_classes # 移到額外的bin里 # 定義有效像素mask并展平為一維 valid_mask (label ! ignore_index) p pred[valid_mask] l label_copy[valid_mask] # 用interruptible的bincount構(gòu)造混淆矩陣 # 混淆矩陣形狀: (num_classes1, num_classes1)最后一個bin是忽略像素 count torch.bincount(l * (num_classes 1) p, minlength(num_classes 1) ** 2) confusion count.view(num_classes 1, num_classes 1) # 只取前num_classes行和前num_classes列忽略bin是第num_classes行/列 confusion confusion[:num_classes, :num_classes] # 逐類計算IoU iou_list [] for c in range(num_classes): tp confusion[c, c].item() fp confusion[:, c].sum().item() - tp fn confusion[c, :].sum().item() - tp denom tp fp fn iou tp / denom if denom 0 else float(nan) iou_list.append(iou) # 只對有效類別求平均nan直接忽略 valid_iou [iou for iou in iou_list if iou iou] # 過濾nan miou sum(valid_iou) / len(valid_iou) if valid_iou else 0.0 return iou_list, miou這段邏輯很簡單bincount把一個像素的(ground_truth類別, 預(yù)測類別)二元組線性化為一個整數(shù)下標然后統(tǒng)計頻次最后reshape回混淆矩陣。有效像素這里只排除了ignore_index但實際場景中你很可能還需要排除預(yù)測為num_classes之外值的越界情況尤其是模型輸出類別數(shù)和你統(tǒng)計類別數(shù)不一致時。另一個常見操作是把預(yù)測和標注都直接拉到0到num_classes-1范圍內(nèi)再統(tǒng)計但那樣忽略了預(yù)測越界這一信息不利于排障。如果數(shù)據(jù)量大到用PyTorch的bincount都變慢可以用更簡單的逐圖累加方式替代但務(wù)必保持全局累加而不是逐圖平均前文已經(jīng)說過了全局累加的mIoU變化更平穩(wěn)。3.2 訓練中如何實時計算并記錄各類別IoU記錄頻率與評估批次模型訓練的時候每一步都算全量驗證集的mIoU當然不現(xiàn)實常規(guī)做法是每隔N個epoch對驗證集跑一次前向推理所有樣本拼接后統(tǒng)一算mIoU。這里有個非常容易翻車的點驗證集一次能塞進顯存嗎如果顯存有限只能一塊一塊算每一塊的預(yù)測結(jié)果不能急著算IoU而是要把所有batch的TP、FP、FN先按類別累加起來。正確姿勢是寫一個混淆矩陣累積器每跑完一個batch就把該batch的混淆矩陣加到全局混淆矩陣上最后統(tǒng)一計算IoU。這個和3.1中直接用全量像素計算本質(zhì)上是一模一樣的但如果你在每個batch上單獨算IoU再平均結(jié)果會被樣本數(shù)量不均衡嚴重影響。代碼上把混淆矩陣累加器定義成類每個batch調(diào)用update接口傳入pred和label訓練結(jié)束時調(diào)用compute_miou。class ConfusionMatrixAccumulator: def __init__(self, num_classes, ignore_index255): self.num_classes num_classes self.ignore_index ignore_index self.confusion torch.zeros((num_classes, num_classes), dtypetorch.long) def update(self, pred, label): # pred和label形狀均為[B, H, W] valid_mask (label ! self.ignore_index) p pred[valid_mask] l label[valid_mask] # 等價于3.1中的bincount邏輯這里直接用矩陣索引累加 count torch.bincount(l * self.num_classes p, minlengthself.num_classes ** 2) self.confusion count.view(self.num_classes, self.num_classes) def compute(self): ious [] for c in range(self.num_classes): tp self.confusion[c, c].item() fp self.confusion[:, c].sum().item() - tp fn self.confusion[c, :].sum().item() - tp denom tp fp fn ious.append(tp / denom if denom 0 else float(nan)) valid [iou for iou in ious if iou iou] return ious, sum(valid) / len(valid) if valid else 0.0注意update里pred必須已經(jīng)是argmax之后的形狀如果你直接把概率圖傳進來這個類的運算就會錯得離譜。另外在訓練時我建議記錄頻率不是每個epoch都打印全部類別IoU那樣日志太長而是用trainer的eval回調(diào)每5個epoch打印一次全量類別IoU表格其余只打印mIoU和loss。真正發(fā)生突變的是某個特定類別IoU的波動而不是總mIoU這個細節(jié)能幫你早幾個epoch發(fā)現(xiàn)問題。3.3 三張圖像分割神器對比MMSegmentation與TorchMetrics如果你不想自己手寫mIoU計算也可以用現(xiàn)成框架。MMSegmentation里有一個IoU的Metric類它內(nèi)置了ignore_index、accumulate等參數(shù)底層就是混淆矩陣累加你只需要在配置里指定typeIoU它會在每個驗證周期結(jié)束時輸出各類別IoU和mIoU。這類框架的好處是省事壞處是如果配置不當比如忘記設(shè)ignore_indexNone導致標注邊界被統(tǒng)計容易得到虛高或者虛低的指標。另外有一個輕量的選擇是TorchMetrics的JaccardIndex它可以直接在訓練循環(huán)里用支持num_classes、averagemacro或者nonenone正好返回每個類別的IoU列表。它的工程實現(xiàn)是純Tensor化的延遲很低適合在訓練循環(huán)里每個batch都算但要注意TorchMetrics里的averagemacro默認是加權(quán)平均還是算術(shù)平均不同版本行為有差異務(wù)必讀源碼確認。我個人的習慣是自己的實驗代碼里用3.1的手寫版本因為能控住所有細節(jié)跑大批次對比實驗時用MMSegmentation因為它自帶驗證流程和日志模塊。兩者算出來的數(shù)值應(yīng)該極其接近如果出現(xiàn)偏差優(yōu)先檢查類別排序是否一致——很多框架的類別順序是按ASCII碼或者數(shù)據(jù)集字典序排的不一定是你心里想的背景在0號位。3.4 訓練過程中mIoU不漲但loss下降可能哪里出了問題這個現(xiàn)象非常常見——訓練loss持續(xù)下降但驗證mIoU停滯甚至抖動。最經(jīng)典的原因有兩個。第一個是閾值偏移。如果你的模型輸出沒有做argmax而是直接拿概率圖里的最大值當預(yù)測類別那么只要相對的類別概率大小關(guān)系對了loss已經(jīng)很低了mIoU自然接近飽和。但如果各類別概率分布越來越尖銳但選錯類別mIoU就會紋絲不動。這時你需要檢查的是模型是否過度自信——看各類別平均置信度如果一個類別的平均置信度是0.97但IoU只有0.4那大概率是模型在瞎蒙一個高頻類別。第二個是標注噪聲。語義分割標注的邊界其實非常主觀城市景觀數(shù)據(jù)集中某些類的邊緣像素標注在不同標注員之間可能差異十幾像素。如果你用MSE或CE loss硬學這些噪聲標注模型輸出的概率圖會變得平滑但IoU可能上不去。這里有個玄學經(jīng)驗如果訓練魯棒性差可以把邊緣幾像素的標簽?zāi)ǖ粝喈斢跀U大ignore區(qū)域mIoU反而可能上漲1-2個點。這種做法本質(zhì)上是在告訴模型別學我不確定的東西。4. 各類別mIoU的避坑指南五個最容易翻車的地方4.1 第一個坑ignore_index沒處理好把背景或邊界帶進統(tǒng)計現(xiàn)象mIoU異常偏高或異常偏低一查發(fā)現(xiàn)某個類別——通常是背景——的IoU高達0.99其他類則全部低于0.3最終mIoU看似正常但完全沒有參考價值。原因很多數(shù)據(jù)集在標注時邊界像素、未標注區(qū)域或特定類別都被標成255或者-1如果代碼里沒有屏蔽這些像素它們會被當成背景類別統(tǒng)計進去背景的TP數(shù)量擴大好幾倍IoU被拉飛了。解決檢查評估代碼里的valid_mask是否排除了ignore_index同時還要注意255這類值在PyTorch里會變成無符號大數(shù)直接參與bincount會把混淆矩陣撐爆。最安全的做法是先把label里所有ignore_index替換成一個在num_classes之外的孤立值再做掩碼統(tǒng)計。4.2 第二個坑類別tensor的精度和device不一致導致靜默出錯現(xiàn)象訓練時一切正常一到算mIoU就報錯或者mIoU值跳變到不合理范圍代碼不報異常但結(jié)果看著詭異。原因pred經(jīng)過了GPU上的argmax算出來的索引值label可能是從Dataset里讀出來的PIL Image轉(zhuǎn)的numpy數(shù)組兩者一個是torch.float32一個是torch.int64且device不同。當你做pred[valid_mask]時類型被隱式轉(zhuǎn)換數(shù)值根本不匹配導致混淆矩陣完全錯位。解決統(tǒng)一 tensor 的類型和device最好統(tǒng)一為torch.long并放到CPU上算mIoU。還有一個容易被忽視的點有些數(shù)據(jù)集的標注類別從1開始而不是0此時要在訓練前做一個整體減1的操作否則所有類別都錯位mIoU直接變成0.1以下。4.3 第三個坑類別數(shù)沒對齊預(yù)測的通道數(shù)和評估的num_classes不一致現(xiàn)象模型最后一層有19個通道但你評估時傳的num_classes21或者反過來。結(jié)果就是某些類別幾乎沒有positive預(yù)測IoU異常低甚至有的類完全沒出現(xiàn)在混淆矩陣里。原因定義模型分類頭時用了num_classes變量定義評估器時可能硬編碼或者從不同配置文件讀取兩處一旦沒同步就會靜默錯位。解決寫一個單例配置類或直接使用同一份config文件里的num_classes并且啟動評估時打印模型類別數(shù) vs 評估類別數(shù)的日志確保兩者一致。這個檢查應(yīng)該寫進CI或訓練腳本里而不是手動確認。4.4 第四個坑小數(shù)位取舍導致論文復現(xiàn)時mIoU對不上現(xiàn)象自己實現(xiàn)在驗證集上跑出的mIoU是0.674但論文報告的是0.680怎么調(diào)都復現(xiàn)不出。多方排查后發(fā)現(xiàn)問題出在統(tǒng)計方式差異——論文可能只在subset上評估或者每張圖單獨算IoU再平均或者四舍五入到小數(shù)點后一位。原因嚴格來說這不是實現(xiàn)的錯而是評估協(xié)議不一致。不同數(shù)據(jù)集的官方評測工具有不同的邊界處理方式比如Cityscapes在eval期間會把邊界像素的預(yù)測類別忽略掉而PASCAL VOC則全部計入。解決看論文的Evaluation段落確認對方有沒有提到ignore pixels、背景類是否算入mIoU、鏡像翻轉(zhuǎn)或其他數(shù)據(jù)增強是否應(yīng)用在推理階段。復現(xiàn)的時候務(wù)必使用官方評估腳本而不是自己寫一套近似實現(xiàn)。如果實在沒有官方腳本就在論文里說清楚自己的評估條件和忽略像素規(guī)則避免別人復現(xiàn)時產(chǎn)生疑慮。4.5 第五個坑類別不均衡導致小類IoU不升反降誤判模型在變差現(xiàn)象某次加了數(shù)據(jù)增強或者改了loss權(quán)重總mIoU從0.75漲到0.76但小類別的IoU從0.31掉到0.28。有人會很緊張覺得模型退化了于是rollback版本。原因mIoU是算術(shù)平均你看到的0.01漲幅可能完全來自大類別的提升而小類別本來就只有幾百個像素個別圖像標注錯誤就足以讓它的IoU波動幾個百分點。這不是模型退化是統(tǒng)計噪聲。解決畫每個類別的TP/FP/FN隨時間變化的曲線而不是只看IoU數(shù)字。如果小類別的TP在增加、FP也在略增一般說明模型在往正確方向走如果FP快速增長但TP不動說明模型在把小類別誤分類成大類別。另外評估集如果太小建議做多次采樣評估并報告均值±方差。5. 從mIoU再往前一步邊界IoU與頻率加權(quán)是什么值不值得投入5.1 邊界IoU對邊緣更敏感適合評估分割質(zhì)量標準mIoU對所有像素的貢獻是一視同仁的因此一張圖上占大面積的類別主導了統(tǒng)計。邊緣像素往往只占不到5%的比例但它們才是決定分割質(zhì)量觀感的關(guān)鍵。為了更精確地評估邊緣質(zhì)量業(yè)界提出了Boundary IoU它的思路很直接只統(tǒng)計預(yù)測和標注的邊界區(qū)域附近的像素。實現(xiàn)上Boundary IoU通常先把標注和預(yù)測各做一次形態(tài)學腐蝕保留邊界帶然后在邊界帶內(nèi)計算TP、FP、FN。如果模型邊緣抖動嚴重標準mIoU可能只掉0.5-1個點但Boundary IoU能掉5-10個點放大問題。這個指標適合用在醫(yī)學圖像、衛(wèi)星圖像等對邊界精度要求極高的任務(wù)中如果只是做傳統(tǒng)的街景分割它的增益未必明顯但可以做參考指標來觀察模型瓶頸。5.2 類別頻率加權(quán)mIoU什么時候該用它來替代原版mIoU對所有類別一視同仁但如果你的應(yīng)用核心是背景和大面積物體小類別幾乎不影響安全性那么不加權(quán)確實說不過去。Frequency Weighted IoUFWIoU就是把每個類別的IoU按像素頻率加權(quán)后求平均這樣占比高的類別對指標的貢獻更大數(shù)值上更接近人眼對整體視覺質(zhì)量的判斷。FWIoU的值通常比mIoU高因為高頻類別的IoU一般也更高。在做產(chǎn)品驗收時我會同時報告mIoU和FWIoU前者告訴算法團隊模型在小類上的極限后者告訴產(chǎn)品團隊模型在真實場景中的體感效果。如果你的目標是發(fā)論文或比賽沖榜建議鎖定mIoU因為它是通行語言如果目標是業(yè)務(wù)交付兩者都看但驗收線畫在FWIoU上更貼合實際。5.3 訓練時直接優(yōu)化mIoU可微近似和置信度校準的兩個選擇既然mIoU是最終評價指標自然有很多人想在訓練時直接優(yōu)化它。mIoU本身不可微因為它內(nèi)部有argmax操作所以常規(guī)做法是用Lovasz-Softmax或者Dice loss做代理它們分別從排序損失和集合相似度的角度近似mIoU。Lovasz-Softmax的做法是把每個像素的誤差按大小排序然后對排序后的誤差向量做凸損失的逐步優(yōu)化它的梯度能讓模型在訓練過程中優(yōu)先修正那些IoU貢獻大的誤分類像素。Dice loss則直接對每個類別的soft輸出和標簽的重疊程度求梯度對小類別天然有放大作用。我自己在遙感影像分割里試下來的經(jīng)驗是Lovasz-Softmax作為輔助loss與CrossEntropy混合mIoU能比純CE高0.5-1.5個點但要注意讓輔助loss的權(quán)重不要過大初始設(shè)0.1-0.3之間再根據(jù)loss數(shù)值動態(tài)調(diào)整。如果你追求的是工程上的確定性還有一種間接但有效的思路先用標準CE訓練到mIoU平臺期再凍結(jié)backbone只訓head并用Dice loss微調(diào)20個epoch往往能再擠出一點mIoU。這種兩段式訓練不需要改模型結(jié)構(gòu)只是換了一下loss調(diào)度策略性價比很高。5.4 大模型時代的新問題類別加權(quán)與logit縮放對mIoU的影響這兩年用的分割模型越來越大有的直接在CLIP、SAM預(yù)訓練權(quán)重上微調(diào)。在這種大模型上各類別的logit尺度差異可能非常大直接做argmax取值會有系統(tǒng)性偏差。你會看到某個高頻類別幾乎霸屏小類別全部消失mIoU低得離譜。解決方法是給logits做類別級別的溫度縮放也就是在argmax前對每個類別的logit乘以一個可學習的系數(shù)這個系數(shù)用驗證集的混淆矩陣來調(diào)優(yōu)。本質(zhì)上等于在評估時做一個類別偏置的補償。這個技巧如果配合類別頻率加權(quán)使用可以顯著提升小類別的IoU代價是多了一個超參數(shù)需要小心過擬合驗證集。一個血淚經(jīng)驗是這個縮放系數(shù)必須在另一個驗證集上確認不能在訓練集上反推否則會把訓練集的偏差放大到評估結(jié)果上。6. 驗證和調(diào)參技巧如何用各類別mIoU反向定位模型問題6.1 可視化混淆矩陣一眼看出哪些類別互相打架一個模型mIoU只有0.63但你不知道是所有類別都差一點還是兩三個類別互相嚴重混淆。這時候把整個驗證集的混淆矩陣畫出來類別之間互相錯認的規(guī)律一清二楚。在Python里可以直接用matplotlib的imshow配合colorbar畫混淆矩陣圖行是真實類別列是預(yù)測類別。你通常會發(fā)現(xiàn)一個規(guī)律形狀相似或語義相近的類別之間互相混淆嚴重比如卡車vs公交車、植被vs樹。看到這種模式就該考慮給模型加更多上下文信息或者在解碼器階段對不同類別分支做區(qū)分。另一個常見規(guī)律是一切類別向背景靠攏如果背景列的數(shù)值整體偏高說明模型把前景當背景的概率很大這是類別不均衡下的典型表現(xiàn)此時可以考慮給前景類別的loss加權(quán)重。6.2 per-image監(jiān)測哪張圖的mIoU最低定位數(shù)據(jù)集本身的坑除了全局混淆矩陣逐張計算圖像IoU并排序找到最低的幾張圖去肉眼檢查是定位數(shù)據(jù)問題的利器。我經(jīng)常發(fā)現(xiàn)mIoU最低的圖像幾乎全是標注有問題的圖——比如路牌被遺漏、小型車輛在遠距離完全沒標注。這類情況光看指標根本無法發(fā)現(xiàn)但一旦發(fā)現(xiàn)把它們從驗證集里剔除或修正mIoU可能立刻提升1-2個點。做法不復雜在eval循環(huán)里為每張圖像維護一個獨立的mIoU列表跑完后按值排序把后5%的圖像路徑導出到一個txt文件再配合可視化腳本截圖保存。需要注意的是這種挑刺只能在調(diào)試階段做一旦決定用某個測試集做最終驗收就不要再反復挑刺刪圖否則指標失去了公正性。這里有一個折中的做法把評估數(shù)據(jù)分成兩個集合一個用于日常迭代找bug一個最終定版只跑一次。6.3 把mIoU拆成precision和recall繞開指標掩蓋問題IoU是precision和recall的一種復合形態(tài)但它把兩者壓縮成單個數(shù)字有時會掩蓋問題。比如某個類別IoU是0.5但你的precision可能是0.9recall只有0.32也就是說模型很少漏檢但誤檢極多導致大量FP在分母里膨脹。這種情況在很多目標比較稀疏的任務(wù)里很常見。排查時我給每類打印precision和recall再看看邊界。如果recall低但是precision高考慮降低置信度閾值、增加正樣本的采樣如果precision低但recall高則考慮收緊邊界或者增加負樣本。本質(zhì)上這可以讓你判斷mIoU低是定位不準還是檢測太少。6.4 多尺度與TTA推理是否能提升mIoU我自己的經(jīng)驗和判斷常見做法是在推理時做多尺度縮放并融合結(jié)果最典型的TTA配置是[0.5, 0.75, 1.0, 1.25, 1.5]五種尺度再把預(yù)測的概率圖縮放回原分辨率做平均。這個操作通常能讓mIoU漲0.3-0.8個點尤其是對邊緣細節(jié)和尺度變化明顯的遙感場景增益明顯。另一個效果很好的技巧是水平翻轉(zhuǎn)做TTA成本只有一次額外前向推理。但老實說TTA帶來的提升在大型backbone上并不總是值得的因為推理時間翻倍甚至五倍。最終是否需要TTA取決于業(yè)務(wù)場景的延遲要求。如果你服務(wù)端推理有50ms預(yù)算加一次水平翻轉(zhuǎn)可能剛好能承受如果是實時視頻流就別做TTA了。這里有一個實際參數(shù)建議驗證階段用TTA做best model的最終確認訓練過程中的epoch評估不要開TTA因為會拖慢實驗循環(huán)。我自己的習慣是先不開TTA跑完一個訓練周期選出一個候選模型然后只對這個候選模型開TTA做最終報告這樣既保證了驗證的嚴謹性又不拖慢實驗迭代速度。6.5 給論文或項目交付的一個mIoU報告模板避免被質(zhì)疑不管你是發(fā)論文還是交付項目最終團隊之間對mIoU的理解不一致會帶來巨大扯皮。建議報告至少包含以下內(nèi)容數(shù)據(jù)集名稱和圖像數(shù)量類別列表和每類的像素占比confusion matrix圖每類precision、recall、IoU和整體mIoU表格是否忽略邊界像素、ignore_index值推理時是否使用TTA及多尺度參數(shù)單GPU還是多GPU平均、world size是否影響B(tài)atchNorm統(tǒng)計量以及評估腳本的commit版本。把這些寫進報告后別人復現(xiàn)你的數(shù)字就非常容易了。如果你的評估腳本和訓練腳本不在同一個倉庫遞交給對方時順便附上一個版本號讓驗收方知道自己的代碼對沒對上。被質(zhì)疑mIoU造假是一件極痛苦的事而這種事大多源于評測約定不清楚而不是真正的偽造。最后說一個我自己的教訓在遙感影像語義分割項目上有一次我從開源的MMSegmentation換到自己的輕量推理管線結(jié)果mIoU掉了0.03。我花了整整兩天排查最后發(fā)現(xiàn)是Resize時插值方式從bilinear變成了nearest導致邊界像素發(fā)生了微妙偏移。從那天起我給自己立了一個規(guī)矩任何評估代碼的改動都用同一組測試圖像跑一遍新舊管線對比mIoU差異差異超過0.1個點就必須找到原因再繼續(xù)。這個習慣救了我很多次。希望幫到你。本文還有配套的精品資源點擊獲取