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

從像素到圖塊:影像如何變成序列

認識視覺 Transformer 背後最大膽的想法:把影像切成圖塊,當成一句由「字」組成的句子來處理。

為什麼要發明新的『看』法?

如果你是從前面的視覺單元一路學過來的,那你已經認識了卷積神經網路(CNN)。它最核心的動作是滑動濾波器:一個帶有可學習權重的小視窗(例如 3×3 像素)在影像上滑過,每滑到一個位置就只看那一小塊鄰域、產生一個數字。把成千上萬個這種小偵測器疊成一層,再把很多層疊起來,網路就會慢慢學到邊緣、然後紋理、然後形狀、最後是物體。每個神經元自始至終都只看到輸入的一小塊區域——它的感受野——而網路是靠著「變得更深」來逐步擴大它的理解。

CNN 由下而上建立理解:早期是小範圍的局部濾波器,越深層感受野才越大。

卷積流程示意圖:輸入影像送入一層層的卷積與池化層,感受野隨深度逐漸變大,最後接到分類器。

這裡要誠實地講出它的限制。因為每個濾波器都是局部的,CNN 必須等訊號穿過許多層之後,才有辦法把影像中相隔很遠的兩個角落關聯起來。一張照片左上角的資訊必須一層一層地接力傳遞,才可能和右下角的資訊碰面。網路最後確實能「看到全局」,但這個能力來得很慢、要靠深度堆出來。對 CNN 來說,全局脈絡是它「掙來」的,而不是一開始就「擁有」的。

視覺 Transformer採取的就是這種「退後一步」的視角。它的承諾既簡單又大膽:讓影像中的每一個部分能立刻看到其他每一個部分,就在最開始的第一層裡,完全不必等深度去接力傳訊息。更了不起的是,它用的正是當年徹底改寫自然語言處理的同一套機制——Transformer——在語言裡,模型一次讀進整個句子,讓每個詞同時關注其他每個詞。本篇要邁出的那一步,就是領悟到:我們可以把同一套架構轉過來用在影像上。但是,要把一張影像餵進一台為「句子」打造的機器,我們得先讓影像長得像一個句子。這就是下一節的妙招。

關鍵妙招:把影像切成圖塊

句子是一串詞,影像是一格格的像素。為了讓兩者長得像,視覺 Transformer 做了一件幾乎天真到好笑的事:它把影像切成一個規則的方格網,每格都是固定大小的正方形圖塊——一塊塊的小磚,就像馬賽克拼貼的碎片,又像把一頁漫畫剪成一格一格的分鏡。這個把連續影像切成離散小塊的步驟,就叫做影像分詞(image tokenization),因為每一塊小磚馬上就要變成一個「token」——也就是視覺世界裡相當於「詞」的東西。

一張 224×224 的影像被切成 14×14 的網格,每塊 16×16 像素——共 196 塊,依閱讀順序排列,就是這個「影像句子」裡的『詞』。

一張方形影像疊上格線,被分割成許多相等的方形圖塊,圖塊依由左到右、由上到下的順序編號。

我們用最標準的設定把它講具體。拿一張高 224、寬 224 像素的影像,選一個 16×16 像素的圖塊大小。沿著高度方向,能放下 224 ÷ 16 = 14 塊;沿著寬度方向,再放下 14 塊。於是得到一個 14×14 的網格,也就是 14 × 14 = 196 塊圖塊。所以這一張影像就變成了一串 196 塊小磚——一個有 196 個『詞』的句子。每塊小磚就是一個 token,而整個架構接下來要做的事,核心就是讓這 196 個 token 彼此交談。

N = \frac{H}{P}\cdot\frac{W}{P}

影像分詞後產生的圖塊數量。

我們一個符號一個符號地讀這條式子。N 是圖塊數量——也就是我們序列的長度。H 是影像高度(像素),W 是影像寬度(像素);這裡兩者都是 224。P 是圖塊的邊長(像素)——一塊方形小磚的大小——這裡是 16。H/P 算的是高度方向能放幾塊圖塊,W/P 算的是寬度方向能放幾塊;兩者相乘就得到網格的總格數。代入數字:N =(224/16)·(224/16)= 14·14 = 196,正好就是我們剛剛用手數出來的數。要注意這裡是整數除法——小磚必須剛剛好把整張影像鋪滿,所以邊長必須能被 P 整除(224 可以被 16 整除;像 225 這種尷尬的尺寸就不行,通常會先縮放或補邊處理)。

把圖塊變成 token:圖塊嵌入

現在我們有了一塊塊小磚,但小磚仍然只是一小方塊原始像素——而 Transformer 想要的,是讓每個 token 都是一個由數字組成的向量,就像語言模型想讓每個詞都是一個向量一樣。把一塊像素小磚轉換成這種向量的步驟,就叫做圖塊嵌入(patch embedding),它是讓影像分詞真正完成的那座橋。它分成兩個小動作:先把圖塊的像素攤平成一長串數字,再把這串數字透過一個可學習的矩陣投影成固定大小的向量。

你可以把圖塊嵌入想成:替每塊小磚發一張名牌——一個精簡的數字向量,用來概括那塊磚裡有什麼(一撮毛、一片天空、一個輪子的邊角)。這正是「詞嵌入」的視覺版本:在詞嵌入裡,「貓」這個詞會變成一個能捕捉它語意的向量。經過這一步之後,我們那 196 塊小磚每一塊都成了一張名牌,整張影像也就成了一個貨真價實的向量序列——一個 Transformer 讀得懂的句子。

z = x_p\,E + b

把一塊攤平後的圖塊投影到嵌入空間。

一個符號一個符號來看:x_p 是攤平後的圖塊——一個長度為 P·P·C 的列向量,其中 C 是色彩通道數(紅、綠、藍共 3 個)。以我們的 16×16 圖塊為例,就是 16·16·3 = 768 個原始數字(圖塊裡每個像素、每個顏色的亮度)。E 是可學習的投影矩陣,形狀為(P·P·C)×D,也就是 768×D,它把這串長 768 的原始數字映射成一個維度為所選嵌入維度 D 的向量。經典的 ViT 取 D = 768,所以一串 768 個輸入會被轉成一張 768 維的名牌(D 不一定要等於輸入大小——這裡的 768=768 只是個常見的巧合)。b 是一個長度為 D 的可選偏置向量,是加到每個嵌入上的一個小小固定位移。把 x_p 乘上 E 再加上 b,就得到 z,也就是最後的 token 向量。

import numpy as np

P, C, D = 16, 3, 768          # patch side, colour channels, embedding dim
N = (224 // P) * (224 // P)   # 14 * 14 = 196 patches

# patches[i] is one 16x16x3 tile; flatten each to length P*P*C = 768
patches = np.random.rand(N, P, P, C)
x = patches.reshape(N, P * P * C)   # shape: (196, 768)

# E is LEARNED during training, not hand-designed
E = np.random.randn(P * P * C, D)   # shape: (768, 768)
b = np.zeros(D)                     # bias, length 768

z = x @ E + b                       # shape: (196, 768) -> 196 token vectors
print(z.shape)                      # (196, 768)
從 196 塊原始像素小磚,到 196 個 token 向量——一次矩陣乘法搞定。

類別 token 與完整流程

還有一個 token 是我們用手加進去的——而且它完全不帶任何像素。在那 196 個圖塊 token 之前,我們會插入一個額外的、可學習的向量,叫做類別 token(class token)(常寫成 [CLS])。它不是影像的任何一塊;它是一個空白的欄位,數值在訓練過程中學出來。它的任務,是在所有圖塊彼此交談的過程中,從它們身上蒐集資訊,並在最後代表整張影像,供我們做出最終的預測。

打個比方:想像一位班代表(或會議記錄員)坐在一間擠滿圖塊的房間裡。這位記錄員一開始無話可說,但他聆聽每個人發言、吸收整場對話的要旨,最後交出一份摘要。接著我們只讀這位記錄員最後的那個向量,把它交給一個小型分類器,去判斷是「貓」還是「狗」。為什麼要特地用一個 token,而不是直接把所有圖塊平均起來呢?因為一個可學習的 token 可以靈活地決定要蒐集什麼、要向蒐集,而不是被迫對每一塊圖塊一視同仁地等權處理。

  1. 切塊:把輸入影像切成一個固定大小的方形圖塊網格(224×224、P=16 → 196 塊)。
  2. 嵌入:把每塊圖塊攤平,再透過可學習的矩陣 E 投影成一個 D 維的 token 向量。
  3. 插入類別 token:在最前面加入額外的可學習 [CLS] 向量,總共得到 197 個 token。
  4. 加上位置:替每個 token 加上一個位置訊號,讓模型知道每塊圖塊原本在網格中的位置。
  5. 編碼:讓所有 token 通過一疊相同的 Transformer 編碼器區塊,圖塊在其中反覆地彼此關注。
  6. 讀出與分類:取出最後的類別 token 向量,送進一個小型分類頭,預測出標籤。
ViT 的端到端流程:影像 → 圖塊 → 嵌入(+類別 token +位置)→ 編碼器堆疊 → 讀取類別 token → 類別標籤。

一張流程圖:影像被切成圖塊,每塊嵌入成向量,前面插入一個類別 token,加上位置訊號,序列通過一疊編碼器區塊,最後類別 token 送入分類器輸出一個標籤。

這就是整個視覺 Transformer,一口氣講完。你會發現,其中很大一部分,不過是我們在本篇建立起來的「庶務工作」——切塊、嵌入、插入、加位置。我們刻意沒打開的兩個盒子,才是真正魔法所在:圖塊是怎麼關注彼此的(注意力的核心,在第 2 篇),以及一個編碼器區塊如何把注意力與其餘元件疊在一起、並隨深度重複(編碼器,在第 3 篇)。眼下,要帶著走的那個關鍵想法,就是這個躍進本身:一張影像,被切成圖塊、貼上名牌之後,就只是一個序列——一個 Transformer 早已懂得如何閱讀的句子。

我們捨棄了什麼,又得到了什麼

每一種架構在還沒看到任何資料之前,就已經對這個世界做了一些假設,而這些內建的假設就叫做它的歸納偏置(inductive bias)。CNN 身上塞滿了很有用的歸納偏置。局部性:它假設彼此相鄰的像素是相關的,這正是為什麼它的濾波器永遠只看小鄰域。平移等變性:它假設一個物體無論出現在哪裡都代表同樣的意思,所以一個在左上角管用的貓偵測器,到了右下角一樣管用,因為是同一個濾波器在到處滑動。這些假設對自然影像來說大致為真,因此它們就像輔助輪——在網路還沒看過多少資料之前,就把它推向合理的解。

視覺 Transformer卻把這些輔助輪幾乎全拆掉了。它讓每塊圖塊從第一層就能關注其他每塊圖塊,幾乎沒有內建任何「相鄰圖塊比較重要」或「位置該以固定方式處理」的假設(它甚至得用手把位置訊號加回去,因為純粹的注意力會把圖塊當成一個無序的集合來看待)。這就是我們做的那筆交易:為了換取瞬間的全局視野與極大的彈性,ViT 必須從資料中學會局部性與平移結構,而不是免費就拿到它們。

用一個乾淨的方式來想像:CNN 是影像的專才,生來就對像素抱持著一些「正確的信念」,所以即使只有不多的資料也能高效地學習——但這些信念同時也把它框住了。ViT 則是通才,帶著極少的先驗信念前來,可以自由地發掘資料所揭示的任何結構——但因為無所依靠,它得看過遠遠更多的範例,原始的彈性才會轉化成真正的本領。一邊是更多的自由與瞬間的全局視野;另一邊則是對資料的強烈飢渴。