这篇论文我前前后后读过五六遍,也照着官方代码手写过一版实现。第一次读到标题“An Image is Worth 16x16 Words”时,我在“Words”这个词上卡了很久。图像里哪来的单词?后来想通了:这里的Words指的不是自然语言里的词,而是Transformer处理的基本单位——token。只要能把图像拆成一组token序列,Transformer就能处理图像,根本不需要修改架构。这就是ViT最核心的表述,也是整篇论文里最容易误解、也最值得展开的地方。
接下来我围绕这篇《ViT: An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》展开。这里的Image Recognition对应图像识别,在ViT的语境里基本等价于图像分类,比如ImageNet、CIFAR这类任务。除了把关键段落逐句解释一遍,我还会专门说说读中文翻译时容易踩的坑,以及从论文到代码复现时的实操细节。适合谁看?准备入门视觉Transformer、想搞懂图像tokenization机制、或者正在看DETR/MAE等后续工作却因为基础概念模糊而卡住的人,这篇都能当你的第一块跳板。
1. 标题里藏着整篇论文的压缩逻辑:16x16和Words
标题是最值得先读的部分。很多翻译版本把它处理成“一张图像相当于16x16个词”,这个说法不算错,但会让人误以为图像被切成了16个词,而不是每16x16像素一小块。正确的读法应该是:一张图像等价于若干个尺寸为16x16的小块,每个小块在后续模型中扮演“单词”(token)的角色。
1.1 token这个翻译到底怎么理解最顺
NLP里的token可以是一个单词、一个子词,甚至一个字符。在ViT里,token就是一个图像patch经过线性投影后得到的向量。整张224x224的图,按16x16的patch尺寸去切,可以得到14x14=196个patch。每个patch展平后是16x16x3=768维向量,经过投影变成D维向量,这个D维向量就是视觉token。
动手算一遍更容易建立体感:如果输入是224x224x3,patch size是16,N=(224/16)^2=196,那么输入序列长度就是196。如果patch size改成14,序列长度变成256;改成32,序列长度变成49。序列越长,self-attention的复杂度平方级增长,所以patch size的选取直接影响训练成本。ViT的核心变体多用16,是精度和计算量的折中。
1.2 为什么不用卷积也能做视觉任务
传统CNN有三个天然假设:局部性、平移等变性、层级结构。卷积核只在局部滑窗,所以天然关注邻近像素;权重共享让同一特征不管出现在图像哪里都能被检测到;池化和步长逐渐扩大感受野。这三个假设在小数据集上非常好用,因为它们把很多视觉先验直接内建到网络结构里,不需要大量数据去学。
ViT的做法是把这个先验全部抽掉。Patch Embedding处虽然还保留了一点局部性(一个patch内部的像素直接被线性映射),但之后的Transformer层里,每个token都能看到所有其他token,注意力权重完全由数据学习得到。这样做的代价是:在小数据集上很难学会“相邻像素往往属于同一物体”这类基本规律,所以需要海量数据来补偿。这也引出了论文里最重要的实验结论:规模是关键。
从工程视角看,标题里的“Words”其实是在给后来的所有视觉Transformer工作定调:任何能token化的信号,都能塞进Transformer。图像切成patch,后续的视频切成tube、点云切成group,都是同一套逻辑。所以读懂ViT,不只是读懂一个分类模型,而是读懂一种处理思路的范式转变。
2. Patch Embedding:图像变成词序列的那道工序
论文在Method部分的第一句就定义了输入变换。原文大概是这样的:
“The standard Transformer receives as input a 1D sequence of token embeddings. To handle 2D images, we reshape the image x∈R^{H×W×C} into a sequence of flattened 2D patches x_p∈R^{N×(P^2·C)}, where (H,W) is the resolution of the original image, C is the number of channels, (P,P) is the resolution of each image patch, and N=HW/P^2 is the number of patches.”
翻译过来就是:标准Transformer接收的是一维token embedding序列。为了处理二维图像,我们把图像x重塑为展平的二维patch序列,N是patch数量。然后每个patch通过可学习的线性投影映射到D维。这一整段其实就是整个ViT最核心的“输入接口”。
2.1 三种等价写法,建议掌握前两种
在实际代码里,Patch Embedding有三种常见写法:
- 手动切patch再线性投影,也就是最贴近论文公式的写法。用einops的rearrange可以一行完成:
x = rearrange(x, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p) x = nn.Linear(p * p * c, embed_dim)(x)- 用卷积代替,我个人最推荐。因为kernel_size和stride都等于patch size,卷积天然实现了“不重叠地切patch并投影”这个操作:
x = nn.Conv2d(in_channels=c, out_channels=embed_dim, kernel_size=p, stride=p)(x) x = x.flatten(2).transpose(1, 2)- 用Unfold(im2col)再线性映射,工程上少见,但理论上是等价的一种实现。
三种方式数学等价,但第二种工程效率最高。我实测在同样batch下,卷积实现的patch embedding比手动切patch的内存更友好。timm库的ViT实现也默认用Conv2d,算是社区验证过的选择。
注意线性投影的输入维度是P^2·C,输出维度是D,也就是transformer的hidden size。D不是随便定的,它决定整个模型宽度。ViT-Base用D=768,ViT-Large用D=1024,ViT-Huge用D=1280。这个投影矩阵是在整个训练过程中学习的,本质上是让模型自己判断哪些像素组合对后续分类最有用。
2.2 class token:为什么要在序列最前面插一个特殊向量
光有196个patch token,Transformer输出后怎么得到整图的分类结果?最简单的做法是全局平均池化,但ViT选择了另一个方案:复制BERT的[CLS] token设计,在序列最前面加一个可学习的class token。
这个class token本身不来自图像,它只是一个随机初始化的向量,和所有patch token一起进Attention。由于它没有固定语义,模型会通过训练慢慢把对分类有用的全局信息“汇聚”到这个位置上。最后分类时,只取class token对应的输出向量,过一个LayerNorm和一个线性分类头。论文实验表明class token和全局池化效果接近,但class token的优势在于和Transformer的逻辑闭环:输入是序列,输出也是序列,不需要引入额外的池化算子。
2.3 位置编码:用1D还是2D,结果出人意料
Transformer本身不区分顺序,所以必须给序列里的每个token一个位置标记。ViT用的是一维可学习位置编码,直接加到token向量上:z_0 = [x_class; x_p^1 E; x_p^2 E; ... ; x_p^N E] + E_pos。很多读者第一次看到这里会疑惑:图像明明是二维的,为什么用一维位置编码?论文里专门比较了一维位置编码、二维位置编码、相对位置编码,结论是三种方案性能差不多,所以作者选择最简单的1D方案。原因可以这样理解:patch序列本身已经是二维图像被强行拉直后的结果,模型完全有能力通过注意力机制去还原patch之间的空间关系;位置编码只需要提供一个不重复的位置线索,具体怎么解释这个线索,模型会自己学。
值得注意的是,训练完的position embedding可视化之后会有明显的空间结构——相邻patch的编码向量更相似,这相当于模型在自己摸索“像素在二维平面上有邻居关系”这一事实。复现时建议把这个编码矩阵画成热力图看看,你会更直观地理解位置编码到底在做什么。
3. Transformer Encoder:为什么“原封不动”反而是最聪明的决定
ViT在架构上的最大“卖点”其实就是“没改”。它没有像早期工作那样在CNN里塞注意力模块,也没有设计图像专用的attention变体,而是直接把NLP的Transformer Encoder搬了过来。
3.1 从公式看Encoder内部的四个部件
论文把每个Encoder block描述得很清楚:
z'l = MSA(LN(z{l-1})) + z_{l-1} z_l = MLP(LN(z'_l)) + z'_l
其中LN是LayerNorm,MSA是多头自注意力,MLP是两层带GELU激活的全连接。每个block内部结构可以拆成四个部分:
- LayerNorm:先归一化再进注意力/MLP,这种叫Pre-LN,和原始Transformer的Post-LN不同。Pre-LN在训练深层模型时更稳定,梯度更容易流动。
- 多头自注意力:每个token的向量分别映射成Query、Key、Value,计算所有token两两之间的注意力权重,然后加权聚合。
- 残差连接:每个子层都有一条“短路”,让信息能直接跨层传递。
- MLP:对每个token独立做非线性变换,通常是先升维再降维。比如D=768时,MLP内部先升到3072维,再降回768维。
原文里MLP是两层线性层中间夹一个GELU。GELU比ReLU更平滑,在Transformer类模型里几乎成了标配。激活函数的差别看起来小,但在深层次、大规模训练下会影响收敛速度,这是从NLP迁移过来的经验,不是写论文时顺手选的。
3.2 和原版Transformer的关键差异只有三个
- 没有Decoder。ViT只用了Encoder来编码整张图像,分类head直接建在Encoder输出上。它不需要像机器翻译那样生成序列,所以Decoder是多余的。
- 激活函数用GELU而不是ReLU。原始Transformer里FFN用的是ReLU,ViT跟随了BERT的GELU习惯。
- Norm位置前置。Post-LN是在每个子层输出之后加norm,Pre-LN是在子层之前加norm。ViT采用Pre-LN,便于扩大模型规模。
这三处差异单独看都不大,但合在一起决定了ViT能以几十亿甚至几百亿参数在大规模数据上稳定训练。如果你自己改过Transformer结构就会知道,Norm位置这个细节经常被忽略,却是最容易出现“换了个激活就发散”的坑。
3.3 模型配置表:Base/Large/Huge各是什么量级
论文给出了三档模型配置,参数规模从千万到几亿再到几亿不等。以patch size=16时最常见的几档为例:
| 模型 | 层数 | Hidden Size | MLP Size | 注意力头数 | 参数量 |
|---|---|---|---|---|---|
| ViT-B/16 | 12 | 768 | 3072 | 12 | 86M |
| ViT-L/16 | 24 | 1024 | 4096 | 16 | 307M |
| ViT-H/14 | 32 | 1280 | 5120 | 16 | 632M |
模型命名里末尾的数字代表patch size。ViT-B/16就是Base规模、patch size 16。同样隐藏层配置下,patch越小,序列越长,计算量越大,但空间信息越完整。所以ViT-L/16和ViT-L/32相比,前者精度通常更好但训练更贵。阅读翻译版时,看到“模型变体”这个词,可以自动映射到这张表。
3.4 归纳偏置到底去哪了
论文里有一句经常被引用的话:Vision Transformer比CNN的归纳偏置少。所谓归纳偏置,就是结构里预先刻好的假设。CNN通过卷积核天然假设邻近像素有关系、特征平移了仍算同一个特征;ViT没有这些假设,所有关系都靠注意力学习。别小看这句话,它直接解释了为什么ViT在第一篇论文的小规模实验里表现一般:没有先验可用,只能靠大数据来学。
用一句生活化的话总结:CNN像一个出生就带着地方方言的人,学习语言快,但天花板锁死;ViT像一个语言天才,起步难,给足语料后能说得比谁都好。数据规模就是那个临界点。
4. 训练策略:大规模数据不是锦上添花,是必要条件
如果只看架构,很容易觉得ViT和CNN只是在“归纳偏置”上有差别。真正让ViT成立的是训练策略,整套流程一句话概括:先在巨量数据上预训练,再在下游任务上微调。
4.1 预训练数据集为什么至少要ImageNet-21k起步
论文里有一个非常扎眼的实验结论:只在ImageNet-1k(约128万张图)上训练时,ViT精度低于同量级ResNet;在ImageNet-21k(约1400万张图)上训练,两者打平;在JFT-300M(约3亿张图)上训练,ViT明显超过ResNet。作者甚至把论文标题后半截的“at Scale”直接写出来,说明他们自己很清楚:效果的前提是大规模。
为什么规模如此关键?因为Transformer的全局注意力给了模型非常大的自由度。小数据下,模型容易把注意力浪费在大量无关像素组合上;数据足够大时,模型才能从海量样本里统计出“哪些远距离关系是有用的”。这就像让一个没有任何先验知识的人学辨认物体,你至少得让他看足够多的样本。CNN的卷积结构相当于提前告诉他“邻近像素关系更重要”,所以小样本下占优势;ViT不做这个假设,就必须用数据量来换结构灵活度。
4.2 预训练与微调的分辨率策略是最容易忽视的细节
ViT的预训练通常用224x224输入,微调用更高分辨率(比如384x512)。这里有个关键问题:224分辨率下patch size=16,序列长度是196;换到384分辨率,patch size还是16,序列长度变成 (384/16)^2=576。问题是可学习的位置编码只有196个位置,576个位置并没有在预训练里见过。ViT的解决办法是:把预训练好的位置编码做二维插值,扩展到新的序列长度上。
这个操作看起来不起眼,但复现时特别容易踩坑。我第一次做高分辨率微调时,直接丢掉预训练位置编码重新随机初始化,结果模型在验证集上比原来低了快两个点。后来换成双线性插值初始化新位置编码,精度立刻恢复正常。原理很简单:位置编码里已经包含了空间结构,只差一个“按比例放大”的步骤,随机初始化等于让模型从头学空间关系,当然会掉点。
4.3 优化器、学习率与正则:这些超参组合起来才有效
ViT的训练细节继承自NLP的成熟经验,但做了一些适配。通常使用Adam优化器,beta取(0.9, 0.999),配合warmup和cosine学习率衰减;weight decay设得比较高,论文里常用0.1级别;还会用梯度裁剪防止训练不稳定。数据增强层面用到Mixup、CutMix、RandAugment等,这些策略对防止Transformer在小数据下过拟合起了很大作用。
很多复现实验做不出论文精度,不是模型写错了,而是训练策略没跟上。ViT对训练超参的反应比CNN更敏感,尤其是学习率和数据增强强度。你在ImageNet-1k从头训练一个ViT-B/16,想达到timm上85%的精度,纯靠默认配置是不够的,通常需要配合反复调过的augmentation pipeline、EMA和长时间训练。这和论文结论一致:ViT能力上限高,但不好驯服。
5. 实验结果怎么读:不是“ViT全面碾压CNN”,而是有条分水岭
论文的Results部分信息量很大,但很多人只记住了“ViT在ImageNet上88.55%”这句,很容易得出“Transformer就是比CNN强”的结论。实际上实验结果更微妙,也更值得琢磨。
5.1 数据集规模与模型精度的关系曲线
论文里最关键的一张图是“不同预训练数据集大小下,ViT与ResNet/BiT的精度差异”。横轴是预训练数据规模,纵轴是迁移到ImageNet后的top-1精度。结果非常清晰:当预训练数据只有ImageNet-1k时,ViT的曲线明显低于CNN;当预训练数据大到JFT-300M时,ViT的曲线反超。
这告诉你两件事。第一,评价ViT不能脱离数据规模谈效果;第二,所谓“CNN的归纳偏置”并不是永远的优势,在数据足够时,自由度高反而让模型能摆脱先验限制,学出更有效的表征。这个规律后来在MAE、CLIP等一系列工作中反复出现,几乎成了视觉预训练的一条铁律。
5.2 ViT在不同变体、不同迁移任务上的表现
论文报告的表格很多,我倾向于只看几个关键数:在ImageNet上单模型top-1最好成绩88.55%;在ImageNet-21k上90.72%;在CIFAR-100上94.55%;在VTAB(由19个视觉任务组成的benchmark)上77.63%。这几个指标在当年已经超过了同等训练条件下的BiT(Big Transfer)系列。
VTAB这个榜单值得多说一句,它把任务分成自然图像、专用图像(比如卫星图、医学图)和结构化图像(比如计数、3D深度)三类。ViT在这三类上的表现有差异,但在整体平均上仍然领先。后续几乎每一篇视觉Transformer论文都会报告VTAB结果,因为它是衡量迁移能力的重要标尺。
5.3 可视化注意力:论文里最有画面感的部分
ViT论文里有一组可视化结果,把class token对每个patch的注意力权重画成热力图叠加在原图上。你会发现,模型无需任何监督,就在注意力热力图里出现了类似分割的目标轮廓。有些head负责抓全局轮廓,有些head负责抓局部细节,这种“分工”完全是自注意力学出来的。这组图从直觉上说服了很多人:视觉Transformer不是黑盒,它在早期的层里已经学会了描述空间关系和物体边界。
复现时你也能做同样的可视化,只需要把最后一层class token对所有token的attention weight取出来,reshape成14x14再插值到原图尺寸即可。这一步会帮助你更直观理解“Transformer在图像上到底看到了什么”。
6. 翻译与理解:几个容易让人理解偏的术语
这篇论文的中文翻译流传很广,但翻译过程中有几个术语特别容易产生歧义,我单独列出来。
6.1 token、embedding、patch 三者至少要有明确对立
- patch:图像被切出的小块,比如16x16像素的一个方块。它还是图像层面的概念。
- token:patch进入模型后的身份,可以理解成序列中的一个元素。
- embedding:token对应的连续向量,也就是模型内部的数值表示。
三者的关系是:图像被切分成patch,每个patch通过线性投影变成embedding,这些embedding组成的序列在Transformer里被称为token序列。翻译版如果不区分这三个词,读起来会一头雾水。建议读的时候心里始终带着这条流水线。
6.2 “head”不是“头”,“hidden size”不是“隐层数”
ViT论文里的head通常指分类头(classification head),也就是接在Encoder最后做类别输出的那个线性层。很多人第一次看会以为是“注意力头”,但上下文里如果提到“classification head”,它就是分类器;“attention head”才是多头注意力里的一个头。hidden size指的是向量维度D,不是隐藏层数量。MLP size指MLP中间层的宽度。这些术语在不同翻译版本里容易被混用,导致理解偏差。
顺带说,“fine-tune”翻译成“微调”没毛病,但要注意它和“从头训练”(train from scratch)是相对的概念。ViT如果不预训练直接ImageNet-1k从头训练,效果很一般;先在大数据上预训练再微调,才能发挥功力。
6.3 把论文摘要完整读一遍,胜过零散刷片段
论文摘要虽然短,但信息密度极高。我直接给出一个比较贴近原意的翻译:
“尽管Transformer架构已经成为自然语言处理任务的事实标准,它在计算机视觉中的应用仍然有限。我们证明,与普遍假设相反,这种架构不需要针对视觉任务做任何修改。将图像划分为16x16大小的词序列,并作为输入交给一个标准Transformer,就足以让模型在图像分类上达到很好的效果。当模型在足够大的数据集上预训练并迁移到多个中小规模数据集后,ViT取得了与领先的卷积网络相当甚至更好的结果,同时训练成本显著更低。”
这段摘要里“at scale”被翻译成“大规模”没有歧义,但它真正的含义是:只有在足够的规模下,这个结论才成立。所以论文标题本身就是一篇压缩版导读。
6.4 论文里反复出现的“transfer”和“downstream”指的是什么
预训练在JFT或ImageNet-21k上做完之后,模型要去处理具体任务,比如CIFAR-100分类、VTAB里的某个视觉任务。这个过程论文里叫transfer(迁移),这些具体任务叫downstream tasks(下游任务)。所有视觉Transformer预训练模型的精度报告,都有一个“预训练数据+微调任务”的完整上下文。只看微调精度不看预训练数据规模,几乎无法横向比较模型好坏。
7. 从ViT到Deformable DETR:同一套思想的扩散
ViT的另一个重要贡献是它让整个视觉社区开始用Transformer语言重新描述问题。很快,DETR把目标检测变成集合预测,Deformable DETR又解决了DETR收敛慢、小目标检测差的问题。想理解这些工作,ViT的基本概念是第一条线索。
7.1 DETR把检测从“框回归”变成“序列预测”
DETR的思路是:不再像Faster R-CNN那样预设大量候选框,而是让Transformer直接输出固定数量的目标集合。图像经过CNN backbone得到特征,展平后加上位置编码作为token序列,Decoder通过注意力机制从这些token中解码出目标框和类别。从本质上看,DETR就是把“图像内容”和“目标预测”都建模成序列到序列的问题,和ViT的“图像到token序列”是一脉相承的。
7.2 Deformable DETR把“全图注意力”改造成“稀疏多尺度注意力”
Deformable DETR是DETR的重要改进,它和ViT的关联在于注意力机制的理解上。DETR的attention每个query要对所有key计算权重,收敛慢且计算开销大。Deformable DETR的做法是:只看当前query附近的少量采样点,并通过可学习的偏移量评估位置偏移。这样既保留了Transformer的全局建模能力,又把计算复杂度从和feature map尺寸平方相关降到了和采样点数线性相关,同时还能自然地在多尺度特征图上做注意力。
如果从ViT的角度看Deformable DETR,你会看到同样的骨架:输入先token化,attention负责建模关系,位置信息通过编码注入。Deformable只是把“对所有token做注意力”换成了“对一小部分相关位置做注意力”,本质是对Transformer注意力稀疏化的一种工程优化。所以不要把它们当成两种互不相干的技术,它们共享的是同一个方法论:只要你能把视觉输入变成有意义的token序列,Transformer的注意力机制就能工作,问题只是怎么让它更高效。
7.3 为什么建议先读ViT再读这些工作
ViT是所有视觉Transformer工作的入门锚点。它把“图像如何变成token”“position embedding怎么加”“pre-train加fine-tune的套路”这些概念都讲透了。后续的MAE、CLIP、SAM也都建立在“图像可以被token化”这一前提下。你花两天把ViT吃透,之后再读Deformable DETR、DINO、SAM等论文时,很多概念都不用重新学,只需要关注它们各自在attention结构或训练范式上的改动。
8. 复现笔记:从论文到代码的几点经验
最后一章说说实操。我复现ViT的时候踩过不少坑,挑几个最值得记录的分享出来。
8.1 位置编码插值:最容易被忽视的精度杀手
前文提到微调分辨率时的位置编码插值。这里再强调一次:如果从224分辨率迁移到384,最好把预训练的position embedding按二维网格双线性插值到新的网格尺寸。多个开源实现里都内置了这个函数,但如果你自己写训练脚本,很容易漏掉。漏掉之后模型不会崩,只是精度悄悄掉一个点以上,排查起来非常麻烦。
8.2 分类头的微调策略:不要整个头都随便初始化
预训练时的分类头类别数和下游任务类别数往往不同,微调时一般会换一个新的线性层。这里要注意:可以把新head的学习率调高一些,backbone的lr调低一些,这样收敛快很多。ViT原论文微调时对head用了更高的学习率,这个细节在中文复现教程里经常被省略。
8.3 训练过程中多留几个checkpoint,尤其是最后一个阶段
Transformer类模型在训练后期往往会出现精度突然上涨的情况。ViT论文用了long training schedule,JFT预训练加上fine-tune的总时长不短。我建议至少每个epoch都保存一次,最好保留最后几轮的checkpoint,因为早停太早可能丢掉最好的模型。某些跑完实验后把中间checkpoint全删掉的操作,在纯CNN时代可能无所谓,在ViT这种大规模预训练范式里非常可惜。
8.4 我的建议阅读顺序
如果你是从零开始:不要一上来就啃代码,先读论文的摘要和结论,再照着本文第2、3章的公式把输入输出关系写一遍,然后打开官方仓库对比实现。读论文比读代码更能建立整体图景。如果只看中文翻译,建议重点对照原图表理解,毕竟图表比文字更不容易被翻译带偏。我这几年读下来,最大的体会是:ViT的“简单”其实是强者的简单——它把复杂度从结构转移到了数据和训练策略上。搞清楚这个前提,你对后续所有视觉Transformer工作的理解都会快很多。