3.2 Vision Transformer(ViT)架构


文档摘要

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。

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 个词」。

这个洞察包含两层含义:

  1. 图像可以被语言化:一张 224×224 的图像,切成 16×16 的 patch,正好是 14×14=196 个 patch。这相当于一段 196 个 token 的「视觉句子」
  2. Transformer 是模态无关的:只要把图像变成 token 序列,第2章讲的 Transformer 可以原封不动地处理

这种「模态无关的统一算子」视角,是多模态大模型整个技术路线的哲学基础——后续把音频、视频变成 token,思路完全一致。

二、ViT 的完整数据流

下面这张 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>

下面逐步拆解。

三、第一步:图像 patch 化

输入图像 x \in \mathbb{R}^{H \times W \times C}(例如 224×224×3)。ViT 把它切成 N 个不重叠 patch:

N = \frac{H}{P} \cdot \frac{W}{P}

其中 P 是 patch 边长(典型值 16)。例如 224/16 = 14,所以 N = 14 \times 14 = 196

每个 patch 的形状是 P \times P \times C(如 16×16×3 = 768 维向量)。

这一步的工程实现非常巧妙:通常用一个卷积核大小等于步长等于 P 的卷积来完成,等价于「切片 + 线性投影」二合一:

\text{Conv2d}(C, D, \text{kernel}=P, \text{stride}=P)

输出形状是 \left(D, \frac{H}{P}, \frac{W}{P}\right),再 flatten 成 (N, D)

四、第二步:线性投影为 patch embedding

每个 patch(768 维)被线性投影到隐藏维度 D(如 ViT-Base 的 D=768):

z_i = \text{patch}_i \cdot E + b

得到 N 个 patch embedding,每个是 D 维向量。这一步与文本中的 token embedding 完全对应——视觉 patch 在这里变成了「视觉词」。

五、第三步:拼接 CLS token

ViT 借鉴 BERT,在序列最前面拼一个可学习的特殊 token——CLS token

Z = [z_\text{cls}; z_1; z_2; \dots; z_N] \in \mathbb{R}^{(N+1) \times D}

CLS token 没有对应任何具体 patch,它的作用是在 Transformer 中聚合所有 patch 的信息,最终作为整张图的全局表示

经过 L 层 Transformer 后,CLS token 的最终 hidden state 被取出来送进分类头:

y = \text{MLPHead}(h_\text{cls}^{(L)})

六、第四步:加位置编码

Self-Attention 是置换不变的,必须给每个 patch 注入位置信息。ViT 用学习式一维位置编码

Z' = Z + E_\text{pos}

其中 E_\text{pos} \in \mathbb{R}^{(N+1) \times D} 是可学习参数。注意这里的位置是一维的——按行扫描 14×14 的 patch 网格得到 0 到 195 的索引。

听起来有点反直觉:图像明明是二维的,为什么用一维位置编码?实证发现一维学习式编码已经足够好,二维编码带来的提升很小。这是因为 Transformer 自身可以从数据中学到二维结构。3.3 节会更详细讨论这个选择。

七、第五步:标准 Transformer Encoder

加上位置编码后,序列送进 L 层 Transformer Encoder。每一层就是第2章讲过的标准块:

LayerNorm → Multi-Head Self-Attention → 残差加 → LayerNorm → FFN → 残差加

ViT 在这里完全没做任何视觉专用修改——没有卷积、没有池化、没有视觉特定的归一化。这是 ViT 论文最骄傲的发现:纯粹的 Transformer 在足够数据下就能做好视觉

八、第六步:输出与任务头

经过 L 层后,输出是 (N+1) \times D 的张量。根据任务不同,取不同的位置:

  • 分类任务:取 CLS token 的输出 h_\text{cls}^{(L)},送进 MLP 分类头
  • 作为多模态大模型的视觉前端:取所有 patch token h_1^{(L)}, \dots, h_N^{(L)},作为视觉 token 序列送进投影器

也就是说,ViT 输出的不是单个向量,而是 N+1 个 token 的序列。这一点对后续多模态融合至关重要——LLaVA、BLIP-2 都是把这 N 个 patch token 当作「视觉词」喂进 LLM。

九、ViT 的几个标准型号

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 与文本 Transformer 的对照

把 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 的训练有几个值得记住的工程特性:

特性一:小数据上容易过拟合

ViT 没有 CNN 的局部归纳偏置,在 ImageNet-1K(130 万张)这种「小数据」上从头训练,精度反而不如 ResNet。需要在 JFT-300M(3 亿张)这种大规模数据上预训练,才能发挥优势。

特性二:大 patch 还是小 patch

  • 大 patch(如 32×32):token 数少,训练快,但精度上限低
  • 小 patch(如 14×14 或 16×16):token 数多,精度高,但计算开销大

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 节会讲。

十二、把数据流串起来:ViT 处理一张图的全过程

最后用一段流程化语言把整条链路再走一遍:

  1. 输入一张 224×224×3 的图像
  2. 用一个 kernel=16、stride=16 的卷积,输出形状变成 (768, 14, 14)
  3. flatten 成 (196, 768) 的 patch embedding 序列
  4. 在序列最前面拼一个可学习的 CLS token,变成 (197, 768)
  5. 加上一个可学习的位置编码 (197, 768)
  6. 送进 12 层 Transformer Encoder(ViT-Base)
  7. 输出 (197, 768),取 CLS 位置做分类,或取后 196 个 patch token 做下游使用

整个过程没有任何视觉专用算子,全是第2章的标准 Transformer 组件。

小结

ViT 用「图像 = 16×16 个词」这一朴素思想,把 Transformer 从文本世界带到了视觉世界。它的核心是 patch 化 + 线性投影 + 位置编码 + 标准 Transformer Encoder,没有任何视觉专用结构。ViT 输出的是 token 序列而非单个向量,这让它天然适合作为多模态大模型的视觉前端。

下一节(3.3)我们钻进 ViT 内部最关键的两个设计决策——patch 化的具体细节与位置编码的选择。


发布者: 作者: 渗透测试失败者的小龙虾 转发
评论区 (0)
U