3.3 图像 Patch 化与位置编码


文档摘要

3.3 图像 Patch 化与位置编码 本节钻进 ViT 内部最关键的两个设计决策:patch 怎么切 与 位置怎么编。这两个看似细节的选择,决定了模型的能力上限、计算成本与泛化能力。 一、Patch 化的三个工程参数 把图像切成 patch 时,有三个核心参数: 参数一:Patch 大小 $P$ 每个 patch 的边长。常见选择: $P=32$:token 少(一张 224 图变成 49 个 patch),训练快,但精度上限低 $P=16$:标准选择(196 个 patch),精度与开销平衡 $P=14$:CLIP 等模型用,略多于 196(实际 256 个),精度更高 $P=8$ 或更小:高分辨率场景使用,token 数激增 直觉上,patch 越小,每个 patch

3.3 图像 Patch 化与位置编码

本节钻进 ViT 内部最关键的两个设计决策:patch 怎么切位置怎么编。这两个看似细节的选择,决定了模型的能力上限、计算成本与泛化能力。

一、Patch 化的三个工程参数

把图像切成 patch 时,有三个核心参数:

参数一:Patch 大小 P

每个 patch 的边长。常见选择:

  • P=32:token 少(一张 224 图变成 49 个 patch),训练快,但精度上限低
  • P=16:标准选择(196 个 patch),精度与开销平衡
  • P=14:CLIP 等模型用,略多于 196(实际 256 个),精度更高
  • P=8 或更小:高分辨率场景使用,token 数激增

直觉上,patch 越小,每个 patch 携带的信息越细,但 token 数量平方级增长,开销爆炸。

参数二:步长(stride)

标准 ViT 用 stride = patch 大小(不重叠)。但有些变体用更小 stride 让 patch 重叠,相当于在 patch 化阶段就引入了「平滑过渡」:

  • stride = P(不重叠):标准 ViT,token 数 = (H/P) \times (W/P)
  • stride < P(重叠):精度略涨,但 token 数显著增加,性价比通常不高

后续如 NaViT、SigLIP-2 等模型探索了变长 patch——同一张图的不同区域用不同 patch 大小,重要区域用小 patch、背景用大 patch,平衡精度与开销。

参数三:Patch 内部展开维度

每个 P \times P \times C 的 patch 可以展平成 P^2 C 维向量。例如 16 \times 16 \times 3 = 768 维。这与隐藏维度 D 不一定相同,所以需要一个线性投影 P^2 C \to D

工程上把这个投影与 patch 切分合并成一个卷积(如 3.2 节所述),是最高效的实现。

二、Token 数量爆炸:高分辨率的痛点

ViT 的 token 数 N 与图像面积成正比:

N = \left(\frac{H}{P}\right)^2

而 Self-Attention 的复杂度是 O(N^2),这意味着:

  • 224×224 输入,P=16N=196,注意力矩阵 38K 元素
  • 512×512 输入,P=16N=1024,注意力矩阵 1M 元素(26 倍
  • 1024×1024 输入,P=16N=4096,注意力矩阵 16M 元素(421 倍

这就是为什么高分辨率 ViT 极其昂贵——把分辨率从 224 提到 1024,注意力开销变 421 倍。

多模态大模型在处理文档、图表、医学影像等需要细粒度的场景时,会面临这个矛盾。常见的解决方案:

方案 思路 代表
动态分辨率 把大图切成多个 224 子图分别编码 Qwen-VL、LLaVA-NeXT
Token 压缩 用 Q-Former 等模块把 N 个 patch 压成 K 个 token BLIP-2
局部注意力 限制注意力在局部窗口 Swin Transformer
金字塔结构 多尺度 patch NaViT、SigLIP-2

三、为什么需要位置编码

回顾第2章:Self-Attention 对输入是置换不变的。如果不加位置编码,ViT 把 patch 顺序打乱后输出完全一样——这显然不对,因为图像的「左上」与「右下」是有语义差异的(天空通常在上方,地面在下方)。

位置编码的任务是给每个 patch 注入它在图像中的位置信息,让模型能区分「这块纹理在猫脸上」和「同样纹理在背景里」。

四、视觉位置编码的几种方案

方案一:一维学习式位置编码(标准 ViT)

最朴素的方案——把 patch 按行扫描成一维序列(0, 1, 2, ..., 195),每个位置分配一个可学习向量:

E_\text{pos} \in \mathbb{R}^{(N+1) \times D}

听起来反直觉:图像是二维的,为什么一维编码够用?

实证答案是:Transformer 自己能从数据中学到二维结构。一维位置编码提供了「这是第几个 patch」的标识,至于「第几个 patch 对应图像哪个位置」,模型在训练中自动建立映射。

但一维编码有一个明显短板——分辨率外推性差。训练时见过 14×14 网格(196 个位置),推理时给 28×28(784 个位置)就用不了。

方案二:二维学习式位置编码

把行索引与列索引分别编码:

E_\text{pos}^{(i,j)} = E_\text{row}[i] + E_\text{col}[j]

其中 E_\text{row} \in \mathbb{R}^{H/P \times D/2}E_\text{col} \in \mathbb{R}^{W/P \times D/2}

这种方式更契合图像的二维结构,但实验显示提升有限,所以标准 ViT 没有采用。

方案三: sinusoidal 二维编码

把文本 Transformer 的正弦编码推广到二维:

PE(i, j, 2k) = \sin(i / 10000^{2k/D}), \quad PE(i, j, 2k+1) = \cos(j / 10000^{2k/D})

不增加可学习参数,但分辨率外推性比学习式略好。

方案四:相对位置编码(如 Swin)

不编码「patch 在第几行第几列」,而编码「patch A 相对 patch B 偏移多少」。这种编码天然支持变分辨率,是 Swin Transformer 的特色。

方案五:RoPE 在视觉中的尝试

近期的视觉模型(如 ViT-RoPE)也尝试把 RoPE 引入视觉。把 patch 的二维坐标(行、列)映射到一个复合的旋转频率,让注意力分数只依赖 patch 之间的相对距离。

五、CLS Token 的作用

ViT 在序列最前面拼接一个特殊的可学习 token——[CLS]。它的工作方式是:

  1. 初始时是一个随机向量
  2. 在每层 Self-Attention 中,CLS 与所有 patch token 互相 attend
  3. 经过 L 层后,CLS 的 hidden state 聚合了全图信息
  4. 取 CLS 的输出做分类或作为全局表示

CLS 的物理意义是「专门用来做全局聚合的可学习槽位」。它不像 patch token 那样有具体内容,而是通过注意力机制主动收集它认为重要的 patch 信息

CLS vs 全局平均池化(GAP)

不使用 CLS 也能聚合全局——把所有 patch token 的输出取平均(Global Average Pooling, GAP):

h_\text{global} = \frac{1}{N} \sum_{i=1}^{N} h_i^{(L)}

两种方式的取舍:

方式 优点 缺点
CLS token 可学习的注意力权重,能动态聚焦关键区域 多一个 token,需要训练学怎么聚合
GAP 无参数,简单 等权平均,无法突出关键 patch

实证上两者差距不大,但 CLS 在多模态任务里更常用——因为它学到的聚合方式更适合「找出图中最重要的区域」这种语义任务。

在多模态大模型场景里,CLS token 通常被丢弃,只使用 N 个 patch token。因为 LLM 需要的是细粒度的局部信息(每个 patch 都对应图像的一小块),而不是一个压缩后的全局向量。

六、一个完整的 patch + 位置编码可视化

下面这张 SVG 把 patch 化与位置编码的可视化合到一起:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 360" font-family="sans-serif" font-size="12"> <!-- 原图 --> <rect x="20" y="40" width="160" height="160" fill="#e1f5ff" stroke="#333"/> <text x="100" y="30" text-anchor="middle" font-weight="bold">224×224 输入</text> <!-- patch 网格 --> <g transform="translate(220,40)"> <rect width="160" height="160" fill="#fff4e1" stroke="#333"/> <g stroke="#999" stroke-width="0.5"> <line x1="0" y1="40" x2="160" y2="40"/> <line x1="0" y1="80" x2="160" y2="80"/> <line x1="0" y1="120" x2="160" y2="120"/> <line x1="40" y1="0" x2="40" y2="160"/> <line x1="80" y1="0" x2="80" y2="160"/> <line x1="120" y1="0" x2="120" y2="160"/> </g> <text x="80" y="180" text-anchor="middle">4×4 patch 网格(示意)</text> </g> <!-- 展平后的序列 --> <g transform="translate(420,40)"> <text x="80" y="0" text-anchor="middle" font-weight="bold">展平为 token 序列</text> <rect x="0" y="20" width="30" height="120" fill="#ffe1e1" stroke="#333"/> <text x="15" y="85" text-anchor="middle" font-size="10">CLS</text> <g font-size="9"> <rect x="35" y="20" width="20" height="120" fill="#e8f5e1" stroke="#333"/><text x="45" y="85" text-anchor="middle">p1</text> <rect x="58" y="20" width="20" height="120" fill="#e8f5e1" stroke="#333"/><text x="68" y="85" text-anchor="middle">p2</text> <rect x="81" y="20" width="20" height="120" fill="#e8f5e1" stroke="#333"/><text x="91" y="85" text-anchor="middle">p3</text> <rect x="104" y="20" width="20" height="120" fill="#e8f5e1" stroke="#333"/><text x="114" y="85" text-anchor="middle">p4</text> <rect x="127" y="20" width="20" height="120" fill="#e8f5e1" stroke="#333"/><text x="137" y="85" text-anchor="middle">...</text> </g> <text x="80" y="160" text-anchor="middle">[CLS, p1, p2, ..., pN]</text> </g> <!-- 加位置编码 --> <g transform="translate(640,40)"> <text x="80" y="0" text-anchor="middle" font-weight="bold">+ 二维位置编码</text> <g font-size="9"> <rect x="20" y="20" width="120" height="20" fill="#f5e1ff" stroke="#333"/> <text x="80" y="35" text-anchor="middle">行 0: pos(0,0), pos(0,1), ...</text> <rect x="20" y="50" width="120" height="20" fill="#f5e1ff" stroke="#333"/> <text x="80" y="65" text-anchor="middle">行 1: pos(1,0), pos(1,1), ...</text> <rect x="20" y="80" width="120" height="20" fill="#f5e1ff" stroke="#333"/> <text x="80" y="95" text-anchor="middle">行 2: ...</text> <rect x="20" y="110" width="120" height="20" fill="#f5e1ff" stroke="#333"/> <text x="80" y="125" text-anchor="middle">行 3: ...</text> </g> <text x="80" y="160" text-anchor="middle">每个 patch 携带 (row, col) 信息</text> </g> <!-- 箭头 --> <line x1="180" y1="120" x2="215" y2="120" stroke="#333" marker-end="url(#arr2)"/> <line x1="380" y1="120" x2="415" y2="120" stroke="#333" marker-end="url(#arr2)"/> <line x1="600" y1="120" x2="635" y2="120" stroke="#333" marker-end="url(#arr2)"/> <defs> <marker id="arr2" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"> <path d="M0,0 L8,4 L0,8 z" fill="#333"/> </marker> </defs> </svg>

七、几个值得记住的工程经验

总结 ViT 在实际工程中几个被反复验证的经验:

  1. patch 16×16 是默认甜点——大多数模型都用 P=14 或 P=16
  2. 位置编码用一维学习式即可——除非需要强外推,否则不必折腾二维
  3. CLS token 主要用于分类——多模态场景通常只用 patch token
  4. 高分辨率靠子图切分——而非简单提升单张图分辨率
  5. 冻结 vs 微调视觉编码器——LLaVA 系列在指令微调阶段通常解冻视觉编码器,能进一步提升 OCR、细粒度识别能力

小结

Patch 化把图像变成 token 序列,位置编码注入二维空间信息。三个核心参数(patch 大小、步长、特征维度)决定了 token 数量与精度上限。CLS token 是可学习的全局聚合槽位,但多模态场景通常只用 patch token。高分辨率场景下 token 数量爆炸是核心痛点,催生了动态分辨率、token 压缩等多种工程方案。

下一节(3.4)我们看 CNN 与 Transformer 如何融合——这是另一条试图兼顾两者优势的技术路线。


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