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

Multimodal

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

  • Hiểu cách Vision Transformer biến ảnh thành dãy token: cắt thành patch (ô ảnh), chiếu tuyến tính, thêm vị trí. Tính được số token ảnh N=(H/P)(W/P)N = (H/P)(W/P).
  • Mô tả được mẫu chung của LLM đa phương thức (multimodal): bộ mã hóa ảnh (vision encoder) → lớp chiếu (projector) → LLM, và hai giai đoạn huấn luyện của LLaVA: phần nào đóng băng (freeze, giữ nguyên trọng số), phần nào học.
  • So sánh được ba cách ghép ảnh với LLM: nối vào chuỗi (LLaVA), cross-attention (Llama 3.2 Vision), token chung từ đầu (Chameleon).
  • Cài được phép cắt patch bằng NumPy, patch embedding và projector bằng PyTorch, rồi kiểm số token.

Ở bài API cho hình ảnh và video, bạn gửi một tấm ảnh kèm câu hỏi, và mô hình trả lời đại ý “có một hình vuông đỏ ở giữa”. Bài đó nói nhà cung cấp biến ảnh thành token ảnh. Bài này mở chiếc hộp đó.

Mỗi loại dữ liệu (chữ, ảnh, âm thanh, video) gọi là một phương thức (modality); mô hình nhận nhiều phương thức gọi là mô hình đa phương thức.

Điểm mấu chốt: khối Transformer không quan tâm vectơ đến từ đâu. Nó nhận một dãy vectơ cùng số chiều và trả ra một dãy vectơ. Nếu làm ra được một dãy vectơ “kể” nội dung tấm ảnh, Transformer xử lý được ngay.

Hình dung một bức tranh ghép. Cắt ảnh thành lưới ô vuông, đọc các ô từ trái sang phải, từ trên xuống dưới, như đọc chữ trên trang sách. Mỗi ô là một “từ”, tấm ảnh thành một “câu”: ảnh 224×224224 \times 224 cắt ô 16×1616 \times 16 cho câu dài 196 từ. Đó chính là ViT.

Thứ nhất, vectơ của các ô phải có nghĩa: ô có con mèo thì vectơ phải mang ý “mèo”, không chỉ là màu điểm ảnh. Đó là việc của bộ mã hóa ảnh, thường lấy từ CLIP.

Thứ hai, bộ mã hóa ảnh và LLM được huấn luyện riêng, nên “nói hai thứ tiếng”: vectơ ảnh có DvD_v chiều theo cách hiểu của bộ mã hóa, embedding chữ có DD chiều theo cách hiểu của LLM. Cần một người phiên dịch nhỏ đứng giữa: projector.

Còn đưa ảnh cho LLM thế nào? Hãy nghĩ tới cách đưa ảnh cho người đọc sách:

  • Dán ảnh vào sách, xen giữa các trang chữ: ảnh chiếm trang. Đây là cách nối chuỗi của LLaVA.
  • Đặt ảnh ở bàn bên cạnh, cứ vài đoạn người đọc liếc sang: sách không dày thêm, nhưng cần thêm “mắt” để liếc. Đây là cross-attention của Llama 3.2 Vision.
  • Viết lại ảnh bằng chính bảng chữ cái của sách, để một người vừa đọc vừa viết được cả hai. Đây là Chameleon.

Bài toán của bài này là: làm sao biến một tấm ảnh thành dãy vectơ cùng “ngôn ngữ” với embedding của LLM, ghép dãy đó vào đâu, và huấn luyện phần nào để LLM trả lời được về ảnh mà không quên cách đọc chữ?

Ảnh có chiều cao HH, chiều rộng WW (tính bằng điểm ảnh) và CC kênh màu (C=3C = 3 với ảnh RGB). Ở bài này HH là chiều cao ảnh, không phải số đầu attention như bài Họ LLaMA. Chữ nghiêng WW (chiều rộng) khác ma trận đậm W\mathbf{W} của projector ở Bước 4, và HH khác ma trận Hv\mathbf{H}_v. Cỡ patch là PP, số patch là NN. Vectơ ra khỏi bộ mã hóa ảnh có DvD_v chiều, embedding của LLM có DD chiều, độ dài chuỗi LLM đọc là LL.

Chia ảnh thành các ô P×PP \times P không chồng nhau. Theo chiều cao có H/PH/P hàng ô, theo chiều rộng có W/PW/P cột ô, nên số ô là tích của hai số đó:

N=HP⋅WP=HWP2N = \frac{H}{P} \cdot \frac{W}{P} = \frac{HW}{P^2}

Công thức cần HH và WW chia hết cho PP; nếu không, ảnh được co giãn, cắt hoặc đệm trước. Mỗi patch có P⋅PP \cdot P điểm ảnh, mỗi điểm CC số, nên trải phẳng thành vectơ xi∈RP2C\mathbf{x}_i \in \mathbb{R}^{P^2 C}.

PP nằm ở mẫu số và bị bình phương: giảm nửa cỡ patch, hay gấp đôi mỗi cạnh ảnh, thì số token gấp 4.

Mỗi patch được nhân với một ma trận học được E\mathbf{E}, rồi cộng thêm một vectơ vị trí:

zi=xi E+pi,E∈R(P2C)×Dv\mathbf{z}_i = \mathbf{x}_i\,\mathbf{E} + \mathbf{p}_i, \qquad \mathbf{E} \in \mathbb{R}^{(P^2 C) \times D_v}

Ở đây xi\mathbf{x}_i là vectơ hàng của patch thứ ii, zi∈RDv\mathbf{z}_i \in \mathbb{R}^{D_v} là token ảnh, pi\mathbf{p}_i là embedding vị trí (position embedding, chính là vectơ vị trí ở bài Xây dựng LLM đầu tiên) học được của ô thứ ii.

Vì sao một E\mathbf{E} chung cho mọi patch? Giống bảng embedding từ dùng chung cho mọi vị trí trong câu: “mèo” ở đầu hay cuối câu vẫn là một vectơ. Hệ quả là hai patch giống hệt nhau cho cùng một xiE\mathbf{x}_i\mathbf{E} dù nằm ở hai góc ảnh (ô NumPy kiểm điều này), nên cần pi\mathbf{p}_i để biết ô nằm ở đâu. ViT đánh số ô theo thứ tự đọc và dùng embedding vị trí một chiều; tác giả thử loại hai chiều và không thấy lợi đáng kể. ViT thêm một token lớp (class token) học được ở đầu dãy (giống token [CLS] của BERT), rồi cho cả dãy qua các khối Transformer encoder chuẩn. Ngoài bước cắt patch, ViT gần như không dùng hiểu biết riêng nào về ảnh: quan hệ không gian giữa các ô do mô hình tự học từ dữ liệu.

Về cài đặt, “cắt patch rồi nhân E\mathbf{E}” chính là một phép tích chập (convolution) có kernel P×PP \times P và bước nhảy (stride) PP, nên thư viện thường viết bằng Conv2d. Ra khỏi bộ mã hóa, ta có Zv∈RN×Dv\mathbf{Z}_v \in \mathbb{R}^{N \times D_v}: mỗi hàng là đặc trưng của một ô, đã “nhìn” các ô khác qua self-attention.

CLIP (Radford và cộng sự, 2021) học trên 400 triệu cặp ảnh và chú thích từ Internet. Với một lô NbN_b cặp, nó tính độ giống cosine giữa mọi ảnh và mọi chú thích, được bảng Nb×NbN_b \times N_b mà đường chéo là các cặp đúng, rồi dùng cross-entropy theo cả hàng lẫn cột để điểm trên đường chéo cao hơn hẳn phần còn lại. Nhờ vậy đặc trưng ảnh của CLIP đã gần với ngôn ngữ: điểm xuất phát tốt cho LLM. LLaVA dùng bộ mã hóa ViT-L/14 của CLIP (patch 14×1414 \times 14). Nhưng CLIP chỉ ghép ảnh với chữ, không sinh câu nào.

LLaVA (Liu và cộng sự, 2023) nối đặc trưng ảnh vào LLM bằng một lớp tuyến tính:

Hv=Zv W,W∈RDv×D\mathbf{H}_v = \mathbf{Z}_v\,\mathbf{W}, \qquad \mathbf{W} \in \mathbb{R}^{D_v \times D}

Mỗi hàng DvD_v số của Zv\mathbf{Z}_v thành một hàng DD số, đúng số chiều embedding từ của LLM. Bài báo viết Hv=WZv\mathbf{H}_v = \mathbf{W}\mathbf{Z}_v theo cột; ở đây viết theo hàng cho khớp với code. Tác giả cố ý chọn lớp đơn giản để thử nghiệm nhanh. LLaVA-1.5 (Liu và cộng sự, 2023) thay nó bằng một MLP hai lớp và báo cáo kết quả tốt hơn:

Hv=ϕ(Zv W1+b1) W2+b2\mathbf{H}_v = \phi\big(\mathbf{Z}_v\,\mathbf{W}_1 + \mathbf{b}_1\big)\,\mathbf{W}_2 + \mathbf{b}_2

ϕ\phi là hàm kích hoạt (activation function). Bài báo chỉ nói “MLP hai lớp”; code của bài dùng GELU, trùng với code chính thức của LLaVA-1.5 (mlp2x_gelu: Linear → GELU → Linear).

Ở dạng đơn giản nhất (và trong code của bài), token ảnh đứng trước embedding của token chữ, thành một chuỗi:

[HvHt]∈RL×D,L=N+Ltext\begin{bmatrix} \mathbf{H}_v \\ \mathbf{H}_t \end{bmatrix} \in \mathbb{R}^{L \times D}, \qquad L = N + L_{\text{text}}

Ht\mathbf{H}_t có LtextL_{\text{text}} hàng, mỗi hàng là embedding của một token chữ trong câu hỏi. Khi huấn luyện, LLaVA còn đổi ngẫu nhiên thứ tự ảnh và câu hỏi ở lượt đầu. Từ đây LLM làm việc như thường: dự đoán token kế tiếp. Khi huấn luyện, hàm mất mát chỉ tính trên các token của câu trả lời.

Số token ảnh là chi phí thật. Self-attention chấm điểm mọi cặp vị trí, nên số cặp tỉ lệ với L2L^2. Câu hỏi 50 token kèm 576 token ảnh cho 6262≈392626^2 \approx 392 nghìn cặp, so với 502=2 50050^2 = 2\,500 nếu chỉ có chữ (tự tính). Token ảnh còn chiếm cửa sổ ngữ cảnh và KV cache như token chữ.

Ảnh H×WH \times WPPNNGặp ở đâu
224×224224 \times 22416196ViT-B/16 trong bài báo ViT (tự tính)
224×224224 \times 22414256ViT-L/14 của CLIP ở cỡ ảnh 224 (tự tính); báo cáo Llama 3 ghi đúng 256 patch cho bộ mã hóa của họ
336×336336 \times 33614576CLIP ViT-L 336px của LLaVA-1.5 (tự tính)
448×448448 \times 448141 024Tự tính: cạnh gấp đôi dòng 256
Giai đoạnDữ liệuBộ mã hóa ảnhProjectorLLM
1. Căn chỉnh đặc trưng595K cặp ảnh và chú thích, lọc từ CC3M (Conceptual Captions, khoảng 3 triệu cặp)Đóng băngHọcĐóng băng
2. Tinh chỉnh toàn bộLLaVA-Instruct-158K: hội thoại, mô tả chi tiết, suy luậnĐóng băngHọcHọc

Giai đoạn 1. Mỗi cặp thành một hội thoại một lượt: câu hỏi kiểu “hãy mô tả ngắn ảnh này”, đáp án là chú thích gốc. Chỉ W\mathbf{W} được cập nhật. Tác giả gọi đây là huấn luyện một “bộ tách từ cho ảnh” tương thích với LLM đóng băng: dạy người phiên dịch trong khi hai bên giữ nguyên.

Giai đoạn 2. Bộ mã hóa ảnh vẫn đóng băng; projector và cả LLM (Vicuna) được cập nhật trên dữ liệu làm theo chỉ dẫn có ảnh. Đây là instruction tuning của bài Họ GPT và fine-tune đa nhiệm, giờ có thêm ảnh. 158K mẫu này do GPT-4 chỉ đọc chữ sinh ra từ chú thích và khung tọa độ đối tượng của ảnh, không phải do người viết.

LLaVA-1.5 giữ khung này, dùng projector MLP, ảnh 336×336336 \times 336 và thêm dữ liệu hỏi đáp về ảnh từ các bộ học thuật.

Cách nối chuỗi đặt token ảnh vào trong chuỗi; cross-attention để ảnh ở ngoài. Gọi X∈RL×D\mathbf{X} \in \mathbb{R}^{L \times D} là biểu diễn của các token chữ ở một lớp của LLM, Z∈RN×Dv\mathbf{Z} \in \mathbb{R}^{N \times D_v} là đặc trưng ảnh. Một lớp cross-attention tính:

Q=XWQ,K=ZWK,V=ZWVCrossAttn(X,Z)=softmax ⁣(QK⊤d)V\begin{aligned} \mathbf{Q} &= \mathbf{X}\mathbf{W}_Q, \quad \mathbf{K} = \mathbf{Z}\mathbf{W}_K, \quad \mathbf{V} = \mathbf{Z}\mathbf{W}_V \\ \mathrm{CrossAttn}(\mathbf{X}, \mathbf{Z}) &= \mathrm{softmax}\!\left(\frac{\mathbf{Q}\mathbf{K}^\top}{\sqrt{d}}\right)\mathbf{V} \end{aligned}

Truy vấn (query) đến từ chữ, khóa và giá trị (key, value) đến từ ảnh; dd là số chiều mỗi đầu. Bảng điểm có cỡ L×NL \times N: mỗi token chữ “liếc” sang mọi ô ảnh. Kết quả có LL hàng, đúng bằng số token chữ, nên chuỗi không dài thêm token nào.

Ba nguồn cần tách bạch:

  • Báo cáo Llama 3, mục 7 (Llama Team, 2024) mô tả các thí nghiệm, và ghi rõ các mô hình này khi đó chưa sẵn sàng phát hành. Bộ mã hóa là ViT-H/14 (630 triệu tham số, học kiểu đối chiếu trên 2.5 tỉ cặp ảnh và chữ), được pre-train trên ảnh 224×224224 \times 224, chia thành 256 patch; khi huấn luyện adapter, ảnh được đưa vào ở độ phân giải cao hơn, tối đa bốn mảnh 336×336336 \times 336. Lớp cross-attention được chèn sau mỗi lớp self-attention thứ tư của LLM; riêng bản 405B, chúng thêm khoảng 100 tỉ tham số. Khi huấn luyện bộ adapter này, nhóm tác giả cập nhật cả bộ mã hóa ảnh nhưng cố ý không cập nhật LLM (mục 1 và 7).
  • Model card Llama 3.2 Vision (Meta, phát hành 25/9/2024): hai cỡ 11B và 90B, dựng trên Llama 3.1 chỉ có chữ, thêm một bộ adapter ảnh huấn luyện riêng gồm các lớp cross-attention đưa biểu diễn của bộ mã hóa ảnh vào LLM; pre-train trên 6 tỉ cặp ảnh và chữ.
  • Bài giới thiệu Llama 3.2 trên blog của Meta (25/9/2024) xác nhận cho bản phát hành: khi huấn luyện adapter, Meta cập nhật bộ mã hóa ảnh nhưng cố ý giữ nguyên tham số của LLM, nên khả năng xử lý chữ không đổi và bản Vision thay thẳng được cho Llama 3.1 (“drop-in replacement”).

Báo cáo Llama 3 nêu bốn lý do: phát triển phần nhìn và phần ngôn ngữ song song được; tránh rắc rối của việc huấn luyện chung ảnh và chữ; khả năng xử lý chữ không bị ảnh hưởng vì LLM giữ nguyên; và ảnh độ phân giải đầy đủ không phải đi qua các mạng truyền thẳng của LLM, nên suy luận hiệu quả hơn.

Chameleon (Chameleon Team, 2024) đi hướng ngược lại: không có bộ mã hóa ảnh riêng. Một bộ tách từ cho ảnh lượng tử hóa (quantization) ảnh 512×512512 \times 512 thành 1 024 token rời rạc (khác với lượng tử hóa trọng số ở bài PEFT): mỗi token là số thứ tự trong một sổ mã (codebook) 8 192 mục, nằm luôn trong từ vựng chung 65 536 token cùng token chữ. Một Transformer duy nhất, huấn luyện từ đầu bằng dự đoán token kế tiếp, đọc và sinh được cả chữ lẫn ảnh xen kẽ. Bài báo gọi đây là fusion sớm (early fusion, trộn sớm), đối lập với fusion muộn (late fusion): các mô hình giữ bộ mã hóa riêng cho từng phương thức, như Flamingo (Alayrac và cộng sự, 2022) hay LLaVA. Cái giá, theo chính tác giả: huấn luyện khó giữ ổn định, và bộ tách từ ảnh yếu với ảnh chứa nhiều chữ.

Hình dưới vẽ cách nối chuỗi của LLaVA, cũng là cách ô PyTorch làm lại. Hãy nhìn chỗ token ảnh và token chữ gặp nhau: sau projector, chúng chỉ là các vectơ cùng số chiều, xếp chung một hàng.

Ảnh 224 × 2241234516Lưới minh họa 4 × 4,ảnh thật có 16 × 16 ôBộ mã hóa ảnhđóng băngĐặc trưng ảnh1…256ProjectorhọcCâu hỏi:Ảnh có gì?BảngembeddingToken ảnhToken chữ1…256Ảnhcógì…256 token ảnh10 token chữL= 266LLMCâu trả lời
Ảnh 224×224224 \times 224, patch 14×1414 \times 14 cho N=256N = 256 token ảnh; cùng 10 token chữ, LLM đọc một chuỗi L=266L = 266 vectơ DD chiều, khớp ô PyTorch. Cross-attention của Llama 3.2 không có trong hình, xem mục Lý thuyết và bảng dưới.

Bảng dưới đặt ba cách cạnh nhau. Cột “Huấn luyện gì” là cấu hình trong bài báo của từng mô hình.

Tiêu chíLLaVA (nối chuỗi)Llama 3.2 Vision (cross-attention)Chameleon (fusion sớm)
Ảnh thành gìĐặc trưng CLIP, qua projectorĐặc trưng của bộ mã hóa ảnhToken rời rạc từ sổ mã
Ảnh nằm ở đâuTrong chuỗi, chiếm NN vị tríNgoài chuỗi, chữ nhìn sang qua cross-attentionTrong chuỗi, 1 024 token mỗi ảnh 512×512512 \times 512
Huấn luyện gìProjector, rồi thêm LLM; bộ mã hóa đóng băngAdapter và bộ mã hóa; LLM đóng băngCả mô hình, từ đầu
Sinh ảnhKhôngKhôngCó
Điểm mạnhĐơn giản, rẻ, dựng trên LLM có sẵnKhả năng xử lý chữ giữ nguyên, chuỗi không dài thêmĐọc và viết xen kẽ chữ, ảnh
Cái giáToken ảnh tốn ngữ cảnh và tính toánNhiều tham số mới cho cross-attentionHuấn luyện khó ổn định, tốn kém

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

Ô dưới đây lấy một ảnh ngẫu nhiên 32×32×332 \times 32 \times 3 (hạt giống 0), cắt thành patch 8×88 \times 8, chiếu mỗi patch bằng một ma trận E\mathbf{E} thành vectơ Dv=64D_v = 64 chiều, rồi đếm số token cho vài cỡ ảnh. Mẹo reshape rồi transpose tách mỗi trục ảnh thành “số thứ tự ô” và “vị trí trong ô”.

Cắt ảnh thành patch, chiếu thành token

# Cắt một ảnh nhỏ thành patch rồi chiếu tuyến tính thành token, như ViT
import numpy as np
rng = np.random.default_rng(0)
H, W, C = 32, 32, 3 # ảnh 32x32, 3 kênh màu
P, D_v = 8, 64 # cỡ patch, số chiều của token ảnh
img = rng.random((H, W, C))
def to_patches(x):
# Chia lưới (H/P) x (W/P) ô, đọc từ trái sang phải, trên xuống dưới,
# rồi trải phẳng mỗi ô thành P*P*C số
grid = x.reshape(H // P, P, W // P, P, C).transpose(0, 2, 1, 3, 4)
return grid.reshape(-1, P * P * C)
patches = to_patches(img)
print("patches:", patches.shape)
print("patch 0 = top-left:", np.array_equal(patches[0], img[:P, :P].ravel()))
print("patch 1 = next right:",
np.array_equal(patches[1], img[:P, P:2 * P].ravel()))
# Một ma trận E dùng chung cho mọi patch: P*P*C số -> vectơ D_v chiều
E = rng.normal(0, 0.02, (P * P * C, D_v))
tokens = patches @ E
print("tokens:", tokens.shape, "E params:", E.size)
# Chép ô trên trái xuống góc dưới phải: hai ô giống nhau, hai chỗ khác nhau
img2 = img.copy()
img2[-P:, -P:] = img[:P, :P]
tokens2 = to_patches(img2) @ E
print("same token at two places:", np.allclose(tokens2[0], tokens2[-1]))
# Số token N = (H/P)(W/P) với vài cỡ ảnh hay gặp
for h, p in [(224, 16), (224, 8), (224, 14), (336, 14), (448, 14)]:
print(f"{h}x{h}, P={p}: N = {(h // p) ** 2}")

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

Đọc kết quả:

  • patches: (16, 192): N=(32/8)2=16N = (32/8)^2 = 16 patch, mỗi patch 8⋅8⋅3=1928 \cdot 8 \cdot 3 = 192 số.
  • Hai dòng True tiếp theo: patch 0 là ô góc trên trái, patch 1 là ô ngay bên phải nó: đọc hết một hàng rồi mới xuống hàng.
  • tokens: (16, 64): E\mathbf{E} có 192⋅64=12 288192 \cdot 64 = 12\,288 tham số, dùng chung cho cả 16 patch.
  • same token at two places: True: chép ô trên trái xuống góc dưới phải thì hai ô ở hai đầu ảnh cho cùng một vectơ. Thiếu embedding vị trí, mô hình không phân biệt được chúng.
  • Bảng cuối: khớp bảng ở mục Lý thuyết, thêm dòng P=8P = 8: 784 token, gấp đúng 4 lần 196 của P=16P = 16.

File dưới đây dựng lại mẫu chung ở cỡ nhỏ: một Conv2d đóng vai bộ mã hóa ảnh (chỉ có bước patch embedding), một nn.Embedding đóng vai bảng embedding của LLM, cả hai đóng băng như giai đoạn 1 của LLaVA. Ở giữa là projector MLP hai lớp 64→128→12864 \to 128 \to 128. File kiểm Conv2d bằng “cắt patch rồi nhân ma trận”, ghép token ảnh với 10 token chữ, rồi lan truyền ngược một hàm mất mát giả để xem gradient đi tới đâu. 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

Token ảnh, projector và chuỗi cho LLM

Cần cài
pip install torch
Phần cứng
CPU là đủ. Vài giây trên máy phát triển, phần lớn là thời gian nạp PyTorch
# Patch embedding, projector và chuỗi cho LLM kiểu LLaVA, cỡ nhỏ chạy CPU
import torch
from torch import nn
torch.manual_seed(0)
B, C, H, W, P = 2, 3, 224, 224, 14 # 2 ảnh, 3 kênh, 224x224, patch 14x14
D_v, D, L_text = 64, 128, 10 # chiều token ảnh, chiều LLM, số token chữ
# Hai phần "đã huấn luyện sẵn", đóng băng như giai đoạn 1 của LLaVA
patch_embed = nn.Conv2d(C, D_v, kernel_size=P, stride=P) # thay bộ mã hóa
text_embed = nn.Embedding(1000, D) # thay bảng embedding của LLM
patch_embed.requires_grad_(False)
text_embed.requires_grad_(False)
# 1. Patch embedding: Conv2d với kernel = stride = P
img = torch.randn(B, C, H, W)
z = patch_embed(img).flatten(2).transpose(1, 2) # (B, N, D_v)
print("image tokens:", tuple(z.shape))
# Cùng kết quả khi cắt patch (unfold) rồi nhân một ma trận E
cols = nn.functional.unfold(img, kernel_size=P, stride=P).transpose(1, 2)
E = patch_embed.weight.flatten(1) # (D_v, C*P*P)
same = cols @ E.T + patch_embed.bias
print("conv == unfold + matmul:", torch.allclose(z, same, atol=1e-4))
# 2. Projector: MLP hai lớp D_v -> D -> D (GELU là lựa chọn của bài)
projector = nn.Sequential(nn.Linear(D_v, D), nn.GELU(), nn.Linear(D, D))
h_v = projector(z) # (B, N, D)
# 3. Token ảnh đứng trước embedding của token chữ: một chuỗi cho LLM
ids = torch.randint(0, 1000, (B, L_text))
seq = torch.cat([h_v, text_embed(ids)], dim=1)
print("sequence for the LLM:", tuple(seq.shape))
# Loss giả, chỉ để xem gradient đi tới đâu
seq.pow(2).mean().backward()
print("grad in projector:", projector[0].weight.grad is not None)
print("grad in patch_embed:", patch_embed.weight.grad is not None)
for name, m in [("patch_embed", patch_embed), ("projector", projector),
("text_embed", text_embed)]:
n = sum(p.numel() for p in m.parameters())
k = sum(p.numel() for p in m.parameters() if p.requires_grad)
print(f"{name}: {n} params, {k} trainable")

Kết quả mẫu

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

image tokens: (2, 256, 64)
conv == unfold + matmul: True
sequence for the LLM: (2, 266, 128)
grad in projector: True
grad in patch_embed: False
patch_embed: 37696 params, 0 trainable
projector: 24832 params, 24832 trainable
text_embed: 128000 params, 0 trainable

Đọc kết quả:

  • (2, 256, 64): mỗi ảnh 224×224224 \times 224, patch 14, cho N=16⋅16=256N = 16 \cdot 16 = 256 token Dv=64D_v = 64 chiều.
  • conv == unfold + matmul: True: Conv2d với kernel bằng stride đúng là “cắt patch, trải phẳng, nhân E\mathbf{E}” của Bước 2.
  • (2, 266, 128): sau projector, token ảnh có D=128D = 128 chiều như token chữ; 256+10=266256 + 10 = 266 là độ dài LL của chuỗi LLM đọc.
  • Gradient và tham số: chỉ projector có gradient và có tham số được học: 64⋅128+128+128⋅128+128=24 83264 \cdot 128 + 128 + 128 \cdot 128 + 128 = 24\,832. Đúng cấu hình giai đoạn 1 của LLaVA.

Ở mô hình thật, DD lên tới hàng nghìn (Llama 3 8B có D=4 096D = 4\,096), LLM có hàng tỉ tham số, và projector chỉ là một phần rất nhỏ.

Câu 1/4 Một mô hình kiểu LLaVA dùng ảnh 336×336336 \times 336, patch 14×1414 \times 14, nên mỗi ảnh có 576 token ảnh trong chuỗi. Bạn thay bằng bộ mã hóa nhận ảnh 672×672672 \times 672, vẫn patch 14×1414 \times 14. Điều nào đúng?
Câu 2/4 Bạn có một bộ mã hóa ảnh lấy từ CLIP và một LLM chỉ đọc chữ, cả hai đã huấn luyện xong, cùng vài trăm nghìn cặp ảnh và chú thích. Làm theo giai đoạn 1 của LLaVA, bạn cập nhật phần nào?
Câu 3/4 Một nhóm muốn thêm khả năng đọc ảnh cho LLM của họ, với hai yêu cầu: điểm trên các bài kiểm tra chỉ có chữ không được đổi, và token ảnh không được chiếm chỗ trong cửa sổ ngữ cảnh. Cách nào đáp ứng cả hai?
Câu 4/4 Ảnh một bàn cờ có nhiều patch trắng giống hệt nhau ở nhiều chỗ. Trong ViT, sau phép chiếu xiE\mathbf{x}_i \mathbf{E}, các patch này có cùng một vectơ. Điều gì giúp mô hình biết mỗi patch nằm ở đâu?

Ý chính:

  • LLM chỉ đọc dãy vectơ. ViT cắt ảnh thành N=HW/P2N = HW/P^2 patch, chiếu mỗi patch bằng một ma trận chung E\mathbf{E} và cộng embedding vị trí. Số token ảnh là chi phí thật: tốn ngữ cảnh, và số cặp attention tăng theo bình phương độ dài chuỗi.
  • Mẫu chung: bộ mã hóa ảnh (thường từ CLIP, đã học ghép ảnh với chữ) → projector (Dv→DD_v \to D) → LLM. LLaVA nối token ảnh vào chuỗi, L=N+LtextL = N + L_{\text{text}}, và huấn luyện hai giai đoạn: chỉ projector, rồi projector cùng LLM; bộ mã hóa luôn đóng băng.
  • Llama 3.2 Vision dùng cross-attention: truy vấn từ chữ, khóa và giá trị từ ảnh, chuỗi không dài thêm. LLM giữ nguyên (thí nghiệm Llama 3 và bài giới thiệu Llama 3.2), nên khả năng xử lý chữ không đổi.
  • Chameleon lượng tử hóa ảnh thành token rời rạc trong cùng từ vựng với chữ (1 024 token mỗi ảnh 512×512512 \times 512), nên đọc và sinh được cả hai, đổi lại huấn luyện khó hơn.

Đọ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