目录
上一门课,我们把一句话沿着 Token ID、Embedding、Transformer 一路送进大模型;现在换一张图片,它没有词表,也没有现成的“单词边界”,怎样才能进入同样的计算框架?
看一张游戏截图时,人一眼就能同时看见角色、掩体和准星,而模型最先拿到的却是一个像素数组,里面没有“敌人”这个标签,只有每个位置上的红、绿、蓝数值。
这一课先记住一条主线:图片切成小块 → 每块投影成向量 → 加上位置 → Transformer 让各块交换信息。
读完这一课,你应该能算出一张图片产生多少个视觉 Token,并解释它和文字 Token 的区别。
1、为什么要把图片切成小块?
1.1 Transformer 需要序列,图片天然是二维的
Transformer 接收的基本形式是“一排向量”:文字经过分词与查表后,已经是这样的序列;图片则通常是 $H\times W\times C$ 的数组,其中 $H$ 是高度,$W$ 是宽度,RGB 图片的通道数 $C=3$。
最直接的办法是把每个像素当一个 Token,但一张 $224\times224$ 的图片就有 50,176 个位置,而普通全局 Attention 要比较每一对位置,序列一长,计算代价就会迅速增加。
ViT 把图片按网格切成等大的小块,也就是 Patch,再把每块变成一条向量,就像把拼图拆成卡片,每张卡片保留一片画面和它的位置。[1]
1.2 Patch 不是物体,也不是一个词
一个 Patch 可能包含半只眼睛、一片背景,或者同时包含角色边缘和天空,因为网格边界不会自动沿着物体轮廓切开。切 Patch 只是安排输入,识别物体要靠后续学习。
这也解释了为什么它被叫作视觉 Token:它是参与序列计算的基本单元,而不是图片中一个天然存在的“词”。
| 文字进入 LLM | 图片进入基础 ViT |
|---|---|
| Tokenizer 得到离散 Token ID | 按网格切出连续像素块 |
| 根据 ID 查 Embedding 表 | 对每块像素做可学习的线性投影 |
| 加入文字位置线索 | 加入 Patch 位置线索 |
| Transformer 处理序列 | Transformer 处理序列 |
2、图片如何变成一排向量?
先用一张小图片看清楚这个过程:假设它只有 $4\times4$ 个像素的 RGB 图片。把它切成 $2\times2$ 的小块,就得到 A、B、C、D 四块,每块有 12 个数;再用同一套投影规则,把每块的 12 个数变成 6 个特征值。
一块图片对应一条向量,四块图片就对应四条向量。“一排向量”指把这四条向量按顺序排起来,不是把整张图片压成一条向量。
图中的 [4,6] 可以理解成一张四行六列的表:每行是一块图片的表示,每列是一个特征维度。加入 CLS 后多一行,变成 [5,6];加位置与经过 Attention 都不会再改变这个形状。
下面把这个小例子换回常用配置:四块变成 196 块,每条向量从 6 维换成 768 维,计算过程仍然一样。图中的特征符号与连线仅作示意,不代表训练模型的实际数值。
2.1 第一步:切块,再把每块摊平
先假设高宽都能被 Patch 边长 $P$ 整除,且各块没有重叠,那么 Patch 数量是:
\[N=\frac{H}{P}\frac{W}{P},\qquad D_{patch}=P^2C\]对于 $224\times224\times3$、$P=16$:一行 14 块,一共 $N=196$ 块;每块摊平成 $16\times16\times3=768$ 个数。
一张 RGB 图片 224 × 224 × 3
切成网格 14 × 14 个 Patch
每块摊平 196 × 768
“摊平”只是按固定顺序重排像素,本身不学习参数,也不会丢掉每块内部的数值;模型再通过后续投影权重学习这些像素的组合方式。
2.2 第二步:所有 Patch 共享一套投影规则
设第 $i$ 块摊平后为行向量 $x_i\in\mathbb R^{P^2C}$,视觉模型的隐藏宽度为 $D$:
\[e_i=x_iW_E+b_E,\qquad W_E\in\mathbb R^{P^2C\times D},\quad b_E\in\mathbb R^D\]这里 $W_E$ 是训练中学出来的参数,所有 Patch 共用它,不同位置不需要各自一套投影权重。
投影把像素组合成模型可以使用的特征,但每一维通常没有可以直接命名的固定含义。
如果 $D=768$,形状仍是 $196\times768$,但数值已改变;输入像素值的数量与隐藏向量的维度相等,只是这个配置的巧合,也可以选 $D=192$,得到 $196\times192$。
2.3 第三步:位置与 CLS 分别解决什么问题?
把卡片随意打乱,人会看不懂拼图,而 Self-Attention 也没有天然的二维坐标知识。没有位置机制时,它对输入排列是等变的:输入怎么换序,对应输出也怎么换序。
经典 ViT 为每个序列位置加入可学习向量,还在最前面放一个可学习的 CLS Token,用于汇总分类所需的信息:
\[Z_0=[x_{cls};e_1;e_2;\ldots;e_N]+E_{pos},\qquad E_{pos}\in\mathbb R^{(N+1)\times D}\]公式中的分号表示沿 Token 维拼接,CLS 则是单独学习的向量,既不是从某块图片裁出来的,也不是像素平均值;它和 Patch 一起经过多层计算,最后通过分类头输出类别分数。
位置表虽然按一维序列保存,但在固定的按行排列规则下,每个索引对应一个确定的二维网格位置,因此经典 ViT 不必显式输入一对行列坐标。改变分辨率时,Patch 网格和位置表长度也会改变,通常需要位置插值或另行设计位置机制。
2.4 第四步:每块开始“看见”其他块
在单个 Attention Head 中,忽略 Batch 维:
\[Q=ZW_Q,\quad K=ZW_K,\quad V=ZW_V\] \[A=\operatorname{softmax}_{\text{每行}}\left(\frac{QK^T}{\sqrt{d_h}}\right),\qquad O=AV\]$d_h$ 是每个 Head 的宽度,$A$ 的第 $i$ 行描述当前位置从哪些位置读取信息,这些权重作用于 V,得到加权结果。基础 ViT 使用双向注意力,每个 Patch 可以读取所有 Patch 和 CLS,不需要文字自回归生成的因果 Mask。
这样,角色边缘的一块就能结合周围背景和其他身体部位的信息,不过 Attention 权重只表示信息混合关系,不等于物体识别证据。
经典 ViT 使用 Pre-LN 和残差连接,第 $\ell$ 层可以写成:
\[\widetilde Z_\ell=Z_{\ell-1}+\operatorname{MSA}(\operatorname{LN}(Z_{\ell-1}))\] \[Z_\ell=\widetilde Z_\ell+\operatorname{MLP}(\operatorname{LN}(\widetilde Z_\ell))\]MSA 用多头注意力混合不同位置的信息,MLP 则对每个位置分别做非线性变换,二者通常都保持序列形状不变。分类时,取最终归一化后的 CLS 接分类头,得到 logits。
| 阶段 | 带 Batch 的形状($B=2,D=768$) |
|---|---|
| 输入图片(PyTorch 通道在前) | [2, 3, 224, 224] |
| 每块摊平 | [2, 196, 768] |
| Patch Embedding | [2, 196, 768] |
| 拼接 CLS、加位置 | [2, 197, 768] |
| 每个 Encoder Block 后 | [2, 197, 768] |
| 取 CLS 接 10 类分类头 | [2, 10] |
3、用最小代码把过程连起来
下面手写切块、投影、CLS 和位置拼接,Encoder 使用 PyTorch 标准组件,构成一个尚未训练、用来检查前向过程的 Mini-ViT。
这段代码可以在已安装 PyTorch 的环境中运行,其中随机图片只用于检查形状,输出不代表识别结果。
查看 Mini-ViT 完整代码
import torch
from torch import nn
class MiniViT(nn.Module):
def __init__(self, image_size=32, patch_size=4,
dim=192, heads=3, depth=2, classes=10):
super().__init__()
assert image_size % patch_size == 0
assert dim % heads == 0
self.image_size = image_size
self.patch_size = patch_size
n = (image_size // patch_size) ** 2
self.proj = nn.Linear(3 * patch_size ** 2, dim)
self.cls = nn.Parameter(torch.zeros(1, 1, dim))
self.pos = nn.Parameter(torch.zeros(1, n + 1, dim))
nn.init.normal_(self.cls, std=0.02)
nn.init.normal_(self.pos, std=0.02)
block = nn.TransformerEncoderLayer(
d_model=dim, nhead=heads, dim_feedforward=4 * dim,
dropout=0.0, activation="gelu",
batch_first=True, norm_first=True,
)
self.encoder = nn.TransformerEncoder(block, num_layers=depth)
self.norm = nn.LayerNorm(dim)
self.head = nn.Linear(dim, classes)
def patchify(self, x):
b, c, h, w = x.shape
p = self.patch_size
assert c == 3 and h == w == self.image_size
# [B,C,H/P,W/P,P,P] -> [B,H/P,W/P,C,P,P]
patches = x.unfold(2, p, p).unfold(3, p, p)
return patches.permute(0, 2, 3, 1, 4, 5).reshape(
b, (h // p) * (w // p), c * p * p
)
def forward(self, x):
tokens = self.proj(self.patchify(x))
cls = self.cls.expand(x.shape[0], -1, -1)
z = torch.cat([cls, tokens], dim=1) + self.pos
z = self.norm(self.encoder(z))
return self.head(z[:, 0])
torch.manual_seed(0)
model = MiniViT().eval()
x = torch.randn(2, 3, 32, 32)
with torch.no_grad():
patches = model.patchify(x)
tokens = model.proj(patches)
logits = model(x)
print(patches.shape) # torch.Size([2, 64, 48])
print(tokens.shape) # torch.Size([2, 64, 192])
print(logits.shape) # torch.Size([2, 10])
3.1 为什么不能直接 reshape 图片?
[B,C,H,W] 的连续存储顺序不等于“逐个空间 Patch”,所以直接把整个数组 reshape(B,N,-1),通常会把错误的像素组合在一起。这里先用 unfold 取块,再用 permute 按行排列块,最后才摊平每块。
可以用一张数字图片检查顺序:
probe = torch.arange(32 * 32).reshape(1, 1, 32, 32)
probe = probe.expand(1, 3, 32, 32).float()
print(model.patchify(probe)[0, 0, :16])
# 第一个通道、左上角 4×4 的像素:
# 0,1,2,3,32,33,34,35,64,65,66,67,96,97,98,99
3.2 Conv2d 为什么也能实现同一件事?
nn.Conv2d(3, dim, kernel_size=p, stride=p) 在每个不重叠区域使用共享线性权重,把输出 [B,D,H/P,W/P] 的空间维摊平再转置,就得到 [B,N,D]。它与上述 Linear 方案在对应权重下等价,常用于高效实现 Patch Embedding。
两种写法的权重都可学习,不过使用 Conv2d 做输入投影,并不意味着后面的主体是 CNN。
4、视觉向量怎样走到语言里?
ViT 得到视觉表示后,还需要通过训练目标和连接模块服务语言任务,下面看三个经典例子。
4.1 CLIP:让图片和文字可以比较
CLIP 分别编码图片和文字,通过图文配对训练,让匹配的表示更接近,其中视觉骨干可以是 ViT,也可以是 ResNet。[2]
图片 → 图像编码器 → 汇总与投影 → 图像向量
描述 → 文本编码器 → 汇总与投影 → 文本向量
↓
在共同空间比较相似度
ViT 回答“怎样处理图片”,CLIP 的训练目标进一步回答“怎样使视觉表示与语言对应”。对比损失与温度参数留到 CLIP 课展开。
4.2 LLaVA:给 LLM 接上一条视觉输入路线
经典 LLaVA 连接预训练视觉编码器与 LLM,由连接模块把视觉特征映射到语言模型所使用的宽度,再通过视觉指令训练获得图文问答能力,具体结构随版本变化。[3]
图片 → Vision Encoder → Patch 特征 → Projector
↓
问题 → 文本 Embedding ─────────→ LLM → 回答
常见做法是使用一组 Patch 特征,而不是整张图片的单个分类向量;LLM 接收的是连续视觉向量,不需要把每块 Patch 查成文字 Token ID。投影维度匹配是接口条件,语义对齐仍需要训练。
4.3 Qwen-VL:连接器还可以改变序列长度
初代 Qwen-VL 使用视觉编码器、视觉语言适配器和 LLM 的组合,并面向理解、文字读取和定位等任务训练。这里把初代架构作为学习例子,不能据此推断所有后续 Qwen-VL 版本使用相同连接器。[4]
连接器还可能压缩序列,因此编码器产生的 Patch 数,不一定等于送入 LLM 的视觉 Token 数。
视频还要处理时间与帧间关系,单帧 ViT 只是其中一部分。
5、动手实验:Patch 大小与 Token 数
改变图片边长和 Patch 大小,观察网格与 Token 数,再点击“下一步”按行查看各块;画面只演示切块,不代表模型识别结果。
左上角 Patch → 768 个像素值 → 768 维向量 → 加上位置向量。
固定 $P=16$,边长从 224 翻倍到 448,Patch 数从 196 变成 784,即四倍,而普通全局 Attention 的分数矩阵规模约为十六倍;算上 CLS 时是 $(785/197)^2\approx15.88$ 倍。这里比较的是每个 Head 的分数矩阵元素数,不是整个模型耗时或显存的精确倍数;投影、MLP 与 Attention 实现也会影响实际成本。
做成动画时,可以依次展示切块、摊平与投影、加入位置和 Attention 信息流,并将向量数值与连线标明为示意。
6、容易混淆的七件事
| 误解 | 应该怎样理解 |
|---|---|
| 一个 Patch 就是一个物体 | Patch 是固定网格,物体可以跨越许多块 |
| 图片 Token 也要查文字词表 | 基础 ViT 的 Token 是连续像素投影,没有文字 Token ID |
| 摊平就是压缩 | 摊平是重排;投影是否降维取决于 $P^2C$ 与 $D$ |
| 768 是图片固有维度 | 它是具体配置;隐藏宽度可以另选 |
| ViT 总有 197 个 Token | 197 来自 224/16 与一个 CLS,其他配置不同 |
| 加位置后模型就懂空间 | 位置提供线索,模型还需要通过训练利用它 |
| Attention 热图能证明判断正确 | 权重是信息混合线索,需要结合输出、数据与验证 |
还有两个实现边界:真实图片要按模型约定进行缩放、裁剪、通道排列和归一化;高宽无法被 $P$ 整除时,需要明确 padding、裁剪或动态处理策略。本课的最小代码直接拒绝不匹配的尺寸,避免悄悄丢像素。
7、记住这条主线
ViT 先把二维像素组织成一排向量,再通过训练学出视觉表示:Patch 决定序列的基本粒度,投影改变表示方式,位置补充空间线索,Transformer 则让局部内容结合全局上下文。CLS 是经典分类汇总方式,多模态模型可以另外选择 Patch 特征和连接器。
[B,3,H,W]
↓ 切块:N=(H/P)×(W/P)
[B,N,3P²]
↓ 共享线性投影
[B,N,D]
↓ CLS + Position
[B,N+1,D]
↓ 双向 Transformer Encoder
分类头 / 图文对齐 / 连接语言模型
离开这一课前,试着不看正文回答:
- $384\times384$、$P=16$ 有多少个 Patch?加入一个 CLS 后呢?(576 与 577。)
- 为什么
[B,C,H,W]不能直接随意 reshape 成 Patch 序列? - 去掉位置信息并重新排列 Patch,会怎样影响对应输出和 CLS 汇总?
- 为什么让视觉向量和文字向量维度相等,还不足以让 LLM 看懂图片?
- 游戏截图里一个目标只有十几个像素宽,应怎样验证缩放与裁剪是否丢掉证据?
下一课学习 Vision Encoder:同样以 ViT 为骨干,分类训练、CLIP 图文对齐和 DINO 自监督为什么会学出不同的表示?先把“图片怎样进入 Transformer”讲清楚,再去理解“训练目标怎样塑造它看到的世界”。
8、参考文献(References)
- Dosovitskiy, A., Beyer, L., Kolesnikov, A., et al. “An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale.” ICLR, 2021. arXiv:2010.11929.↩
- Radford, A., Kim, J. W., Hallacy, C., et al. “Learning Transferable Visual Models From Natural Language Supervision.” ICML, 2021. arXiv:2103.00020.↩
- Liu, H., Li, C., Wu, Q., and Lee, Y. J. “Visual Instruction Tuning.” NeurIPS, 2023. arXiv:2304.08485.↩
- Bai, J., Bai, S., Yang, S., et al. “Qwen-VL: A Versatile Vision-Language Model for Understanding, Localization, Text Reading, and Beyond.” arXiv preprint arXiv:2308.12966, 2023.↩