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

從像素到標籤:分類任務

認識電腦視覺中聽起來最簡單、卻最深奧的任務——把一格格像素變成一個有把握的詞。

分類器到底在做什麼

我們先從最乾淨的工作說明開始。一個影像分類器吃進一張影像,吐出一個詞。就這樣。輸入是一張影像——回想前面的單元,對電腦而言影像就是一個 H×W×3 的張量:一個 H 列乘 W 行的網格,每個格子上有 3 個數字(該像素的紅、綠、藍亮度)。輸出則是從一份固定、事先講好的 K 個類別清單中挑出的單一標籤——這份清單在模型誕生前就由人決定好了,例如當 K = 4 時的 {貓、狗、鳥、車}。

一張圖看懂分類任務:像素網格進入,從固定清單中輸出剛好一個標籤。

一張動物影像送進標示為「分類器」的方框,該方框從一份候選類別名稱的短清單中輸出單一被選中的標籤。

一個親切的想像方式:把它想成一位郵件分揀員站在 K 個貼了標籤的箱子前。每一封到達的信(影像)都必須被丟進剛好一個箱子(類別)。分揀員不會臨時發明新箱子,也不會拒絕選擇——每封信都會落到某處。這條「永遠剛好一個箱子」的規則就是分類的全部個性,我們之後會一再回到這個畫面。

最後,讓我們先把範圍劃清楚,免得後面混淆。分類用一個涵蓋整張圖的標籤回答「這是什麼影像?」。它的鄰居們回答的是不同問題:偵測回答「物體在哪裡?」,做法是在每個物體周圍畫框;分割回答「哪些像素屬於什麼?」,做法是替每個像素塗上一個類別。同樣的原始像素,三種非常不同的輸出形狀。整個這條學習軌只談第一種——把像素網格變成一個有自信的詞。

標記答案:獨熱標籤

在機器能學會回答之前,我們得先把正確答案寫成它能拿來運算的形式。人類聽到「答案是狗」就很開心,但網路處理的是數字,而且關鍵在於——它需要衡量自己現在錯得有多離譜。所以我們把正確答案編碼成一個數字向量,而不是寫成「狗」這個詞,甚至也不是只寫一個索引 1。

標準做法是獨熱編碼——把它想成一張完美的答案卡。你做一個向量,每個類別佔一格,所有格子先填 0,再在真正類別的那一格放一個 1。「獨熱」(one-hot)的字面意思就是剛好有一格是「熱的」(等於 1),其餘全是冷的(等於 0)。

y_c = \begin{cases} 1 & \text{if } c \text{ is the true class} \\ 0 & \text{otherwise} \end{cases}, \qquad \sum_{c=1}^{K} y_c = 1

獨熱目標向量 y。

逐個符號來讀:y 是標籤向量(整張答案卡),c 是走遍類別、從 1 到 K 的索引。這條規則說,只有當類別 c 真的是正確答案時,y_c 這一格才是 1,其餘每個類別都是 0。右邊那部分 ∑ y_c = 1,只是在說所有格子加起來等於 1。用我們依序排列的 K = 4 個類別(貓、狗、鳥、車)來具體看看。如果正確答案是,獨熱向量就是 y = [0, 1, 0, 0]:貓 0、狗 1、鳥 0、車 0。把答案換成「車」,它就變成 [0, 0, 0, 1]。簡單,但精確。

從原始分數到機率:softmax

現在輪到模型這一方。你在前面單元見過的深度網路,最後會有一層輸出 K 個原始實數,每個類別一個。這些數字叫做 logits(對數機率值/原始分數)。一個 logit 只是未校準的分數:越大代表「網路越偏向這個類別」,但這些值可以是任何實數——負的、零、5.3,什麼都行。它們絕對不是機率:既不落在 0 到 1 之間,加起來也不會剛好等於什麼。

logits 從哪來:像素流過卷積管線,最後一層輸出 K 個原始分數,每個類別一個。

一張卷積神經網路的示意圖:輸入影像通過堆疊的卷積與池化層,最後在一層產生長度為 K 的 logits 向量。

要把那些原始分數變成一個誠實的機率分布,我們套用 softmax。softmax 的任務是:接收任意 K 個實數,回傳 K 個全部非負、且加總剛好為 1 的數——一個有效的機率分布,我們可以把它解讀成模型對每個類別的信心。

p_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}

softmax 把 logits 變成機率。

逐個符號:z_i 是類別 i 的 logit(原始分數)。指數 e^{z_i} 一次做兩件事——它讓每個值都嚴格為正(不准有負機率),並且放大差距,讓稍大的 logit 變成明顯更大的數。分母把所有類別 j = 1…K 的 e^{z_j} 加總,這個正規化項強迫輸出加起來等於 1。結果 p_i 就是模型賦予類別 i 的機率。用 logits [2.0, 1.0, 0.1] 實算一遍:取指數得 e^2.0 ≈ 7.39、e^1.0 ≈ 2.72、e^0.1 ≈ 1.11;總和 ≈ 11.21;各自除以總和得 [0.66, 0.24, 0.10](確實加起來為 1)。注意 logits 之間的差距決定了結果有多尖銳:第一個 logit 領先 1.0 分,就變成了自信的 66%。

import numpy as np

def softmax(z):
    z = np.array(z, dtype=float)
    z = z - z.max()          # subtract the max first for numerical stability
    e = np.exp(z)            # exponentiate: every value becomes positive
    return e / e.sum()       # normalise so the outputs sum to 1

logits = [2.0, 1.0, 0.1]
probs = softmax(logits)
print(probs)                 # ~ [0.659, 0.242, 0.099]
print(probs.sum())           # 1.0
幾行就寫完 softmax。在取指數前先減掉最大值可避免溢位,且完全不改變結果。

做出決定:argmax 決策規則

softmax 給了我們一個豐富的機率向量,像 [0.66, 0.24, 0.10]——但任務要求的是一個答案。我們怎麼下決定?用 argmax 決策規則:就預測機率最高的那個類別。回到郵件分揀員——他聆聽全部 K 個信心值,把信丟進最大聲的那個箱子。這裡最大聲的是機率 0.66 的類別 1,所以我們就預測類別 1。

\hat{y} = \arg\max_{i}\; p_i \;=\; \arg\max_{i}\; z_i

挑出機率最大的那個索引——等價於挑最大的 logit。

逐個符號:p_i 是類別 i 的機率。算子 arg max(對 i)回傳的不是最大的;它回傳的是讓那個值最大的索引 i。所以 ŷ(讀作「y-hat」,模型的單一預測類別)就是勝出的那個類別索引。第二個等號 arg max p_i = arg max z_i,源自上一節 softmax 的單調性:既然 softmax 從不重排,機率最大的類別永遠就是 logit 最大的類別。具體來說,對 [0.66, 0.24, 0.10],arg max 是索引 1;對 logits [2.0, 1.0, 0.1],arg max 也是索引 1——同一個贏家,根本不需要 softmax。

兩個實務小註腳。第一,平手:若兩個類別的最高機率剛好相同,規則就有歧義,函式庫會用固定慣例打破平手(通常選最小的索引)——在實數 logits 下很少見,但值得知道。第二,一個免費的加速:因為對機率取 arg max 等於對 logits 取 arg max,所以在推論時(當你只需要預測標籤、不需要機率),你可以完全略過 softmax,直接讀出最大的 logit。只有當你真的想要機率數字時——用於訓練、校準或回報信心——才需要 softmax。

一個標籤還是多個?單標籤對多標籤

該來挑戰我們一路依賴的那個假設了:每張影像都剛好有一個誠實的答案。常常並非如此。一張照片可能真的同時包含一隻狗和一個飛盤;一個街景同時有車紅綠燈。把這樣的影像硬塞進一個箱子會丟掉真實資訊——這張圖真的同時是好幾樣東西。

這把世界一分為二。單標籤分類——我們目前建立的一切——假設剛好有一個贏家,這正是為什麼 softmax(一場機率加總為 1 的單一競賽)加上 argmax 會如此自然契合:更像狗必然意味更不像貓。相對地,多標籤分類允許好幾個類別同時為真,所以我們必須拋棄那種非全即無的競賽。注意這仍然是影像分類——同樣是像素到標籤的精神——只是把「剛好一個」的規則放寬了。

概念上的修正很直覺:多標籤不是用一個跨所有類別共享的 softmax,而是問 K 個獨立的是/否問題——「有狗嗎?(是/否)」、「有飛盤嗎?(是/否)」等等,每個各自獨立判斷。每個問題用一個 sigmoid 壓成 0–1 的答案(單一類別的機率,允許好幾個類別同時很高),而且各有自己的門檻——比方說,機率超過 0.5 就判定為存在。沒有競賽,也沒有被迫產生的單一贏家。

損失如何教會網路:一分鐘看懂交叉熵

我們現在有兩個形狀相同的向量:獨熱目標 y(真相,例如 [0,1,0,0])與 softmax 輸出 p(模型的猜測,例如 [0.10,0.70,0.15,0.05])。訓練需要一個單一數字,告訴我們這個猜測有多,我們才能推動網路做得更好。那個數字就是交叉熵損失。最乾淨的直覺:交叉熵衡量模型聽到正確答案時的驚訝程度。如果模型本來就把高機率壓在正確類別上,真相毫不意外,損失就很小;如果它把低機率放在那裡,真相就是個震驚,損失就很大。

\mathcal{L} = -\sum_{c=1}^{K} y_c \log p_c \;=\; -\log p_{\text{true}}

交叉熵,會塌縮成正確類別機率的負對數。

逐個符號:總和走遍所有類別 c = 1…K。y_c 是獨熱目標——記得它在唯一的真類別上是 1,其餘各處都是 0。p_c 是模型對類別 c 的預測機率。log 是讓懲罰見血的關鍵:接近 1 的機率取對數會接近 0(無懲罰),但很小的機率取對數會是個很大的負數(巨大懲罰),前面的負號再把它翻成正的,於是損失成了我們要最小化的東西。優雅之處在這裡:由於 y 是獨熱的,每一個 y_c = 0 的項都直接消失,只剩真類別那唯一一項存活。所以整個總和塌縮成 L = −log(p_true)——模型賦予正確類別的機率的負對數。分布裡的其他部分不會直接影響損失。

我們用自然對數來感受一下數字。假設模型給真類別(狗)的機率是 0.70。那麼損失是 L = −log(0.70) ≈ 0.357。現在想像訓練讓它變好:當 p_true = 0.90,損失降到 −log(0.90) ≈ 0.105;在 0.99 時 ≈ 0.010;而當 p_true → 1,損失 → 0(毫不意外,完美)。反方向走它就咬得很痛:當 p_true = 0.10,損失是 −log(0.10) ≈ 2.303。所以把正確類別的機率往上推,正是縮小損失的方法——這就是訓練拉動的那根槓桿。

import numpy as np

# one-hot target for class "dog" (index 1) out of 4 classes
y = np.array([0, 1, 0, 0])

# model's predicted probabilities (output of softmax)
p = np.array([0.10, 0.70, 0.15, 0.05])

# cross-entropy collapses to -log(probability of the true class)
loss = -np.sum(y * np.log(p))
print(loss)                 # -log(0.70) = 0.357

prediction = np.argmax(p)   # 1  ->  "dog"  (the decision rule from earlier)
獨熱真相遇上 softmax 猜測:損失就是真類別機率的負對數。
為什麼平滑的損失重要:連續的損失曲面讓網路能一步一小步地往下滾,朝更好的權重前進。

一個彎曲的損失曲面,一顆球往最低點滾下,示意梯度下降一步步地降低損失。

這就走完了本篇的整段旅程:像素 → logits → softmax → 機率 → argmax,並以交叉熵作為老師,替每個猜測打分,讓網路得以進步。後面的一切都建立在這副骨架上。我們只預覽了網路如何真正學習;第 3 篇會推導交叉熵的梯度,並完整鋪陳現代訓練配方。不過接下來,第 2 篇會問一個我們一直繞著走的更尖銳的問題:有了分類器之後,我們該如何衡量它到底好不好?