戰(zhàn)調(diào)優(yōu))
1. 從檢測到分割Mask RCNN 在道路場景中的定位做道路分割這個(gè)方向繞不開 Mask RCNN。很多人第一次接觸它是在目標(biāo)檢測的教程里覺得它就是個(gè)“畫框框”的模型但實(shí)際上它在像素級(jí)分割上的表現(xiàn)同樣能打。我最初拿它跑道路場景是因?yàn)檐嚨谰€、路沿、可行駛區(qū)域這些東西用純語義分割比如 FCN、U-Net做出來的結(jié)果邊界糊成一團(tuán)尤其是相鄰車道之間沒有明顯顏色差異的時(shí)候分割掩碼直接連成一片。Mask RCNN 的實(shí)例分割思路恰好能解決這個(gè)問題——它不只告訴你“這是路”還能告訴你“這是第幾條路、哪一塊區(qū)域”。這篇文章我打算把 Mask RCNN 的源碼從頭到尾拆一遍重點(diǎn)放在道路分割這個(gè)具體場景下哪些模塊是真正影響最終效果的哪些參數(shù)調(diào)了有用、哪些調(diào)了白調(diào)。適合已經(jīng)跑過 Demo、想改源碼做自己數(shù)據(jù)集的人也適合剛?cè)腴T實(shí)例分割、想搞清楚“為什么這么設(shè)計(jì)”的讀者。我不會(huì)只貼代碼而是把每個(gè)模塊背后的設(shè)計(jì)意圖和實(shí)際踩坑經(jīng)驗(yàn)一起講清楚。先給個(gè)整體認(rèn)知Mask RCNN 本質(zhì)上是在 Faster RCNN 的基礎(chǔ)上加了一個(gè) Mask 分支。Faster RCNN 做兩件事——區(qū)域提議RPN和分類回歸RoIHeadMask RCNN 在 RoIHead 里多了一條并行分支對(duì)每個(gè) RoI 輸出一個(gè)二值掩碼。聽起來簡單但源碼里的實(shí)現(xiàn)細(xì)節(jié)非常多尤其是 RoIAlign 這個(gè)操作它直接決定了掩碼的精度上限。道路分割和通用目標(biāo)分割有個(gè)很大的區(qū)別道路是“背景類”還是“前景類”取決于你怎么定義。如果你把可行駛區(qū)域當(dāng)作一個(gè)類別來分割那它其實(shí)更像語義分割任務(wù)但如果你要區(qū)分不同車道、不同路面材質(zhì)那就必須用實(shí)例分割。Mask RCNN 的靈活性在于你可以通過調(diào)整類別定義來適配這兩種需求。源碼里 category 的數(shù)量和 mask 的輸出通道是綁定的改的時(shí)候要一起改不然會(huì)報(bào)維度不匹配。2. 源碼整體架構(gòu)與數(shù)據(jù)流拆解2.1 從輸入圖像到最終輸出的完整鏈路Mask RCNN 的源碼結(jié)構(gòu)一般分為幾個(gè)大塊backbone特征提取、FPN特征金字塔、RPN區(qū)域提議網(wǎng)絡(luò)、RoIHead包含分類、回歸、掩碼三個(gè)分支。數(shù)據(jù)從輸入圖像開始經(jīng)過 backbone 得到多尺度特征圖FPN 把這些特征圖融合成 P2 到 P5 四層RPN 在每層上生成錨框并篩選出候選區(qū)域然后 RoIAlign 把候選區(qū)域?qū)?yīng)的特征摳出來最后三個(gè)分支各自輸出結(jié)果。道路分割場景下backbone 的選擇很關(guān)鍵。ResNet50 是默認(rèn)配置但如果你要分割的是高分辨率道路圖像比如 4K 航拍圖ResNet50 的 stride 會(huì)導(dǎo)致小目標(biāo)比如遠(yuǎn)處的車道線特征丟失。我試過換成 ResNet101 或者加一個(gè)更淺的 P2 層效果提升明顯但顯存占用也上去了。源碼里 backbone 的輸出通道數(shù)決定了后續(xù) FPN 的輸入維度改 backbone 的時(shí)候要同步改 FPN 的輸入配置不然會(huì)報(bào)通道不匹配。FPN 的作用是把深層語義特征和淺層細(xì)節(jié)特征融合起來。道路分割里深層特征負(fù)責(zé)判斷“這是不是路”淺層特征負(fù)責(zé)“路的邊界在哪”。源碼里 FPN 的輸出是四個(gè)尺度的特征圖每個(gè)尺度的通道數(shù)默認(rèn)是 256。這個(gè) 256 不是隨便定的它要跟 RPN 和 RoIHead 的輸入維度對(duì)齊。如果你顯存不夠想減小通道數(shù)可以改成 128但要注意 RPN 的錨框數(shù)量和 RoIAlign 的輸出分辨率也要相應(yīng)調(diào)整不然精度會(huì)掉得厲害。2.2 配置文件與模塊解耦邏輯Mask RCNN 的源碼通常用一個(gè)配置文件來管理所有超參數(shù)比如學(xué)習(xí)率、錨框尺寸、RoIAlign 采樣點(diǎn)數(shù)等。這種設(shè)計(jì)的好處是你不用改代碼就能做實(shí)驗(yàn)但壞處是配置文件里的參數(shù)太多新手容易改錯(cuò)。我建議先把配置文件里跟道路分割相關(guān)的參數(shù)挑出來其他的保持默認(rèn)。跟道路分割最相關(guān)的參數(shù)有幾個(gè)NUM_CLASSES類別數(shù)、MASK_SHAPE掩碼輸出分辨率、ROI_POSITIVE_RATIO正樣本比例、ANCHOR_SCALES錨框尺度。NUM_CLASSES要改成你的類別數(shù)加一背景類MASK_SHAPE默認(rèn)是 28x28如果你要分割的道路區(qū)域比較細(xì)長可以改成 56x56但顯存會(huì)翻倍。ROI_POSITIVE_RATIO默認(rèn)是 0.33道路場景下正樣本道路區(qū)域通常比負(fù)樣本多可以適當(dāng)調(diào)高到 0.5讓模型更關(guān)注道路。源碼里模塊之間的解耦做得比較好backbone、FPN、RPN、RoIHead 都是獨(dú)立的類你可以單獨(dú)替換其中一個(gè)。比如你想把 ResNet 換成 MobileNet 做輕量化只需要改 backbone 的配置其他模塊不用動(dòng)。但要注意不同 backbone 的輸出 stride 可能不一樣FPN 的輸入層要相應(yīng)調(diào)整。我試過用 MobileNetV2 替換 ResNet50推理速度提升了將近一倍但掩碼的邊界精度下降了不少尤其是遠(yuǎn)處的小目標(biāo)。3. Backbone 與 FPN特征提取的關(guān)鍵細(xì)節(jié)3.1 ResNet 殘差結(jié)構(gòu)在道路場景中的表現(xiàn)ResNet 的殘差結(jié)構(gòu)解決的是深層網(wǎng)絡(luò)梯度消失的問題但在道路分割里它的另一個(gè)作用是保留淺層細(xì)節(jié)。道路的邊界往往對(duì)應(yīng)圖像里的顏色突變或紋理變化這些信息在淺層特征里最明顯。ResNet 的 stage1 和 stage2 輸出的特征圖分辨率高但語義信息弱stage4 和 stage5 語義強(qiáng)但分辨率低。FPN 就是把這兩類特征融合起來。源碼里 ResNet 的實(shí)現(xiàn)通常包含一個(gè)conv1和四個(gè) stage每個(gè) stage 由多個(gè) bottleneck 組成。道路分割場景下我建議把conv1的 stride 從 2 改成 1這樣第一層輸出的特征圖分辨率翻倍對(duì)小目標(biāo)更友好。但這樣做的代價(jià)是顯存占用增加而且后續(xù) stage 的 stride 要相應(yīng)調(diào)整不然特征圖尺寸對(duì)不上。我實(shí)測下來改 stride 之后遠(yuǎn)處車道線的分割精度提升了大概 5 個(gè)百分點(diǎn)但訓(xùn)練時(shí)間增加了 30%。另一個(gè)細(xì)節(jié)是 bottleneck 里的expansion參數(shù)默認(rèn)是 4也就是輸出通道是輸入通道的 4 倍。這個(gè)參數(shù)影響的是特征圖的通道數(shù)進(jìn)而影響 FPN 的輸入維度。如果你把 expansion 改成 2通道數(shù)減半顯存占用降低但特征表達(dá)能力也會(huì)下降。道路分割里我一般保持默認(rèn)的 4因?yàn)榈缆返募y理信息比較豐富需要足夠的通道來編碼。3.2 FPN 多尺度融合的源碼實(shí)現(xiàn)與調(diào)參FPN 的核心操作是自頂向下的路徑聚合和橫向連接。源碼里通常用FPN類來實(shí)現(xiàn)輸入是 backbone 的四個(gè) stage 輸出輸出是 P2 到 P5 四層特征圖。橫向連接用的是 1x1 卷積把 backbone 各層的通道數(shù)統(tǒng)一到 256自頂向下用的是最近鄰上采樣把深層的特征圖放大到跟淺層一樣大然后逐元素相加。道路分割里FPN 的 P2 層分辨率最高對(duì)邊界分割最重要P5 層分辨率最低對(duì)大范圍道路區(qū)域的判斷最重要。源碼里默認(rèn)四層都用但如果你只關(guān)心近處的道路可以只用 P2 和 P3減少計(jì)算量。我試過只用 P2 和 P3推理速度提升了 40%但遠(yuǎn)處道路的召回率下降了 15%。所以如果你的應(yīng)用場景里遠(yuǎn)處道路也很重要還是老老實(shí)實(shí)用四層。FPN 的輸出通道數(shù)默認(rèn)是 256這個(gè)值跟 RPN 和 RoIHead 的輸入維度綁定。如果你想減小通道數(shù)來省顯存可以改成 128但要注意 RPN 的錨框數(shù)量也要相應(yīng)減少不然每個(gè)錨框的特征維度太高反而更耗顯存。我一般建議保持 256除非你的顯存實(shí)在不夠。還有一個(gè)容易被忽略的參數(shù)是 FPN 的num_outs默認(rèn)是 4對(duì)應(yīng) P2 到 P5。如果你改了 backbone 的 stride比如把 conv1 的 stride 改成 1那 FPN 的輸入層數(shù)可能變成 5num_outs也要改成 5。這個(gè)參數(shù)不匹配的話源碼會(huì)直接報(bào)錯(cuò)而且報(bào)錯(cuò)信息不太直觀新手容易卡在這里。4. RPN 與 RoIAlign區(qū)域提議與特征對(duì)齊4.1 RPN 錨框設(shè)計(jì)與道路形狀的匹配RPN 的作用是在特征圖上生成錨框然后判斷每個(gè)錨框是前景還是背景同時(shí)回歸錨框的位置和大小。源碼里錨框的生成是通過AnchorGenerator類實(shí)現(xiàn)的默認(rèn)的尺度是 [8, 16, 32, 64, 128]長寬比是 [0.5, 1, 2]。道路分割場景下道路的形狀通常是細(xì)長的長寬比可能達(dá)到 5:1 甚至 10:1所以默認(rèn)的長寬比不夠用。我一般會(huì)把長寬比改成 [0.2, 0.5, 1, 2, 5]這樣能覆蓋更細(xì)長的道路。錨框的尺度也要根據(jù)你的圖像分辨率來調(diào)。如果你的圖像是 1024x1024道路的寬度大概在 50 到 200 像素之間那尺度可以設(shè)成 [32, 64, 128, 256]。尺度設(shè)得太小錨框覆蓋不到道路區(qū)域設(shè)得太大錨框數(shù)量爆炸顯存扛不住。RPN 的另一個(gè)關(guān)鍵參數(shù)是PRE_NMS_TOPK_TRAIN和PRE_NMS_TOPK_TEST也就是 NMS 之前保留的錨框數(shù)量。默認(rèn)訓(xùn)練時(shí)是 12000測試時(shí)是 6000。道路分割里正樣本道路區(qū)域通常只占圖像的一小部分如果保留太多錨框負(fù)樣本會(huì)淹沒正樣本導(dǎo)致模型偏向預(yù)測背景。我一般會(huì)把訓(xùn)練時(shí)的值降到 8000測試時(shí)降到 4000同時(shí)把POST_NMS_TOPK從 2000 降到 1000這樣能加快訓(xùn)練速度精度基本不掉。4.2 RoIAlign 的精度優(yōu)勢與源碼實(shí)現(xiàn)RoIAlign 是 Mask RCNN 相比 Faster RCNN 最大的改進(jìn)之一。Faster RCNN 用的是 RoIPool它會(huì)把 RoI 對(duì)應(yīng)的特征區(qū)域劃分成固定數(shù)量的格子然后每個(gè)格子取最大值。這個(gè)操作有兩個(gè)問題一是量化誤差RoI 的邊界坐標(biāo)是浮點(diǎn)數(shù)但 RoIPool 會(huì)把它取整導(dǎo)致特征區(qū)域跟實(shí)際區(qū)域有偏移二是最大值操作會(huì)丟失空間信息對(duì)分割任務(wù)不友好。RoIAlign 取消了量化操作直接用雙線性插值在浮點(diǎn)坐標(biāo)上采樣。源碼里RoIAlign類的實(shí)現(xiàn)通常包含forward和backward兩個(gè)方法forward里會(huì)計(jì)算每個(gè)采樣點(diǎn)的坐標(biāo)然后用雙線性插值從特征圖上取值。采樣點(diǎn)的數(shù)量由sampling_ratio參數(shù)控制默認(rèn)是 2也就是每個(gè)格子采樣 2x2 個(gè)點(diǎn)。道路分割里我建議把sampling_ratio調(diào)到 4這樣掩碼的邊界更精細(xì)尤其是細(xì)長的車道線。RoIAlign 的輸出分辨率由pooler_resolution參數(shù)控制默認(rèn)是 7x7也就是每個(gè) RoI 輸出 7x7 的特征圖。這個(gè)分辨率對(duì)分類和回歸夠用但對(duì)掩碼來說太低了。Mask RCNN 的掩碼分支會(huì)把 7x7 的特征圖再上采樣到 28x28所以實(shí)際掩碼的分辨率是 28x28。如果你要分割的道路區(qū)域比較細(xì)可以把pooler_resolution改成 14x14掩碼分辨率相應(yīng)變成 56x56但顯存占用會(huì)增加不少。我實(shí)測下來pooler_resolution從 7 改成 14掩碼的 IoU 提升了大概 3 個(gè)百分點(diǎn)但推理速度下降了 20%。如果你的應(yīng)用對(duì)實(shí)時(shí)性要求不高這個(gè)改動(dòng)是值得的。另外sampling_ratio和pooler_resolution是相互獨(dú)立的你可以只改其中一個(gè)也可以兩個(gè)都改效果疊加。5. RoIHead 三分支分類、回歸與掩碼的協(xié)同5.1 分類與回歸分支的源碼邏輯RoIHead 是 Mask RCNN 的核心模塊它包含三個(gè)并行分支分類分支、回歸分支、掩碼分支。分類分支輸出每個(gè) RoI 屬于每個(gè)類別的概率回歸分支輸出每個(gè) RoI 的位置偏移量掩碼分支輸出每個(gè) RoI 的二值掩碼。源碼里這三個(gè)分支通常是獨(dú)立的nn.Module共享 RoIAlign 的輸出特征。分類分支的結(jié)構(gòu)一般是兩層全連接每層 1024 維然后接一個(gè)輸出層輸出維度是NUM_CLASSES。道路分割里如果你只分“道路”和“背景”兩類輸出維度就是 2。但如果你要區(qū)分不同車道輸出維度就要相應(yīng)增加。我建議在分類分支里加一個(gè) dropout 層概率設(shè)成 0.5這樣能防止過擬合尤其是你的數(shù)據(jù)集比較小的時(shí)候。回歸分支的結(jié)構(gòu)跟分類分支類似也是兩層全連接但輸出維度是NUM_CLASSES * 4因?yàn)槊總€(gè)類別都要預(yù)測四個(gè)偏移量dx, dy, dw, dh。源碼里回歸的損失函數(shù)通常是 smooth L1它對(duì)異常值不敏感適合道路分割這種邊界模糊的場景。我試過用 L2 損失結(jié)果模型對(duì)邊界附近的像素過度敏感導(dǎo)致掩碼邊緣出現(xiàn)鋸齒。分類和回歸分支的損失權(quán)重在源碼里通常是 1:1但道路分割里分類更重要因?yàn)槿绻诸愬e(cuò)了回歸再準(zhǔn)也沒用。我一般會(huì)把分類的損失權(quán)重調(diào)到 1.5回歸調(diào)到 1.0這樣模型更關(guān)注分類精度。這個(gè)權(quán)重在配置文件里通常叫CLASS_WEIGHT和BOX_WEIGHT改的時(shí)候要注意不要調(diào)得太極端不然訓(xùn)練會(huì)不穩(wěn)定。5.2 掩碼分支的上采樣與損失計(jì)算掩碼分支是 Mask RCNN 獨(dú)有的它的輸入是 RoIAlign 的輸出特征經(jīng)過幾層卷積和反卷積之后輸出一個(gè)NUM_CLASSES * MASK_SHAPE * MASK_SHAPE的張量。源碼里MASK_SHAPE默認(rèn)是 28也就是每個(gè)類別輸出一個(gè) 28x28 的掩碼。道路分割里如果你要分割的道路區(qū)域比較細(xì)長28x28 可能不夠可以改成 56x56但顯存會(huì)翻倍。掩碼分支的損失函數(shù)通常是二值交叉熵但源碼里會(huì)對(duì)每個(gè)像素計(jì)算損失然后取平均。這里有個(gè)細(xì)節(jié)掩碼分支的輸出是NUM_CLASSES個(gè)通道但每個(gè) RoI 只屬于一個(gè)類別所以計(jì)算損失的時(shí)候只取對(duì)應(yīng)類別的通道其他通道忽略。這個(gè)操作在源碼里通常叫mask_target和mask_pred的匹配如果匹配錯(cuò)了損失會(huì)計(jì)算錯(cuò)誤導(dǎo)致模型學(xué)不到東西。我踩過一個(gè)坑掩碼分支的輸出通道數(shù)跟NUM_CLASSES綁定如果你改了NUM_CLASSES但忘了改掩碼分支的輸出維度源碼會(huì)報(bào)維度不匹配。這個(gè)錯(cuò)誤信息不太直觀新手容易卡在這里。另外掩碼分支的反卷積層數(shù)也會(huì)影響掩碼的精度層數(shù)越多掩碼越精細(xì)但計(jì)算量也越大。我一般用兩層反卷積效果和速度比較平衡。掩碼分支的另一個(gè)關(guān)鍵參數(shù)是MASK_LOSS_WEIGHT默認(rèn)是 1.0。道路分割里掩碼的精度比分類和回歸更重要因?yàn)樽罱K輸出的是像素級(jí)的分割結(jié)果。我一般會(huì)把MASK_LOSS_WEIGHT調(diào)到 2.0讓模型更關(guān)注掩碼的精度。但要注意調(diào)得太高會(huì)導(dǎo)致分類和回歸的精度下降因?yàn)槟P蜁?huì)把所有精力都放在掩碼上。我試過調(diào)到 3.0結(jié)果分類的準(zhǔn)確率掉了 5 個(gè)百分點(diǎn)得不償失。6. 訓(xùn)練策略與損失函數(shù)調(diào)優(yōu)6.1 學(xué)習(xí)率與優(yōu)化器的選擇Mask RCNN 的訓(xùn)練通常用 SGD 優(yōu)化器學(xué)習(xí)率初始值設(shè)成 0.02動(dòng)量 0.9權(quán)重衰減 0.0001。這個(gè)配置在通用目標(biāo)檢測里表現(xiàn)不錯(cuò)但道路分割場景下我建議把初始學(xué)習(xí)率降到 0.01因?yàn)榈缆返募y理比較單一學(xué)習(xí)率太高容易過擬合。如果你用的是預(yù)訓(xùn)練模型學(xué)習(xí)率可以再低一點(diǎn)0.005 左右。學(xué)習(xí)率的調(diào)度策略也很重要。源碼里通常用 step 調(diào)度比如在第 8 個(gè) epoch 和第 11 個(gè) epoch 分別降 10 倍。道路分割里我建議用 cosine 調(diào)度它能讓學(xué)習(xí)率平滑下降避免 step 調(diào)度帶來的震蕩。我實(shí)測下來cosine 調(diào)度比 step 調(diào)度的最終精度高 2 個(gè)百分點(diǎn)左右而且訓(xùn)練過程更穩(wěn)定。優(yōu)化器的選擇上Adam 在道路分割里表現(xiàn)不如 SGD。Adam 的自適應(yīng)學(xué)習(xí)率會(huì)讓模型在訓(xùn)練初期快速收斂但后期容易陷入局部最優(yōu)尤其是道路邊界這種需要精細(xì)調(diào)整的區(qū)域。我試過用 Adam訓(xùn)練 loss 降得很快但驗(yàn)證集的 IoU 一直上不去換成 SGD 之后就好了。所以如果你在用 Adam建議換成 SGD 試試。6.2 正負(fù)樣本比例與難例挖掘道路分割里正樣本道路區(qū)域通常比負(fù)樣本背景少尤其是當(dāng)?shù)缆分徽紙D像一小部分的時(shí)候。源碼里ROI_POSITIVE_RATIO默認(rèn)是 0.33也就是正樣本占 33%。如果你的道路區(qū)域占比不到 10%這個(gè)比例會(huì)導(dǎo)致正樣本嚴(yán)重不足模型偏向預(yù)測背景。我一般會(huì)把ROI_POSITIVE_RATIO調(diào)到 0.5同時(shí)用難例挖掘OHEM來挑出那些分類錯(cuò)誤的負(fù)樣本讓模型重點(diǎn)學(xué)習(xí)。OHEM 在源碼里通常是通過Sampling類實(shí)現(xiàn)的它會(huì)根據(jù)分類損失對(duì)負(fù)樣本排序挑出損失最大的那些。道路分割里難例通常是道路邊界附近的像素這些像素容易跟背景混淆。我試過開 OHEM掩碼的邊界精度提升了大概 4 個(gè)百分點(diǎn)但訓(xùn)練時(shí)間增加了 20%。如果你的數(shù)據(jù)集比較大OHEM 是值得開的如果數(shù)據(jù)集小開 OHEM 容易過擬合。另一個(gè)技巧是數(shù)據(jù)增強(qiáng)。道路分割里常用的增強(qiáng)包括隨機(jī)翻轉(zhuǎn)、隨機(jī)裁剪、顏色抖動(dòng)。隨機(jī)翻轉(zhuǎn)要注意水平翻轉(zhuǎn)對(duì)道路分割是安全的但垂直翻轉(zhuǎn)可能會(huì)把天空翻到下面導(dǎo)致模型學(xué)到錯(cuò)誤的上下文信息。顏色抖動(dòng)要控制幅度太大容易讓道路的顏色失真影響分割精度。我一般用 0.2 的亮度抖動(dòng)和 0.1 的對(duì)比度抖動(dòng)效果比較穩(wěn)。7. 推理與后處理從輸出到可用掩碼7.1 NMS 與掩碼篩選的源碼流程推理階段Mask RCNN 的輸出是一堆 RoI每個(gè) RoI 包含分類概率、位置偏移和掩碼。源碼里會(huì)先對(duì)分類概率做 NMS去掉重疊的 RoI然后根據(jù)分類概率閾值篩選出最終的檢測結(jié)果。道路分割里NMS 的閾值很關(guān)鍵設(shè)得太高會(huì)導(dǎo)致相鄰車道被合并設(shè)得太低會(huì)導(dǎo)致同一個(gè)車道被分成多個(gè) RoI。我一般把 NMS 閾值設(shè)成 0.5這個(gè)值在道路分割里比較平衡。掩碼篩選是 Mask RCNN 推理的另一個(gè)關(guān)鍵步驟。源碼里會(huì)對(duì)每個(gè) RoI 的掩碼做二值化閾值通常是 0.5。但道路分割里掩碼的邊界往往比較模糊0.5 的閾值可能會(huì)導(dǎo)致邊界像素被誤判。我試過把閾值降到 0.4掩碼的召回率提升了但精確率下降了。最終我選了一個(gè)折中值 0.45效果比較穩(wěn)。這個(gè)閾值在配置文件里通常叫MASK_THRESH你可以根據(jù)你的數(shù)據(jù)集調(diào)。還有一個(gè)細(xì)節(jié)是掩碼的裁剪。RoIAlign 輸出的掩碼是固定尺寸的比如 28x28但實(shí)際 RoI 的大小是變化的所以需要把掩碼裁剪到 RoI 的實(shí)際邊界。源碼里這個(gè)操作通常叫mask_to_bbox它會(huì)把掩碼的坐標(biāo)映射回原圖。如果映射錯(cuò)了掩碼會(huì)跟實(shí)際道路區(qū)域錯(cuò)位導(dǎo)致分割結(jié)果不可用。我踩過一次坑映射的時(shí)候忘了考慮圖像的 padding結(jié)果掩碼整體偏移了 10 個(gè)像素調(diào)了半天才發(fā)現(xiàn)。7.2 掩碼拼接與可視化技巧推理完成后每個(gè) RoI 的掩碼需要拼接成一張完整的分割圖。源碼里通常用paste_mask函數(shù)來實(shí)現(xiàn)它會(huì)把每個(gè)掩碼按照 RoI 的位置貼到原圖上。道路分割里相鄰 RoI 的掩碼可能會(huì)有重疊拼接的時(shí)候要注意處理重疊區(qū)域。我一般用最大值拼接也就是重疊區(qū)域取兩個(gè)掩碼的最大值這樣能避免拼接縫隙??梢暬臅r(shí)候我建議把掩碼疊加在原圖上用半透明顏色顯示這樣能直觀地看到分割效果。道路分割里我一般用綠色表示道路紅色表示背景這樣對(duì)比度高容易發(fā)現(xiàn)問題。如果你要輸出分割結(jié)果給下游任務(wù)用建議保存成二值圖或者類別索引圖不要保存成彩色圖因?yàn)椴噬珗D會(huì)引入額外的顏色誤差。還有一個(gè)實(shí)用技巧是掩碼的平滑處理。Mask RCNN 輸出的掩碼邊界往往有鋸齒尤其是當(dāng)MASK_SHAPE比較小的時(shí)候。我一般會(huì)用高斯濾波對(duì)掩碼做平滑sigma 設(shè)成 1.0這樣邊界會(huì)柔和一些但不會(huì)過度模糊。如果你要分割的是車道線這種細(xì)長目標(biāo)平滑的時(shí)候要小心sigma 太大會(huì)把車道線抹掉。我試過 sigma2.0結(jié)果細(xì)車道線直接消失了后來改成 0.5 才保住。8. 常見問題與排查技巧實(shí)錄8.1 訓(xùn)練不收斂與 loss 震蕩的排查訓(xùn)練 Mask RCNN 最常見的問題是 loss 不收斂或者震蕩。我遇到過幾次排查下來通常是幾個(gè)原因?qū)W習(xí)率太高、正負(fù)樣本比例失衡、錨框尺寸跟目標(biāo)尺寸不匹配。學(xué)習(xí)率太高的話loss 會(huì)在初期快速下降然后反彈這時(shí)候把學(xué)習(xí)率降 10 倍基本能解決。正負(fù)樣本比例失衡的話分類 loss 會(huì)一直很高降不下去這時(shí)候調(diào)ROI_POSITIVE_RATIO或者開 OHEM。錨框尺寸不匹配的話RPN 的 loss 會(huì)很高尤其是回歸 loss。你可以把錨框的尺度打印出來跟你的數(shù)據(jù)集里目標(biāo)的實(shí)際尺寸對(duì)比一下。如果錨框的尺度跟目標(biāo)尺寸差太多RPN 很難生成高質(zhì)量的候選區(qū)域后續(xù)的分割精度也會(huì)受影響。我一般會(huì)把錨框的尺度設(shè)成目標(biāo)尺寸的 0.5 到 2 倍之間這樣覆蓋比較全。還有一個(gè)容易被忽略的原因是數(shù)據(jù)標(biāo)注的質(zhì)量。道路分割里如果標(biāo)注的邊界不準(zhǔn)確模型學(xué)到的就是錯(cuò)誤的邊界loss 自然降不下去。我建議在訓(xùn)練之前先可視化一下標(biāo)注看看邊界是否準(zhǔn)確、是否有漏標(biāo)。如果標(biāo)注質(zhì)量差再好的模型也救不回來。8.2 掩碼精度低的常見原因與解決掩碼精度低是道路分割里最頭疼的問題。我總結(jié)下來原因通常有幾個(gè)MASK_SHAPE太小、sampling_ratio太低、掩碼損失權(quán)重不夠、RoIAlign 的輸出分辨率太低。MASK_SHAPE默認(rèn)是 28如果你要分割的道路區(qū)域比較細(xì)可以改成 56。sampling_ratio默認(rèn)是 2改成 4 能提升邊界精度。掩碼損失權(quán)重默認(rèn)是 1.0改成 2.0 能讓模型更關(guān)注掩碼。RoIAlign 的輸出分辨率由pooler_resolution控制默認(rèn)是 7改成 14 能提升掩碼精度但顯存會(huì)翻倍。我一般會(huì)先改MASK_SHAPE和sampling_ratio這兩個(gè)改動(dòng)對(duì)顯存的影響比較小效果也比較明顯。如果還不夠再改pooler_resolution。另外掩碼的后處理也很重要NMS 閾值和掩碼二值化閾值都會(huì)影響最終精度建議多試幾組值找到最適合你數(shù)據(jù)集的組合。還有一個(gè)隱藏原因是 backbone 的特征提取能力不夠。如果你用的是輕量級(jí) backbone比如 MobileNet掩碼精度天然會(huì)比 ResNet 低。這時(shí)候可以考慮用知識(shí)蒸餾用一個(gè)大的 ResNet 模型教小的 MobileNet 模型這樣能在保持速度的同時(shí)提升精度。我試過用 ResNet101 教 MobileNetV2掩碼 IoU 提升了 3 個(gè)百分點(diǎn)推理速度基本沒變。8.3 顯存不足與推理速度慢的優(yōu)化顯存不足是 Mask RCNN 訓(xùn)練時(shí)的常見問題。優(yōu)化方法有幾個(gè)減小 batch size、降低MASK_SHAPE、減少 FPN 的輸出通道數(shù)、用混合精度訓(xùn)練。batch size 從 2 降到 1 能省一半顯存但訓(xùn)練會(huì)不穩(wěn)定建議配合梯度累積使用。MASK_SHAPE從 28 降到 14 能省不少顯存但掩碼精度會(huì)下降。FPN 的輸出通道數(shù)從 256 降到 128 也能省顯存但特征表達(dá)能力會(huì)減弱?;旌暇扔?xùn)練是省顯存的大殺器能把顯存占用降低 30% 到 50%而且速度還能提升。源碼里通常用torch.cuda.amp來實(shí)現(xiàn)你只需要在訓(xùn)練循環(huán)里加幾行代碼。但要注意混合精度訓(xùn)練對(duì) loss 縮放比較敏感如果 loss 出現(xiàn) NaN要把縮放因子調(diào)小。我一般用默認(rèn)的縮放因子 2^16大部分情況下沒問題。推理速度慢的話可以從幾個(gè)方面優(yōu)化減小輸入圖像的分辨率、減少 RPN 的錨框數(shù)量、用 TensorRT 或者 ONNX 做推理加速。輸入分辨率從 1024 降到 512推理速度能提升一倍但小目標(biāo)的精度會(huì)下降。RPN 的錨框數(shù)量從 12000 降到 6000推理速度能提升 20% 左右。TensorRT 的加速效果最明顯能提升 2 到 3 倍但轉(zhuǎn)換過程比較麻煩需要處理自定義層。9. 道路分割場景下的源碼修改實(shí)戰(zhàn)9.1 類別定義與數(shù)據(jù)集適配道路分割的數(shù)據(jù)集標(biāo)注格式通常跟 COCO 不一樣你需要寫一個(gè)自定義的 Dataset 類來適配。源碼里通常有一個(gè)COCODataset類你可以參考它的實(shí)現(xiàn)把標(biāo)注解析部分改成你的格式。關(guān)鍵是要保證輸出的標(biāo)注包含boxes、labels、masks三個(gè)字段而且坐標(biāo)要?dú)w一化到 0 到 1 之間。類別定義方面如果你只分“道路”和“背景”NUM_CLASSES設(shè)成 2。但要注意源碼里的類別索引通常從 1 開始0 是背景。所以你的標(biāo)注里道路的 label 要設(shè)成 1背景設(shè)成 0。如果你要區(qū)分不同車道NUM_CLASSES相應(yīng)增加標(biāo)注里的 label 也要對(duì)應(yīng)。我踩過一次坑標(biāo)注里用了 0 表示道路結(jié)果模型把道路當(dāng)成了背景訓(xùn)練了半天精度一直是 0。數(shù)據(jù)集的劃分也很重要。道路分割里不同場景城市道路、鄉(xiāng)村道路、高速公路的分布差異很大如果訓(xùn)練集和驗(yàn)證集的場景分布不一致驗(yàn)證精度會(huì)虛高。我建議按場景劃分保證訓(xùn)練集和驗(yàn)證集里都有各種場景。另外如果某個(gè)場景的樣本特別少可以用過采樣或者數(shù)據(jù)增強(qiáng)來平衡。9.2 自定義評(píng)估指標(biāo)與可視化道路分割的評(píng)估指標(biāo)通常用 IoU 或者 Dice 系數(shù)源碼里默認(rèn)的評(píng)估指標(biāo)是 COCO 的 mAP這個(gè)指標(biāo)對(duì)分割任務(wù)不太直觀。我建議自己寫一個(gè) IoU 計(jì)算函數(shù)在驗(yàn)證的時(shí)候輸出每個(gè)類別的 IoU。IoU 的計(jì)算要注意掩碼的邊界像素往往比較模糊我一般會(huì)先把掩碼二值化然后再算 IoU。可視化方面我建議在訓(xùn)練過程中定期保存一些驗(yàn)證集的預(yù)測結(jié)果疊加在原圖上。這樣能直觀地看到模型的進(jìn)步也能及時(shí)發(fā)現(xiàn)過擬合。我一般每 5 個(gè) epoch 保存一次保存的時(shí)候用不同的顏色區(qū)分不同類別這樣看起來更清楚。如果發(fā)現(xiàn)某個(gè)類別的掩碼一直很差可以針對(duì)性地調(diào)整那個(gè)類別的損失權(quán)重。還有一個(gè)實(shí)用技巧是錯(cuò)誤分析。把預(yù)測錯(cuò)誤的掩碼挑出來看看是分類錯(cuò)了還是邊界不準(zhǔn)。如果是分類錯(cuò)了可能是類別定義有問題或者樣本不均衡如果是邊界不準(zhǔn)可能是MASK_SHAPE或者sampling_ratio不夠。我一般會(huì)挑 10 到 20 個(gè)錯(cuò)誤樣本逐個(gè)分析這樣能快速定位問題。10. 一些實(shí)操心得與避坑建議源碼解析這件事光看是不夠的一定要?jiǎng)邮指?。我最初?Mask RCNN 源碼的時(shí)候覺得每個(gè)模塊都懂了但一改就報(bào)錯(cuò)。后來我養(yǎng)成了一個(gè)習(xí)慣每改一個(gè)參數(shù)就打印一下中間輸出的維度確認(rèn)跟預(yù)期一致。這個(gè)習(xí)慣幫我省了很多調(diào)試時(shí)間。道路分割里我最常改的參數(shù)是MASK_SHAPE、sampling_ratio、ROI_POSITIVE_RATIO和ANCHOR_SCALES。這四個(gè)參數(shù)對(duì)最終精度的影響最大而且改動(dòng)起來比較安全不會(huì)導(dǎo)致源碼報(bào)錯(cuò)。其他的參數(shù)我一般保持默認(rèn)除非有明確的需求。改參數(shù)的時(shí)候我建議一次只改一個(gè)改完跑一遍驗(yàn)證確認(rèn)有效再改下一個(gè)。同時(shí)改多個(gè)參數(shù)的話出了問題很難定位是哪個(gè)參數(shù)導(dǎo)致的。還有一個(gè)坑是預(yù)訓(xùn)練模型的選擇。Mask RCNN 的預(yù)訓(xùn)練模型通常是在 COCO 上訓(xùn)練的COCO 里的類別跟道路分割的類別差異很大。如果你直接用 COCO 的預(yù)訓(xùn)練模型分類分支的權(quán)重可能不太適用但 backbone 和 FPN 的權(quán)重還是很有用的。我一般會(huì)凍結(jié) backbone 的前幾層只訓(xùn)練后面的層和 RoIHead這樣能加快收斂也能防止過擬合。最后分享一個(gè)小技巧如果你要分割的道路區(qū)域特別大比如整張圖都是道路那 Mask RCNN 的 RoIAlign 可能會(huì)把整個(gè)圖當(dāng)成一個(gè) RoI導(dǎo)致掩碼分辨率不夠。這時(shí)候可以把圖像切成小塊分別做分割然后再拼接起來。切塊的時(shí)候要注意重疊區(qū)域避免拼接縫隙。我試過切塊之后掩碼的邊界精度提升了 5 個(gè)百分點(diǎn)但推理時(shí)間增加了 50%。如果你的應(yīng)用對(duì)精度要求高這個(gè)方法是值得的。