8.1 CLIP:對比式學習與零樣本遷移(2021)#
一、對比學習的思想源流#
在 2021 年之前,深度學習的視覺模型幾乎全部是單模態的:輸入一張圖,輸出標籤、框、遮罩。這種設定有一個根本限制——訓練時的標籤空間就是部署時的能力上限。要讓模型認識「斑馬」,就必須有人事先定義「斑馬」這個類別,並收集足夠的照片;模型不會因為看過幾萬張含斑馬的圖片,就學會辨認一種從未命名過的斑馬圖樣。
自然語言恰好相反:文字的類別空間是開放的,而且其中蘊含了豐富的語意關係(斑馬是一種有黑白條紋的、像馬的動物)。如果有一個函式能把圖片與描述該圖片的自然語言句子映射到同一個向量空間,那麼「視覺辨識」就不再受制於標籤集:任何可以用文字描述的概念,理論上都能被辨識。
CLIP(Contrastive Language-Image Pre-training,Radford et al., 2021)做的事情,本質上就是建立這個橋樑。但它的技術血統並不是「Transformer 跨界視覺」那樣簡單——它背後有一整條對比學習(contrastive learning)的技術線,而這條線的源頭要追溯到 2010 年的 Noise Contrastive Estimation。
1.1 Noise Contrastive Estimation(2010, Mnih & Teh)#
問題設定:給定一組資料 \(\{x_1, x_2, \dots, x_N\}\) ,我們想學到一個機率模型 \(p_\theta(x)\) 。如果直接用最大概似法,需要估計所有可能的 \(x\) 的機率(正規化常數 \(Z = \int p_{\text{data}}(x) \, dx\) ),這在高維連續空間中是無法計算的。
NCE 的思路:與其直接對資料的最大概似做最佳化,不如把「辨識真實樣本與虛構樣本」變成一個二分分類問題。
具體而言,我們從背景分布(background distribution) \(q(x)\) 中抽取虛假樣本,构造一個混合模型:
其中 \(N\) 是偽造樣本的數量。於是對數概似函數變成
在最佳解處,這個目標等價於讓 \(p_\theta\) 逼近真實分布,但不需要計算正規化常數——因為分類問題本身就把 \(Z\) 吸收進了分母的相對比較中。
關鍵直覺:NCE 把「密度估計(density estimation)」這個難題,轉換成「相對排序(relative ranking)」這個容易題。模型不需要知道「這張圖在所有圖中有多罕見」,只需要知道「這張圖比背景分布產生的圖更真實多少」。
這個思想在深度學習時代重新煥發光彩,原因是一個純粹的工程理由:GPU 的架構擅長的是 batch 內部的批次矩陣運算。NCE / InfoNCE 的「把正樣本與負樣本放進同一個 batch 一起比較」的形式,正好落在 GPU 最擅長的計算型態上。
1.2 InfoNCE(2018, Oord et al.):互資訊的下界#
InfoNCE 是 NCE 在連續表徵空間中的推廣,也是 CPC、SimCLR、MoCo、CLIP 全部共用的目標函數骨架。
設定:給定兩族變數 \(u\) (例如影像的表徵)與 \(v\) (例如文字的表徵)。我們有 \(N\) 對「正樣本」 \((u_i, v_i)\) ,以及抽樣自產品分布 \(p(u)p(v)\) 的「負樣本」。
InfoNCE 目標:
更常見的寫法是用打分函數 \(f_\theta(u,v)\) 與 softmax 分母中的負樣本集合 \(\{v_1, \dots, v_N\}\) 來寫:
其中 \(s(u, v)\) 是相似度函數(CLIP 中為餘弦相似度乘上可學習溫度)。
互資訊下界的證明(sketch)。定義互資訊
我們希望用一個只依賴 \(p(u,v)\) 的判別器(正樣本對 vs. 產品分布的負樣本對)來估計 \(I\) 。Posner et al. (2016) 證明了如下下界:
其中 \(D(u,v)\) 是判別器給「正樣本對」的機率。InfoNCE 正是取 \(D(u,v) \propto \exp(f_\theta(u,v))\) ,並把負樣本數 \(N\) 吸收進打分中的做法,因此
也就是
這裡有兩個極重要的實務推論:
- 下界是「越小越好」的:最小化 \(\mathcal{L}_{\text{InfoNCE}}\) 等價於最大化互資訊的下界估計。下界的差距(bound gap)取決於 \(f_\theta\) 的表達能力。
- **互資訊的估計有上限 \(\log N\) **:因為在 batch 內只有 \(N\) 個負樣本,模型最多只能把「正樣本」從 \(N\) 個候選中選出來。因此 batch size 直接決定了對比學習能達成的上限——這正是後面 SimCLR 與 MoCo 的分歧點,也是 CLIP 官方明確指出「large batch 是必要的(32k)」的原因。
常見誤解:為什麼不直接估計互資訊?
因為互資訊在高維連續空間中是不可估計的: \(I(u;v) = \mathbb{E}[\log p(v|u)]\) 需要知道真實的條件分布 \(p(v|u)\) ,這正是我們要學的東西——循環論證。而且互資訊在實務上存在嚴重的高估問題(overestimation):variational lower bound 的估計器很容易給出偏高的值,尤其當 \(u, v\) 共享大量低階特徵時(例如兩張圖都是藍色天空),模型會「作弊」地把無關的共現特徵當成互資訊的證據。
因此 InfoNCE 的下界形式不只是方便,它本身就是一種正則化:它強迫模型去找那些能讓正樣本在 \(N\) 個候選中被「挑出來」最具鑑別力的特徵,而非所有共有的特徵。
1.3 CPC(2018, Oord et al.):用上下文預測未來#
Contrastive Predictive Coding(CPC)由 Aäron van den Oord 與 Yaziel Dosovitskiy 等人提出,是通往 CLIP 的直線道路。它把 InfoNCE 用在「時序預測」上:
- Encoder \(g_\text{enc}(x_{t+k})\) 將過去的片段編碼成表徵 \(c_t = g_\text{enc}(x_{1:t})\) 。
- 從 \(c_t\) 隨機抽一個節點 \(z_t\) 作為 query。
- 用捲積網路 \(g_\text{enc}\) 把未來的片段 \(x_{t+1}, \dots, x_{t+K}\) 編碼成多個預測候選 \(c_{t+1}, \dots, c_{t+K}\) 。
- 用 InfoNCE 目標訓練:讓 \(z_t\) 與「正確的未來」 \(c_{t+k}\) 的相似度大於與其餘候選的相似度。
為什麼這條路徑特別有啟發性? 因為 CPC 把「無標籤的自監督學習」和「跨模態(audio-text)」結合起來:他們在 CPC 的目標上額外加入了文字模態,成功做到「在沒有對齊標註的情況下,把視覺與文字對齊」——這正是兩年後 CLIP 的核心想法,只不過 CLIP 把「預測未來片段」換成了「辨認同一張圖的標題」。
1.4 SimCLR(2020, Chen et al.):投影頭、溫度與批次大小#
SimCLR(SimCLR v1/v2,Google Brain)把對比學習推到影像自監督的主流地位。它的三個核心設計直接遺傳給 CLIP。
(一)NT-Xent 損失
給定一個 batch \(N\) 張圖,每張圖做兩種隨機增強得到 \(\tilde{x}_i^{(1)}\) 與 \(\tilde{x}_i^{(2)}\) ,總共 \(2N\) 個樣本。定義
其中正樣本對 \((i,j)\) 滿足 \(j = i \pm N\) (同一張圖的兩種增強),其餘 \(2N - 2\) 個都是負樣本。SimCLR 的完整損失為
其中 \(j(i)\) 是配對函數。
(二)projection head 為什麼重要
SimCLR 發現:對比學習應用在投影後的特徵 \(\mathbf{z} = h( g(x) )\) 上,而不是直接作用在 encoder 輸出 \(g(x)\) 上。理由是:encoder 的輸出需要保留對下游任務有用的全部資訊(例如邊緣、紋理、零件),而對比損失只需要模型關心「哪個樣本來自同一張圖」,這個特定的任務可能只需要某些子空間。把對比任務推到一個獨立的 MLP 頭(projection head)上,可以讓 encoder 保留更通用的特徵。
這個設計在 CLIP 中以另一種形式出現:CLIP 並不對比 encoder 的最終輸出,而是對比各自的投影後特徵(投影到同一個低維空間),但保留各自的 encoder 供其他用途(如文字編碼器也被單獨使用)。
(三)溫度 \(\tau\) 的作用
SimCLR 使用 \(\tau = 0.5\) (v2 用 0.1)。在 softmax 形式中,較小的 \(\tau\) 會銳化分佈,讓「最相似的那一個負樣本」受到更強的懲罰。等價地,可以把 \(\mathrm{sim}/\tau\) 理解為把 logits 乘上 \(1/\tau\) 。SimCLR 的消融實驗顯示: \(\tau\) 從 0.5 降到 0.1 帶來約 1–2 個百分點的 top-1 提升。
CLIP 與 ALIGN 使用可學習的對數溫度(initial value 為 1/100 或 1/60),讓模型自己在訓練中決定銳利程度。
(四)批次大小是關鍵(也是最昂貴的代價)
SimCLR 的消融結果非常明確:
| 批次大小 | 線性探針 top-1 (ImageNet) |
|---|---|
| 256 | 64.7% |
| 1024 | 69.8% |
| 4096 | 74.0% |
| 8192 | 76.1% |
原因:如 1.2 節所述,InfoNCE 的互資訊下界上限是 \(\log N\) 。負樣本數越多,「辨認出正確配對」這件事越難,模型被迫發掘更細緻的特徵。批次大小太小,模型可以用粗略的線索(例如整體色調)就完成辨識,於是學不到細粒度特徵。
早期如何處理這個瓶頸? 在 TPU 資源極為有限的 2019 年,SimCLR 團隊只能使用代理變數(proxy task)來間接取得大規模對比學習的效果,例如
- PACS(Photo, Art, Cartoon, Sketch):以「影像域」為標籤做分類,把域標籤當作對比學習的代理任務。理由是判斷「這兩張圖來自同一個域」需要學到形狀、紋理等風格不變特徵,與對比學習的目標同源。
- 低解析度:把影像縮小到 32×32 以加速 batch 的構造。
這些代理任務的效果遠不如直接的大規模對比學習,也留下了「如何在沒有大規模 TPU 的情況下做對比學習」的問題——這正是 MoCo 的切入點。
1.5 MoCo(2020, He et al.):動量對比與記憶庫#
Momentum Contrast(MoCo,Facebook AI Research)針對批次大小問題給出了另一個答案:用記憶庫(memory queue)代替大 batch。
核心機制:
- 動量更新(momentum update):encoder 分成兩份——query encoder 與 key encoder。Key encoder 的參數以指數移動平均更新:
這個設計來自 MoCo v2 的重要發現:用 SGD 動量更新 key encoder 會破壞一致性——因為 SGD 的更新步長會造成 query 與 key 參數快速不一致,而對比學習要求兩者表示同一個編碼器。指數移動平均保證 \(\theta_k\) 以極慢的速度跟隨 \(\theta_q\) ,使特徵空間保持穩定。
-
記憶庫(queue):維護一個大小為 \(K\) (如 65,536)的 FIFO 佇列,儲存過去 batch 產生的 key 特徵。當前 batch 的 key 被 push 進佇列,最舊的 pop 出。因此負樣本數量從 batch size 提升到 queue size(65k),而計算成本只增加了一點點。
-
不需大 batch 也能模擬大規模對比:理論上,queue 的負樣本數是「過時」的(stale),這是一個近似,但實驗證明非常有效。
MoCo vs SimCLR 的取捨:
| SimCLR | MoCo | |
|---|---|---|
| 負樣本來源 | 當前大 batch | 記憶庫(65k) |
| 計算成本 | 高(需要大 batch) | 低(batch 可小) |
| 記憶體需求 | 高 | 中(存的是特徵而非參數) |
| 需要的加速器 | 大規模 TPU 集群 | 單張 GPU 即可 |
| 效果(ImageNet 線性探針) | ~76.1% | ~74.6%(MoCo v2) |
對 CLIP 的啟示:CLIP 走了 SimCLR 的路線(大規模 batch),但把「同圖增強」換成了「跨模態配對」——這使得 batch 必須是成對的圖文資料,而非同一批影像。WebLI 這類大規模清洗過的圖文語料庫(2022, Google)正是這條路線的延伸,之後衍生出 BLIP-2(2023)與 FLAVA(2022)等「CLIP 的後裔」。
二、CLIP(Radford et al., 2021):雙塔架構#
2.1 雙塔(two-tower)架構#
CLIP 的架構非常對稱:影像與文字各自通過一個 encoder,映射到同一個低維空間。
mermaid
flowchart LR
A[Image x] --> B[Image Encoder: ResNet or ViT]
B --> C[Projection to embedding space]
C --> D[Image embedding I]
E[Text tokens] --> F[Text Encoder: Transformer 77 tokens]
F --> G[Projection to embedding space]
G --> H[Text embedding T]
D --> I[Cosine similarity matrix N x N]
H --> I
I --> J[Symmetric cross entropy loss]
Image Encoder:
- ViT 版本:把影像切成 \(32 \times 32\) 的 patch( \(224/32 = 7\) ),得到 49 個 token,加上 CLS token 經過 12 層 Transformer。CLIP 訓練了 ViT-B/32、ViT-B/16、ViT-L/14 三種。
- ResNet 版本:把 ResNet-50 與 ResNet-101 的最後一個 stage 改成注意力層(引入 "attention pooling" 取代 global average pooling),並把通道數加倍(ResNet-101 的 bottleneck 從 256 改成 512)以提升容量。
Text Encoder:Transformer,但改變了架構細節以服務對比學習:
| 組件 | 標準 Transformer | CLIP 的取捨 |
|---|---|---|
| Tokenizer | BPE(byte pair encoding) | 使用 BPE with a 49152- vocab(byte-level),較大的詞表讓每個 token 承載更多資訊 |
| Context length | 512 | 77(硬性截斷) |
| Positional embedding | 絕對 / 正弦 | ** Learned absolute positional embedding**,長度 77 |
| LayerNorm | 前置(pre-LN) | 後置(post-LN),配合最後一層的 LayerNorm(final layer norm),這是 ALIGN 的做法,實測在對比學習上更穩定 |
| 輸出 | 每個位置的 hidden state | 取 EOS token(序列最後一個非 padding token)的 hidden state 作為句子表徵 |
2.2 為什麼是 77 個 token?#
77 不是任意的數字,它是 ContextualCLIP(Sariyildiz et al., 2021)的上下文長度上限。ContextualCLIP 是 CLIP 的同時期工作(甚至更早投稿到 arXiv),它專門研究「CLIP 能處理多長的文字」,結論是因為 CLIP 使用 context length 77,超過就會被截斷。
而 77 這個數字的來源要更早:77 是 SentencePiece / BPE 在句子中最長 token 序列的統計長度上限。OpenAI 在 GPT-2 時代就用 1024,GloVe 時代用的是 300。
這個設計帶來長期的深遠影響,也是 3.4 節會詳述的「CLIP 的侷限」之一:
- 77 個 token 大約對應 40–60 個英文單詞,足以覆蓋絕大多數 caption,但無法容納長篇描述。
- 由於 attention 是因果遮罩(causal mask),句子的表徵只由 EOS token 承載——也就是說,一句話的語意必須被「壓縮」進單一位置。若文字太長,後半段被截斷,前面的內容可能就看不到。
- 所有後續的 SD 1.x 系統都沿用了 77 這個限制(直到 SDXL 才改成 77+77 的雙編碼器)。
2.3 CLIP 的對比損失#
定義。給定一個 batch,包含 \(N\) 對 (影像, 文字) 資料 \((x_i, y_i)\) , \(i = 1, \dots, N\) 。經過編碼與投影後,得到
其中 \(\mathrm{Norm}\) 是 L2 正規化, \(d\) 為嵌入維度(ViT-B/32 為 512,ViT-L/14 為 768)。相似度以餘弦相似度乘上可學習溫度計算:
令 \(\tau\) 為可學習的溫度參數(實作上用 \(\log \tau\) 參數化,初始值 \(\log(1/100)\) )。
核心技巧:把檢索問題變成分類問題。這是 CLIP 最重要的技術貢獻之一,值得單獨強調。
一個自然的想法是「把文字抽成一堆特徵,再用最近鄰搜尋」。但這需要一個明顯的決策邊界,而邊界從何而來?CLIP 的做法是:
在一個 batch 中,把「圖片找回自己的標題」與「圖片找回別人的標題」當作一個 \(N\) 類的分類問題,用交叉熵訓練。
具體而言,對第 \(i\) 張圖,正確標籤是 \(j = i\) 。損失函數為
對稱地,文字找回自己的圖片:
總損失為兩者平均:
用矩陣形式寫得更清楚。定義 logits 矩陣
則
其中 \(\mathrm{CE}\) 為交叉熵, \(\mathbf{1}\) 是全 1 的標籤向量(對角線為正)。
這個「檢索轉分類」的轉換帶來三個好處:
- 免費得到負樣本:batch 內所有非對角線元素自動成為負樣本,負樣本數 = batch size。這就是 1.4 節說的「批次大小決定互資訊上限」的來源。
- 免費得到梯度:交叉熵有閉式解的梯度(softmax 減 one-hot),實作簡單且數值穩定。
- 隱含的機率語意:softmax 之後的 \(p_{ij} = \exp(s_{ij}) / \sum_k \exp(s_{ik})\) 可以直接讀成「給定這張圖,它對應這段文字的機率」。這正是 2.6 節 zero-shot 分類的基礎。
雙塔架構的真正代價:因為正樣本對是「已知配對」,loss 對「哪個正樣本配哪個」沒有監督,同一 batch 內錯配(image i 配 text j, \(i \ne j\) )會被視為負樣本——這叫做 false negative。當語料中有大量語意重複的 caption(例如兩張圖都是「一隻狗在草地上跑」),對比損失會強行把兩個幾乎相同的語意推開,這是 CLIP 已知的一個訓練效率與語意一致性的折衷。
2.4 Zero-shot 分類:把類別搬到文字描述器#
這是 CLIP 最有影響力的能力,也是 3.1 節 BoW+SVM 的直接對照組。
問題設定:ImageNet 有 1000 個類別。若要訓練一個 1000 類分類器,需要標註資料。而 CLIP 的目標是:完全不使用 ImageNet 的訓練集標註。
做法:把類別名稱寫成自然句子,用文字編碼器取得它們的特徵,把這些特徵直接當作分類器的權重。
步驟一:Prompt 模板化。直接用類別名稱(如 "dog")效果很差,因為 CLIP 是在真實 caption 上訓練的,自然語言的分佈與單一名詞不同。CLIP 使用多個模板,例如:
a photo of a {c}.
a blurry photo of a {c}.
a black and white photo of a {c}.
a close-up photo of a {c}.
a photo of the large {c}.
a photo of the small {c}.
...
步驟二:Prompt ensembling(模板集成)。對一個類別 \(c\) ,把它的 \(80\) 個模板分別編碼,然後取平均再正規化,得到該類別的原型向量:
其中 \(\mathcal{P}\) 是模板集合。
步驟三:分類。對測試影像 \(x\) ,計算影像特徵 \(\mathbf{I}\) 與所有類別原型的相似度,取 argmax:
數學本質:這是一個最近原型分類器(nearest-prototype classifier),等價於第 3.1 節中以每類一個原型向量(centroid)做的分類器。差別在於:
| BoW + SVM(3.1 節) | CLIP zero-shot | |
|---|---|---|
| 原型的來源 | 從標註訓練集統計出來的視覺特徵統計量 | 從自然語言描述出來的文字特徵 |
| 分類器訓練 | 需要標註資料(SVM 訓練) | 不需要訓練,直接用預先算好的原型 |
| 類別空間 | 封閉(只能辨識訓練時定義的類別) | 開放(任何能用語言描述的概念) |
| 特徵維度 | SIFT 128 維稀疏 | 512/768 維稠密語意空間 |
為什麼這個對照極為重要? 因為它顯示了一種把監督負擔從標註轉移到自然語言先驗的新典範。以往我們要辨識新概念,必須收集樣本;現在只要寫一句話,模型就已經「見過」這個概念。
2.5 結果與影響#
ImageNet zero-shot 準確率:
| 模型 | 影像編碼器 | ImageNet zero-shot top-1 |
|---|---|---|
| ResNet-50 全量監督 | ResNet-50 | 76.1%(linear probe)/ 73.8%(full FT) |
| CLIP ViT-B/32 | ViT-B/32 | 75.5% |
| CLIP ViT-B/16 | ViT-B/16 | 76.2% |
| CLIP ViT-B/32(80 模板) | ViT-B/32 | 76.4% |
| CLIP ViT-L/14 | ViT-L/14 | 75.5%(不同 prompt 設定)/ 80.0%+(後續報告) |
(註:原始論文報告 ViT-B/32 為 75.5%,使用 80 個模板的 prompt ensembling 版本為 76.4%;ViT-L/14 在原始論文中為 75.5%,在後續 OpenAI 報告與其他評測基準上更高。本表列出的是論文的量級,用以說明:CLIP 的 zero-shot 表現可與 ResNet-50 的全量監督相當。)
線性探針(linear probe)的對照實驗:CLIP 論文裡最關鍵的一組對比是:
- zero-shot:完全不用 ImageNet 訓練集標註。
- linear probe:凍結 CLIP 的影像 encoder,只訓練一個線性分類器(linear layer)於 ImageNet 訓練集上。
- full fine-tune:完整微調。
結果:linear probe 的準確率大幅高於 zero-shot(ViT-B/32 約 83.1%),顯示 CLIP 的特徵「知道更多」但零樣本機制無法全部取出。這裡有一個必須說清楚的誤解:
常見誤解:「CLIP 的 zero-shot 已經達到人類水平,linear probe 只是錦上添花。」 事實上,zero-shot 與 linear probe 之間有 5–8 個百分點的差距。zero-shot 的瓶頸不在 encoder 的能力,而在於「類別名稱 + 模板」能否完整地表達該類別的視覺概念(例如 ImageNet 有 1000 個類別,包含許多以構圖或情境而非外觀定義的類別)。
零樣本遷移的廣度:CLIP 在 30+ 資料集上的 zero-shot 遷移,於大部分資料集上超過 ResNet-50 的全量監督 linear probe。這是 CLIP 影響力的關鍵:它的價值不在 ImageNet,而在跨越任務與領域的廣泛遷移——它讓「用自然語言描述任務」成為一種可行的定址方式。
規模化紅利:WebLI(2022, Google)
CLIP 的成功引發了一個直接的追問:把對比預訓練的資料量再放大一個數量級會怎樣?Google 的 WebLI(Web-scale Language-Image pretraining)用約 14 億(1.4B)圖文對(從網路挖掘並用 ALT / n-gram 過濾與平衡)重新訓練,得到 LiT(Language-Image Transformer)。LiT 在多數基準上大幅超越原始 CLIP,並帶出了一整條「CLIP 的後裔」:
- FLAVA(2022, Meta):以單塔架構在多個粒度(區域、影像、全域)上做對比學習,並加入文字-only 的 loss。
- BLIP / BLIP-2(2022–2023, Salesforce):BLIP 引入 Bootstrapping Latent Image-Text Pairs(用 captioner 生成合成 caption 擴充配對資料);BLIP-2 引入 Q-Former——一個輕量的可訓練查詢 Transformer,把凍結的影像 encoder 與凍結的 LLM 對接起來,參數量僅約 35M。這是通向第 9 章多模態模型的直接橋樑。
- SigLIP(2023, Google):改用 sigmoid loss(取代 softmax)來計算對比損失,處理大量負樣本時更有效率,並讓模型更早達到 batch size 擴展的飽和點。
多模態與教學(2022, OpenAI)
OpenAI 發布 Multimodal & Multitask Learning(模型名為 "CLIP-based multimodal model"),用 100 億(10B)對圖文資料訓練。其技術原理完全是 zero-shot 的延伸:
- 把每個類別/概念視為一段文字,得到該概念的文字特徵。
- 對輸入影像計算其影像特徵。
- 用 softmax 把分數轉成機率分布。
- 模型能線性組合多個概念:例如同時給予「一張照片」、「一個微笑」、「戴墨鏡」、「紅色」等多個文字描述,把它們的特徵平均後作為查詢方向,就能生成兼具這些屬性的輸出。
這個「以文字特徵的線性組合作為條件」的機制,正是下一節 8.2 中 cross-attention 作為文字注入方式的思想先驅。
2.6 CLIP 的可解釋性與可視化#
CLIP 論文裡最具影響力的定性結果之一是注意力圖(attention map)的可視化:取影像 encoder 中某個 attention head 的 \(7 \times 7\) patch 注意力圖,用上採樣放大後疊在原圖上,會看到模型在尋找與文字描述相關的區域。
例如查詢「a photo of a dog」,注意力會集中到狗的臉;查詢「a photo of a car」,注意力集中到車身;查詢「a photo of a flower」,集中到花心。
這個結果有雙重意義:
- 它是 CLIP 語意能力的直接證據:模型不是靠全局色彩統計作弊,而是真的在對象區域上建立語意對應。
- 它開啟了「零樣本分割」研究:沿著這條線,2021–2022 年出現了 DINO、MaskCLIP、GroupViT、PixelCLIP 等一系列「用 CLIP 的注意力圖做無監督分割」的工作——這是一個 beautiful 的副產品:對比學習不只學會了辨識,也學會了定位。
三、CLIP 的侷限與後續修正#
CLIP 的成功極為醒目,但它的失敗模式同樣鮮明。更重要的是,這些失敗模式正是通往多模態視覺語言模型(VLM)的鋪陳:當人們發現「把類別搬到文字描述器」有各種各樣的限制,就自然會想——如果文字描述器本身可以更豐富、更精確、可以隨輸入而改變呢?
3.1 批次偏誤(batch bias)#
Radford 等人在 CLIP 論文中報告了一類他們稱為 batch bias 的行為:CLIP 在給定一組候選答案時,傾向於選出「最像其中一個候選」的那個,而不是「唯一正確」的那個。
典型範例:給 CLIP 一張有斑馬與另一種動物(例如牛)的圖片,query 是「斑馬」。CLIP 有時會答「牛」,因為在這組候選裡,圖中的條紋更接近牛身上的某些花紋分佈。
機制解釋:CLIP 的 zero-shot 分類本質是「在候選集合中做相對排序」。它沒有被訓練來判斷「這個候選是否正確」,只被訓練來判斷「哪個候選與圖片最匹配」。當候選集合被替換(原本的 1000 類 ImageNet 變成使用者自訂的 5 個選項),排序的基準也隨之改變。這是一種語意漂移(semantic drift)。
後續修正(說明後續工作的方向):
- Calibration / Debiasing:在分類前對 logits 做先驗校正(如 subtracting the mean logit of each class over the test set),以去除類別先驗偏誤(prior bias)。
- Contextual Calibration(Zhao et al., 2021):計算每個類別在「無意義輸入」下的平均分數作為基線,再從實際分數中減去。
- 更根本的修正:改用「是/否」形式的驗證式 prompt——對每個候選分別問「這張圖是 {c} 嗎?」並取機率而非排序。這把「排序」還原成「二元判斷」,正是後來 VLM 的標準做法。
3.2 對抗性樣本脆弱#
Goodfellow et al.(2014) 的經典結果:對一個分類器,可以構造極小的擾動 \(\delta\) ( \(\| \delta \|_\infty \le \epsilon = 0.007\) ),使得 \(f(x + \delta) \ne f(x)\) ,而肉眼幾乎看不出差別。
CLIP 上的對抗脆弱性:CLIP 的 zero-shot 準確率雖高,但對抗攻擊的成功率同樣極高(研究報告顯示在 ImageNet 上 \(\epsilon = 4/255\) 的 PGD 攻擊可讓準確率從 75% 掉到接近 0%)。原因很直觀:
- CLIP 的決策依賴細微的、全域分佈的特徵統計,而非局部可解釋的物體部件。
- 對抗訓練(adversarial training)大幅降低準確率,說明 CLIP 並未學到「人本該有的穩健不變性」。
這裡的教訓:CLIP 學到的是語意等價類別(statistical regularity),而不是因果性的物件表徵。這解釋了為什麼後來的 VLM 需要結合文字指令、物件偵測、分割等更強的歸納偏置。
3.3 OCR 與細粒度文字失敗#
CLIP 的文字 encoder 使用的是低層級 tokenizer,且影像 encoder 以 32×32 的 patch 切塊(ViT-B/32),每個 patch 涵蓋 32×32 像素。對比之下,人類讀招牌上的字只需分辨少數像素的形狀。
實測結果:CLIP 在「讀出圖片中的文字」這類任務上表現極差——例如給它一張寫著 "STOP" 的路牌照片,它會回答「紅色標誌」而不是「STOP」。這是 CLIP 的一個著名弱點。
原因:
- 對比目標不鼓勵字元級精確度:loss 只要求區分「整段 caption 屬於哪張圖」,不需要分辨 caption 中每個字是否正確。
- 解析度不足:patch 大小 32px,遠大於一個字元的筆畫寬度。
- Tokenizer 的粒度:BPE 以 subword 為單位,不保證字形資訊。
對比參照:CLIP 之外,OCR 專用模型(CRNN、TrOCR)在讀字上遠勝 CLIP;而 2023 年之後的 VLM(GPT-4V、Gemini)在這方面大幅改善——但這是因為它們把 OCR 當成一種可調用的技能,而不是靠對比 loss 順帶學會。
3.4 概念幻覺與偏見#
CLIP 從網路資料學習,因此繼承了網路資料的偏見。已知問題包括:
- 刻板印象強化:對「醫生」查詢的 zero-shot 結果偏向男性形象;對「CEO」之類的職業也呈現性別偏斜。這不是隨機誤差,而是資料分布的鏡像。
- 有害內容的傳遞:CLIP 可以作為 CLIP-Ret 檢索系統的編碼器,而 CLIP-Ret 曾被用於生成歧視性或仇恨內容的搜尋結果。
- 概念混淆(concept conflation):文本編碼器有時會把「常見但不精確的概念」當作語意上等價,導致隱性偏見(例如把特定族群與特定職業關聯)。
這方面的理論框架:CLIP 學到的是「圖像中出現過的常見事物」的關聯。這意味著它的零樣本能力,本質上是對社會中存在的、可被文字描述的可見屬性的模型化。這一點在第 10.1 節(深偽與數位足跡)與第 10.2 節(資料偏見)會繼續展開。
3.5 長文字失敗#
77 token 的截斷有幾個層面的影響:
- 資訊直接遺失:句子後半段被丟棄,EOS token 的表徵看不到那些內容。
- EOS 表徵的瓶頸:整句話的語意必須壓縮進單一位置向量——這是一個資訊瓶頸,尤其對複合敘述不利。
- 對 VLM 的啟示:這正是 GPT-4V / LLaVA 等系統不使用 EOS 單一 token、而是把整個 token 序列交給 LLM 的原因之一。CLIP 的架構限制,後來被更靈活的架構取代。
四、零樣本不等於「沒有學習」#
這是一個需要明確澄清的誤解,也是理解第 9 章的關鍵。
Zero-shot 的準確語意:沒有使用目標任務的標註資料進行訓練。
它並不意味著:
- 沒有使用任何資料(事實上用了 4 億圖文對);
- 沒有學習任何參數(事實上數億參數全部學好了);
- 沒有使用標籤(使用的是 caption,而非類別標籤——但 caption 本身就是人類提供的監督訊號)。
更精確的描述:Zero-shot 把「類別」這個資訊從權重搬到文字描述器。原本在 BoW+SVM 的架構中,類別原型是從訓練影像統計出來的;現在原型是由自然語言的先驗(語意空間的結構)提供的。
因此我們可以說:
這個分解式有兩個實踐意義:
- 改進 prompt 就能改進準確率——因為改進的是「文字描述器的先驗品質」這一項。這解釋了為什麼 prompt engineering 在 CLIP 時代如此重要。
- 它為 in-context learning 鋪墊。如果類別可以由「當下的自然語言描述」決定,那麼在第 9 章,模型能否根據「上下文中的圖片 + 提問」即時決定要抽取什麼概念?答案是肯定的——這正是多模態 in-context learning 的起點。
五、實作:PyTorch 最小 CLIP#
以下實作包含三個部分:一個小型的對比模型、對比損失、以及 zero-shot 分類器。
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------- 1. 影像編碼器(此處用小型 CNN 示範,實務上換成 ViT) ----------
class TinyImageEncoder(nn.Module):
def __init__(self, embed_dim: int = 256):
super().__init__()
self.backbone = nn.Sequential(
nn.Conv2d(3, 32, 3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
)
# projection head:對比作用在投影後的特徵上(SimCLR 的教訓)
self.projection = nn.Sequential(
nn.Linear(128, 256), nn.GELU(), nn.Linear(256, embed_dim)
)
def forward(self, x):
h = self.backbone(x).flatten(1)
z = self.projection(h)
return F.normalize(z, dim=-1) # L2 正規化 -> 餘弦相似度
# ---------- 2. 文字編碼器(簡化版 Transformer encoder) ----------
class TinyTextEncoder(nn.Module):
def __init__(self, vocab_size=10000, context_length=32, embed_dim=256,
n_head=4, n_layer=2):
super().__init__()
self.context_length = context_length
self.token_embed = nn.Embedding(vocab_size, embed_dim)
self.pos_embed = nn.Embedding(context_length, embed_dim)
layer = nn.TransformerEncoderLayer(
d_model=embed_dim, nhead=n_head, dim_feedforward=embed_dim * 4,
batch_first=True, norm_first=False) # post-LN,CLIP 的做法
self.transformer = nn.TransformerEncoder(layer, num_layers=n_layer)
self.final_norm = nn.LayerNorm(embed_dim)
self.projection = nn.Linear(embed_dim, embed_dim)
def forward(self, tokens):
B = tokens.size(0)
pos = torch.arange(self.context_length, device=tokens.device).unsqueeze(0).expand(B, -1)
h = self.token_embed(tokens) + self.pos_embed(pos)
causal = torch.triu(torch.ones(self.context_length, self.context_length,
device=tokens.device), diagonal=1).bool()
h = self.transformer(h, mask=causal)
h = self.final_norm(h)
# 取 EOS token(序列中最後一個非 padding token)的隱層作為句子表徵
lengths = (tokens != 0).sum(dim=1) - 1
eos = h[torch.arange(B, device=tokens.device), lengths.clamp(min=0)]
z = self.projection(eos)
return F.normalize(z, dim=-1)
# ---------- 3. 對比損失:把「檢索」變成分類 ----------
def clip_contrastive_loss(image_emb: torch.Tensor, text_emb: torch.Tensor,
logit_scale: torch.Tensor) -> torch.Tensor:
"""
image_emb, text_emb: (N, d),已 L2 正規化
logit_scale: 可學習的 log(1/tau),需要 clamp 以避免數值爆炸
"""
scale = logit_scale.exp().clamp(max=100.0)
logits = scale * image_emb @ text_emb.t() # (N, N)
labels = torch.arange(logits.size(0), device=logits.device)
loss_img = F.cross_entropy(logits, labels) # 圖找回自己的文字
loss_txt = F.cross_entropy(logits.t(), labels) # 文字找回自己的圖
return (loss_img + loss_txt) / 2
# ---------- 4. 訓練迴圈骨架 ----------
def train_step(model, image_batch, text_batch, optimizer):
logit_scale = model.logit_scale
optimizer.zero_grad()
z_img = model.image_encoder(image_batch)
z_txt = model.text_encoder(text_batch)
loss = clip_contrastive_loss(z_img, z_txt, logit_scale)
loss.backward()
optimizer.step()
return loss.item()
# ---------- 5. Zero-shot 分類:把類別搬到文字描述器 ----------
@torch.no_grad()
def zero_shot_classifier(text_encoder, classnames, templates, device,
context_length=32):
"""
classnames: list[str],如 ['cat', 'dog', 'car']
templates : list[str],如 ['a photo of a {}.', 'a blurry photo of a {}.']
回傳 (embed_dim, n_classes) 的矩陣,每一欄是一個類別的原型向量。
"""
prototypes = []
for cname in classnames:
embeddings = []
for tmpl in templates:
prompt = tmpl.format(cname)
tokens = tokenizer(prompt, context_length).unsqueeze(0).to(device)
embeddings.append(text_encoder(tokens)[0])
proto = torch.stack(embeddings).mean(dim=0)
prototypes.append(F.normalize(proto, dim=-1))
return torch.stack(prototypes, dim=1) # (d, n_classes)
@torch.no_grad()
def predict(image_encoder, image, class_prototypes):
"""分類 = 影像特徵與類別原型的最近鄰搜尋(argmax 餘弦相似度)"""
z = image_encoder(image) # (B, d)
logits = z @ class_prototypes # (B, n_classes)
probs = logits.softmax(dim=-1) # softmax -> 可解讀為機率
return probs.argmax(dim=-1), probs
要點回顧:
F.normalize之後,內積即餘弦相似度;logit_scale對應可學習溫度。logits的第 \(i\) 列是「圖 \(i\) 對所有文字」的機率分布——這就是「每張圖在所有文字中找回自己的標題」。zero_shot_classifier的 prototype 平均正是 prompt ensembling;predict中 softmax 後的分數可直接當作信心度。- 實務上必須使用夠大的 batch(數千以上)才能讓對比學習有效,這正是 CLIP 使用 32k batch 的原因。
六、小結:本節在整本書因果鏈中的位置#
mermaid
flowchart TD
A[對比學習: NCE to InfoNCE] --> B[SimCLR / MoCo: 批次大小與負樣本]
B --> C[CLIP: 跨模態雙塔對比]
C --> D[零樣本分類: 類別變成文字描述]
D --> E[WebLI / LiT / BLIP-2: 規模化紅利]
D --> F[下一節 8.2: 把文字接到擴散模型]
D --> G[第九章: VLM 的 in-context 能力]
C --> H[侷限: 批次偏誤 / 對抗脆弱 / OCR / 偏見]
H --> G
三個因果環節必須記牢:
- 對比學習提供了語義橋樑。沒有 InfoNCE 這種「把配對問題當成分類問題」的形式,就沒有 CLIP 的檢索即分類技巧,也沒有之後把文字條件塞進擴散模型的基礎(第 8.2 節)。
- CLIP 讓「類別」變成「語句」,從而使 zero-shot 成為可能——這是第 3.1 節 BoW+SVM 的自然 generalizations,也是之後所有 prompt-based 工作(含 8.3 節的 ControlNet 使用者生態)的基礎設施。
- CLIP 的失敗模式定義了下一代的問題:批次偏誤指出「排序不等於判斷」,對抗脆弱指出「統計規律不等於因果表徵」,77-token 限制指出「單一 EOS 向量是瓶頸」——這三條線分別通向第 9 章的 VLM 指令遵循、多模態穩健性,以及真正的序列級多模態對接。
下一節,我們把這座橋樑架到生成的一端:文字不只是一個查詢向量,而是變成能「把輪廓畫進去」的條件訊號。