拿掉候選區域那一階段
上一篇我們建立了兩階段家族——從 R-CNN 一路到 Faster R-CNN。這些偵測器很準確,但它們需要分兩趟處理:先用候選區域網路(RPN)提出幾百個「這裡也許有物件」的候選框,再用第二個網路把每個框裁切出來逐一分類。這種「先提候選、再分類」的拆分正是瓶頸。即使是家族中最快的 Faster R-CNN,每秒也只能跑幾個影格——拿來標註相簿沒問題,但用在即時影片、需要對周遭即時反應的機器人、或必須在小小電池晶片上執行的手機 App,就太慢了。
單階段的想法簡單到近乎激進:把候選區域那一步整個丟掉。它不再先問「物件可能在哪?」再問「它們是什麼?」,而是讓單階段的物件偵測模型一次就把兩個問題直接答完——對鋪在特徵圖上的密集網格中的每一個位置同時作答。回想你在先前單元見過的卷積特徵圖:那是一張由格子組成的網格,每個格子都濃縮了影像中的一小塊區域。單階段偵測器在這張網格上接一個小小的預測頭,讓每個格子在「一次前向傳遞」中同時吐出類別分數與一個框。有些設計更進一步,給每個格子幾個錨框(預設的參考形狀)去微調——這個概念我們會在第 3 節深入。
示意圖:特徵圖網格上,預測頭在每個格子產生類別分數與框座標。
YOLO:把偵測當成網格回歸
第一個把這條路真正跑通、而且端到端訓練的偵測器是 YOLO——「You Only Look Once(你只看一次)」。它的配方漂亮又直接:把輸入影像切成 S×S 的網格;哪個格子裡含有某物件的「中心點」,那個格子就負責偵測這個物件。每個格子預測 B 個候選邊界框,每個框用五個數字描述——x、y、w、h,以及一個信心分數——再加上整個格子共用的一組 C 個類別機率。所以格子的任務是:「如果有個物件的中心落在我這裡,這是我對它的框的幾個猜測、我有多確定、以及我覺得它是什麼。」
YOLO 的完整輸出就是一個這種形狀的張量。
把這個形狀仔細讀一遍——它就是整個模型輸出,濃縮成一行。S×S 是網格(原始 YOLO 取 S=7,所以有 49 個格子)。每個格子存 B·5 + C 個數字。B 是該格子預測的框數(YOLO 取 B=2);那個 5 是每個框的五元組(x、y、w、h、信心分數);C 是物件類別數(PASCAL VOC 上 C=20)。代進去:7×7×(2·5 + 20) = 7×7×30。這個 7×7×30 的張量就是整張影像的全部預測——沒有候選區域、沒有第二趟。注意這裡天生的不對稱:每個格子預測 B 個框,卻只有「一組」共用的類別機率,所以單一格子最終只能認定一個類別。這正是早期 YOLO 在兩個不同類別的物件共用同一格子時會吃癟的原因。
那每個框的「信心分數」到底是什麼意思?YOLO 把它定義為 Pr(object) × IoU——框內含有任何物件的機率,乘上這個框預期跟真實物件重疊得多好(也就是它的交集比聯集 IoU)。所以信心分數把「這裡有東西嗎?」和「我的框夠貼嗎?」捲成同一個數字;空格子應該把它壓到零。要在測試時把它變成可用、且專屬某類別的分數,你做一個乘法:類別專屬分數 = 信心分數 × 類別機率。一個信心分數 0.85、「狗」機率 0.90 的框,最終狗分數就是 0.85 × 0.90 = 0.765——而這正是第 2 篇講的 NMS 與 mAP 會拿去處理的那個數字。
# Decoding one YOLO grid cell (PASCAL VOC config: S=7, B=2, C=20) # Image is 448x448, so each cell covers 448/7 = 64 px. # Look at cell (row=3, col=4) and its best of the B boxes: tx, ty, tw, th, conf = 0.30, 0.60, 0.50, 0.40, 0.85 # raw box predictions p_dog = 0.90 # top class probability for this cell # x, y are offsets WITHIN the cell (0..1); w, h are fractions of the whole image. cell = 448 / 7 # 64 px per cell cx = (4 + tx) * cell # center x = (4 + 0.30) * 64 = 275.2 px cy = (3 + ty) * cell # center y = (3 + 0.60) * 64 = 230.4 px bw = tw * 448 # width = 0.50 * 448 = 224.0 px bh = th * 448 # height = 0.40 * 448 = 179.2 px # Final class-specific score = box confidence * class probability score_dog = conf * p_dog # 0.85 * 0.90 = 0.765 -> fed to NMS / mAP
YOLO 的訓練損失:定位 + 物件性 + 無物件 + 分類。
它看起來很忙,但其實只是一堆平方誤差項相加,每一項都由一個指示函數把關。𝟙_ij^obj 在「格子 i 的第 j 個框負責某個真實物件」時為 1(否則為 0);𝟙_ij^noobj 則相反。戴帽子的符號是預測值,沒戴帽子的是目標值。第 1 行懲罰框中心 (x, y)。第 2 行懲罰寬與高——但是透過它們的平方根 √w、√h,這是個刻意的小技巧:同樣差 10 個像素,發生在小框上比在大框上嚴重得多,平方根會壓縮大尺寸,讓小框得到公平的權重。第 3 行把「有物件」的框的信心分數 C 推向它真正的 IoU;第 4 行把「空」框的信心分數推向 0;第 5 行是分類誤差,只在含有物件的格子裡計算。兩個旋鈕設定優先順序:λ_coord = 5 等於說「把框抓準很重要」,而 λ_noobj = 0.5 則把無物件項調小。為什麼要調小它?在 7×7 的網格上大多數格子是空的,所以無物件項出現的次數遠多於其他所有項;若放著不管,網路只要到處都預測「這裡沒東西」就能把損失壓低,淹沒那些真的含有物件、卻很稀少的格子所貢獻的梯度。λ_noobj 是針對這種不平衡的粗糙修補——把這個念頭記著,因為第 5 節會用優雅得多的方式治同一種病。
在一個格子上鋪著幾個形狀不同的虛線參考框(錨框),每個都能朝物件調整。
錨框與 SSD:多種形狀、多種尺度
YOLO 的格子是從零開始預測框,這很難——網路得在毫無起點的情況下學會物件形狀的所有變化。你在第 3 篇初次見到的錨框,正是給密集網格一個起跑優勢:在特徵圖的每一個位置都鋪上一組小而固定的參考框「詞彙」——幾種長寬比(高、方、寬)搭配幾種尺度。網路不再無中生有;對每一個錨框,它只預測一個把錨框朝附近真實物件變形的偏移量,外加一個「裡面是什麼」的類別分數。微調一個好的起始猜測,遠比直接回歸絕對座標來得容易。
一個格子上疊著多個長寬比與大小各異的虛線矩形,代表錨框。
網路怎麼學會「哪個錨框該去追哪個物件」?靠的是訓練時用交集比聯集 IoU做「錨框配對」。對每一個真實框,我們計算它跟每一個錨框的 IoU,再把這個物件指派給跟它重疊最多的那個(或那些)錨框——這些就成為「正錨框」,負責預測這個物件的類別與偏移量。對所有物件的 IoU 都很低的錨框會被標成「負(背景)」,落在模糊中間地帶的錨框通常直接忽略。於是每個錨框各自專精:高瘦的錨框學會抓人和路燈,寬扁的錨框學會抓汽車和巴士。
把預測的偏移量 (t_x, t_y, t_w, t_h) 變回真正的框 (b)。
這就是四個原始偏移量如何變成一個真正的框,而它正是第 3 篇那組回歸目標的「反運算」。先有一個位在網格位置 (c_x, c_y)、預設大小為 (p_w, p_h) 的錨框。網路輸出四個數字 (t_x, t_y, t_w, t_h)。對中心,我們把 t_x、t_y 送進 sigmoid σ,它會把任何實數壓進 0–1 區間;再加上格子座標 c_x、c_y,就讓預測的中心點留在自己的格子裡,所以錨框不會跑到老遠去搶一個遙遠的物件——這在訓練時是關鍵的穩定器。對大小,我們把錨框的寬高分別乘上 e^{t_w} 與 e^{t_h}。指數永遠是正的,所以框絕不會出現負寬度,而且它讓縮放變成「乘法式」的:t_w = 0 表示錨框不變(e^0 = 1),t_w = +0.69 把它放大一倍(e^0.69 ≈ 2),t_w = −0.69 把它縮成一半。訓練時只要把這些公式反過來——給定一個配對好的錨框與它的真實框,就能算出網路該瞄準的目標 t 值。
SSD——Single Shot MultiBox Detector(單次多框偵測器)——加進第二個大想法:別只從一張特徵圖預測,要同時從好幾張預測。一個很深的卷積主幹網路本來就會產生一疊特徵圖,越往深處越小、越粗。SSD 在其中好幾層都接上「錨框+類別」的預測頭。淺層、高解析度的圖仍看得到細節,它們的錨框就抓小物件;深層、低解析度的圖各自濃縮了一大片區域,它們的錨框就抓大物件——全部在同一趟前向傳遞裡完成。這種多尺度預測正是單階段偵測器當初最需要補的弱點,也預告了下一節的特徵金字塔。
看見每一種尺度:特徵金字塔網路(FPN)
SSD 的多層技巧,掀開了關於「尺度」的一個更深的兩難。當影像通過很深的主幹網路時,後面每一張特徵圖在語意上都更豐富——越深的層已經把邊緣組成紋理、紋理組成部件、部件組成整個物件的概念——但在空間上也越粗,因為池化與步幅會把圖縮小。所以深層的圖知道「這是一隻狗」,卻只能在很模糊的解析度上知道:對大物件很棒,對小物件幾乎是瞎的。淺層的圖剛好相反:空間上很細緻、位置很清楚,但語意很弱——它看到的是邊緣與色塊,不是「狗」。SSD 對小物件的預測偏偏來自那些淺層、語意又弱的層,這就限制了它在小物件上的表現。
特徵金字塔網路(FPN)不選邊站,而是直接化解這個兩難。在一般「由下往上」的主幹網路之上,它加了一條「由上往下」的路徑:拿最深、語意最強的那張圖,把它上採樣回更大的尺寸,再透過「側向連接」(一個 1×1 卷積把通道對齊,然後逐元素相加)跟主幹中同解析度的較淺圖合併。沿著這疊圖一路重複下去。最後得到的是一座特徵金字塔,它的每一層都「同時」空間細緻「且」語意強——淺層的圖繼承了深層圖的「狗味」,又保住了自己清晰的位置資訊。
卷積主幹網路產生的一疊逐漸變小的特徵圖。
打個比方:想像同一座城市的一疊地圖——國家層級的地圖、區域地圖、城市地圖、街道地圖。街道地圖很細,卻沒有標註;國家地圖標註豐富,卻縮得太遠、沒辦法精準定位任何東西。FPN 就像回頭把國家地圖上的標註抄到每一張更細的地圖上,於是街道地圖同時擁有精準的街道「和」豐富的名稱。具體來說,這跟先前單元的「感受野」相連:網路深處的一個單元有很大的感受野(它「看見」影像的一大塊,適合大物件),而淺層的單元感受野很小(適合小物件)。FPN 讓偵測器能在金字塔的每一層上鋪設與該層尺度相稱的錨框——細層放小錨框,粗層放大錨框。
示意圖:一小塊輸入區域,經過一層層卷積後擴張成一個大的感受野。
破解類別不平衡:Focal Loss 與 RetinaNet
即使有了 FPN,單階段偵測器在準確度上仍然落後兩階段,而 RetinaNet 的作者發現真正的罪魁禍首是「損失函數」,不是架構。一個密集的單階段偵測器會鋪上極大量的錨框——每張影像數以萬計,常常是 10^4 到 10^5 個。在任何一張影像裡,真正跟某個真實物件重疊的,只有區區數十個;其餘全是背景。兩階段偵測器從不必面對這股洪流,因為候選階段早就把候選框篩到只剩幾百個、而且大致平衡。單階段偵測器看到的,卻是那片原始、嚴重失衡的汪洋。
這就是它為何拖垮一般交叉熵的原因。每個背景錨框都很「容易」——網路很快就學會以大約 90% 的信心說出「背景」——所以單一個的損失很小。但這種錨框有好幾萬個,而非常多個小損失加起來仍是一座大山,把那少數困難又有趣的例子(半遮住的物件、罕見類別)的損失整個埋掉。梯度最後指向那些容易的負例想要的方向;罕見的正例幾乎推不動它。網路於是在優化錯誤的目標,準確度也就卡住了。YOLO 的 λ_noobj 是針對這件事的粗糙嘗試;我們其實能做得好很多。
Focal loss = 交叉熵 × 一個把容易例子靜音的調變因子。
Focal loss 不過是交叉熵外加一個因子而已。逐一讀懂各部件:p_t 是模型對「真正類別」所給的預測機率——模型答對又有信心時它很高,答錯時它很低。光是 −log(p_t) 這一項就是普通的交叉熵。新加的因子 (1 − p_t)^γ 是「調變因子」,而 γ ≥ 0 是「聚焦參數」,控制要多積極地把容易的例子靜音。α_t 是一個簡單的類別平衡權重(典型值是 0.25),進一步調節正負例的比例。魔法全在 (1 − p_t)^γ:對一個分類得很好的例子,p_t 接近 1,所以 (1 − p_t) 接近 0,再把它取 γ 次方,因子就被推向 0——容易例子的損失被壓掉了。對一個困難、被分錯的例子,p_t 很小,(1 − p_t) 接近 1,因子也就停在接近 1,它的損失幾乎原封不動。
把這些零件鎖在一起,你就得到 RetinaNet = 主幹網路(例如 ResNet)+ 提供多尺度特徵的 FPN 頸部 + 鋪在金字塔每一層的密集錨框 + 馴服不平衡的 focal loss。它的招牌成果,終結了長久以來「單階段就等於『快但比較不準』」的成見:RetinaNet 在準確度上追平、甚至超越當時最好的兩階段偵測器,同時還跑得更快。這個教訓很震撼——瓶頸根本不是少了候選階段,而是一個讓容易的背景把訊號淹掉的損失。把損失修好,單階段的速度就免費附送了。
單階段對上兩階段:如何選擇偵測器
讓我們沿著四個實務上最重要的軸,把整個偵測家族整理一遍:速度、準確度、對小物件或密集物件的處理能力,以及實作複雜度。要記得這些都會隨著架構進步而變動——以下是那場經典、定義一個時代的比較,用來說明「設計空間」,而不是一張固定的排行榜。
Faster R-CNN(兩階段):準確度的標竿,而且在歷史上對小而擁擠的物件表現最強,因為它的候選階段與逐區域微調讓每個候選都得到細心對待。代價是速度——兩趟前向再加上逐區域的運算——以及最多的可動零件。當準確度至上、你又有伺服器級 GPU 和一些延遲預算時,選它。SSD(單階段、多尺度錨框):比 Faster R-CNN 快得多、準確度也還不錯,一趟就從好幾張特徵圖預測。沒有 FPN 時它的小物件準確度普普,而且對錨框怎麼設定很敏感。是個穩健的折衷選擇。
YOLO(單階段、網格):速度冠軍,也是即時應用的自然首選。後來的版本(v3 之後)加入了錨框與多尺度預測,把早期的小物件落差補上了一大半,但它的設計哲學始終是「速度優先」。SSD 與 YOLO 是你在邊緣裝置上會優先考慮的兩個。RetinaNet(單階段、FPN + focal loss):偏向準確度的單階段偵測器——它證明了你可以用單階段的速度拿到兩階段等級的準確度,代價是比 YOLO 稍慢一點。當你想要高準確度、卻又負擔不起兩階段流水線時,選它。
但請注意貫穿這篇每一個快速偵測器的那條線:錨框。錨框很強大,卻拖進一堆要手動調的超參數——幾種尺度、哪些長寬比、每個位置幾個錨框,以及決定配對的 IoU 門檻——而最佳設定還會因資料集而異,所以每次都得重調。更糟的是,做完這一切之後,這裡的每一個偵測器都還得倚賴 NMS(第 2 篇)這個獨立、啟發式的後處理步驟來清掉重複的框。於是很自然會問:我們能不能完全不用錨框就偵測物件——甚至連 NMS 也不要?這個問題正是最後一篇的動力,我們將走向「無錨框」,並認識把偵測重新定義成「直接集合預測」的 Transformer 式偵測器。