【大模型】跟着张量走一遍 VLM:图像如何变成 LLM 的 token?|多模态大模型专栏③ 跟着张量走一遍 VLM图像如何变成 LLM 的 token多模态大模型专栏③一句话讲透本篇借助一个零依赖的小 demo跟随一个 batch 的张量从头走到尾看清「图像 → patch → ViT → Projector → 拼进文本流 → LLM」每一步的 shape 变化。走完会发现VLM 中真正新的组件仅有一个 Projector。写在前面前两篇 我们建立了两件事① VLM 的三组件架构② CLIP 如何训练出那双眼睛。但很多读者反馈架构图和损失函数都懂了可一张图到底是怎么一步步变成 LLM 能处理的 token 序列的脑子里还是糊的。这很正常——因为中间隔着太多 shape 变换光看公式记不住。本篇的解法很直接写一个玩具 VLM打印每一个中间张量的 shape跟着数据走一遍。零依赖纯 Python不用 torch/numpy复制就能跑玩具维度8×8 的小图、4 个 patch、d_v8、d_h12——以clarity realism为原则便于清晰追踪每一步 shapeNLP 视角每个步骤都标注它在 NLP 里的等价物本文代码对应code/01_vision/04_walkthrough_demo.pypython 04_walkthrough_demo.py即可运行。本文展示的所有输出都是真实运行结果。一、实验设置一个极简的玩具 VLM我们手搓一个最小可运行的 VLM 前向过程。超参数故意设得很小HW8# 8x8 的小图P4# patch 大小 4x4 - 一共 4 个 patchC3# RGB 三通道d_v8# 视觉隐藏维数d_h12# LLM 隐藏维数N_visual(H//P)*(W//P)# 4 个视觉 token模拟的 prompt 是bos image What is in this image ?8 个文本 token其中image是占位符。整个流程分 7 个 Stage下面逐个走。二、Stage A-B图像 → patchCV 的 “tokenization”Stage A图像就是一个张量image shape(8, 8, 3) 8x8x3 grid of RGB pixel values in [0,1] ↩ NLP 对照raw text string还没分词是连续的字符/像素图像在模型眼里就是(H, W, C)的数值网格。对应到 NLP这就是还没分词的原始文本。Stage B切 patch 分词核心类比patches shape(4, 48) N (H/P)*(W/P) (8/4)*(8/4) 4 patches; each patch flattened to P*P*C 48 floats ↩ NLP 对照BPE 分词 → token ids把连续图像切成离散 patch 切词这是整篇最关键的一步。把 8×8 的图按 4×4 切块得到(8/4)×(8/4) 4个 patch每个 patch 摊平成4×4×3 48维向量。记住这个类比它会贯穿整个 VLMCV视觉NLP文本连续像素图像原始文本字符串切 patchBPE 分词每个 patch 摊平成向量每个 token 的 idpatch 数 N (H/P)(W/P)序列长度 L 真实 VLM 用 224×224 的图、P14会得到(224/14)² 256个 patch。高分辨率如 448就是 1024 个——这就是第一篇说的一张图比一段文本还长的来源。三、Stage C-Dpatch embedding ViT和 BERT 几乎一样Stage Cpatch → embedding查表patch_embeddings shape(4, 8) (N4, d_v8) — like token embeddings, but from images ↩ NLP 对照nn.Embedding(ids) → token_emb (L, d)查表把离散 id 变成稠密向量 [CLS] token pos shape(5, 8) (1 N) 5 tokens; ViT now sees a sentence of patches ↩ NLP 对照BERT 的 [CLS] 位置编码机制完全一样每个 48 维的 patch经一层 Linear 映射成d_v8维的 embedding——和nn.Embedding把 token id 变成向量是同一回事只是一个用查表、一个用线性投影。然后像 BERT 一样前面拼一个[CLS]加上位置编码序列从 4 变成 5。Stage DViT Transformer没有新东西vision_features shape(5, 8) shape unchanged: (1N, d_v). All tokens have mixed information. ↩ NLP 对照Transformer encoder —— 和 BERT/GPT 主体一模一样没有新东西 Attention matrix (5x5, which token attends to which): CLS - [0.19 0.16 0.21 0.24 0.19] p0 - [0.20 0.19 0.22 0.19 0.19] p1 - [0.20 0.20 0.19 0.22 0.20] p2 - [0.32 0.16 0.17 0.20 0.15] p3 - [0.27 0.19 0.19 0.18 0.17]ViT 跑了一个 Transformer blockshape 不变还是(5, 8)但每个 token 已经混合了其他 token 的信息。看到这个 attention 矩阵NLP 工程师应该很有亲切感——它和 BERT 的自注意力完全一样只是这里的词是图像 patch。比如p2行对CLS的注意力最高0.32说明第 2 号 patch 的信息在往 CLS 汇聚。结论到这一步为止整个过程没有任何超出 Transformer 范畴的新东西。四、Stage EProjectorVLM 唯一的新组件patch_features (drop CLS) shape(4, 8) visual_embeddings shape(4, 12) (N, d_h) (4, 12). Now LLM-shaped, ready to splice. ↩ NLP 对照NLP 没有这一步 —— 这是 VLM 独有的翻译层把视觉空间对齐到 LLM 空间先丢掉[CLS]LLaVA 只保留 patch token然后过一个2 层 MLP Projector把d_v8维翻译成d_h12维——也就是 LLM 的隐藏维数。这是整个 VLM 流程里唯一 NLP 不存在的组件也是第一篇说的VLM 区别于纯 LLM 的灵魂。它的作用就是当翻译官SigLIP 说的视觉语是 8 维LLM 听懂的语言是 12 维Projector 负责对齐。动手建议将d_h改为 4096接近真实 LLM 的 hidden_size可发现 Projector 的参数量主要由d_v × d_h决定——这正是 Projector 通常仅有数百万至数千万参数、远小于 LLM 的原因。五、Stage F-G拼进文本流 loss maskStage Fsplice把视觉 token 缝进文本text_embeddings (pre-splice) shape(8, 12) (L_text8, d_h12). The image slot is a placeholder. full_sequence (post-splice) shape(11, 12) (L_text - 1 N) 7 4 11 tokens ↩ NLP 对照把 prompt 里的 image 占位符原地替换成 N 个真正的视觉 token这是 VLM 最巧妙的设计prompt 里原本有个image占位符1 个 token现在把它原地替换成 4 个真正的视觉 token。序列从 8 变成8 - 1 4 11。替换后视觉 token 和文本 token 就平权地住在同一条序列里了。Stage GLLM 看到的样子 loss maskToken layout (T text token, V visual token): T V V V V T T T T T T The LLM runs causal self-attention over this 11-token sequence. Loss is computed only on assistant tokens (appended after this in real training). Visual tokens NEVER contribute to loss — they are inputs, not targets. ↩ NLP 对照label mask只在 assistant 文本 token 上算 loss视觉 token 永远 mask 掉LLM 看到的就是这条T V V V V T T T T T T的序列1 个bos 4 个视觉 token 6 个文本 token。它在上面跑 causal attention和纯文本 LLM 没有任何区别。但有一个训练时的关键细节loss 只在 assistant 回复的文本 token 上算视觉 token 永远被 mask 掉、不参与 loss——因为它们是输入不是目标。这个 label mask 是 VLM 训练最容易踩坑的地方参见code/03_sft/sft_mask_walkthrough.py。六、核心一图NLP 路径 vs VLM 路径把上面 7 步串起来就是这张对照图demo 末尾的真实输出【NLP 路径】 text --BPE-- ids (L,) --Embedding-- token_emb (L, d) --Transformer-- hidden (L, d) -- LLM 【VLM 路径】 image (8,8,3) --patch 切分-- patches (4, 48) ◀ 相当于 BPE 分词 --Linear-- patch_emb (4, 8) ◀ 相当于 Embedding --CLS pos-- (5, 8) --ViT-- vis_feat (5, 8) ◀ Transformer无新东西 --Projector-- visual_emb (4, 12) ◀ VLM 独有翻译到 LLM 维度 --splice-- full_seq (11, 12) ◀ 视觉 文本拼成一条序列 -- LLM和 NLP 的 LLM 是同一个 一句话LLM 完全没变只是 token 从哪来 换了—— NLP 用 BPE 切词VLM 用 patch 切图 Projector 把图翻译成 LLM 的语言。patch 切分 分词Linear EmbeddingCLS posViT TransformerProjector · VLM 独有splice 拼进文本image (8,8,3)patches (4, 48)patch_emb (4, 8)(5, 8)vis_feat (5, 8)visual_emb (4, 12)full_seq (11, 12)LLM · 与 NLP 同一个七、从这次 walkthrough 里带走的 5 个洞察切 patch 就是 CV 的分词——一旦想通这一点VLM 的恐惧感就少了一半。ViT 没有任何新东西——它就是 BERT把 patch 当 token 而已。懂 Transformer 就懂 ViT。Projector 是 VLM 唯一的新组件——一个把视觉维度翻译到 LLM 维度的 MLP参数量不大却是 Stage 1 训练的唯一主角。splice 后视觉 token 与文本 token 平权——(11, 12)里没人知道哪 4 个是图、哪 7 个是字LLM 一视同仁。视觉 token 不参与 loss——它们是输入不是目标label mask 必须正确处理否则训练会崩。八、动手验证强烈建议这篇的价值在于自己跑一遍。打开code/01_vision/04_walkthrough_demo.py做三个小实验把H W 16patch 数从 4 变成 16亲眼验证高分辨率 → 视觉 token 变多这条因果链。把P 2patch 更细token 更多——理解 patch 大小如何权衡粒度与开销。改image_slot_idx把视觉 token 拼到 prompt 的不同位置理解 splice 的灵活性。 本篇涉及代码文件说明code/01_vision/04_walkthrough_demo.py零依赖端到端 walkthrough本文增强版每步含「NLP 对照」 末尾路径对照图小结Stage做了什么NLP 等价A图像 (H,W,C) 张量raw textB切 patchBPE 分词Cpatch embedding CLS posEmbedding 位置编码DViTTransformer无新东西EProjector 维度翻译NLP 无VLM 独有Fsplice 拼进文本流替换image占位符GLLM attention loss masklabel mask下一篇我们进入对齐机制从 BLIP-2 的 Q-Former 到 LLaVA 的 MLP Projector再到 Qwen-VL 的 M-RoPE搞懂视觉 token 如何获得 2D 位置感知、动态分辨率怎么工作。关注专栏更新第一时间通知。专栏《从 NLP 到 VLM多模态大模型研发实战》上一篇② 手写 CLIP 本篇③ 张量级 walkthrough 下一篇④ 对齐机制上· Projector