3.2 Vision Transformer(ViT)架构 2020 年 Google 的论文 An Image Is Worth 16x16 Words 提出了 Vision Transformer(ViT)。它的核心思想极其朴素:把图像切成 patch,把每个 patch 当作一个 token,然后用标准 Transformer Encoder 处理。本节我们完整走一遍 ViT 的数据流。 一、ViT 的核心洞察 ViT 论文标题本身就是它的全部思想——「一张图相当于 16×16 个词」。 这个洞察包含两层含义: 图像可以被语言化:一张 224×224 的图像,切成 16×16 的 patch,正好是 14×14=196 个 patch。
2020 年 Google 的论文 An Image Is Worth 16x16 Words 提出了 Vision Transformer(ViT)。它的核心思想极其朴素:把图像切成 patch,把每个 patch 当作一个 token,然后用标准 Transformer Encoder 处理。本节我们完整走一遍 ViT 的数据流。
ViT 论文标题本身就是它的全部思想——「一张图相当于 16×16 个词」。
这个洞察包含两层含义:
这种「模态无关的统一算子」视角,是多模态大模型整个技术路线的哲学基础——后续把音频、视频变成 token,思路完全一致。
下面这张 SVG 图描述了 ViT 的端到端处理流程:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 360" font-family="sans-serif" font-size="13"> <!-- 图像 --> <rect x="20" y="120" width="80" height="80" fill="#e1f5ff" stroke="#333"/> <text x="60" y="220" text-anchor="middle">224×224 图像</text> <!-- patch 网格 --> <g transform="translate(140,120)"> <rect width="120" height="120" fill="none" stroke="#666"/> <line x1="30" y1="0" x2="30" y2="120" stroke="#999"/> <line x1="60" y1="0" x2="60" y2="120" stroke="#999"/> <line x1="90" y1="0" x2="90" y2="120" stroke="#999"/> <line x1="0" y1="30" x2="120" y2="30" stroke="#999"/> <line x1="0" y1="60" x2="120" y2="60" stroke="#999"/> <line x1="0" y1="90" x2="120" y2="90" stroke="#999"/> <text x="60" y="140" text-anchor="middle">14×14 patch 网格</text> </g> <!-- 线性投影 --> <rect x="300" y="120" width="100" height="80" fill="#fff4e1" stroke="#333" rx="8"/> <text x="350" y="155" text-anchor="middle">线性投影</text> <text x="350" y="175" text-anchor="middle">Conv 16×16, stride 16</text> <text x="350" y="220" text-anchor="middle">→ [196, D]</text> <!-- CLS token --> <rect x="440" y="120" width="60" height="80" fill="#ffe1e1" stroke="#333" rx="4"/> <text x="470" y="155" text-anchor="middle">[CLS]</text> <text x="470" y="175" text-anchor="middle">token</text> <!-- + 位置编码 --> <circle cx="540" cy="160" r="18" fill="#e8f5e1" stroke="#333"/> <text x="540" y="165" text-anchor="middle">+</text> <text x="540" y="220" text-anchor="middle">位置编码</text> <!-- Transformer Encoder --> <rect x="600" y="100" width="120" height="120" fill="#f5e1ff" stroke="#333" rx="8"/> <text x="660" y="145" text-anchor="middle">Transformer</text> <text x="660" y="165" text-anchor="middle">Encoder</text> <text x="660" y="185" text-anchor="middle">× L 层</text> <!-- CLS 输出 --> <rect x="760" y="120" width="80" height="80" fill="#ffe1e1" stroke="#333" rx="4"/> <text x="800" y="155" text-anchor="middle">CLS 表示</text> <text x="800" y="175" text-anchor="middle">→ 分类头</text> <!-- 箭头 --> <line x1="100" y1="160" x2="135" y2="160" stroke="#333" marker-end="url(#arr)"/> <line x1="260" y1="160" x2="295" y2="160" stroke="#333" marker-end="url(#arr)"/> <line x1="400" y1="160" x2="435" y2="160" stroke="#333" marker-end="url(#arr)"/> <line x1="500" y1="160" x2="525" y2="160" stroke="#333" marker-end="url(#arr)"/> <line x1="555" y1="160" x2="595" y2="160" stroke="#333" marker-end="url(#arr)"/> <line x1="720" y1="160" x2="755" y2="160" stroke="#333" marker-end="url(#arr)"/> <defs> <marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"> <path d="M0,0 L8,4 L0,8 z" fill="#333"/> </marker> </defs> </svg>
下面逐步拆解。
输入图像 x \in \mathbb{R}^{H \times W \times C}(例如 224×224×3)。ViT 把它切成 N 个不重叠 patch:
其中 P 是 patch 边长(典型值 16)。例如 224/16 = 14,所以 N = 14 \times 14 = 196。
每个 patch 的形状是 P \times P \times C(如 16×16×3 = 768 维向量)。
这一步的工程实现非常巧妙:通常用一个卷积核大小等于步长等于 P 的卷积来完成,等价于「切片 + 线性投影」二合一:
输出形状是 \left(D, \frac{H}{P}, \frac{W}{P}\right),再 flatten 成 (N, D)。
每个 patch(768 维)被线性投影到隐藏维度 D(如 ViT-Base 的 D=768):
得到 N 个 patch embedding,每个是 D 维向量。这一步与文本中的 token embedding 完全对应——视觉 patch 在这里变成了「视觉词」。
ViT 借鉴 BERT,在序列最前面拼一个可学习的特殊 token——CLS token:
CLS token 没有对应任何具体 patch,它的作用是在 Transformer 中聚合所有 patch 的信息,最终作为整张图的全局表示。
经过 L 层 Transformer 后,CLS token 的最终 hidden state 被取出来送进分类头:
Self-Attention 是置换不变的,必须给每个 patch 注入位置信息。ViT 用学习式一维位置编码:
其中 E_\text{pos} \in \mathbb{R}^{(N+1) \times D} 是可学习参数。注意这里的位置是一维的——按行扫描 14×14 的 patch 网格得到 0 到 195 的索引。
听起来有点反直觉:图像明明是二维的,为什么用一维位置编码?实证发现一维学习式编码已经足够好,二维编码带来的提升很小。这是因为 Transformer 自身可以从数据中学到二维结构。3.3 节会更详细讨论这个选择。
加上位置编码后,序列送进 L 层 Transformer Encoder。每一层就是第2章讲过的标准块:
LayerNorm → Multi-Head Self-Attention → 残差加 → LayerNorm → FFN → 残差加
ViT 在这里完全没做任何视觉专用修改——没有卷积、没有池化、没有视觉特定的归一化。这是 ViT 论文最骄傲的发现:纯粹的 Transformer 在足够数据下就能做好视觉。
经过 L 层后,输出是 (N+1) \times D 的张量。根据任务不同,取不同的位置:
也就是说,ViT 输出的不是单个向量,而是 N+1 个 token 的序列。这一点对后续多模态融合至关重要——LLaVA、BLIP-2 都是把这 N 个 patch token 当作「视觉词」喂进 LLM。
Google 发布的 ViT 有三个标准规模,被后续工作广泛复用:
| 型号 | 层数 L | 隐藏维度 D | 头数 h | 参数量 |
|---|---|---|---|---|
| ViT-Small | 12 | 384 | 6 | ~22M |
| ViT-Base | 12 | 768 | 12 | ~86M |
| ViT-Large | 24 | 1024 | 16 | ~307M |
| ViT-Huge | 32 | 1280 | 16 | ~632M |
CLIP ViT-L/14(CLIP 论文中使用的视觉塔)就是 ViT-Large、patch 大小 14,是 LLaVA、BLIP-2 等多模态大模型最常用的视觉前端。
把 ViT 与第2章讲的文本 Transformer 放在一起对照,能看到惊人的对称性:
| 步骤 | 文本 Transformer | ViT |
|---|---|---|
| 输入 | 句子字符串 | 图像张量 |
| 切分 | BPE 分词 | Patch 切分 |
| Embedding | 词嵌入查表 | 线性投影(或卷积) |
| 特殊 token | [CLS] / [SEP] / [BOS] | [CLS] |
| 位置编码 | 一维 sinusoidal 或 RoPE | 一维学习式 |
| 主干 | Transformer Encoder | Transformer Encoder(完全相同) |
| 输出 | token 序列 + CLS | patch token 序列 + CLS |
这种对称性正是 ViT 设计的精髓——用同一套算子处理不同模态。后续把音频、视频搬过来时,思路完全一致。
ViT 的训练有几个值得记住的工程特性:
ViT 没有 CNN 的局部归纳偏置,在 ImageNet-1K(130 万张)这种「小数据」上从头训练,精度反而不如 ResNet。需要在 JFT-300M(3 亿张)这种大规模数据上预训练,才能发挥优势。
CLIP 实验发现 ViT-L/14 比 ViT-L/16 略好,而 ViT-L/14 在高分辨率上微调(如 336 分辨率)能再涨 1-2 个点。这就是为什么 LLaVA 等模型默认用 ViT-L/14@336。
ViT 训练时位置编码是固定的(如 224 分辨率对应 196 个位置),推理时给 512 分辨率就破坏了位置编码的对应关系。这就是为什么后续出现了 NaViT(原生任意分辨率)、SigLIP-2(多分辨率训练)等改进——第3.5 节会讲。
最后用一段流程化语言把整条链路再走一遍:
整个过程没有任何视觉专用算子,全是第2章的标准 Transformer 组件。
ViT 用「图像 = 16×16 个词」这一朴素思想,把 Transformer 从文本世界带到了视觉世界。它的核心是 patch 化 + 线性投影 + 位置编码 + 标准 Transformer Encoder,没有任何视觉专用结构。ViT 输出的是 token 序列而非单个向量,这让它天然适合作为多模态大模型的视觉前端。
下一节(3.3)我们钻进 ViT 内部最关键的两个设计决策——patch 化的具体细节与位置编码的选择。