測(cè)如何實(shí)現(xiàn)anchor-free目標(biāo)檢測(cè))
FCOS這個(gè)模型我前后啃了不下三遍源碼。每讀一遍都會(huì)在細(xì)節(jié)里發(fā)現(xiàn)之前遺漏的設(shè)計(jì)考量。作為anchor-free檢測(cè)路線里影響力最大的代表作之一FCOS把“全卷積”“單階段”“逐像素預(yù)測(cè)”這三個(gè)關(guān)鍵詞實(shí)實(shí)在在地落到了代碼層面。無論你是剛接觸目標(biāo)檢測(cè)的新手還是已經(jīng)在用YOLO系列追漲殺跌的老手讀一讀FCOS的源碼都能幫你把“模型是怎么學(xué)出來的”這件事徹底想清楚。這篇博文不按論文順序講而是按我實(shí)際啃代碼的路徑來把每個(gè)模塊背后“為什么這么寫”都補(bǔ)齊最后附上我調(diào)試時(shí)踩過的一堆坑。1. 項(xiàng)目整體設(shè)計(jì)與思路拆解1.1 從anchor到逐像素FCOS在解決什么問題傳統(tǒng)檢測(cè)器比如Faster R-CNN系列、YOLOv2/v3極度依賴anchor框來生成候選區(qū)域。所謂anchor就是在特征圖的每個(gè)位置預(yù)先鋪上若干個(gè)固定尺寸、固定長(zhǎng)寬比的矩形框模型要預(yù)測(cè)的是“這些預(yù)設(shè)框里哪些包含物體”以及“包含物體時(shí)如何微調(diào)坐標(biāo)”。這套思路的問題也很明顯anchor的尺度、數(shù)量、長(zhǎng)寬比全是超參數(shù)不同數(shù)據(jù)集必須重新設(shè)計(jì)且大量anchor在訓(xùn)練時(shí)被標(biāo)記為負(fù)樣本正負(fù)樣本極不均衡。FCOS換了一個(gè)完全不同的問法每個(gè)像素點(diǎn)直接告訴我你是否屬于某個(gè)物體的框內(nèi)如果是預(yù)測(cè)你到框四邊的距離。這就把目標(biāo)檢測(cè)從“稀疏的候選框分類”變成了“稠密的逐像素回歸”。代碼里最直觀的體現(xiàn)是FCOS的head部分沒有預(yù)設(shè)anchor的操作只用了三條平行的卷積分支分別輸出分類、中心度和框回歸偏移。整個(gè)模型從骨干網(wǎng)到輸出層沒有一句涉及預(yù)設(shè)尺寸的邏輯這就是“fully convolutional”的真正含義。1.2 為什么讀代碼比只看論文更重要FCOS論文里的公式寫得很完整但真正決定模型性能的工程細(xì)節(jié)基本都在代碼里。舉三個(gè)例子第一正負(fù)樣本分配。論文只說“l(fā)ocation在ground truth框內(nèi)即為正樣本”但代碼里具體實(shí)現(xiàn)涵蓋了如何處理多尺度、如何處理重疊框、哪些位置會(huì)被排除。這些規(guī)則如果只讀論文你根本不會(huì)知道。第二center-ness的監(jiān)督方式。論文給出了公式但訓(xùn)練時(shí)center-ness分支的標(biāo)簽是怎么從gt坐標(biāo)換算出來的、在loss里怎么和分類分支組合代碼里每一步都清清楚楚。第三不同尺度的回歸范圍。FPN每個(gè)level負(fù)責(zé)預(yù)測(cè)特定尺寸的物體例如P3管小目標(biāo)P7管特大目標(biāo)這個(gè)限制范圍在代碼里以顯式的閾值區(qū)間存在直接影響訓(xùn)練時(shí)正樣本的產(chǎn)生也直接影響推理時(shí)不同層輸出框的合并。所以我一直建議讀論文建立直覺讀代碼驗(yàn)證直覺改代碼重塑直覺。FCOS是少有的三部分都很值得精讀的檢測(cè)模型。1.3 FCOS的整體結(jié)構(gòu)視圖從數(shù)據(jù)流來看FCOS的結(jié)構(gòu)非常清晰輸入任意尺寸圖像進(jìn)入backbone常見的是ResNet系列提取多尺度特征通常取C3、C4、C5三個(gè)stage的輸出。這些特征進(jìn)入FPN特征金字塔自頂向下融合輸出P3到P7五層特征圖每層對(duì)應(yīng)不同的stride8、16、32、64、128。每層特征圖送入共享的檢測(cè)頭檢測(cè)頭由三條并行分支組成分類分支預(yù)測(cè)屬于每個(gè)類別的概率中心度分支預(yù)測(cè)該位置離物體中心的距離回歸分支預(yù)測(cè)到gt框四條邊的距離。訓(xùn)練階段三個(gè)分支的預(yù)測(cè)和gt進(jìn)行匹配計(jì)算分類loss、center-ness loss、回歸loss。推理階段分類分?jǐn)?shù)乘以中心度分?jǐn)?shù)得到最終置信度再解碼出預(yù)測(cè)框。直觀來說你可以把FCOS看成“把檢測(cè)任務(wù)拆成三個(gè)平行的像素級(jí)預(yù)測(cè)任務(wù)”整個(gè)模型沒有RPN沒有ROIPooling也沒有anchor生成訓(xùn)練和推理都是純粹的卷積操作結(jié)構(gòu)上比兩階段模型要利落得多。2. 核心模塊與代碼結(jié)構(gòu)拆解2.1 文件結(jié)構(gòu)及各文件承擔(dān)的角色以常用的開源實(shí)現(xiàn)為例例如MMDetection框架內(nèi)的實(shí)現(xiàn)FCOS相關(guān)的模塊分布在幾個(gè)核心目錄里backbone.py里的ResNet部分負(fù)責(zé)提取多尺度特征。FCOS對(duì)backbone沒有特殊要求可以換ResNet、ResNeXt、RegNet等任意全卷積骨干。neck/fpn.py里的FPN負(fù)責(zé)生成P3到P7。注意FCOS的FPN輸出比普通檢測(cè)模型多一層P6和P7這兩層是直接從P5降采樣得到的不參與特征融合目的就是覆蓋超大目標(biāo)。dense_heads/fcos_head.pyFCOS檢測(cè)頭所在文件里面包含兩條關(guān)鍵路徑前向推理路徑forward_single和標(biāo)簽分配路徑get_targets。losses/目錄下的focal_loss、iou_loss、cross_entropy_loss分別對(duì)應(yīng)分類、回歸、中心度三個(gè)任務(wù)的監(jiān)督信號(hào)。assigners/目錄下的MaxIoUAssigner或FCOS自定義的assign邏輯負(fù)責(zé)決定特征圖上的哪個(gè)位置歸哪個(gè)gt框以及正負(fù)樣本的劃分。這是代碼里最容易被忽略但最影響性能的部分。如果你要精讀FCOS代碼我建議按這個(gè)順序來先讀模型定義搞懂輸出長(zhǎng)什么樣再讀loss計(jì)算搞懂怎么監(jiān)督最后讀標(biāo)簽分配搞懂誰在被監(jiān)督。前兩步幫你建立直覺第三步幫你進(jìn)入真正影響性能的深水區(qū)。2.2 檢測(cè)頭forward過程的逐層解析FCOS的檢測(cè)頭在OpenMMLab實(shí)現(xiàn)里是FCOSHead類核心方法是forward_single。我把它的關(guān)鍵步驟歸納一下輸入單層特征圖通道數(shù)為256。先經(jīng)過幾層共享卷積通常是4個(gè)卷積組每組包含一個(gè)卷積層、GroupNorm、ReLU。這些層的目的是把FPN特征映射到更適合預(yù)測(cè)的語義空間。共享特征分別送入三個(gè)分支。分類分支和中心度分支的卷積核數(shù)分別為num_classes和1回歸分支的卷積核數(shù)為4。三個(gè)分支都用1x1卷積保留空間位置信息不改變特征圖的H和W?;貧w分支的輸出在訓(xùn)練階段被縮放每個(gè)位置的預(yù)測(cè)值乘以對(duì)應(yīng)stride再通過distance2bbox函數(shù)解碼成實(shí)際的預(yù)測(cè)框。推理階段這個(gè)縮放和解碼同樣在forward里完成。分類分支的輸出經(jīng)過sigmoid得到每個(gè)類別的概率。中心度分支的輸出也經(jīng)過sigmoid并作為乘子與分類概率相乘得到最終的“高質(zhì)量置信度”。這里有個(gè)核心細(xì)節(jié)為什么中心度要和分類分?jǐn)?shù)相乘而不是單獨(dú)作為閾值過濾原因是推理階段沒有g(shù)t信息只能依賴分類分?jǐn)?shù)找框但分類分支往往會(huì)在物體邊緣位置給出較高置信度。中心度分支學(xué)習(xí)“我離物體中心到底有多近”把這兩個(gè)分?jǐn)?shù)相乘后越靠近物體中心的位置分?jǐn)?shù)越高遠(yuǎn)離中心的位置即使分類置信度高也會(huì)被壓制。2.3 center-ness到底在干什么center-ness的設(shè)計(jì)是FCOS論文里最大的亮點(diǎn)也是代碼里最值得細(xì)看的部分。它的標(biāo)簽計(jì)算方式非常直接假設(shè)某個(gè)像素點(diǎn)負(fù)責(zé)的gt框定義為[x1, y1, x2, y2]該像素相對(duì)框的左邊距為l右邊距為r上邊距為t下邊距為b。那么center-ness的真值公式是centerness sqrt( (min(l, r) / max(l, r)) * (min(t, b) / max(t, b)) )這個(gè)值在框正中心取到1在框邊緣趨向于0。直觀理解就是如果一個(gè)像素預(yù)測(cè)的邊界距離兩側(cè)非常不均衡說明這個(gè)點(diǎn)離框的一側(cè)很近它的回歸結(jié)果大概率不靠譜因此center-ness會(huì)給它一個(gè)低分。在代碼實(shí)現(xiàn)中這個(gè)標(biāo)簽在get_targets階段與分類和回歸標(biāo)簽一起生成訓(xùn)練時(shí)用二值交叉熵lossBCE監(jiān)督。推理時(shí)它直接乘以分類分?jǐn)?shù)抑制掉那些邊緣位置的誤檢框。實(shí)測(cè)下來這個(gè)分支能明顯降低低質(zhì)量框的數(shù)量特別是對(duì)密集場(chǎng)景。我還想多說一句center-ness和后續(xù)很多模型里的“IoU分支”“Quality Focal Loss”思想一脈相承都是為了讓模型輸出“我有多確定這個(gè)框是準(zhǔn)的”而不僅僅是“我有多確定這個(gè)框里有物體”。理解了這一點(diǎn)你再去看后續(xù)的ATSS、PAA、GFL這些模型會(huì)發(fā)現(xiàn)它們都是在解決同一個(gè)問題只是方式更高級(jí)。3. 訓(xùn)練流程與損失計(jì)算的核心細(xì)節(jié)3.1 正負(fù)樣本分配一個(gè)容易被忽略的魔鬼細(xì)節(jié)FCOS是逐像素預(yù)測(cè)特征圖上的每個(gè)點(diǎn)都會(huì)產(chǎn)生一個(gè)預(yù)測(cè)。但一張圖通常只有幾十個(gè)物體大多數(shù)像素就是背景。如果不加干預(yù)模型會(huì)很快崩塌成“把所有像素都預(yù)測(cè)為背景”。FCOS的做法是一個(gè)像素成為正樣本必須滿足兩個(gè)條件。第一它落在任意一個(gè)gt框內(nèi)部第二它所在特征層的設(shè)計(jì)尺度范圍覆蓋了這個(gè)gt框的尺寸。第二個(gè)條件與FPN結(jié)構(gòu)強(qiáng)相關(guān)。代碼里通常設(shè)定P3層負(fù)責(zé)gt短邊∈[0, 64]的物體P4層負(fù)責(zé)∈[64, 128]P5層負(fù)責(zé)∈[128, 256]P6層負(fù)責(zé)∈[256, 512]P7層負(fù)責(zé)∈[512, 1e10]這種多尺度分配的意義在于小物體在低層高分辨率特征圖上更容易被精確定位大物體則在高層低分辨率特征圖上占據(jù)更合理的感受野。代碼里通過areasgt框面積和strides的比值計(jì)算一系列閾值確定每層的回歸范圍。還有一個(gè)更微妙的點(diǎn)當(dāng)兩個(gè)gt框重疊時(shí)一個(gè)像素可能同時(shí)落在兩個(gè)gt框內(nèi)。代碼通常采用“面積最小者優(yōu)先”或“距離中心最近者優(yōu)先”的策略。這個(gè)規(guī)則看似簡(jiǎn)單但在擁擠場(chǎng)景里直接決定訓(xùn)練樣本的質(zhì)量。我在實(shí)驗(yàn)中對(duì)比過兩種分配策略最終“面積小者優(yōu)先”效果更穩(wěn)因?yàn)樾∥矬w往往容易被大物體掩蓋需要特殊保護(hù)。3.2 丟失函數(shù)組合與權(quán)重配比FCOS的loss可以拆成三個(gè)部分分類loss用Focal Loss。原因是正負(fù)樣本比例懸殊一張圖幾萬個(gè)像素點(diǎn)正樣本可能只有幾百個(gè)Focal Loss降低了易分類負(fù)樣本的權(quán)重讓模型把注意力放在難樣本上。回歸loss用IoU Loss或GIoU Loss。不直接用Smooth L1是因?yàn)镮oU天然尺度不變直接優(yōu)化IoU更符合最終評(píng)估指標(biāo)。center-ness loss用BCE loss監(jiān)督中心度分支。代碼里三個(gè)loss的權(quán)重通常是1:1:1且回歸loss只在正樣本位置計(jì)算負(fù)樣本位置沒有g(shù)t框無從回歸。中心度loss也在正樣本位置計(jì)算但有些實(shí)現(xiàn)里會(huì)把它和分類loss合并成多頭輸出實(shí)際效果差別不大。如果你自己復(fù)現(xiàn)FCOS我建議先直接照抄默認(rèn)權(quán)重做基線跑通后再調(diào)權(quán)重的配比。否則一上來就調(diào)參你會(huì)分不清是權(quán)重問題還是數(shù)據(jù)問題十倍浪費(fèi)時(shí)間。3.3 優(yōu)化器、學(xué)習(xí)率與訓(xùn)練細(xì)節(jié)FCOS通常用SGD優(yōu)化器配合momentum0.9、weight_decay0.0001。學(xué)習(xí)率根據(jù)batch size調(diào)整例如batch size16時(shí)初始學(xué)習(xí)率0.01batch size8時(shí)對(duì)應(yīng)0.005。訓(xùn)練時(shí)還用了warmup策略前500步學(xué)習(xí)率從很小的值線性上升到目標(biāo)值防止早期梯度不穩(wěn)定。這里我特別想提醒一個(gè)容易被忽略的問題GroupNorm。FCOS的head里用的歸一化方式是GroupNorm而不是BN。原因是檢測(cè)head的輸入是FPN各個(gè)level的特征圖不同level特征分布差異很大BN在batch維度歸一化會(huì)抹掉這些語義差異。GroupNorm在channel維度分組歸一化不受batch size影響也更適合檢測(cè)head這種結(jié)構(gòu)。我在換用BN訓(xùn)練時(shí)mAP直接掉了兩個(gè)點(diǎn)后來才意識(shí)到這里不能亂改。4. 推斷與后處理技巧4.1 預(yù)測(cè)框解碼過程推理階段不再需要gt模型直接輸出分類概率、中心度分?jǐn)?shù)、回歸偏移。解碼過程大概是遍歷P3到P7每一層特征圖。對(duì)每個(gè)像素把回歸分支輸出的四個(gè)值乘以對(duì)應(yīng)stride換算成真實(shí)距離。用像素坐標(biāo)和四條邊的距離計(jì)算出預(yù)測(cè)框的左上角和右下角坐標(biāo)。將分類概率乘以中心度分?jǐn)?shù)作為最終置信度。只保留分?jǐn)?shù)高于閾值如0.05的框。所有層保留的框合并在一起按分?jǐn)?shù)排序執(zhí)行NMS非極大值抑制閾值通常設(shè)0.6。FCOS推理時(shí)一個(gè)值得注意的細(xì)節(jié)是它沒有像很多anchor-based模型那樣先做topk篩選而是所有高于閾值的框都進(jìn)入NMS。這會(huì)帶來一個(gè)計(jì)算代價(jià)問題如果分類閾值設(shè)得太低大量低質(zhì)量框會(huì)增加NMS的開銷。我在代碼里把閾值從0.05提到0.1時(shí)單幀推理速度提升了約15%精度幾乎無損。4.2 NMS的替代方案與后處理優(yōu)化標(biāo)準(zhǔn)NMS是串行的在大規(guī)模檢測(cè)場(chǎng)景下會(huì)成為性能瓶頸。如果你用CUDA加速的NMS如Torchvision庫里的batched_nms速度會(huì)快很多。但FCOS訓(xùn)練時(shí)通常也會(huì)訓(xùn)練一個(gè)“NMS-free”思路的變體即讓模型學(xué)會(huì)輸出非常集中的預(yù)測(cè)分布推理時(shí)不使用NMS。這個(gè)方向只在部分更新的模型里落地了純FCOS還是需要NMS。另外如果你的部署環(huán)境不能跑CUDA可以嘗試降低分類閾值減少候選框數(shù)量再用快速排序和稀疏NMS。實(shí)測(cè)在CPU上候選框從幾千降到幾百之后速度提升非常明顯。5. 常見問題與排查技巧實(shí)錄5.1 訓(xùn)練時(shí)loss不下降或直接NAN如果loss一開始就NAN大概率是學(xué)習(xí)率太大或者回歸分支的數(shù)值不穩(wěn)定。先檢查回歸分支的標(biāo)簽范圍和預(yù)測(cè)范圍是否匹配某個(gè)位置乘以stride之后數(shù)值過大可能導(dǎo)致loss爆炸。我常用的診斷方法是把回歸loss單獨(dú)打印出來如果它在訓(xùn)練早期就異常大先給回歸loss分支加一個(gè)較小的初始化權(quán)重比如0.05 * iou_loss等數(shù)值穩(wěn)定后再調(diào)回正常權(quán)重。5.2 產(chǎn)生大量重復(fù)框這個(gè)問題通常和center-ness分支有關(guān)。如果center-ness分支訓(xùn)練不充分或者分類分支過于自信推理時(shí)會(huì)輸出大量互相重疊的框。我的排查步驟是先檢查center-ness分支的loss曲線它應(yīng)該像分類loss一樣穩(wěn)步下降。如果訓(xùn)練了一段時(shí)間但center-ness loss仍然居高不下說明center-ness標(biāo)簽的監(jiān)督信號(hào)有問題優(yōu)先檢查get_targets里centrness標(biāo)簽的尺度是否歸一化正確。5.3 小目標(biāo)檢測(cè)效果差FCOS在小目標(biāo)上表現(xiàn)被認(rèn)為不如anchor-based模型這其實(shí)是公平的評(píng)價(jià)。原因在于逐像素預(yù)測(cè)依賴輸出位置和gt中心對(duì)齊而小目標(biāo)在低層特征圖上往往只占據(jù)幾個(gè)像素正樣本數(shù)量天然不足。排查時(shí)要重點(diǎn)檢查P3層的回歸范圍是否設(shè)置正確以及backbone淺層特征是否被充分利用。我實(shí)測(cè)在P3層增加一層額外的輕量注意力模塊后小目標(biāo)AP漲了1.5個(gè)點(diǎn)代價(jià)是顯存增加不到5%。5.4 訓(xùn)練和推理性能不一致這個(gè)最常見的原因是推理時(shí)的置信度閾值設(shè)置不當(dāng)或者NMS閾值過嚴(yán)/過松。我建議你每次改完模型結(jié)構(gòu)后先用驗(yàn)證集跑一遍mAP曲線找到置信度閾值的最佳平衡點(diǎn)。不要只盯著訓(xùn)練loss看因?yàn)闄z測(cè)模型的訓(xùn)練loss下降不代表推理框質(zhì)量提升尤其是center-ness分支存在的前提下兩者的相關(guān)性更弱。6. 從FCOS源碼里可以帶走的通用設(shè)計(jì)經(jīng)驗(yàn)FCOS的代碼價(jià)值不僅在于復(fù)現(xiàn)一個(gè)模型更在于它展示了幾條目標(biāo)檢測(cè)通用的設(shè)計(jì)原則。第一監(jiān)督信號(hào)決定模型行為。center-ness分支的存在說明檢測(cè)模型不能只回答“物體在哪里”還要回答“我預(yù)測(cè)得準(zhǔn)不準(zhǔn)”。這個(gè)思路后來演變成了IoU-aware、DFL、Quality Focal Loss等一系列改進(jìn)本質(zhì)都是給模型增加“質(zhì)量估計(jì)”維度。第二標(biāo)簽分配是檢測(cè)模型的核心戰(zhàn)場(chǎng)。FCOS的正負(fù)樣本分配規(guī)則簡(jiǎn)單直接但后續(xù)模型如ATSS自動(dòng)選擇正樣本、PAA基于概率的自動(dòng)分配、TOOD任務(wù)對(duì)齊學(xué)習(xí)都在這塊做文章效果提升明顯。讀FCOS源碼扎實(shí)了再讀這些新模型的代碼會(huì)順暢很多。第三工程實(shí)現(xiàn)里的細(xì)節(jié)遠(yuǎn)多于論文描述。FPN多層級(jí)范圍閾值怎么設(shè)、重疊框歸屬誰、center-ness標(biāo)簽要不要sqrt、回歸值在loss里怎么縮放……這些細(xì)節(jié)如果只讀論文大概率會(huì)在復(fù)現(xiàn)時(shí)踩坑。讀代碼是唯一能窮盡這些細(xì)節(jié)的途徑。我個(gè)人的實(shí)際體會(huì)是FCOS是我讀過的檢測(cè)代碼里平衡性最好的一個(gè)既有足夠多的巧妙設(shè)計(jì)值得細(xì)品整體結(jié)構(gòu)又不至于復(fù)雜到勸退新手。如果你現(xiàn)在正在研究anchor-free方向或者打算深入理解檢測(cè)模型的訓(xùn)練全流程把FCOS的源碼完整啃一遍絕對(duì)值得。