JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

當分類變難:細粒度、不平衡與結構化標籤

真實世界的類別很亂——上千種幾乎一樣的物種、常見類別氾濫而稀有類別寥寥、還有排成樹狀的標籤。

細粒度分類:分辨幾乎一模一樣的東西

到目前為止,我們的影像分類範例都相當好分辨:貓 vs. 狗 vs. 汽車。但許多真實問題要你區分看起來幾乎一模一樣的類別。細粒度分類就是在一個大類別之內,分辨外觀極度相似的子類別——問的不是「這是不是鳥?」,而是「這是田納西鶯還是橙冠蟲森鶯?」同樣的味道也出現在狗的品種、汽車車款、飛機機型上。整體輪廓一模一樣,只有細節不同。

核心困難在於兩種變異之間的殘酷不對稱。類別間(inter-class)的差異(兩個物種之間)可能極小——兩種鶯也許只差在一道淡淡的翼斑、或喉部稍微黃一點。同時,類別內(intra-class)的變異(同一物種之內)卻很巨大——同一種鳥會以不同的姿勢、光線、季節、背景出現。所以,陰影裡的 A 鶯照片,可能跟另一張 A 鶯照片差得比跟 B 鶯照片還多。真正能區分類別的訊號又小、又侷限在局部、又很容易被淹沒。

卷積網路把影像轉成一疊特徵圖。較深的特徵圖會對局部部位有反應——正好就是翼斑或喉色出現的地方。

示意圖:一張輸入影像被轉換成一連串特徵圖網格的活化值,較後層凸顯出小小的局部部位。

這正是為什麼一般性的全域特徵不夠用,也是為什麼前面學程介紹的特徵圖在這裡成了主角。回想一下:卷積網路在每一層都會產生一個空間網格的特徵——每個格子描述影像的一小塊。細粒度方法正是倚賴這種「定位能力」:它們用更高解析度的輸入,好讓微小部位不被抹掉;並加上「部位注意力(part-attention)」,學會把分數聚焦在最具鑑別力的區域(翅膀、鳥喙、車頭燈組),而不是對整張畫面取平均。具體來說:要把一隻鳥判為橙冠蟲森鶯,好的模型實際上會放大去看牠的冠部與喉部,比較那裡的局部特徵,並忽略牠停棲的樹枝。請記住這個「看對的小部位」的想法——第 5 節的「優雅退化」就建立在它之上。

多標籤:一張影像有多個正確答案

第 1 篇指南預告了一個岔路;在這裡我們走另一條分支。先前的一切都假設每張影像「恰好」有一個正確標籤——這是單標籤(single-label)分類,也就是 softmax 與 top-1 準確率背後的設定。但一張街景照片可以同時、且合理地包含一個人、一隻狗、「以及」一個飛盤。在多標籤分類裡,類別「並非」互斥:好幾個可以同時為真,也可以「一個都不為真」(空場景)。光是這個改變,就讓 softmax 失效了。

為什麼 softmax 會失敗?softmax 強迫各類別機率彼此競爭、且加總為 1——多給「狗」一點,就必須從「人」那裡拿走一些。當類別在爭奪「同一個名額」時,這正是你要的;但當它們可以共存時,這就完全錯了。解法是「拿掉競爭」:對每個類別各自套用一個獨立的 sigmoid,把每個 logit 變成它自己的「是/否」機率,並用對「所有類別加總」的二元交叉熵(binary cross-entropy)來訓練——每個類別都是它自己的一道「在或不在」的小問題。

p_c = \dfrac{1}{1+e^{-z_c}}, \qquad \mathcal{L} = -\sum_{c}\Big[\, y_c \log p_c + (1-y_c)\,\log(1-p_c) \,\Big]

對每個類別各自的 sigmoid + 二元交叉熵,並對所有類別加總。

讓我們逐個符號來讀。索引 c 跑過每一個類別。z_c 是網路對類別 c 輸出的原始分數(logit)。sigmoid 1/(1+e^{-z_c}) 把這個分數壓進嚴格介於 0 與 1 之間的機率 p_c——而且關鍵在於,它「獨立於其他每個類別」地這麼做,所以各個 p_c 不必加總為 1。y_c 是類別 c 的真實標籤:該類別出現在影像中就是 1,沒出現就是 0。再看損失:當類別「確實存在」(y_c = 1)時,第一項 -log p_c 生效,並隨著 p_c 升向 1 而變小,於是模型因為「有自信它在」而得到獎勵;當類別「不存在」(y_c = 0)時,換第二項 -log(1-p_c) 主導,並隨著 p_c 降向 0 而變小,獎勵「有自信它不在」。對 c 加總,只是把對每個類別的判決加起來。算個小數字:若一個存在的類別 p_c = 0.9,它的損失是 -log(0.9) ≈ 0.105(很小,很好);若模型錯給了 p_c = 0.1,損失就是 -log(0.1) ≈ 2.303(很大,是一記強力推動)。

實作範例——一張真實標籤為 {人、狗、飛盤} 的影像。假設各 sigmoid 輸出為:人 0.88、狗 0.73、飛盤 0.60、貓 0.05、車 0.12。這是五個「獨立的是/否機率」,不是一場競爭。要把它們變成預測,我們套用一個每類別各自的門檻(threshold)(比方說 0.5):凡是達到或超過門檻的就進入預測「集合」,低於的就捨棄。這裡的預測是 {人、狗、飛盤}——完全正確。注意輸出是一個「集合」,不是單一贏家;而且各類別的門檻可以不同(對「稀少但代價高」的類別,可能用較低的門檻以多抓到一些)。這也是為什麼目標表示法不一樣:多標籤不是用只有一個 1 的 one-hot 向量,而是用可以帶好幾個 1(或一個都沒有)的多熱(multi-hot)二元向量。

# Single-label (mutually exclusive): softmax forces classes to compete
probs = softmax(logits)          # values sum to 1 across classes
pred  = argmax(probs)            # exactly ONE label wins

# Multi-label (independent truths): sigmoid per class, no competition
probs = sigmoid(logits)          # each in (0,1); do NOT sum to 1
preds = [c for c in classes if probs[c] >= threshold[c]]  # a SET

# Target representation
one_hot   = [0, 0, 1, 0, 0]      # single-label: exactly one 1
multi_hot = [1, 0, 1, 1, 0]      # multi-label: any number of 1s
單標籤用 softmax+argmax+one-hot,對比多標籤用 sigmoid+門檻+multi-hot。

因為答案是一個集合,評估方式也跟著改變。top-1 準確率失去意義——根本沒有單一的「最高」真值。取而代之,我們對每個標籤分別計分,用精確率(precision)(在我們預測為「存在」的東西中,有多少真的存在)與召回率(recall)(在真正存在的東西中,我們抓到了多少),並用平均精度均值(mean average precision, mAP)跨類別做總結——它會獎勵在每個門檻下都把對的標籤排在錯的標籤之上。把這個跟單標籤乾淨地對比一下:在單標籤裡,目標是 one-hot 標籤編碼——一個除了單一個 1 之外全是 0 的向量,編碼「這些之中恰好一個為真」。多標籤只是「鬆綁」了這個約束,而所有後續選擇(sigmoid、BCE、門檻、mAP)都是從鬆綁它而自然導出的。

長尾辨識:富者愈富的問題

教科書裡的資料集很整齊:每個類別的樣本數大致相同。真實世界並非如此。把任何經過鏡頭的東西都拍下來,你會得到上千張常見麻雀的照片,而某種稀有特有種雀鳥卻剛好只有四張。這就是長尾 / 類別不平衡分類:少數幾個「頭部(head)」類別握有絕大多數樣本,而一條長長的「尾部(tail)」由許多類別組成,每個類別卻只有寥寥幾筆。若按出現頻率畫出來,曲線會有一個高聳的頭與一條又長又細的尾——名字由此而來。

為什麼樸素訓練會辜負尾部?回想一下:訓練損失是對「整個資料集」加總(平均)的交叉熵。若資料集由頭部主導,模型只要把常見類別做到滿分、幾乎從不預測稀有類別,就能把這個平均值壓得很低——尾部貢獻的項數太少,微不足道。更糟的是,最佳化會「實際把幾何結構往頭部彎」:決策邊界與各類別的偏置項(bias terms)會漂移,使模型預設給出頭部答案,因為大多數時候猜「麻雀」都會對。尾部於是被擠進特徵空間裡薄薄的縫隙,或乾脆被忽略。

對於稀有的尾部類別,要看的是精確率與召回率,而非整體準確率:模型可以準確率很高,卻幾乎從不召回尾部類別。

一張精確率—召回率示意圖,對比「預測為正」與「實際為正」的集合,說明稀有類別如何被漏掉。

這正是第 2 篇指南那句警告的具體化:在不平衡之下,整體準確率會說謊。想像一個賞鳥 App,頭部物種的每類別準確率有 97%,但尾部物種只有 18%。因為頭部提供了大多數的測試影像,頭條的 top-1 準確率也許會亮出一個光鮮的 93%——而一位依賴它去認稀有物種的賞鳥人,卻幾乎每六次有五次被它辜負。這個單一數字掩蓋了一道巨大的「頭部 vs. 尾部」每類別準確率落差。你只有「逐類別」去量,才看得見這道落差。

修補長尾:重採樣、重加權與解耦

對付長尾 / 類別不平衡分類有一套標準工具,每一樣都從不同角度攻擊同一個根因——頭部把尾部淹沒了。第一樣是重採樣(re-sampling):改變每個訓練批次的長相。把稀有類別「過採樣(oversample)」(讓它們那少少幾張圖更常出現),並/或把常見類別「欠採樣(undersample)」(少給一些),好讓模型「一批接一批」看到更平衡的組合,使尾部真的能在梯度上反推回去。陷阱是:過於激進的過採樣會把同樣幾張稀有影像重複很多遍,可能對它們過擬合;而大量欠採樣則會丟掉真實的頭部資料——所以通常是「微調」它,而不是開到最大。

第二樣工具是對損失做重加權(re-weighting):資料不動,但讓「尾部的錯」算得比「頭部的錯」更重,於是加總損失就不能再靠忽略稀有類別來壓低。最樸素的做法是按反頻率(1/n_c)為每個類別加權,但那太激進了:某個頭部類別的第 1000 張照片,幾乎教不了模型任何新東西(影像高度重疊),所以把 1000 個樣本當成比 1 個珍貴 1000 倍,會大幅高估頭部。「有效樣本數(effective number of samples)」的想法修正了這點:由於邊際效益遞減,n_c 個樣本裡真正的資訊量會飽和,因此我們改用一個「有效計數」來加權,而非原始計數。

w_c = \dfrac{1-\beta}{1-\beta^{\,n_c}}, \qquad \mathcal{L} = \sum_{c} w_c \,\mathcal{L}_c

透過有效樣本數做的「類別平衡」損失加權。

逐個符號:n_c 是類別 c 的訓練樣本數;L_c 是該類別原本的損失(例如它的交叉熵貢獻);w_c 則是我們乘上去的權重。超參數 β(beta)落在 [0,1),控制「額外樣本多快停止增加價值」——它是「邊際效益遞減」的旋鈕。注意 β^{n_c} 會隨 n_c 增大而趨近 0,所以對大型頭部類別,分母 (1 − β^{n_c}) 會趨近 1(權重小);但對極小的尾部類別,它仍然很小(權重大)——正是尾部需要的那記助力。兩個極限讓它一目了然:當 β→0,每個 w_c→(1−0)/(1−0)=1,也就是「不加權」;當 β→1,它趨近單純的反頻率加權。用 β = 0.99 實算:n_c = 1000 的頭部類別得到 w ≈ (0.01)/(1 − 0.99^1000) ≈ 0.01/0.99996 ≈ 0.0100;n_c = 10 的尾部類別得到 w ≈ (0.01)/(1 − 0.99^10) ≈ 0.01/0.0956 ≈ 0.105。尾部被加權約為頭部的 10 倍——但要注意,原始反頻率本來會要求 1000/10 = 100 倍,所以「有效樣本數」這招把一個 100 倍的過度修正,緩和成比較理智的約 10 倍。

# Class-balanced re-weighting via 'effective number of samples'
beta = 0.99
for c in classes:
    n_c = count[c]                          # samples in class c
    eff_num = (1 - beta ** n_c) / (1 - beta)  # 'effective' count
    w[c] = 1.0 / eff_num                     # rare class -> larger weight
normalize(w)                                  # keep total loss scale stable
loss = sum(w[c] * cross_entropy_c for c in classes)
有效樣本數重加權(Cui 等人,2019):經過緩和的反頻率權重。

第三樣、也常是最有效的工具,是把表徵學習與分類器解耦(decoupling)。一個出人意料的實證發現是:好的「特徵」最好是在資料「自然的(不平衡)」樣貌下學成——重採樣反而會傷到特徵抽取器;而真正被往頭部扭曲的,恰恰是「分類器」(最後那層線性層及其偏置)。所以你分兩階段訓練:先在自然資料上學主幹網路,然後凍結特徵,「只」對最後的分類器做再平衡(例如用平衡採樣重訓它,或乾脆把它的各類別偏置重新縮放)。如此你既得到強健的特徵,又得到公平的決策規則,而不必付出「整條端到端重採樣」帶來的過擬合代價。

階層式分類:住在樹上的標籤

真實的標籤集合很少是一張扁平清單;它們通常是一棵「分類樹(taxonomy)」——特定類別巢狀地嵌在更廣的類別之內。ImageNet 本身就建立在 WordNet 這棵樹上:哈士奇是狗的一種,狗是哺乳類的一種,哺乳類是動物的一種(哈士奇 ⊂ 狗 ⊂ 哺乳類 ⊂ 動物)。這些標籤自帶現成的關係,而忽略這些關係,等於把就擺在標籤空間裡的資訊白白丟掉。

階層式分類從兩個方面認真看待這棵樹。第一,預測應當尊重這棵樹:如果模型說「哈士奇」,它就隱含地也承諾了「狗」「哺乳類」「動物」,而一個合理的系統絕不會輸出一個與自身祖先矛盾的葉節點。第二——這也是核心——並非所有錯誤都一樣重。把哈士奇錯認成阿拉斯加雪橇犬(都是「狗」底下的兩個葉節點)是個小小的、可原諒的失手;把狗錯認成車輛則是災難性的。扁平的影像分類把兩者都當成單純的「錯」,給一樣的分數。而這棵樹讓我們能說:一種錯誤遠比另一種便宜。

  1. 在多個層級預測:為粗、中、細各層級各給模型一個輸出頭——例如「動物 vs. 車輛」,再「狗 vs. 貓」,再「哈士奇 vs. 阿拉斯加雪橇犬」——並讓每一個頭對著它那層的標籤訓練。
  2. 強制一致性:對「細層答案與粗層答案互相矛盾」的預測加以懲罰或禁止(粗層頭把它叫成車輛、細層卻說是「哈士奇」),讓粗層與細層的預測說同一個故事。
  3. 不確定時退回到有把握的祖先:若模型在哈士奇與阿拉斯加雪橇犬之間決定不了,就讓它高信心地回答父節點「狗」,而不是硬猜一個葉節點——給一個「部分正確、仍然有用」的答案,而不是擲銅板。

為困難問題選對指標

本篇裡的每一個困難設定,都會悄悄地讓單純的 top-1 準確率失效,因此我們以「把指標對準問題」作結——這跟第 2 篇是同一個教訓,只是現在逐案套用。一句拇指法則:指標必須編碼你「真正在乎」的東西,因為你量什麼,訓練與選模就會去追什麼。

對長尾資料,偏好用平衡(每類別平均)準確率而非單純的 top-k 準確率。單純的 top-1 是「對影像取平均」,所以頭部主導;每類別平均則是先算每個類別各自的準確率、再把這些平均起來,讓每個類別——不論頭尾——都有一票。尾部終於算數了。對細粒度問題,倚靠 top-5(它能原諒真正相似物種之間「差一點」的錯認),並閱讀混淆矩陣(confusion matrix),它精準揭示哪些物種被誤認成哪些——對角線之外的熱點,就是你的困難配對。

一張混淆矩陣:列是真實類別,行是預測。對角線之外發亮的格子,正是細粒度模型一直互相錯認的相似配對。

一張網格熱圖,對角線明亮代表正確預測,少數對角線外發亮的格子標記出系統性被混淆的類別配對。

對階層式標籤,使用會「為正確祖先給部分分數」的階層感知指標(hierarchy-aware metrics):真值是「哈士奇」時答「狗」,應該比答「卡車」得分更高。常見做法是按「預測與真值在樹上的距離」來為錯誤加權,於是錯認兩個品種幾乎不損分,而把一個 跨越整棵樹的類別搞混則被重重懲罰。對多標籤,完全捨棄 top-1,改報告每標籤精確率/召回率加上平均精度均值(mAP),兩者合起來描述「預測集合」在各門檻下與「真實集合」吻合得有多好。

一個簡短的實算對比把這點釘死。假設頭部物種達到 98% 的每類別準確率,尾部物種只有 20%,而頭部提供了大部分測試影像。頭條 top-1 也許會落在大約 0.95×0.98 + 0.05×0.20 ≈ 94%——看起來棒極了。但平衡準確率是把類別「平等地」取平均:若尾部類別數與頭部差不多,每類別平均 ≈ (0.98 + 0.20)/2 ≈ 0.59。同一個模型,單看你信哪個指標,就是「94%」或「59%」,而「59%」那個,才對得上「在乎稀有物種」的那位使用者。