Bỏ qua để đến nội dung

Ảnh + văn bản (1): CLIP, BLIP, GLIP

Sau bài này, bạn sẽ:

  • Hiểu học đối chiếu (contrastive learning) giữa ảnh và chữ: hai bộ mã hóa, ma trận độ tương đồng N×NN \times N, đường chéo là các cặp đúng.
  • Tự dẫn được hàm mất mát InfoNCE đối xứng của CLIP, và giải thích được vai trò của nhiệt độ (temperature) τ\tau.
  • Dùng được CLIP để phân loại zero-shot (không cần ảnh có nhãn) bằng prompt "a photo of a {label}", và biết vì sao gộp nhiều prompt giúp ích.
  • Phân biệt được CLIP, BLIP và GLIP: mỗi mô hình học gì, cho ra gì, dùng cho việc gì; nhận ra các giới hạn đã biết của CLIP (đếm, quan hệ vị trí, chữ trong ảnh).

Bộ phân loại bị khóa vào danh sách nhãn

Phần tiêu đề “Bộ phân loại bị khóa vào danh sách nhãn”

Một mô hình phân loại ảnh học trên ImageNet có đúng 1 000 đầu ra. Nó biết “golden retriever” và “tabby cat”. Nhưng bạn hỏi “ảnh này có phải xe đạp gấp không?” thì nó chịu: không có đầu ra nào cho nhãn đó. Muốn thêm nhãn, bạn phải gom ảnh, gán nhãn, rồi huấn luyện lại.

Trong khi đó, Internet có hàng trăm triệu ảnh đã có chữ đi kèm: chú thích, alt-text, tiêu đề bài viết. Chữ đó lộn xộn, nhưng mô tả nội dung ảnh bằng ngôn ngữ tự do, không bị khóa vào một danh sách.

Hãy tưởng tượng một trò chơi. Bạn có một xấp 4 tấm ảnh và 4 mẩu chú thích, bị xáo trộn. Việc của bạn: ghép mỗi ảnh với đúng chú thích của nó.

Để chơi giỏi, bạn không cần biết “nhãn lớp” nào. Bạn chỉ cần hiểu ảnh và hiểu chữ đủ để thấy chúng nói cùng một chuyện. Chơi trò này với 32 768 ảnh và 32 768 chú thích mỗi lượt, hàng tỉ lượt, bạn sẽ học được rất nhiều khái niệm về thế giới.

Khi đã chơi giỏi, phân loại trở thành một ván ghép đôi: viết mỗi nhãn thành một câu (“a photo of a folding bike”, “a photo of a cat”), rồi xem ảnh ghép với câu nào nhất. Muốn thêm nhãn mới thì chỉ cần viết thêm một câu.

Ghép đôi mới chỉ trả lời “ảnh và câu có khớp không”. Hai việc khác còn bỏ ngỏ: viết câu mô tả cho ảnh (không chỉ chọn câu có sẵn), và chỉ ra vật nằm ở đâu trong ảnh. BLIP lo việc thứ nhất, GLIP lo việc thứ hai.

Bài toán của bài này là: làm sao học từ các cặp ảnh và chữ trên mạng một không gian chung, trong đó ảnh và câu cùng nghĩa nằm gần nhau, rồi dùng nó để phân loại theo nhãn tùy ý, viết chú thích, và tìm vật theo cụm từ?

Một lô (batch) có NN cặp (ảnh, chú thích). Ở bài này NN là số cặp trong lô, không phải số patch như bài ViT. Bộ mã hóa ảnh ff (một ViT hoặc ResNet) và bộ mã hóa chữ gg (một Transformer) đưa ảnh thứ ii và chú thích thứ jj về hai vectơ cùng số chiều DeD_e, gọi là không gian embedding chung (joint embedding space). Chia mỗi vectơ cho độ dài của nó, ta được:

ui=f(ảnhi)∥f(ảnhi)∥,vj=g(chữj)∥g(chữj)∥\mathbf{u}_i = \frac{f(\text{ảnh}_i)}{\lVert f(\text{ảnh}_i) \rVert}, \qquad \mathbf{v}_j = \frac{g(\text{chữ}_j)}{\lVert g(\text{chữ}_j) \rVert}

Vì ui\mathbf{u}_i và vj\mathbf{v}_j đều dài 1, tích vô hướng của chúng chính là độ tương tự cosine (cosine similarity), nằm trong [−1,1][-1, 1].

Bước 1: ma trận độ tương đồng N×NN \times N

Phần tiêu đề “Bước 1: ma trận độ tương đồng N×NN \times NN×N”

Xếp mọi tích vô hướng thành một ma trận S∈RN×N\mathbf{S} \in \mathbb{R}^{N \times N}:

Sij=ui⊤vjS_{ij} = \mathbf{u}_i^\top \mathbf{v}_j

Hàng ii là ảnh ii so với mọi chú thích; cột jj là chú thích jj so với mọi ảnh. Đường chéo SiiS_{ii} là NN cặp đúng; N2−NN^2 - N ô còn lại là các cặp ghép sai. Ta không cần ai gán nhãn: chính cách lô được tạo ra cho biết đáp án.

Bước 2: mỗi hàng là một bài phân loại NN lớp

Phần tiêu đề “Bước 2: mỗi hàng là một bài phân loại NNN lớp”

Nhìn hàng ii: “trong NN chú thích, cái nào thuộc về ảnh ii?”. Đó là phân loại NN lớp với đáp án là cột ii. Ta đổi điểm thành xác suất bằng softmax, sau khi chia cho nhiệt độ τ>0\tau > 0:

pij=exp⁡(Sij/τ)∑k=1Nexp⁡(Sik/τ)p_{ij} = \frac{\exp(S_{ij} / \tau)}{\sum_{k=1}^{N} \exp(S_{ik} / \tau)}

Mất mát của hàng ii là cross-entropy với đáp án là cột ii, tức −ln⁡-\ln của xác suất gán cho cặp đúng:

Liảnh→chữ=−ln⁡pii=−ln⁡exp⁡(Sii/τ)∑k=1Nexp⁡(Sik/τ)\mathcal{L}_i^{\text{ảnh} \to \text{chữ}} = -\ln p_{ii} = -\ln \frac{\exp(S_{ii} / \tau)}{\sum_{k=1}^{N} \exp(S_{ik} / \tau)}

Dạng “một mẫu đúng giữa nhiều mẫu sai, chấm bằng softmax” này có tên là InfoNCE (van den Oord và cộng sự, 2018).

Chiều ngược lại cũng là một trò chơi: “trong NN ảnh, ảnh nào thuộc về chú thích jj?”. Đó là softmax theo cột jj của cùng ma trận:

Ljchữ→ảnh=−ln⁡exp⁡(Sjj/τ)∑k=1Nexp⁡(Skj/τ)\mathcal{L}_j^{\text{chữ} \to \text{ảnh}} = -\ln \frac{\exp(S_{jj} / \tau)}{\sum_{k=1}^{N} \exp(S_{kj} / \tau)}

Mẫu số giờ cộng theo cột (đổi chỉ số hàng kk), còn tử số vẫn là ô trên đường chéo. CLIP lấy trung bình của hai chiều, mỗi chiều trung bình trên cả lô:

L=12(1N∑i=1NLiảnh→chữ+1N∑j=1NLjchữ→ảnh)\mathcal{L} = \frac{1}{2}\left(\frac{1}{N}\sum_{i=1}^{N}\mathcal{L}_i^{\text{ảnh} \to \text{chữ}} + \frac{1}{N}\sum_{j=1}^{N}\mathcal{L}_j^{\text{chữ} \to \text{ảnh}}\right)

Trong code, đây chỉ là cross-entropy trên S/τ\mathbf{S}/\tau và trên S⊤/τ\mathbf{S}^\top/\tau, với nhãn là 0,1,…,N−10, 1, \dots, N-1. Bài báo CLIP có giả mã chưa tới mười dòng cho đúng việc này.

Bước 4: gradient kéo đường chéo lên, đẩy phần còn lại xuống

Phần tiêu đề “Bước 4: gradient kéo đường chéo lên, đẩy phần còn lại xuống”

Vì sao hàm này làm đúng điều ta muốn? Viết lại mất mát hàng ii bằng cách tách log của phân số thành hiệu hai log:

Liảnh→chữ=−Siiτ+ln⁡∑k=1Nexp⁡ ⁣(Sikτ)\mathcal{L}_i^{\text{ảnh} \to \text{chữ}} = -\frac{S_{ii}}{\tau} + \ln \sum_{k=1}^{N} \exp\!\left(\frac{S_{ik}}{\tau}\right)

Lấy đạo hàm theo một ô SikS_{ik} của hàng đó. Số hạng đầu chỉ chứa SiiS_{ii}, nên cho −1/τ-1/\tau khi k=ik = i và 0 khi k≠ik \ne i. Số hạng sau là log của một tổng mũ; đạo hàm của nó theo SikS_{ik} là 1τ⋅exp⁡(Sik/τ)∑k′exp⁡(Sik′/τ)=pik/τ\frac{1}{\tau} \cdot \frac{\exp(S_{ik}/\tau)}{\sum_{k'} \exp(S_{ik'}/\tau)} = p_{ik}/\tau. Cộng lại:

∂Liảnh→chữ∂Sik=pik−[k=i]τ\frac{\partial \mathcal{L}_i^{\text{ảnh} \to \text{chữ}}}{\partial S_{ik}} = \frac{p_{ik} - [k = i]}{\tau}

Ở đây [k=i][k = i] bằng 1 khi k=ik = i, bằng 0 khi khác. Gradient descent đi ngược dấu gradient, nên:

  • Ô đường chéo có gradient (pii−1)/τ<0(p_{ii} - 1)/\tau < 0: ô này được kéo lên, cho tới khi piip_{ii} gần 1.
  • Ô ngoài đường chéo có gradient pik/τ>0p_{ik}/\tau > 0: ô này bị đẩy xuống, và bị đẩy mạnh hơn khi pikp_{ik} lớn. Chú thích sai nhưng “trông giống” (gọi là mẫu âm khó, hard negative) bị đẩy nhiều nhất.

Lô càng lớn thì mỗi ảnh có càng nhiều mẫu sai để phân biệt. CLIP dùng lô 32 768 cặp.

Cosine nằm trong [−1,1][-1, 1], nên với τ=1\tau = 1 hai logit chỉ chênh nhau tối đa 2. Softmax trên những logit gần nhau như vậy luôn khá phẳng: dù ghép đúng hoàn toàn, mất mát vẫn không xuống thấp được. Chia cho τ\tau nhỏ kéo giãn khoảng cách: với τ=0.01\tau = 0.01, khoảng chênh tối đa thành 200.

Nhưng τ\tau quá nhỏ thì một cặp sai có cosine nhỉnh hơn cặp đúng một chút đã làm mất mát rất lớn, và huấn luyện dễ mất ổn định. CLIP không chọn tay mà học τ\tau, qua tham số tt với 1/τ=et1/\tau = e^{t} (học tt thì τ\tau luôn dương). Theo bài báo, τ\tau khởi tạo ở 0.07 và bị chặn để 1/τ1/\tau không vượt quá 100, vì vượt quá làm huấn luyện mất ổn định. Ô LocalCode ở dưới in ra 1/τ=100.01/\tau = 100.0 của checkpoint đã học: τ\tau chạm đúng mức chặn.

Chú ý: τ\tau ở đây cùng vai trò với nhiệt độ TT của phần LLM (chia logit trước softmax), chỉ khác ký hiệu theo bài báo.

CLIP (Radford và cộng sự, 2021) huấn luyện từ đầu trên 400 triệu cặp (ảnh, chữ) thu từ Internet. Nhóm tác giả thử năm bộ mã hóa ảnh ResNet và ba ViT (ViT-B/32, ViT-B/16, ViT-L/14); bộ mã hóa chữ là một Transformer. Mô hình tốt nhất (ViT-L/14 ở ảnh 336×336336 \times 336) đạt 76.2% độ chính xác top-1 zero-shot trên ImageNet, ngang ResNet-50 gốc mà không dùng ảnh nào trong 1.28 triệu ảnh có nhãn của ImageNet (số theo bài báo, 2021).

Có KK nhãn. Viết mỗi nhãn kk thành một câu, đưa qua bộ mã hóa chữ, được vk\mathbf{v}_k. Với ảnh mới có embedding u\mathbf{u}:

y^=arg⁡max⁡k  u⊤vk,P(y=k∣ảnh)=exp⁡(u⊤vk/τ)∑k′=1Kexp⁡(u⊤vk′/τ)\hat{y} = \arg\max_{k} \; \mathbf{u}^\top \mathbf{v}_k, \qquad P(y = k \mid \text{ảnh}) = \frac{\exp(\mathbf{u}^\top \mathbf{v}_k / \tau)}{\sum_{k'=1}^{K} \exp(\mathbf{u}^\top \mathbf{v}_{k'} / \tau)}

Đây đúng là một hàng của ma trận S\mathbf{S}, với các câu nhãn đứng ở vị trí chú thích. Bài báo CLIP nhìn nó như một hồi quy logistic nhiều lớp không có hệ số tự do: các vk\mathbf{v}_k chính là trọng số của lớp phân loại, và bộ mã hóa chữ là thứ sinh ra trọng số từ tên nhãn.

Vì sao cần prompt. Chú thích trong dữ liệu hiếm khi là một từ đơn, nên chỉ đưa tên nhãn “crane” vừa lệch phân phối, vừa mơ hồ (cần cẩu hay con sếu?). Bài báo dùng mẫu "A photo of a {label}." làm mặc định, giúp tăng 1.3% trên ImageNet; với từng bộ dữ liệu còn thêm ngữ cảnh, ví dụ "A photo of a {label}, a type of pet.".

Gộp nhiều prompt (prompt ensembling). Viết mỗi nhãn bằng MM mẫu khác nhau ("a photo of a big {label}", "a blurry photo of a {label}"…), lấy trung bình các embedding chữ rồi chuẩn hóa lại:

vˉk=∑m=1Mvk(m)∥∑m=1Mvk(m)∥\bar{\mathbf{v}}_k = \frac{\sum_{m=1}^{M} \mathbf{v}_k^{(m)}}{\left\lVert \sum_{m=1}^{M} \mathbf{v}_k^{(m)} \right\rVert}

Chia cho MM rồi chuẩn hóa cho cùng kết quả như chuẩn hóa thẳng tổng, nên công thức bỏ 1/M1/M. Gộp trong không gian embedding nên chỉ tính một lần cho mỗi nhãn, chi phí lúc dự đoán như một prompt. Với 80 mẫu, bài báo báo cáo thêm 3.5% trên ImageNet so với một prompt mặc định; cộng cả hai cách là gần 5%.

  • Đếm và việc trừu tượng. Bài báo ghi CLIP zero-shot yếu với đếm số vật thể (CLEVRCounts) và đoán khoảng cách tới xe gần nhất (KITTI Distance), có lúc gần như đoán mò. Ô LocalCode thử đếm ba hình tròn.
  • Quan hệ và vị trí. Embedding ảnh là một vectơ cho cả ảnh, và học đối chiếu trên chú thích web ít khi buộc mô hình phân biệt thứ tự từ. Yuksekgonul và cộng sự (2023) cho thấy nhiều mô hình kiểu này, có CLIP, cư xử như “túi từ” (bag of words): khó chọn giữa “the horse is eating the grass” và “the grass is eating the horse”, và với các câu quan hệ, phần lớn mô hình gần hoặc dưới mức đoán mò.
  • Chữ trong ảnh. CLIP đọc được chữ in khá tốt, đến mức chữ có thể đánh lừa nó: Goh và cộng sự (2021) mô tả tấn công bằng chữ (typographic attack), dán chữ ghi tên một vật khác lên ảnh là có thể làm đổi nhãn. Ngược lại, với chữ số viết tay của MNIST, CLIP zero-shot chỉ đạt 88%, thua cả hồi quy logistic trên điểm ảnh thô (theo bài báo CLIP).
  • Từ đa nghĩa và phân biệt chi tiết. “Boxer” là giống chó hay võ sĩ? Phân biệt các đời xe hay các loài hoa cũng là điểm yếu được bài báo ghi nhận.

CLIP chỉ chấm điểm cặp, không sinh câu nào. BLIP (Li và cộng sự, 2022) muốn một mô hình làm được cả việc hiểu (truy xuất, hỏi đáp về ảnh) lẫn việc sinh (viết chú thích). Nó dựng một hỗn hợp encoder–decoder đa phương thức (multimodal mixture of encoder-decoder, MED), chạy được ở ba chế độ, mỗi chế độ một mục tiêu học:

Chế độMục tiêu họcLàm gì
Hai bộ mã hóa riêng (ảnh, chữ)ITC (image-text contrastive): học đối chiếu như CLIPKéo cặp đúng lại gần trong không gian chung
Bộ mã hóa chữ có điều kiện ảnh: thêm cross-attention nhìn sang đặc trưng ảnhITM (image-text matching): phân loại nhị phân “khớp” hay “không khớp”Xét kỹ từng cặp; cặp âm được lấy mẫu trong lô, ưu tiên cặp có điểm ITC cao (mẫu âm khó)
Bộ giải mã chữ có điều kiện ảnh: self-attention nhân quảLM (language modeling): dự đoán token kế tiếpViết chú thích cho ảnh

Bộ mã hóa chữ và bộ giải mã chữ dùng chung mọi tham số trừ các lớp self-attention, vì theo tác giả, khác biệt giữa “đọc” và “viết” nằm chủ yếu ở đó. ITC của BLIP theo ALBEF (Li và cộng sự, 2021): thêm một bộ mã hóa động lượng (momentum encoder) để tạo nhãn mềm, vì trong lô có thể có cặp “sai” mà thật ra khớp.

CapFilt: lọc chú thích nhiễu. Chữ đi kèm ảnh web thường không mô tả ảnh (“IMG_2031.jpg”, “sale 50%”). BLIP tách mô hình đã pre-train thành hai, fine-tune riêng trên COCO (bộ dữ liệu ảnh có chú thích do người viết):

  1. Bộ sinh chú thích (captioner, chế độ LM) viết một câu mới cho mỗi ảnh web, bằng top-p sampling. Tác giả thấy câu đa dạng giúp nhiều hơn câu “an toàn” của beam search.
  2. Bộ lọc (filter, chế độ ITM) chấm cả câu gốc lẫn câu mới; câu nào đầu ITM đoán là không khớp ảnh thì bị bỏ.

Các cặp còn lại, cộng dữ liệu do người gán nhãn, thành bộ dữ liệu mới để pre-train một mô hình mới. Bộ dữ liệu gốc có 14 triệu ảnh; bài báo thử thêm tới 129 triệu ảnh (có LAION).

BLIP-2 (Li và cộng sự, 2023) đi bước tiếp: đóng băng cả một bộ mã hóa ảnh có sẵn lẫn một LLM có sẵn, chỉ học một cầu nối nhỏ là Q-Former (188 triệu tham số). Q-Former có 32 vectơ truy vấn học được, mỗi vectơ 768 chiều; chúng nhìn sang đặc trưng ảnh qua cross-attention và rút ra một bản tóm tắt 32×76832 \times 768, nhỏ hơn nhiều so với 257×1 024257 \times 1\,024 của ViT-L/14. Một lớp tuyến tính chiếu 32 vectơ đó sang số chiều embedding của LLM rồi đặt trước token chữ, như “prompt mềm” bằng ảnh. Cách nối ảnh vào LLM này, và cách đơn giản hơn của LLaVA, là chủ đề của bài Ảnh + văn bản (2): LLaVA và các biến thể.

CLIP và BLIP làm việc với cả ảnh. Phát hiện vật thể (object detection) thì cần biết vật nằm ở đâu: mỗi vật một hộp bao (bounding box) kèm nhãn. Một bộ phát hiện kinh điển lấy đặc trưng của RR vùng ảnh, O∈RR×d\mathbf{O} \in \mathbb{R}^{R \times d}, nhân với ma trận trọng số W∈Rc×d\mathbf{W} \in \mathbb{R}^{c \times d} của cc lớp cố định:

Scls=O W⊤∈RR×c\mathbf{S}_{\text{cls}} = \mathbf{O}\,\mathbf{W}^\top \in \mathbb{R}^{R \times c}

GLIP (Li và cộng sự, 2022) nhận ra phát hiện vật thể là một trường hợp riêng của grounding (phrase grounding, định vị cụm từ): cho một câu, tìm vùng ảnh ứng với từng cụm từ trong câu. Cách làm: viết danh sách lớp thành một prompt, ví dụ “person. bicycle. car. … toothbrush”, đưa qua bộ mã hóa chữ thành P∈RM×d\mathbf{P} \in \mathbb{R}^{M \times d} (MM token), rồi thay W\mathbf{W} bằng P\mathbf{P}:

Sground=O P⊤∈RR×M\mathbf{S}_{\text{ground}} = \mathbf{O}\,\mathbf{P}^\top \in \mathbb{R}^{R \times M}

Mỗi ô là điểm khớp giữa một vùng và một token. Đây là cùng ý với ma trận S\mathbf{S} của CLIP, nhưng ở mức vùng–từ thay vì ảnh–câu. Vì prompt là chữ tự do, đổi danh sách lớp không cần đổi kiến trúc.

Hai điểm khác CLIP:

  • Fusion sâu (deep fusion). CLIP chỉ gặp nhau ở tích vô hướng cuối cùng; GLIP trộn đặc trưng ảnh và chữ qua cross-attention ở vài lớp cuối của hai bộ mã hóa, nên đặc trưng ảnh “biết” câu đang hỏi gì. Tác giả cho rằng điều này quan trọng cho chất lượng.
  • Dữ liệu grounding mở rộng bằng nhãn giả. Một mô hình GLIP “thầy” học trên dữ liệu có người gán, rồi tự vẽ khung cho các cụm danh từ trong 24 triệu cặp ảnh và chữ từ web; mô hình “trò” học trên tổng cộng 27 triệu mẫu grounding (3 triệu có người gán).

Theo bài báo (2022), GLIP-L chưa thấy ảnh COCO nào khi pre-train vẫn đạt 49.8 AP zero-shot trên COCO và 26.9 AP trên LVIS (AP, average precision, là thước đo chuẩn của phát hiện vật thể).

  • Bài chỉ nói ý chính của ba mô hình; kiến trúc phát hiện vật thể (đề xuất vùng, khớp khung) thuộc phần phát hiện vật thể, chưa có bài riêng.
  • Ảnh vào LLM (LLaVA, BLIP-2 nối với LLM) thuộc bài Ảnh + văn bản (2): LLaVA và các biến thể.
  • Học đối chiếu không cần chữ, chỉ dùng hai góc nhìn của cùng một ảnh, thuộc bài Self-supervised learning với ảnh.

Hình dưới vẽ một lô NN cặp của CLIP. Hãy nhìn vào ma trận ở giữa: các ô trên đường chéo là cặp đúng và được kéo lên, mọi ô còn lại bị đẩy xuống. Mỗi hàng là một bài “chọn chú thích cho ảnh”, mỗi cột là một bài “chọn ảnh cho chú thích”.

Bảng dưới đặt ba mô hình cạnh nhau. Cột “Mục tiêu học” cho biết vì sao đầu ra của mỗi mô hình khác nhau.

Tiêu chíCLIP (2021)BLIP (2022)GLIP (2022)
Mục tiêu họcĐối chiếu ảnh–câu (InfoNCE đối xứng)ITC, ITM và LM cùng lúcKhớp vùng–từ (grounding), gộp cả dữ liệu phát hiện vật thể
Ảnh và chữ gặp nhau ở đâuChỉ ở tích vô hướng cuốiCross-attention trong bộ mã hóa và bộ giải mã chữFusion sâu ở các lớp cuối của hai bộ mã hóa
Đầu raMột vectơ cho ảnh, một vectơ cho câuĐiểm khớp ảnh–câu, hoặc một câu chú thíchHộp bao kèm cụm từ trong prompt
Dữ liệu (theo bài báo)400 triệu cặp từ web14 triệu ảnh (tới 129 triệu), chú thích lọc bằng CapFilt27 triệu mẫu grounding (24 triệu có nhãn giả)
Dùng choPhân loại zero-shot, tìm ảnh bằng chữ, bộ mã hóa ảnh cho LLMViết chú thích, hỏi đáp về ảnh, truy xuấtPhát hiện vật thể theo nhãn tùy ý, tìm vật theo câu

Code trong bài chạy thử lần cuối tháng 11/2026: ô NumPy với Python 3.14 và NumPy 2.4 (bản có trong Pyodide), ô CLIP với Python 3.14, PyTorch 2.15 và transformers 5.22 trên CPU. Ô NumPy chạy ngay trên trang, ô CLIP chạy trên máy bạn. Hai ô độc lập, không dùng biến của nhau.

Ma trận tương đồng và InfoNCE bằng NumPy

Phần tiêu đề “Ma trận tương đồng và InfoNCE bằng NumPy”

Ô dưới đây tạo 4 “ý” ngẫu nhiên (hạt giống 0), mỗi ý sinh một embedding ảnh và một embedding chữ bằng cách cộng hai nhiễu khác nhau, nên cặp cùng ý có tương quan. Ô chuẩn hóa, tính ma trận S\mathbf{S}, tính mất mát đối xứng với ba giá trị τ\tau cho cách ghép đúng và cho một cách ghép sai (ảnh ii với chú thích i+1i+1), rồi phân loại zero-shot một “ảnh” mới của ý “dog”.

Ma trận tương đồng, InfoNCE và zero-shot

import numpy as np
rng = np.random.default_rng(0)
N, D = 4, 8 # 4 cặp (ảnh, chú thích), embedding 8 chiều
labels = ["cat", "dog", "car", "tree"]
# Mỗi cặp có một "ý" chung; ảnh và chữ là hai bản có nhiễu của ý đó
concept = rng.normal(size=(N, D))
img = concept + 0.5 * rng.normal(size=(N, D))
txt = concept + 0.5 * rng.normal(size=(N, D))
def normalize(a): # chia mỗi hàng cho độ dài của nó
return a / np.linalg.norm(a, axis=1, keepdims=True)
def cross_entropy(logits): # nhãn đúng của hàng i là cột i
z = logits - logits.max(axis=1, keepdims=True)
log_p = z - np.log(np.exp(z).sum(axis=1, keepdims=True))
return -np.diag(log_p).mean()
def clip_loss(S, tau): # trung bình của chiều hàng và chiều cột
return (cross_entropy(S / tau) + cross_entropy(S.T / tau)) / 2
I, T = normalize(img), normalize(txt)
S = I @ T.T # ma trận cosine N x N
np.set_printoptions(precision=2, suppress=True)
print(S)
S_wrong = I @ T[[1, 2, 3, 0]].T # ghép sai: ảnh i với chú thích i+1
print(f"log N = {np.log(N):.3f}")
for tau in [1.0, 0.1, 0.01]:
print(f"tau={tau}: right={clip_loss(S, tau):.3f}"
f" wrong={clip_loss(S_wrong, tau):.3f}")
# Zero-shot: ảnh mới của "dog", so với 4 câu nhãn
new_img = normalize(concept[[1]] + 0.5 * rng.normal(size=(1, D)))
cos = (new_img @ T.T)[0]
z = cos / 0.07 # tau khởi tạo của CLIP
p = np.exp(z - z.max()) / np.exp(z - z.max()).sum()
print("cos: ", cos)
print("probs:", " ".join(f"{x:.4f}" for x in p), "->", labels[p.argmax()])
print("argmax per row:", [labels[k] for k in S.argmax(axis=1)])

Lần chạy đầu cần tải Python, khoảng 10 MB.

Đọc kết quả:

  • Ma trận 4×44 \times 4: ô lớn nhất của mỗi hàng nằm trên đường chéo (0.81, 0.77, 0.71, 0.66), dù vài ô ngoài đường chéo không nhỏ, như 0.41 ở hàng 2.
  • log N = 1.386: mức mất mát khi mô hình đoán đều, ln⁡4\ln 4.
  • tau=1.0: ghép đúng chỉ được 0.930, không xa ln⁡4\ln 4, vì logit chênh nhau quá ít. Đúng điều Bước 5 nói.
  • tau=0.1 và tau=0.01: ghép đúng xuống 0.023 rồi 0.000, nhưng ghép sai vọt lên 6.815 rồi 67.923. τ\tau nhỏ làm mô hình “tự tin”, thưởng lớn khi đúng và phạt nặng khi sai.
  • Zero-shot: cosine của ảnh mới với 4 câu nhãn là [-0.57 0.78 0.23 0.25]; chia cho τ=0.07\tau = 0.07 rồi softmax cho xác suất 0.9991 ở “dog”. Dòng cuối lấy argmax theo từng hàng của S\mathbf{S} và ra đúng thứ tự cat, dog, car, tree.

CLIP thật: phân loại zero-shot ảnh tự vẽ

Phần tiêu đề “CLIP thật: phân loại zero-shot ảnh tự vẽ”

File dưới đây tải checkpoint openai/clip-vit-base-patch32 (bộ mã hóa ảnh ViT-B/32) qua thư viện transformers. Trọng số khoảng 600 MB; trên máy chạy thử, thư viện tải cả bản pytorch_model.bin lẫn model.safetensors, nên bộ nhớ đệm chiếm khoảng 1.2 GB. File vẽ ba ảnh bằng PIL (tròn đỏ, vuông xanh dương, tam giác vàng), phân loại với 5 nhãn trong đó có nhãn “bẫy” cùng màu khác hình và cùng hình khác màu, rồi thử đếm một ảnh có ba hình tròn đỏ. Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.

Chạy trên máy bạn

CLIP phân loại zero-shot hình tự vẽ

Cần cài
pip install torch transformers pillow
Phần cứng
CPU là đủ. Lần đầu mất vài phút để tải trọng số; các lần sau khoảng 1 phút trên máy chạy thử, phần lớn là nạp thư viện và mô hình
import torch
from PIL import Image, ImageDraw
from transformers import CLIPModel, CLIPProcessor
torch.manual_seed(0)
NAME = "openai/clip-vit-base-patch32" # lần đầu tải 0.6 tới 1.2 GB
model = CLIPModel.from_pretrained(NAME).eval()
processor = CLIPProcessor.from_pretrained(NAME)
def draw(shape, color, n=1):
# Ảnh 224 x 224 nền trắng, n hình cùng loại xếp thành hàng ngang
img = Image.new("RGB", (224, 224), "white")
d = ImageDraw.Draw(img)
step = 180 // n
size = step * 3 // 4
for k in range(n):
x0, y0 = 22 + k * step + (step - size) // 2, 112 - size // 2
x1, y1 = x0 + size, y0 + size
if shape == "circle":
d.ellipse((x0, y0, x1, y1), fill=color)
elif shape == "square":
d.rectangle((x0, y0, x1, y1), fill=color)
else:
d.polygon([((x0 + x1) // 2, y0), (x1, y1), (x0, y1)],
fill=color)
return img
def zero_shot(images, labels):
prompts = [f"a photo of {label}" for label in labels]
inputs = processor(text=prompts, images=images,
return_tensors="pt", padding=True)
with torch.no_grad():
out = model(**inputs)
# logits = cosine / tau; softmax theo hàng: mỗi ảnh một phân phối
return out.logits_per_image.softmax(dim=1)
print(f"1/tau = {model.logit_scale.exp().item():.1f}")
shapes = [("circle", "red"), ("square", "blue"), ("triangle", "yellow")]
labels = ["a red circle", "a blue circle", "a red square",
"a blue square", "a yellow triangle"]
probs = zero_shot([draw(s, c) for s, c in shapes], labels)
for (s, c), row in zip(shapes, probs):
cells = " ".join(f"{p:.3f}" for p in row.tolist())
print(f"{c} {s}: {cells} -> {labels[row.argmax()]}")
# Đếm: ảnh có 3 hình tròn đỏ, hỏi có mấy hình
counts = ["one red circle", "two red circles", "three red circles",
"four red circles"]
row = zero_shot([draw("circle", "red", n=3)], counts)[0]
cells = " ".join(f"{p:.3f}" for p in row.tolist())
print(f"3 circles: {cells} -> {counts[row.argmax()]}")

Kết quả mẫu

Lấy từ một lần chạy thật.

1/tau = 100.0
red circle: 0.940 0.056 0.001 0.002 0.001 -> a red circle
blue square: 0.000 0.042 0.000 0.957 0.000 -> a blue square
yellow triangle: 0.000 0.000 0.000 0.000 1.000 -> a yellow triangle
3 circles: 0.043 0.070 0.504 0.384 -> three red circles

Đọc kết quả:

  • 1/tau = 100.0: logit_scale của checkpoint là ln⁡100\ln 100, đúng mức chặn của bài báo. logits_per_image của thư viện chính là S/τ\mathbf{S}/\tau, nên softmax(dim=1) là softmax theo hàng như công thức zero-shot.
  • Ba hình đơn: cả ba đều đúng. Năm số trên mỗi dòng theo thứ tự nhãn “a red circle”, “a blue circle”, “a red square”, “a blue square”, “a yellow triangle”. Nhãn bẫy nhận nhiều xác suất nhất là “a blue circle”: 0.056 cho hình tròn đỏ (cùng hình, khác màu) và 0.042 cho hình vuông xanh (cùng màu, khác hình).
  • Đếm: “three red circles” thắng với 0.504, nhưng “four red circles” vẫn nhận 0.384. So với 0.94–1.00 ở các dòng trên, mô hình kém chắc chắn hẳn khi phải đếm, khớp với giới hạn bài báo ghi nhận. Đây chỉ là một ảnh, không phải phép đo: bạn thử đổi n=3 thành 5 hay 6 để xem.
Câu 1/5 Embedding ảnh và chữ của CLIP đều có độ dài 1, nên độ tương đồng cosine nằm trong [−1,1][-1, 1]. Nếu cố định τ=1\tau = 1 thay vì để mô hình học τ\tau, chuyện gì xảy ra với hàm mất mát trên một lô N=1 000N = 1\,000 cặp?
Câu 2/5 Bạn có CLIP đã huấn luyện xong và muốn phân loại ảnh vào ba nhãn mới: "xe đạp gấp", "xe đạp thể thao", "xe đạp điện". Bạn không có ảnh nào có nhãn. Cách làm đúng tinh thần zero-shot của CLIP là gì?
Câu 3/5 Trong một lô huấn luyện của CLIP có hai cặp mà chú thích gần như giống nhau: "a dog on the grass" và "a dog playing on grass". Hàm mất mát InfoNCE đối xứng xử lý hai cặp này thế nào?
Câu 4/5 Ảnh tải từ web có chú thích gốc là "IMG_2031.jpg sale 50%". Trong CapFilt của BLIP, cặp này đi qua những bước nào?
Câu 5/5 Bạn cần một hệ thống nhận câu "tìm mọi mũ bảo hiểm màu vàng" và trả về hộp bao quanh từng chiếc mũ trong ảnh công trường. Trong ba mô hình của bài, mô hình nào hợp với đầu ra này?

Ý chính:

  • CLIP học bằng trò ghép đôi: trong lô NN cặp, ma trận Sij=ui⊤vjS_{ij} = \mathbf{u}_i^\top \mathbf{v}_j có đáp án trên đường chéo; mất mát là cross-entropy theo hàng và theo cột trên S/τ\mathbf{S}/\tau (InfoNCE đối xứng). Gradient kéo đường chéo lên, đẩy mạnh nhất các mẫu âm khó.
  • τ\tau quyết định softmax sắc hay phẳng: lớn quá thì không học được, nhỏ quá thì mất ổn định. CLIP học τ\tau, khởi tạo 0.07, chặn 1/τ≤1001/\tau \le 100.
  • Zero-shot: viết nhãn thành câu ("a photo of a {label}"), lấy argmax cosine. Gộp nhiều prompt thêm vài phần trăm. Giới hạn: đếm, quan hệ, chữ trong ảnh, khái niệm hiếm.
  • BLIP thêm ITM và LM để vừa hiểu vừa viết, và lọc chú thích web bằng CapFilt; BLIP-2 nối bộ mã hóa ảnh và LLM đóng băng qua Q-Former. GLIP coi phát hiện vật thể là grounding: Sground=OP⊤\mathbf{S}_{\text{ground}} = \mathbf{O}\mathbf{P}^\top giữa vùng và từ.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu