ViT 与 patch-token 原语


文档摘要

ViT 与 patch-token 原语 本节摘要:在谈任何多模态之前,图像必须先变成一段 Transformer 能消化的 token 序列。2020 年的 ViT(Vision Transformer)论文给出了答案:把图像切成 16×16 像素的 patch,做一次线性投影,再加一个位置嵌入。五年后,2026 年的每一个前沿模型(Claude Opus 4.7 原生 2576px、Gemini 3.1 Pro、Qwen3.5-Omni)仍是这样起步的——编码器从 ViT 换成了 DINOv2、SigLIP 2,加了 register token,位置编码换成了 2D-RoPE,但这个原语没变。

ViT 与 patch-token 原语

本节摘要:在谈任何多模态之前,图像必须先变成一段 Transformer 能消化的 token 序列。2020 年的 ViT(Vision Transformer)论文给出了答案:把图像切成 16×16 像素的 patch,做一次线性投影,再加一个位置嵌入。五年后,2026 年的每一个前沿模型(Claude Opus 4.7 原生 2576px、Gemini 3.1 Pro、Qwen3.5-Omni)仍是这样起步的——编码器从 ViT 换成了 DINOv2、SigLIP 2,加了 register token,位置编码换成了 2D-RoPE,但这个原语没变。本节将完整读完 patch-token 这条流水线,并用标准库 Python 从零实现,让 Phase 12 余下章节对「视觉 token」都有一个具体的心理模型。

学习目标

阅读完本节,你应当能够:

  1. 把一张 H×W×3 的图像,转换成带正确位置编码的 patch token 序列。
  2. 给定(patch 大小、分辨率、隐藏维度、深度),算出一个 ViT 的序列长度、参数量与 FLOPs。
  3. 说清让 ViT 从 2020 年研究走向 2026 年生产的三处升级:自监督预训练(DINO/MAE)、register token、原生分辨率打包。
  4. 针对 CLS 池化、均值池化、register token 三种图像级表示,知道下游任务该怎么选。

一、问题与直觉

Transformer 处理的是向量序列。文本天生就是序列(字节或 token)。而图像是三通道像素的二维网格——不是序列。如果你把每个像素都展平,一张 224×224 的 RGB 图就变成了 150528 个 token,在如此长度上做自注意力(对序列长度的平方复杂度)根本不可行。

2020 年前的做法是在前面外挂一个 CNN 特征提取器:ResNet 产出 7×7、每点 2048 维的特征图,把这 49 个 token 喂给 Transformer。能用,但继承了 CNN 的归纳偏置(平移等变性、局部感受野),也丢掉了 Transformer 对尺度的胃口。

Dosovitskiy 等人(2020)问了一个直白的问题:能不能跳过 CNN? 把图像切成固定大小的 patch(比如 16×16 像素),每个 patch 线性投影成一个向量,加位置嵌入,把这段序列喂给一个原版 Transformer。在当时这是异端——没有卷积的视觉。配上足够多的数据(先 JFT-300M,后 LAION),它在 ImageNet 上击败了 ResNet 并持续提升。

到 2026 年,ViT 这个原语已是毫无争议的地基。每一个开源 VLM 的视觉塔都是它的某种后代(DINOv2、SigLIP 2、CLIP、EVA、InternViT)。问题不再是「该不该用 patch」,而是「用什么 patch 大小、什么分辨率方案、什么预训练目标、什么位置编码」。

patch 即 token

给定形状 (H, W, 3) 的图像 x 与 patch 大小 P,你把图像切成 (H/P) × (W/P) 个互不重叠的 patch 网格。每个 patch 是一个 P×P×3 的像素立方体。把每个立方体展平成 3P² 维向量,套上一个共享的线性投影 W_E(形状 3P², D),把每个 patch 映射到模型的隐藏维度 D

对 ViT-B/16 这个经典配置:

  • 分辨率 224、patch 大小 16 → 网格 14×14 → 196 个 patch token。
  • 每个 patch 是 16×16×3 = 768 个像素值,投影到 D = 768
  • 再加一个可学习的 [CLS] token → 序列长度 197。

patch 投影在数学上等价于一个卷积核大小为 P、步幅为 P、输出通道为 D 的二维卷积。生产代码就是这么实现的——nn.Conv2d(3, D, kernel_size=P, stride=P)。「线性投影」是概念说法;「卷积核」是高效实现。

位置嵌入

patch 本身没有顺序——Transformer 看到的是一袋子 patch。早期 ViT 加的是可学习的一维位置嵌入(每个位置一个 768 维向量,共 197 个)。能用,但把模型绑死在了训练分辨率上:推理时改网格,就得对位置表做插值。

现代视觉骨干用 2D-RoPE(Qwen2-VL 的 M-RoPE、SigLIP 2 的默认)或分解的二维位置。2D-RoPE 根据 patch 的(行,列)索引旋转 query 和 key 向量,让模型从旋转角推断出相对二维位置。没有位置表,模型在推理时可以处理任意网格大小。

CLS token、池化输出与 register token

图像级的表示到底是什么?三种选择并存:

  1. [CLS] token。在 patch 序列前 prepend 一个可学习向量。所有 Transformer 块跑完后,CLS token 的隐藏状态就是图像表示。继承自 BERT。原版 ViT、CLIP 在用。
  2. 均值池化。对 patch token 的输出隐藏状态取平均。SigLIP、DINOv2、大多数现代 VLM 在用。
  3. register token。Darcet 等人(2023)观察到,没有显式 sink token 的 ViT 会长出高范数的「伪影」patch,劫持自注意力。加 4~16 个可学习的 register token 能吸收这部分负载,改善密集预测(分割、深度)的质量。DINOv2 和 SigLIP 2 都自带 register。

这个选择对下游任务很关键。CLS 适合分类。对于把 patch token 喂进 LLM 的 VLM,你完全不池化——每个 patch 都变成一个 LLM 输入 token。register 在交接给 LLM 前丢弃(它是脚手架,不是内容)。

预训练:监督、对比、掩码、自蒸馏

2020 年的 ViT 用 JFT-300M 上的监督分类预训练。很快被取代:

  • CLIP(2021):4 亿图文对上的对比学习。本系列第 02 节。
  • MAE(2021,He 等人):掩码 75% 的 patch,重建像素。纯图像上的自监督。
  • DINO(2021)/ DINOv2(2023):师生自蒸馏,不要标签,不要 caption。2023 年的 DINOv2 ViT-g/14 是最强的纯视觉骨干,也是「密集特征」用途的默认选择。
  • SigLIP / SigLIP 2(2023、2025):用 sigmoid 损失的 CLIP,加 NaFlex 支持原生宽高比。2026 年开源 VLM(Qwen、Idefics2、LLaVA-OneVision)的主流视觉塔。

你选什么预训练,决定了骨干擅长什么:CLIP/SigLIP 适合与文本的语义匹配,DINOv2 适合密集视觉特征,MAE 适合作为下游微调的起点。

缩放定律

ViT 缩放(Zhai 等人 2022)确立了一个 ViT 的质量在模型大小、数据量、算力上服从可预测的规律。固定算力下:

  • 更大的模型 + 更多数据 → 更好的质量。
  • patch 大小是序列长度与保真度的权衡杠杆。patch 14(DINOv2/SigLIP SO400m 的典型)比 patch 16 每张图给更多 token,对 OCR 与密集任务更好,速度更慢。
  • 分辨率是另一根大杠杆。从 224 到 384 再到 512 几乎总能涨点,代价是 FLOPs 平方级增长。

ViT-g/14(10 亿参数、patch 14、分辨率 224 → 256 token)与 SigLIP SO400m/14(4 亿参数、patch 14)是 2026 年开源 VLM 的两员主力编码器。

ViT 的参数量

完整计算见 code/main.py。对 224 分辨率下的 ViT-B/16:

patch_embed = 3 * 16 * 16 * 768 + 768 = 591k cls + pos = 768 + 197 * 768 = 152k block = 4 * 768^2 (QKVO) + 2 * 4 * 768^2 (MLP) + 2 * 2*768 (LN) = 12 * 768^2 + 3k = 7.1M 12 blocks = 85M final LN = 1.5k total ≈ 86M

加载任何 checkpoint 前,都用这种方式粗估每个 ViT。骨干大小决定了你下游任何 VLM 的显存下限。

2026 年的生产配置

2026 年大多数开源 VLM 出厂的编码器是原生分辨率(NaFlex)下的 SigLIP 2 SO400m/14。它有:

  • 4 亿参数。
  • patch 大小 14,默认分辨率 384 → 每张图 729 个 patch token。
  • 图像级任务用均值池化;VQA 时 729 个 patch 全部流进 LLM。
  • 4 个 register token,交接给 LLM 前丢弃。
  • 2D-RoPE 配图像级缩放,支持原生宽高比。

这个配置里的每一个决策,都能追溯到一篇你可以读到的论文。

二、从零实现

code/main.py 是一个 patch 分词器与几何计算器。它接收 (图像 H, W, patch P, 隐藏 D, 深度 L),输出:

  • 切块后的网格形状与序列长度。
  • 一个合成 8×8 像素玩具图像的 token 序列(走一遍「展平 + 投影」的完整路径)。
  • 按位置嵌入、patch 嵌入、Transformer 块、分类头拆分的参数量。
  • 目标分辨率下每次前向的 FLOPs。
  • 一张对比表:ViT-B/16 @ 224、ViT-L/14 @ 336、DINOv2 ViT-g/14 @ 224、SigLIP SO400m/14 @ 384。

patch 切块的伪代码

# 输入: image (H, W, 3), patch_size P, 投影矩阵 W_E (3P^2, D) H, W, _ = image.shape assert H % P == 0 and W % P == 0 grid_h, grid_w = H // P, W // P # 网格行列数 patches = [] # 切出每个 P×P×3 patch for i in range(grid_h): for j in range(grid_w): cube = image[i*P:(i+1)*P, j*P:(j+1)*P, :] # P×P×3 patches.append(cube.flatten()) # 长度 3P^2 的向量 # 共 grid_h * grid_w 个 patch,栈成 (N, 3P^2) x = stack(patches) tokens = x @ W_E # (N, D) —— 这就是 patch token # 加 2D-RoPE 或可学习位置嵌入 tokens = add_position(tokens, rows=grid_h, cols=grid_w)

💡 设计要点:nn.Conv2d(3, D, kernel_size=P, stride=P) 一次卷积等价于上面的「切 + 展平 + 投影」三步,且对 GPU 友好得多。理解原理用上面的循环,写生产代码用卷积。

序列长度与参数量

def vit_geometry(H, W, P, D, L, has_cls=True): grid_h, grid_w = H // P, W // P n_patches = grid_h * grid_w seq_len = n_patches + (1 if has_cls else 0) # 参数量(粗估,不含位置嵌入的小项) patch_embed = 3 * P * P * D # 投影矩阵 pos_embed = seq_len * D if learnable_1d else 0 per_block = 12 * D * D # QKVO 4D^2 + MLP 8D^2 blocks = L * per_block total = patch_embed + pos_embed + blocks return {"seq_len": seq_len, "tokens": n_patches, "params": total}

几何对比

配置 分辨率 patch 网格 token 数 参数量
ViT-B/16 224 16 14×14 196 ~86M
ViT-L/14 336 14 24×24 576 ~304M
DINOv2 ViT-g/14 224 14 16×16 256 ~1.1B
SigLIP SO400m/14 384 14 ~28×28 729 ~400M

跑一遍代码,把参数量对上论文里的数字;调一调 patch 大小与分辨率,感受 token 数的成本。

三、框架对比

主流开源视觉塔都已内置 patch 分词,API 形态各异:

  • timm / PyTorch:timm.create_model("vit_base_patch16_224", pretrained=True) 一行加载,内部即 Conv2d(3, 768, kernel_size=16, stride=16) + 可学习 1D 位置嵌入,默认带 CLS token。
  • transformers(ViT):ViTModel.from_pretrained(...) 暴露 patch_embeddingsposition_embeddings,便于做 ablation;切到 SiglipModel / Dinov2Model 时接口一致,但池化策略不同(CLASS → mean → register)。
  • JAX / Flax(原 DINOv2、SigLIP 实现):用 nn.Conv 做分词,2D-RoPE 用 jax.vmap 向量化旋转,原生支持任意网格,无需位置表插值。

工程取舍:需要可复现的论文数字,用 timm;需要接 HuggingFace 流水线,用 transformers;需要原生分辨率与最高吞吐,用各项目的 JAX 实现移植版。

四、可复用产物

本节产出 outputs/skill-patch-geometry-reader.md。给定一个 ViT 配置(patch 大小、分辨率、隐藏维度、深度),它产出 token 数、参数量、显存估算及理由说明。每次为 VLM 选视觉骨干时都用这个 skill——它能避免「token 数爆炸把 LLM 上下文塞满」的意外。

五、练习

  1. 算 Qwen2.5-VL:原生 1280×720 输入、patch 大小 14,patch-token 序列长度是多少?和只用 CLS 表示相比差多少倍?

  2. 视频预算:一帧 1080p(1920×1080)在 patch 14 下产出多少 token?30 FPS、5 分钟视频共多少视觉 token?哪一项最省成本:池化、帧采样,还是 token 合并?

  3. 均值池化:用纯 Python 在 196 个 patch token 上实现均值池化,验证它与 DINOv2 模型 forward 在请求 pooled embedding 时返回的结果一致。

  4. 读论文:读《Vision Transformers Need Registers》(arXiv:2309.16588)第 3 节,用两句话说明 register 吸收的是什么伪影、为什么这对下游密集预测重要。

  5. 支持 patch-n'-pack:改 code/main.py,给定一组不同分辨率的图像,产出一个打包序列与块对角注意力掩码。等你做到第 06 节时回头验证。

本节要点回顾

  1. 图像必须先成 token:Transformer 只吃向量序列,把图像切成 P×P patch、线性投影到 D 维,就是「视觉 token」的原语。
  2. patch 投影 = 步幅卷积:概念上是线性投影,实现上是 Conv2d(3, D, kernel_size=P, stride=P),高效且等价。
  3. 位置编码演进:可学习 1D 表 → 绑死分辨率;2D-RoPE 按行列旋转,支持任意网格,是 2026 年默认。
  4. 三种图像级表示:CLS(BERT 风格,适合分类)、均值池化(SigLIP/DINOv2 主流)、register token(吸收高范数伪影,改善密集预测)。
  5. VLM 不池化:每个 patch 都流进 LLM 当输入 token;register 是脚手架,交接前丢弃。
  6. 三处升级:自监督预训练(DINO/MAE/SigLIP)、register token、原生分辨率打包(NaFlex/Patch-n'-Pack)。
  7. 预训练决定用途:CLIP/SigLIP 适合图文匹配,DINOv2 适合密集特征,MAE 适合下游微调起点。
  8. 缩放定律:固定算力下,更大模型 + 更多数据更好;patch 14 比 16 更细(利于 OCR)但更慢;分辨率 224→384→512 几乎总涨点,FLOPs 平方级涨。
  9. 2026 主力编码器:SigLIP 2 SO400m/14,4 亿参数、patch 14、384 分辨率、729 token、4 个 register、2D-RoPE。
  10. 粗估参数量:每块约 12D²,加 patch 嵌入与位置嵌入,加载 checkpoint 前先算清显存下限。

下一节,我们将进入 CLIP——用对比学习把视觉 token 与文本 token 映射进同一个语义空间,这是后续几乎所有 VLM 跨模态融合的地基。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U