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