為什麼要淘汰錨框?
在前兩篇導覽裡,有一個物件一直在扛重活:錨框。回想一下它的概念。與其要求網路憑空想像出一個框,我們改成在影像的每個位置鋪滿成千上萬個事先設定好的參考矩形——尺寸固定、長寬比固定——而網路只需要做兩件事:(a) 判斷哪些錨框裡有物件,(b) 把選中的錨框稍微挪動、縮放,讓它緊貼住物件。這是個高明的拐杖。Faster R-CNN 的區域提議網路,以及 SSD、RetinaNet 這類單階段偵測器,全都站在它之上。但拐杖終究是拐杖,而這最後一篇導覽要談的,正是如何放下拐杖、靠自己走路。
一張特徵圖網格,在某個格點上疊著數個不同長寬比的矩形。
反對錨框的理由有三項具體成本。第一是超參數:你必須挑選尺度、長寬比、每個位置放幾個錨框,以及決定哪些錨框算正樣本(有物件)、哪些算負樣本(背景)的 IoU 門檻。每一項都得針對不同資料集重新調校——行人需要又高又瘦的錨框,俯視的衛星影像則完全是另一回事——只要調錯,召回率就會悄悄崩掉。第二是密度與失衡:為了覆蓋所有形狀,錨框必須鋪得很密,一張影像就有數萬個,而絕大多數都落在背景上。這正是第 4 篇導覽必須用 focal loss 對付的前景/背景失衡問題。第三是形狀耦合:錨框預先寫死了「物件會是什麼形狀」的假設,於是一個長寬比沒被任何錨框預料到的物件,從一開始就很難被匹配上。
所以本篇的地圖是這樣。我們先探索無錨點偵測,它保留單階段偵測器我們熟悉的密集預測形狀,但改成從「點」而不是從「錨框」來預測每一個框。接著我們會注意到一個頑固的殘留——這些模型仍然需要 NMS——而這個殘留正好成為通往第二條逃生路線的跳板:DETR,它把偵測重新定義為「直接預測一個集合」,既不用錨框、也不用 NMS。為了理解 DETR,我們會繞一小段自成一體的注意力機制小路,然後把整幅圖拼起來。
無錨點偵測:用點與中心定位
如果問題出在錨框,最乾淨的解法就是直接從影像位置預測框。有兩個無錨點偵測家族正是這麼做。關鍵點家族先找出一個有辨識度的點——角點或中心點——再由它長出一個框。密集逐位置家族則把物件內部的每一個位置都變成一個小小回報員,去描述環繞它的那個框。我們依序來看;兩者的直覺都非常具體。
像 CornerNet 與 CenterNet 這類關鍵點偵測器依賴的是熱圖(heatmap)——一張逐像素的機率圖,回答「這裡正好是一個角點(或中心點)嗎?」。CornerNet 預測兩張熱圖,一張給左上角、一張給右下角,再把屬於同一個物件的角點配對;框就是橫跨這對配對角點的那個矩形。CenterNet 更簡單:把物件中心偵測為熱圖上的一個峰值,然後在那個峰值處回歸出寬與高(外加一個小小的偏移量來修正取整誤差)。心智模型是「先找出一個有辨識度的點,再由它長出框」——沒有錨框、沒有形狀先驗,只有一張學出來的圖上的峰值。
密集逐位置家族的代表是 FCOS(Fully Convolutional One-Stage 偵測器),它最值得仔細研究,因為它保留了你已經從 YOLO 與 RetinaNet 認識的密集網格形狀。規則簡單得令人意外:任何落在某個真實標註邊界框內部的特徵圖位置,都被當成正樣本,而這個位置必須直接回歸出四個數字——從它自己到該框的左、上、右、下四條邊的距離。整個過程沒有任何錨框;點本身就是錨點,而那四個距離就是框。
一張特徵圖分別送入分類、框回歸與中心度三個並行的偵測頭。
FCOS 的框編碼:由一個點的四個邊距還原出框的四個角座標。
把它讀成一份小食譜。內部點位在影像座標 (x, y)。網路預測四個正的距離:到左邊的 l、到上邊的 t、到右邊的 r、到下邊的 b。要重建框,就從這個點向外走這些距離——左邊在 x-l、上邊在 y-t、右邊在 x+r、下邊在 y+b——得到框的角座標 (x-l,\,y-t,\,x+r,\,y+b)。實作範例:位在 (x,y)=(50,40) 的點預測出 (l,t,r,b)=(20,20,40,40)。那麼框就是 (50-20,\,40-20,\,50+40,\,40+40)=(30,20,90,80),也就是 左=30、上=20、右=90、下=80。點是由內而外地描述框;正是這一個改變,讓我們得以丟掉錨框。
中心度會壓低那些由靠近框邊界的點所做出的預測。
中心度是 FCOS 巧妙的品質旗標,一個介於 0 與 1 之間的數,表達「這個點在它的框裡有多靠中心?」。看那兩個比值:\min(l,r)/\max(l,r) 只有在 l=r(點正好落在左右正中)時才為 1,而當點漂向某條側邊時就趨近 0;\min(t,b)/\max(t,b) 在上下方向上做同樣的事。把兩者相乘再開根號,就把兩個軸混合成單一分數。實作數字:在正中心 l=r 且 t=b,所以中心度 =\sqrt{1\cdot 1}=1。對於先前那個 (l,t,r,b)=(20,20,40,40) 的點,則是 \sqrt{(20/40)\cdot(20/40)}=\sqrt{0.5\cdot0.5}=0.5。對於緊貼左邊的點,例如 (l,t,r,b)=(5,30,55,30),則是 \sqrt{(5/55)\cdot(30/30)}\approx 0.30。在訓練與測試時,這個分數會乘進分類信心裡,於是搖晃、偏離中心的框被往排名下方壓,而乾淨、置中的預測勝出。
import math
def decode_fcos(x, y, l, t, r, b):
# Rebuild a box from a point and its four edge-distances
left = x - l
top = y - t
right = x + r
bottom = y + b
return (left, top, right, bottom)
def centerness(l, t, r, b):
# ~1 at the box center, ~0 near a box edge
horiz = min(l, r) / max(l, r)
vert = min(t, b) / max(t, b)
return math.sqrt(horiz * vert)
# Center point of the example box (30,20,90,80) is (60,50)
print(decode_fcos(60, 50, 30, 30, 30, 30)) # (30, 20, 90, 80)
print(centerness(30, 30, 30, 30)) # 1.0 -> perfectly central
print(round(centerness(20, 20, 40, 40), 3)) # 0.5 -> off-center point最後一塊拼圖讓 FCOS 變得實用:它與第 4 篇的特徵金字塔網路天生契合。單一張特徵圖無法乾淨地同時處理極小與極大的物件,而一個落在兩個重疊框內部的點,確實是有歧義的。FCOS 用一招同時解決這兩件事:讓每一層金字塔只負責某個物件尺寸的「帶寬」——小物件在高解析度層回歸、大物件在粗解析度層回歸——做法是限制每一層被允許預測的邊距範圍。不同尺寸住在金字塔的不同樓層,既分散了工作量,也化解了大部分的重疊歧義。
還需要 NMS 嗎?無錨點模型的重複框問題
以下這個觀察推動了本篇接下來的內容。無錨點偵測拿掉了錨框——但它並沒有拿掉重複框。想想 FCOS 實際上在做什麼:物件框內部的每一個位置都是正樣本,都會預測自己的框。一個中等大小的物件會涵蓋數十個特徵圖位置,於是數十個相鄰的點各自為同一個物件吐出一個框,全都嚴重重疊。錨框消失了,但那一大堆幾乎一模一樣的框並沒有。
左:一個物件上有數個重疊的框。右:經過 NMS 後只剩一個框。
所以無錨點偵測器仍然依賴我們在第 2 篇遇過的同一道清理工序:非極大值抑制。預測之後,NMS 依分數把框排序、保留最高分的那個、刪掉所有與它重疊超過某個 IoU 門檻的框,然後重複。它有效,但請注意它的本質——一個人手設計、貪婪、而且不可微分的後處理步驟,外掛在網路之外,還有自己的門檻要調。網路其實從來沒有被訓練成「每個物件輸出一個框」;它被訓練成輸出一大堆,然後我們事後再收拾。這就是舊管線最後那塊頑固的拼圖。下一個想法,DETR,終於把它拿掉了——做法是讓模型從一開始就吐出一個乾淨的集合。先把這個念頭記著。
注意力機制速成課
偵測 Transformer建立在 transformer 之上,而你在這條學習階梯上可能還沒遇過它,所以讓我們把一切所仰賴的那個核心想法——自注意力(self-attention)——從零開始、直覺但精確地教一遍。場景是這樣:你有一群元素(影像區塊、詞、或者對 DETR 來說是「物件槽」),每個都用一個向量表示。自注意力就是讓每個元素去看其他所有元素、並根據哪些東西重要來更新自己的機制。這裡完全不需要錨框或卷積;它純粹是在問「誰該跟誰說話」。
訣竅是給每個元素三個學出來的角色。它的 Query(查詢)問「我在找什麼?」。它的 Key(鍵)宣告「這是我能提供的東西」。它的 Value(值)承載「這是我真正的內容」。一個元素去注意其他元素的方式是:拿自己的查詢去和每個元素的鍵比對,把每次比對變成一個關聯權重,再對所有人的值取加權混合。最乾淨的類比是一張柔性的、學出來的查詢表:在普通字典裡你要精確命中一個鍵才能取出它的值;在注意力裡,你拿查詢去和所有鍵按相似程度比對,回傳的是所有值的平滑混合,權重就看每個鍵匹配得多好。
一個查詢向量與多個鍵向量比對,產生權重來組合各個值向量。
縮放點積注意力——整個機制濃縮在一行裡。
我們來逐項拆解。Q、K、V 都是矩陣:把每個元素的查詢向量疊成 Q 的一列、每個鍵疊成 K 的一列、每個值疊成 V 的一列,每個向量長度為 d_k。乘積 QK^{\top} 讓每個查詢乘上每個鍵,產生一張方形表格,裝著所有成對的相似度分數——第 (i,j) 格代表元素 i 的查詢與元素 j 的鍵匹配得多強(點積大就代表「非常相關」)。除以 \sqrt{d_k} 是一個穩定器:長向量的點積會隨維度增大,若不除,分數會大到讓 softmax 飽和成近乎 one-hot 的尖峰、梯度消失;若 d_k=64 就除以 8。\operatorname{softmax} 沿著每一列作用,把那一列的分數壓成總和為 1 的正權重——一個正規的注意力分佈。最後乘上 V,就把每個元素換成所有值的加權混合,權重正是那些注意力權重。小例子:若某個查詢對三個鍵打出 (2.0,\,0.1,\,0.3) 的分數,softmax 大約給出 (0.75,\,0.11,\,0.14),於是輸出約為 0.75\,V_1+0.11\,V_2+0.14\,V_3——主要是第一個元素的內容,再帶一點其他兩個。
一張覆蓋在影像區塊上的熱圖,顯示某個查詢最強烈注意到的位置。
DETR:把偵測視為集合預測
現在我們把一切組裝起來。偵測 Transformer(DETR)把物件偵測重新定義為直接集合預測,它的管線有四個階段。第一,一個 CNN 主幹把影像變成一格格特徵向量,就跟目前為止每一個偵測器一樣。第二,一個 transformer 編碼器對這些特徵套用自注意力,讓每個位置都被全域脈絡充實——模型可以跨越整張影像把車輪和車身關聯起來。第三,一個 transformer 解碼器接收一小撮固定數量 N 個學出來的向量,稱為物件查詢(object queries)(比方說 N=100),並用注意力讓每個查詢從編碼後的影像、以及從其他查詢身上蒐集證據。第四,每個查詢通過一個小小的頭,輸出剛好一個預測:一個類別標籤(真實類別之一,或一個特殊的「無物件」標籤)以及一個框。於是 DETR 一次吐出一組 N 個預測——沒有錨框、沒有滑動視窗,而且如我們將看到的,沒有 NMS。
一個 transformer 區塊:多頭注意力接著一個前饋層,並帶有殘差連接。
物件查詢究竟是什麼?把它們想成 N 個學出來的槽(slots),是網路從零訓練出來的空容器。透過注意力,每個槽學會專精化——某個槽傾向在左上角找物件、另一個專找小物件、又一個專找寬的物件——於是它們整體上散開,覆蓋影像的各個區域與形狀。槽不是框、也不是錨框;它是一個學出來的「對場景的提問」,在蒐集完證據後,最多收斂成一個被偵測到的物件。這些槽在解碼器裡也會互相注意,讓它們得以協調:「那輛車你顧,這個人我顧」。
透過最佳二分(匈牙利)匹配進行集合預測。
這個損失是 DETR 的核心,所以我們慢慢讀。模型輸出 N 個預測 \hat{y}=(\hat{y}_1,\dots,\hat{y}_N)。真實物件是 y=(y_1,\dots,y_N),用「無物件」標籤的複本補滿到長度 N,讓兩份清單一樣長。一個排列 \sigma 就是一個一對一的配對——它說「真實物件 i 由第 \sigma(i) 號預測負責」——而 \mathfrak{S}_N 是所有這種配對的集合。對每一種候選配對,我們把匹配成本 \mathcal{L}_{\text{match}}(y_i,\hat{y}_{\sigma(i)}) 加總,這個成本混合了一個類別項(預測的類別對不對?)與一個框項(通常是 L1 距離加上廣義 IoU——兩個框重疊得多好?)。然後我們挑出 \hat{\sigma},也就是總成本最小的那個配對——那就是 \arg\min。關鍵在於這個指派是一對一的:每個真實物件剛好被一個預測認領,而所有剩下的預測都被配到「無物件」。
有兩件事讓它能運作、也讓它重要。第一,用暴力法搜遍全部 N! 種排列是沒指望的,但這是個經典的指派問題,而匈牙利演算法能在 O(N^3) 時間內精確解出——又快又標準。第二,也是整篇導覽的笑點所在:因為匹配是一對一的,訓練訊號告訴每個真實物件「你們之中剛好只有一個要對我負責」。一個查詢若產生了一個已被別的查詢認領之物件的重複框,它得不到任何獎勵——它最好的選擇是改預測「無物件」。因此模型被訓練成不吐出重複框。這正是為什麼 DETR 不需要非極大值抑制:NMS 過去要事後刪掉的那些冗餘,現在從一開始就不會被產生。第 2 篇那個不可微分的後處理步驟,溶進了可微分的損失裡。
# DETR training loss, in pseudocode
# preds: N predictions, each (class_logits, box)
# gts: M ground-truth objects, each (class, box), M <= N
def detr_loss(preds, gts):
# 1) Build an N x N cost matrix (gts padded with 'no object')
cost = build_cost_matrix(
preds, gts,
class_weight=1.0, # is the class right?
l1_weight=5.0, # box corner distance
giou_weight=2.0, # box overlap (generalized IoU)
)
# 2) Optimal one-to-one assignment (no NMS, no anchors)
pred_idx, gt_idx = hungarian_algorithm(cost) # O(N^3)
# 3) Supervise each matched pair; unmatched preds -> 'no object'
loss = 0.0
for p, g in zip(pred_idx, gt_idx):
loss += classification_loss(preds[p], gts[g])
if gts[g].label != NO_OBJECT:
loss += l1_box_loss(preds[p], gts[g])
loss += giou_loss(preds[p], gts[g])
return loss某個物件查詢的注意力集中在單一物件的邊緣上。
偵測技術的全貌與展望
退一步看,整條學習軌道講的是同一個故事。物件偵測就是定位加上分類——同時說出是什麼和在哪裡(第 1 篇)。為了評分與比較偵測器,我們打造了一套共用的計分工具:用 IoU 量框的重疊、用 NMS 移除重複、用 mAP 在各門檻上總結品質(第 2 篇)。接著我們逐一走過各種架構。兩階段的 Faster R-CNN 家族是先提議再分類:先勾勒出有希望的區域,再為它們標類別並修正——準確但較笨重(第 3 篇)。單階段的 YOLO/SSD/RetinaNet 家族在單次前向中密集預測,靠錨框、特徵金字塔與 focal loss 同時兼顧快與準(第 4 篇)。而在本篇,這個領域卸下了它人手設計的鷹架:無錨點偵測器從點來預測,DETR 則用匈牙利匹配直接預測一個集合(第 5 篇)。
- 定義任務:為每個物件預測一個類別標籤與一個框——定位加分類。
- 評分它:用 IoU 算重疊、用 NMS 去重、用 mAP 公平地為偵測器排名。
- 兩階段:先提議區域,再分類與修正(R-CNN 到 Faster R-CNN)。
- 單階段:搭配錨框、FPN 與 focal loss 的密集預測(YOLO、SSD、RetinaNet)。
- 現代:丟掉錨框(點為基礎、FCOS),連 NMS 也丟掉(DETR 集合預測)。
有兩股張力貫穿整條脈絡,而且兩者都沒有被「解決」——你只是選擇自己在上面的落點。第一是速度對準確度:兩階段偵測器與大型 transformer 用算力換準確度;單階段與即時模型用一點準確度換低延遲;正確的選擇取決於你是在標註研究基準,還是在手機或車上運行。第二是人手設計對學習而來的元件:每一個世代都把一個人為設計的零件——滑動視窗、然後是提議、然後是錨框、然後是 NMS——換成網路端到端學出來的東西。趨勢很清楚:人手要調的旋鈕越來越少、學來的行為越來越多、管線越來越簡單,也越難因為調錯而崩壞。
這個領域往哪裡走?有三股潮流值得關注。即時 transformer 偵測器(RT-DETR 及同類)正在拉近速度差距,使集合預測不再是只能用於研究的奢侈品。開放詞彙與可提示偵測——像 Grounding DINO、OWL-ViT 與 Segment Anything 家族這類模型——讓你能用自由文字名稱、或用點一下的方式來指定要找的物件,而不必侷限在固定的訓練類別清單上;偵測正在變成一種你可以「對它說話」的東西。而偵測也正在與分割、追蹤統一:同一套「查詢加集合」的機制,既能找出框,也能產生遮罩、還能在影片中跟蹤物件,於是這些曾經各自獨立的任務之間的界線,正在溶解成單一通用的「把東西找出來」介面。