向整個網路學習,不需人工標註
想像你要教一個模型認出照片裡成千上萬種不同的東西。老方法很痛苦:雇人在數百萬張影像上一個類別一個類別地畫框、打標籤。這既慢又貴,而且被鎖死在一份固定的類別清單上。但有一個悄悄改變一切的訣竅——網際網路早就替我們標好了。網頁上的每張照片旁邊通常都有描述性文字:alt 文字屬性、圖說、商品標題、一則推文。爬取網路,你就幾乎免費地收割到數億組(影像、文字)配對。沒有人需要坐下來逐一標註;這份配對本來就內建在人們上傳影像的方式裡。
在上一篇我們認識了一個核心概念——共享嵌入空間:一個單一的座標系統,能讓一張狗的圖片和「一隻狗」這幾個字落在彼此附近,於是任何影像與任何文字之間的距離都具有意義。我們當時把它當成一個承諾留著。這篇就是我們真正動手打造那個空間的地方。這套做法叫做 對比式影像-文字預訓練,而讓它廣為人知的著名模型就是 CLIP(Contrastive Language–Image Pre-training,對比式語言-影像預訓練)。你可以把 CLIP 想成一台具體的機器,它吃進那些免費的網路配對,把視覺和語言摺疊進同一個空間裡。
一張示意圖,將影像嵌入與文字嵌入畫成點;箭頭把正確配對拉在一起、把錯誤配對推開。
模型到底在學什麼樣的功課?一句話講完:在一個批次(batch)的影像-圖說配對中,學會哪一段圖說配哪一張影像。這就是全部的監督訊號——沒有類別標籤、沒有邊界框,只有「這兩個是一對、那兩個不是」。因為模型必須把配對和非配對區分開來,這套做法才叫對比式(contrastive):它透過把正確的配對和許多錯誤的配對相互對照來學習。接下來兩節會把這個一句話的想法變成一個架構,再變成精確的數學。
兩個編碼器,一個共享空間
CLIP 由兩座彼此獨立、永不共享權重的高塔組成。第一座是影像編碼器:一個讀進像素、為整張圖片輸出單一摘要向量的網路。在 CLIP 裡,它可以是 Vision Transformer(也就是把影像切成小塊 patch、再對這些 patch 做注意力的 ViT,我們在先前的視覺學程已經見過),也可以是 ResNet(經典的卷積骨幹網路)。第二座是文字編碼器:一個 Transformer,逐 token 讀入圖說,再為整句話輸出單一摘要向量。於是一張圖片變成一個向量,一句話也變成一個向量——這是兩台不同機器各自吐出的兩個輸出。
一張被切成方格 patch 的影像,餵入一個 Transformer,輸出單一向量。
不過這裡有個問題:影像編碼器和文字編碼器天生會輸出大小不同、活在不同座標系統裡的向量——你沒辦法直接比較它們。所以 CLIP 在每座塔上各裝一個小小的線性投影(linear projection),把它的輸出映射到同一個維度,比方說共同長度 512。現在影像向量和文字向量都是同一個 512 維空間裡的點了。最後一步是 L2 正規化:把每個向量除以它自己的長度,讓每個向量都剛好落在單位球面上(長度=1)。做完這步之後,比較一張影像和一段圖說,就只是在量兩支單位箭頭之間的夾角而已。
影像 i 與圖說 j 之間的正規化相似度。
我們逐個符號來讀。\mathbf{img}_i 是批次中第 i 張影像的嵌入向量,\mathbf{txt}_j 是第 j 段圖說的嵌入。雙豎線 \lVert \cdot \rVert 代表「這個向量的長度(範數)」,所以把一個向量除以它自己的範數,就會把它縮放成長度剛好為 1——這就是正規化步驟。中間的點 \cdot 是內積(dot product)。關鍵事實在這裡:兩個單位長度向量的內積,恰好就是它們的餘弦相似度——一個從 −1(指向相反)、經過 0(互相垂直、毫不相關)、到 +1(指向相同、完美相配)的數字。所以 s_{ij} 就是一個分數,告訴你影像 i 跟圖說 j 配得有多好。如果你把全部分數排成一個方格,對角線上的 s_{ii} 就是真正的配對(影像 i 配它自己的圖說 i),對角線以外的全是不相配。為什麼要先正規化?因為若不這麼做,一個向量可能光靠「夠長」就拿到高分,而不是靠指對方向——正規化把長度抹掉,讓只有方向(也就是語意)算數,使每一次比較都站在同一個基準上。這個分數方格,也就是每批次的相似度矩陣,正是下一節要拿來最佳化的對象。
對比式目標函數,一步一步拆解
現在來到 CLIP 的核心。拿一個批次的 N 組影像-圖說配對。把全部 N 張影像和全部 N 段圖說都編碼起來,再用上一節的方式算出每一對的分數,填滿一個 N×N 的相似度矩陣。對角線上的 N 個項目是正確配對;對角線以外那 N²−N 個項目,每一個都是干擾項(distractor)——也就是屬於別張影像的圖說。訓練目標,直覺上說,就是讓每個對角線分數在它自己那一列和那一行裡都最亮,同時把周圍所有的干擾項調暗。
聰明的一招,是把這件事重新表述成一個我們熟悉的分類問題——而且做兩次。看矩陣的某一列:它裝著影像 i 對全部 N 段圖說的分數。問一句:「這 N 段圖說裡,哪一段配這張影像?」正確答案是圖說 i。這恰恰就是一個 N 選一的分類問題,而我們早就知道怎麼用 softmax +交叉熵去訓練它。接著對每一行做同樣的事:「這 N 張影像裡,哪一張配這段圖說?」兩個方向——影像→文字和文字→影像——我們只要把它們的損失平均起來就好。正是這份對稱性,才讓它被稱為對稱式對比損失。
一個 N×N 的相似度分數方格,對角線被標示為列與行兩個方向的正確答案。
對稱式 InfoNCE 損失:影像→文字 加上 文字→影像,再取平均。
我們逐塊拆開。求和裡面的 s_{ii} 是對角線分數——也就是樣本 i 的正確影像-圖說配對——而分母裡的那些 s_{ij} 跑遍全部 N 段圖說,包含那一個正確的加上 N−1 個干擾項。符號 \tau(tau)是溫度(temperature),一個小小的正數,我們在取指數之前先把分數除以它。那個分式 \exp(s_{ii}/\tau)\big/\sum_j \exp(s_{ij}/\tau) 其實就是一個 softmax:它把這一列的原始分數變成 N 個候選上的機率分布,而我們讀出模型分配給正確圖說的那個機率。對這個機率取 -\log 就是普通的交叉熵:如果模型給正確配對機率 1,損失就是 -\log 1 = 0(完美);而當機率往 0 掉,損失就會飆高,懲罰模型。前面的 -\frac{1}{N}\sum_i 把這件事在整個批次上取平均。\mathcal{L}_{\text{t2i}} 是一模一樣的公式,只是改成沿著行而非橫著列來讀,最後的 \mathcal{L} 把兩個方向平均起來。溫度控制的是銳利度:較小的 \tau 等於除以較小的數,會把分數差距放大,讓 softmax 更尖、更自信;較大的 \tau 則把它壓平、趨向亂猜。CLIP 實際上會在訓練中學出 \tau。這整套構造就叫做 InfoNCE 損失。
我們來把一個 N=3 的極小具體例子一步步算到底,好讓算術看得見。假設正規化並做完內積之後,我們得到下面這個 3×3 相似度矩陣(列=影像,行=圖說),溫度設為 τ=0.5:
# similarity matrix s[i][j] (diagonal = correct pairs) # cap1 cap2 cap3 # image1 [ 0.90 0.20 0.10 ] # image2 [ 0.30 0.80 0.20 ] # image3 [ 0.10 0.25 0.85 ] # temperature tau = 0.5 (so dividing by tau means multiplying by 2)
- 取第 1 列(影像 1 的分數):[0.90, 0.20, 0.10]。每個都除以 τ=0.5 →〔1.80, 0.40, 0.20〕。
- 各取指數:exp(1.80)=6.05、exp(0.40)=1.49、exp(0.20)=1.22。
- 把分母加總:6.05 + 1.49 + 1.22 = 8.76。
- 正確(對角線)圖說的 softmax 機率:6.05 / 8.76 = 0.690。模型認為圖說 1 配影像 1 的機率有 69%。
- 這一列的損失:−log(0.690) = 0.371。若是完美的 1.0 會得到 0;這一列因為不夠自信而被罰了 0.371。
# CLIP's symmetric loss in PyTorch-style pseudocode # image_features: [N, d] text_features: [N, d] img = l2_normalize(image_features, axis=1) # unit-length image embeddings txt = l2_normalize(text_features, axis=1) # unit-length text embeddings logits = (img @ txt.T) / tau # N x N similarity / temperature labels = arange(N) # correct pairs lie on the diagonal loss_i2t = cross_entropy(logits, labels, axis=1) # each row -> its caption loss_t2i = cross_entropy(logits, labels, axis=0) # each col -> its image loss = (loss_i2t + loss_t2i) / 2 # symmetric average
零樣本魔法:分類從沒見過的類別
讓 CLIP 一舉成名的回報就在這裡。一旦訓練完成,你可以叫它把影像分類進它從沒被明確訓練過的類別——而且不需要任何額外微調。這個訣竅把分類重新框成配對。要辨認一張照片,先替每個候選類別寫一句簡短的文字提示,例如「a photo of a cat(一張貓的照片)」、「a photo of a dog」、「a photo of a car」。用文字編碼器把這些提示全部嵌入,用影像編碼器把影像嵌入,然後就挑出嵌入最靠近影像的那段圖說。勝出圖說的類別就是你的預測。這正是 零樣本影像分類。
一個影像向量和三個標籤提示向量相比,由 softmax 選出最靠近的那個。
對候選類別做 softmax,再挑出機率最高的。
逐個符號讀:\mathbf{txt}_c 是候選類別 c 的提示「a photo of a {c}」的文字嵌入(所以 \mathbf{txt}_{\text{cat}} 就是「a photo of a cat」的嵌入)。\operatorname{sim}(\mathbf{img}, \mathbf{txt}_c) 就是第 2 節那個一模一樣的餘弦相似度——影像對那一個標籤。\tau 又是溫度。那個分式再一次是 softmax,只是這回它只跑遍你的候選類別,把相似度變成加總為 1 的機率。最後 \arg\max_c 就是「回傳機率最高的那個類別 c」——回傳的是標籤,不是數字。這招之所以能用在 CLIP 從沒當成標籤看過的類別上,是因為共享空間早就懂得這些字:它在預訓練時學會了「cat」這個字的嵌入落在哪裡、附近的貓照片長什麼樣,所以即使是「axolotl(六角恐龍)」這種全新標籤也能落在合理的位置,因為文字編碼器從網路上認得這個字。
我們拿那張貓照片,用 3 個候選標籤、τ=0.5 跑一遍。假設算出的餘弦相似度是 sim(img,「cat」)=0.40、sim(img,「dog」)=0.15、sim(img,「car」)=0.05。除以 τ:〔0.80, 0.30, 0.10〕。取指數:〔2.23, 1.35, 1.11〕,加總為 4.68。機率是貓 2.23/4.68=0.476、狗 1.35/4.68=0.288、車 1.11/4.68=0.236。argmax 是 cat(貓),47.6%——正確,而且完全沒有針對「貓 vs 狗 vs 車」這個任務做過任何訓練。(真正的 CLIP 用的是學出來、小得多的溫度,那會把 0.476 推得更靠近 1,也就是自信得多。)
# Zero-shot classification with CLIP
classes = ["cat", "dog", "car"]
prompts = [f"a photo of a {c}" for c in classes]
txt = l2_normalize(text_encoder(prompts)) # [num_classes, d]
img = l2_normalize(image_encoder(image)) # [d]
sims = img @ txt.T # cosine sim to each class
probs = softmax(sims / tau)
pred = classes[argmax(probs)] # -> "cat"檢索:用文字搜尋圖片
完全相同的機制也能給你搜尋功能。在 影像-文字檢索 中,你有一個龐大的影像圖庫,你想找出和某段文字查詢相配的那些影像,例如「一隻黃金獵犬在雪地裡玩耍」。用文字編碼器把那段查詢嵌入一次,得到一個向量,再把它和每一張影像的嵌入相比,回傳餘弦相似度最高的那些影像。不需要任何新東西——「找出最相配的」其實就是「挑出餘弦相似度最高的」,正是 CLIP 被訓練去做的同一個比較。
一張精確率-召回率示意圖,呈現在前段結果中檢索到多少相關項目。
關鍵在於這是雙向的。文字→影像給你「打一段描述就能搜尋整個照片庫」。影像→文字則反過來:把一張影像交給模型,讓它在一堆圖說裡排名,找出最能描述它的那一段。兩個方向都是同一個內積比較,差別只在於決定哪一邊是固定的查詢、哪一邊是圖庫。標準的計分卡是 recall@K(前 K 召回率):在所有真正與某查詢相配的影像中,有多少比例出現在回傳的前 K 名結果裡?recall@1 很嚴格(最前面那一筆就得對);recall@5 或 recall@10 比較寬容,也更貼近人們實際瀏覽一頁結果的方式。
為什麼雙編碼器設計在實際部署上是這麼大的禮物?因為影像端和文字端是各自獨立編碼的,你可以把圖庫裡的每張影像只嵌入一次、離線完成,把那些向量存進資料庫。到了查詢時,你只需要編碼那段短短的文字、再做快速的向量比較——你永遠不必重跑那台笨重的影像編碼器。正是這個「只算一次」的特性,才讓 CLIP 這類模型能驅動真正的搜尋系統。
強項、限制,以及 CLIP 做不到的事
該來一場誠實的盤點了,因為 CLIP 的限制,恰恰就是驅動這個學程其餘部分的動力。它的超能力是全域的影像-文字配對:「這張圖大致在講什麼?」它快、可擴展、廣得驚人。但讓它快的那個設計——把每張影像、每段圖說都壓成單一向量——同時也是它的天花板。單一向量是一份摘要,而摘要會丟掉細節。所以只要答案取決於摘要捨棄掉的細節,CLIP 就會穩定地表現不佳。
同樣的盲點出現在一整族任務裡。計數:CLIP 很難分辨「三隻狗」和「五隻狗」——全域向量幾乎沒把確切數量編進去。空間關係:「在筆電左邊的杯子」對上「右邊的」常常像在擲硬幣。細粒度屬性:要分辨親緣相近的鳥種或細微的紋理,超出了單一向量能乾淨捕捉的範圍。讀出影像裡的文字(類似 OCR):CLIP 抓得到一個招牌的大意,卻無法可靠地讀出上面的字。這些都不是可以打補丁修掉的臭蟲;它們是「單一向量、全域配對」設計的自然後果。
還有一個誠實的警告:CLIP 從原始網路資料學習,所以它繼承了網路的偏見——人們替影像下圖說時的刻板印象、對不同文化與群體覆蓋的不均、以及偏斜的關聯。因為沒有人篩選過那數億組配對,模型會悄悄吸收網路恰好含有的任何相關性。負責任地使用 CLIP,意味著要去檢測這些偏見,而不是假設一個用「所有東西」訓練出來的模型就因此中立。
每一個限制,都是通往後續各篇的一道門。要正確地綁定屬性與關係,視覺和語言必須深度互動,而不只是在最後的內積處相遇一次——那就是第 3 篇的跨模態注意力與融合。要回答「紅色立方體到底在哪裡?」模型必須指向區域,而不只是替整張圖貼標籤——那就是第 4 篇的接地(grounding),它建立在我們稍早預告過的開放詞彙想法之上。而要真正推理——計數、比較、解釋、聽從關於一張影像的指令——我們得把視覺接到一個能一步步思考的語言模型上,也就是第 5 篇的多模態大型語言模型(multimodal LLMs)。CLIP 是這一切站立其上的地基;同時理解它的威力與它的單一向量天花板,正是讓你做好準備迎接接下來內容的關鍵。