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