视觉 Transformer(ViT)


文档摘要

视觉 Transformer(ViT) 本节摘要:把图像切成 patch,每个 patch 当一个词,跑标准 Transformer,就这么干,别回头。本节从零实现 patch 嵌入、可学习位置嵌入、类 token 和 Transformer 编码器块,搭出一个微型 ViT;讲清 ViT 当初为何被认为非得海量预训练数据不可、DeiT 与 MAE 又如何证明并非如此;对比 ViT、Swin、ConvNeXt 三种架构先验;并用 在小数据集上微调预训练 ViT。读完本节,你就掌握了 2026 年分割、检测、多模态、视频几乎一切视觉模型背后的同一套块结构。 对应原课程:Phase 4 · Lesson 14 · (原英文 )。

视觉 Transformer(ViT)

本节摘要:把图像切成 patch,每个 patch 当一个词,跑标准 Transformer,就这么干,别回头。本节从零实现 patch 嵌入、可学习位置嵌入、类 token 和 Transformer 编码器块,搭出一个微型 ViT;讲清 ViT 当初为何被认为非得海量预训练数据不可、DeiT 与 MAE 又如何证明并非如此;对比 ViT、Swin、ConvNeXt 三种架构先验;并用 timm 在小数据集上微调预训练 ViT。读完本节,你就掌握了 2026 年分割、检测、多模态、视频几乎一切视觉模型背后的同一套块结构。

对应原课程:Phase 4 · Lesson 14 · vision-transformers(原英文 phases/04-computer-vision/14-vision-transformers/docs/en.md)。

学习目标

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

  1. 从零实现 patch 嵌入、可学习位置嵌入、类 token、Transformer 编码器块,搭出最小 ViT。
  2. 解释 ViT 为何曾被视为必须海量预训练数据,直到 DeiT 与 MAE 证明并非如此。
  3. 对比 ViT、Swin、ConvNeXt 的架构先验(无先验、局部窗口注意力、卷积主干)。
  4. timm 在小数据集上微调预训练 ViT,走标准的线性探测 / 微调配方。

一、问题与直觉

整整十年,卷积就是计算机视觉的代名词。CNN 有强归纳偏置——局部性、平移等变性——没人觉得能取代。然后 Dosovitskiy 等(2020)证明:把图像展平成 patch 后直接套一个普通 Transformer,完全不用任何卷积机制,在足够规模上能匹敌甚至超越最好的 CNN。

坑在于「足够规模」。ViT 在 ImageNet-1k 上训不过 ResNet;但在 ImageNet-21k 或 JFT-300M 上预训练、再在 ImageNet-1k 上微调,就能反超。结论是 Transformer 缺乏有用先验,但能从足够多的数据里学到。后续工作(DeiT、MAE、DINO)证明,只要训练配方对——强增强、自监督预训练、蒸馏——ViT 在小数据上也能训好。

到 2026 年,纯 CNN 在边缘设备上仍有竞争力(ConvNeXt 最强),但 Transformer 统治了其他一切:分割(Mask2Former、SegFormer)、检测(DETR、RT-DETR)、多模态(CLIP、SigLIP)、视频(VideoMAE、VJEPA)。ViT 的块结构是必须懂的。

流水线

七步。patch → token → 注意力 → 分类器。每个变体(DeiT、Swin、ConvNeXt、MAE 预训练)只改其中一两步,其余不动。

Patch 嵌入

第一个卷积是秘密。核大小 16、步长 16,把 224×224 的图变成 14×14 网格,每格是 16×16 patch 投影成 768 维嵌入。这一个卷积既切 patch 又做线性投影。

输入: (3, 224, 224) 卷积 (3 -> 768, k=16, s=16, 无填充): 输出: (768, 14, 14) 展平空间: (196, 768)

196 个 patch = 196 个 token。每个 token 的特征维是 768(ViT-B)、1024(ViT-L)或 1280(ViT-H)。

类 token

一个可学习向量前置到序列前:

tokens = [CLS; patch_1; patch_2; ...; patch_196] 形状 (197, 768)

经过 N 个 Transformer 块后,[CLS] 的输出就是全局图像表示。分类头只读这一个向量。

位置嵌入

Transformer 没有内置的空间位置概念。给每个 token 加一个可学习向量:

tokens = tokens + learned_pos_embedding (同样形状 (197, 768))

这个嵌入是模型参数;梯度训练让它适配 2D 图像结构。也有 2D 正弦替代方案,但实践中很少用。

Transformer 编码器块

标准结构。多头自注意力、MLP、残差连接、pre-LayerNorm。

x = x + MSA(LN(x)) x = x + MLP(LN(x)) MLP 是两层带 GELU: Linear(d -> 4d) -> GELU -> Linear(4d -> d)

ViT-B/16 堆 12 个这样的块,每块 12 个注意力头,共 8600 万参数。

为何用 pre-LN

早期 Transformer 用 post-LN(x = LN(x + sublayer(x))),超过 6~8 层就难训,必须热身。pre-LN(x = x + sublayer(LN(x)))能稳定训更深网络且无需热身。每个 ViT 和每个现代 LLM 都用 pre-LN。

Patch 大小权衡

  • 16×16 patch → 196 token,标准。
  • 32×32 patch → 49 token,更快但分辨率更低。
  • 8×8 patch → 784 token,更细但 O(n²) 注意力成本爆。

patch 越大,token 越少,更快但空间细节更少。SwinV2 在层级窗口里用 4×4 patch。

DeiT 在 ImageNet-1k 上训 ViT 的配方

原版 ViT 要 JFT-300M 才能打败 CNN。DeiT(Touvron 等,2020)只改了四点,就在 ImageNet-1k 上把 ViT-B 训到 81.8% top-1:

  1. 重度增强:RandAugment、Mixup、CutMix、Random Erasing。
  2. 随机深度(训练时随机丢掉整个块)。
  3. 重复增强(每批同一张图采样 3 次)。
  4. 从 CNN 教师蒸馏(可选,进一步提升精度)。

每个现代 ViT 训练配方都源自 DeiT。

Swin vs ConvNeXt

  • Swin(Liu 等,2021)——基于窗口的注意力。每块在局部窗口内做注意力,交替块平移窗口以跨窗口混合信息。在保留注意力算子的同时,带回了类似 CNN 的局部性先验。
  • ConvNeXt(Liu 等,2022)——重新设计的 CNN,采纳了 Swin 的架构选择(深度卷积、LayerNorm、GELU、倒瓶颈)。证明差距不在「注意力 vs 卷积」,而在「现代训练配方 + 架构」。

2026 年,ConvNeXt-V2 与 Swin-V2 都是生产级;选哪个取决于推理栈(ConvNeXt 在边缘编译得更好)和预训练语料。

MAE 预训练

掩码自编码器(He 等,2022):随机掩掉 75% 的 patch,只让编码器处理可见的 25%,训一个小解码器从编码器输出重建被掩的 patch。预训练后丢掉解码器,微调编码器。

MAE 让 ViT 在 ImageNet-1k 上单独就能训、达到 SOTA,是当前默认的自监督配方。

二、从零实现

步骤 1:Patch 嵌入

import torch import torch.nn as nn class PatchEmbedding(nn.Module): def __init__(self, in_channels=3, patch_size=16, dim=192, image_size=64): super().__init__() assert image_size % patch_size == 0 self.proj = nn.Conv2d(in_channels, dim, kernel_size=patch_size, stride=patch_size) num_patches = (image_size // patch_size) ** 2 self.num_patches = num_patches def forward(self, x): x = self.proj(x) return x.flatten(2).transpose(1, 2)

一个卷积、一次展平、一次转置。这就是图像到 token 的全部步骤。

步骤 2:Transformer 块

pre-LN、多头自注意力、带 GELU 的 MLP、残差连接。

class Block(nn.Module): def __init__(self, dim, num_heads, mlp_ratio=4, dropout=0.0): super().__init__() self.ln1 = nn.LayerNorm(dim) self.attn = nn.MultiheadAttention(dim, num_heads, dropout=dropout, batch_first=True) self.ln2 = nn.LayerNorm(dim) self.mlp = nn.Sequential( nn.Linear(dim, dim * mlp_ratio), nn.GELU(), nn.Dropout(dropout), nn.Linear(dim * mlp_ratio, dim), nn.Dropout(dropout), ) def forward(self, x): a, _ = self.attn(self.ln1(x), self.ln1(x), self.ln1(x), need_weights=False) x = x + a x = x + self.mlp(self.ln2(x)) return x

nn.MultiheadAttention 负责切头、缩放点积、输出投影。batch_first=True 让形状是 (N, seq, dim)

步骤 3:ViT

class ViT(nn.Module): def __init__(self, image_size=64, patch_size=16, in_channels=3, num_classes=10, dim=192, depth=6, num_heads=3, mlp_ratio=4): super().__init__() self.patch = PatchEmbedding(in_channels, patch_size, dim, image_size) num_patches = self.patch.num_patches self.cls_token = nn.Parameter(torch.zeros(1, 1, dim)) self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, dim)) self.blocks = nn.ModuleList([ Block(dim, num_heads, mlp_ratio) for _ in range(depth) ]) self.ln = nn.LayerNorm(dim) self.head = nn.Linear(dim, num_classes) nn.init.trunc_normal_(self.pos_embed, std=0.02) nn.init.trunc_normal_(self.cls_token, std=0.02) def forward(self, x): x = self.patch(x) cls = self.cls_token.expand(x.size(0), -1, -1) x = torch.cat([cls, x], dim=1) x = x + self.pos_embed for blk in self.blocks: x = blk(x) x = self.ln(x[:, 0]) return self.head(x) vit = ViT(image_size=64, patch_size=16, num_classes=10, dim=192, depth=6, num_heads=3) x = torch.randn(2, 3, 64, 64) print(f"输出: {vit(x).shape}") print(f"参数量: {sum(p.numel() for p in vit.parameters()):,}")

约 280 万参数——CPU 上也能跑的微型 ViT。真正的 ViT-B 是 8600 万;把 dim=768, depth=12, num_heads=12 套进同一个类即可。

步骤 4:健全性检查——单图推理

logits = vit(torch.randn(1, 3, 64, 64)) print(f"logits: {logits}") print(f"probs: {logits.softmax(-1)}")

应无报错地跑通,概率和为 1。

三、框架对比

timm 提供 ImageNet 预训练权重的各种 ViT 变体,一行搞定:

import timm model = timm.create_model("vit_base_patch16_224", pretrained=True, num_classes=10)

timm 是 2026 年视觉 Transformer 的生产默认,在统一 API 下支持 ViT、DeiT、Swin、Swin-V2、ConvNeXt、ConvNeXt-V2、MaxViT、MViT、EfficientFormer 等几十种。

多模态(图像+文本)用 transformers 里的 CLIP、SigLIP、BLIP-2、LLaVA,它们的图像编码器都是 ViT 变体。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-vit-vs-cnn-picker.md:一个提示词——按数据集大小、算力、推理栈,在 ViT、ConvNeXt、Swin 之间挑。
  • skill-vit-patch-and-pos-embed-inspector.md:一个技能——校验 ViT 的 patch 嵌入和位置嵌入形状是否匹配模型期望的序列长度,抓住最常见的移植 bug。

五、练习

  1. (简单) 打印上述微型 ViT 一次前向里每个中间张量的形状。确认:输入 (N, 3, 64, 64) → patch (N, 16, 192) → 加 CLS (N, 17, 192) → 分类器输入 (N, 192) → 输出 (N, num_classes)
  2. (中等) 在第 4 节的合成 CIFAR 数据集上微调预训练 timm ViT-S/16,与 ResNet-18 微调对比,报告训练时间和最终精度。
  3. (困难) 为微型 ViT 实现 MAE 预训练:掩掉 75% 的 patch,训编码器+小解码器重建被掩 patch。评估预训练前后在合成数据上的线性探测精度。

本节要点回顾

  1. ViT = patch 当词 + 标准 Transformer——七步流水线,所有变体只改其中一两步。
  2. Patch 嵌入是第一个卷积——核=步长=patch 大小,既切 patch 又线性投影。
  3. 类 token + 可学习位置嵌入——CLS 前置做全局表示,位置嵌入告诉 Transformer 每个 patch 来自哪。
  4. pre-LN 必须用——x + sublayer(LN(x)),能稳定训深网络且无需热身;所有现代 ViT 和 LLM 都用。
  5. ViT 缺乏先验但能学——大规模预训练能补上,DeiT/MAE/DINO 证明小数据也能训好。
  6. DeiT 四招:重增强、随机深度、重复增强、CNN 蒸馏。
  7. MAE 是默认自监督——掩 75% 重建,丢解码器微调编码器。
  8. Swin(窗口注意力)vs ConvNeXt(现代 CNN)——差距不在注意力 vs 卷积,而在现代训练配方 + 架构;ConvNeXt 在边缘编译更好。
  9. timm 是生产默认——统一 API 下几十种 ViT 变体,多模态图像编码器也都是 ViT。

下一节进入实时边缘部署——把这些模型压到手机、Jetson、浏览器里跑,讲量化、蒸馏、ONNX/TensorRT 工具链。


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