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