☰
Vision Transformer论文精读:核心机制、实验结论与代码复现指南
2026/10/22 20:13:13 网站建设 项目流程

看到“Read Paper--Vision Transformer”这个标题,我第一反应是这才是视觉从业者最缺的东西。很多人打开论文只做两件事:把摘要翻译一遍,然后扫一眼结构图,假装自己读完了。但这篇标题要谈的不是翻译,而是把 Vision Transformer 这篇论文读薄——提炼出核心问题、设计动机、关键实验和可复现细节,再用一个迷你实验把它读厚,让纸上结论变成手里能跑的结果。它适合谁?适合刚接触 Transformer 的算法工程师、准备动手复现论文的学生,也适合已经在用预训练模型但搞不清内部设计的开发者。你不需要完全掌握所有数学细节,但要带着“它为什么要这样做”的问题往下读。

1. 读论文之前,先弄清楚这篇论文解决什么问题

1.1 标题里的三个信息

“Read Paper”本身暗示了一种阅读方法:不是给你翻译一遍原文,而是带着问题去拆解。我之前写过不少论文笔记,最大的体会是,如果你看完一篇文章能一句话说出“它解决什么问题、用什么方法、比什么方案好”,这篇论文才算真正属于你。

“Vision Transformer”这个名字拆开来就是两个部分。“Vision”代表视觉任务的输入,也就是图像;“Transformer”代表一种原本在文本序列上大放异彩的结构。把这两个概念拼在一起,论文要回答的核心问题就非常清楚:图像能不能不做太多卷积操作,直接切成小块,交给 Transformer 来处理,并且达到不逊色于卷积网络的效果?

这在当时是反直觉的。因为卷积网络已经统治视觉任务很多年,它对“局部信息”和“平移不变性”两个先验几乎刻在骨子里。而标准 Transformer 的注意力机制,一开始是没有这些先验的。一个很自然的疑问就出来了:放弃了卷积的归纳偏置,模型还能学明白图像吗?

1.2 阅读这份论文的前置知识清单

读论文最怕的不是公式多,而是没有基础硬啃。我自己读这篇之前,建议你至少具备下面这些概念,不用精通,了解即可。

  • 矩阵乘法:至少知道[B, N, D]这样的张量形状在做什么。
  • 自注意力机制:明白 Q、K、V 分别从哪里来,为什么要做缩放点积。
  • Transformer Encoder 的基本结构:知道 Token 序列输入、多头注意力、前馈网络、残差连接和层归一化的大致位置。
  • 卷积网络的基本直觉:理解“卷积为什么对图像有效”即可,不要求手推反向传播。

如果你完全没有 NLP 背景,也不用慌。你可以把 Transformer 理解成一个“让序列里每个元素都能互相交换信息”的装置:图像被切成若干个小块后,每个小块只是一个元素,它们之间通过注意力分数决定“该看谁”。后续我们会把这些概念翻译成图像语言,你会越看越顺。

2. 模型核心机制拆解:图像如何变成序列

2.1 Patch Embedding:切块这一步为什么至关重要

一张图默认的形状是H×W×C,也就是高度、宽度和通道数。Transformer 处理的是序列,所以第一步必须把图像改造成一串向量。ViT 的解法非常直接:把图像切成一个个固定大小的正方形小块,也就是 Patch。

假设输入图是224×224×3,每个 Patch 大小是16×16,那么整张图会被切成(224/16)×(224/16)=14×14=196个 Patch。每个 Patch 展平后的长度是16×16×3=768,再经过一个可学习的线性投影,压缩或映射成768维的向量。这个过程就是 Patch Embedding。

你可能会问:为什么不能直接拉成一整条向量?因为一整张图的像素数量太多,直接序列化会让序列长度爆炸,自注意力的计算量随序列长度二次增长,根本算不动。切块本身就是一种降维和结构化。

这里有个非常实用的观点:Patch Embedding 其实可以等价地用卷积实现。当卷积核大小等于 Patch 大小、步长也等于 Patch 大小时,输出的每个位置正好对应一个 Patch 的线性投影。这个等价关系在工程实现里很常见,也解释了为什么很多代码里会用大卷积核来完成第一步。

2.2 Position Embedding 与 Class Token:它们各自承担什么任务

自注意力有两个特点:一是全局建模能力强,二是它对输入顺序天然不敏感。你把“第1个Patch”和“第5个Patch”互换位置,如果不显式告诉模型,注意力计算的结果本质上是一样的。但图像中的位置信息非常关键,左上角的 Patch 和中右下角的 Patch 代表完全不同的语义区域。

所以 ViT 需要一套位置编码。原文的做法非常简单,直接用一组可学习的参数,加到 Patch Embedding 上。你可以把它理解成给每个位置发一个“固定身份牌”,模型在训练中自己调整这些身份牌的内容。很多人以为“可学习位置编码”一定比“固定三角函数编码”高明,但原文的实验告诉我们,在图像任务里,1D 的可学习位置编码已经足够,不需要做得太复杂。

Class Token 也是一个容易理解错的地方。它在输入序列的最前面额外拼接一个可学习向量,和所有 Patch Token 一起进入 Transformer。经过多层编码后,最后一层输出序列的第一个位置,也就是这个 Class Token,被拿来接分类头,输出最终类别。

为什么不直接把所有 Patch 的输出平均池化?原文专门做过对比,结论是 Class Token 和平均池化效果差不多。之所以最后用 Class Token,一个合理的解释是:它在训练初期相当于一个“任务查询”,它不知道具体要关注什么,但在每一层注意力中不断向其他 Patch 提问、收集信息、更新自己,最后沉淀出适合分类的全局表示。

3. 从单层到整体:Vision Transformer 的网络结构

3.1 Encoder Block 内部的数学流程

Vision Transformer 的主体就是一堆完全相同的 Encoder Block 堆叠起来。原始论文里,输入经过 Patch Embedding、位置编码、以及 Class Token 拼接之后,就进入这些 Block。每一个 Block 内部的流程可以写成下面两条残差式子:

z = z + MSA(LN(z)) z = z + MLP(LN(z))

这里的LN是 LayerNorm,MSA是多头自注意力,MLP是一个带 GELU 激活的两层前馈网络。注意,Transformer 里每个 Block 并不是一次算完所有内容,而是“先归一化,再过注意力,最后残差相加;再归一化,过 MLP,最后残差相加”。

为什么要在每个模块之前做 LayerNorm?一句话:稳定训练。深层网络叠加之后,输入分布会不稳定,LayerNorm 把每个 Token 的特征拉回到一个可控范围,避免梯度剧烈波动。残差结构则负责让信息可以低损耗地在层间传递,防止层数加深后出现退化。

多头自注意力是 Block 里最核心的计算。假设输入序列有N个 Token,每个 Token 的特征维度是D。先把整个序列分别通过三个参数矩阵,生成 Q、K、V。注意这里的 Q、K、V 都来自同一个输入,所以叫自注意力。

注意力分数的计算如下:

Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V

其中d_k是每个注意力头对应的维度。为什么要除以sqrt(d_k)?这是为了防止维度较大时点积数值过大,导致 softmax 之后梯度非常小。多头的作用则是让模型同时从多个子空间看序列:一个头可能关注颜色接近的 Patch,另一个头可能关注位置对角的 Patch,多个头的结果再拼接起来,最终分类特征更丰富。

3.2 不同规格对应的参数规模

Vision Transformer 的原论文给出了几个常用规格,后缀里的/16通常表示 Patch Size 是 16。为了方便记忆,我把常见配置整理成了下面这张表:

规格Encoder 层数嵌入维度注意力头数参数量(约)
Base127681286M
Large24102416307M
Huge32128016632M

这些数字之间的关系不是随便定的。层数加深意味着模型能建模更复杂的关系;嵌入维度加宽意味着每个 Token 能携带更多信息。但增大模型并不总是更好,更大的模型需要更多数据,否则极易过拟合。

从计算量角度看,自注意力的复杂度是O(N^2 * D)。N是序列长度,D是特征维度。Patch Size 为 16 时,序列长度只有 196,所以 224 分辨率的图像还能接受。如果输入分辨率翻倍,序列长度会变成约 800,计算量就会猛增。这也是后来很多工作研究稀疏注意力或窗口注意力的原因。

4. 实验结论与关键设计选择

4.1 数据规模如何左右 ViT 的发挥

原文最有价值的结论不是“ViT 比 CNN 强”,而是“在足够大的数据上预训练之后,ViT 才能发挥出优势”。如果只在中小规模的图像数据集上从头训练,ViT 的准确率常常低于同等规模的卷积网络,这个现象在不少后来的复现里也出现过。

为什么?卷积网络自带两个先验:局部性和平移等变性。它默认图像里的物体是由局部纹理组成的,并且同一纹理在画面任何位置都有意义。这种先验在小数据集上非常占便宜,因为它不需要学太久。而 ViT 一开始并没有这种先验,它只能靠大量数据自己总结出“哪些局部模式该组合在一起”。

你可以把卷积网络类比成一位戴着“局部视力眼镜”的人,先把图片分成小区域,再看区域之间的关系;把 ViT 类比成一位通读全图再做判断的人,视野足够开阔,但需要足够多的样本积累经验。一旦数据量上来了,ViT 的全局建模能力就成了真正的优势,所以在大规模预训练之后,它能反超卷积网络。

这个结论对实际工程非常重要。如果你手里的任务只有几万张图,直接搬 ViT 很可能不会赢。这时可以优先考虑 CNN,或者用已经在大型数据上预训练好的 ViT 做微调,而不是随机初始化从头训练。

4.2 Patch 大小、层数、宽度等超参的选择逻辑

Vision Transformer 的实验部分花了不少篇幅讨论超参敏感性。官方结论是:模型对大部分超参并不像想象中那么敏感,但有几个点值得关注。

Patch Size 的大小会显著影响计算量和准确率。Patch 越小,序列越长,模型能保留更多空间细节,但同时计算量更大。8×8的 Patch 比16×16的准确率略高,但在 224 分辨率下序列长度变成了 784,训练开销增加不少。如果你的资源有限,从16×16起步是最稳妥的,原论文一系列结果都以它为基准。

关于位置编码,原文对比了 1D、2D 和相对位置编码,结论是三者差距不大。这说明对于图像 Patch 序列,简单的位置信息已经足够帮助模型区分不同区域。不要一上来就设计复杂的位置编码方案,很多情况下反而是画蛇添足。

Class Token 和平均池化的对比同样值得注意。原文发现两者的最终效果差不多,所以设计新模型时,你不必为了“必须使用 Class Token”而纠结。选哪个主要看后续任务好不好接:分类头接 Class Token 很直觉,但用所有 Token 的均值做表示也有它的理论解释。

关于训练策略,原文在大规模数据上训练时使用了不小的数据增强和正则化手段。如果你在小数据集上复现时发现梯度很容易崩,或者验证集一直在震荡,大概率不是因为网络结构错,而是因为训练配置没有匹配你的数据规模。

5. 把论文读厚再读薄:复现与验证

5.1 准备一个可运行的迷你实验

完整的 Vision Transformer 预训练需要极大算力,个人复现没有必要。我每次读论文都会先搭一个“迷你实验”,只要能把核心机制跑通就行。这次我选择了一个模拟项目 X:合成图像分类。

数据生成逻辑很简单:输入是32×32×3的彩色图,Patch Size 取 4,那么序列长度是8×8=64。类别一共有两类:A 类图像左半部分偏亮、右半部分偏暗;B 类图像反过来。每张图再叠加一点随机噪声。任务看起来简单,但模型必须区分左右方向,这天然依赖位置信息。如果位置编码被去掉,模型很难稳定收敛,这个设计正好用来验证位置编码的作用。

模型配置我故意缩小:嵌入维度D=32,注意力头数4,Encoder 层数2,MLP 中间层维数64。数据规模是 2000 张训练图、500 张验证图。这样的任务在普通个人电脑上几十秒就能跑完一个批次,非常适合用来验证对论文的理解。

5.2 核心组件代码与逐段说明

下面的代码我尽量写成框架无关的伪代码,只保留核心逻辑。你在使用具体主流框架时,只需要把这里的Dense、LayerNorm、MultiHeadAttention替换成对应 API 即可。

class PatchEmbed: def __init__(self, patch_size, in_chans, embed_dim): self.patch_size = patch_size self.proj = Dense(patch_size * patch_size * in_chans, embed_dim) def forward(self, x): # x 的形状是 [B, C, H, W] # 切成 N 个 patch,每个 patch 展平成向量 # B: batch size, C: 通道数, H/W: 高宽 # 输出 [B, N, embed_dim] ... class TransformerBlock: def __init__(self, dim, heads): self.norm1 = LayerNorm(dim) self.attn = MultiHeadAttention(dim, heads) self.norm2 = LayerNorm(dim) self.mlp = Sequential( Dense(dim, dim * 4), GELU(), Dense(dim * 4, dim), ) def forward(self, x): x = x + self.attn(self.norm1(x)) x = x + self.mlp(self.norm2(x)) return x class MiniViT: def __init__(self, patch_size, in_chans, embed_dim, depth, heads, num_classes): self.patch_embed = PatchEmbed(patch_size, in_chans, embed_dim) self.cls_token = Parameter(zeros([1, 1, embed_dim])) self.pos_embed = Parameter(zeros([1, 1 + 64, embed_dim])) self.blocks = [TransformerBlock(embed_dim, heads) for _ in range(depth)] self.head = Dense(embed_dim, num_classes) def forward(self, x): tokens = self.patch_embed(x) # [B, 64, D] cls = tile(self.cls_token, [B, 1, 1]) # [B, 1, D] tokens = concat([cls, tokens], axis=1) # [B, 65, D] tokens = tokens + self.pos_embed for block in self.blocks: tokens = block(tokens) cls_out = tokens[:, 0] return self.head(cls_out)

PatchEmbed的作用是把每个 Patch 变成 D 维向量。cls_token是额外的一个可学习向量,在最前面拼进去。pos_embed的形状是1×65×D,因为它把 Class Token 也当做一个位置对待。最后分类头只接 Class Token 对应输出。

代码里最容易出错的地方是序列长度。位置编码的第二个维度是1 + 64,不是 64。如果你忘了把 Class Token 的位置也算进去,程序运行时会直接报维度不匹配。这个错误我在第一次复现时踩过,每次检查代码都要多看这里一眼。

5.3 训练过程中的可复现观察

我在这个迷你实验里的训练配置是:优化器使用 AdamW,学习率 3e-3,批次大小 32,共训练 60 个 Epoch。由于任务简单,验证准确率最终稳定在 95% 以上。

更有价值的观察是对照实验。我把pos_embed去掉,其他条件完全不变,验证准确率会明显下降,而且训练前几个 Epoch 的损失下降得更加犹豫。这说明位置编码确实在帮助模型区分“左半部分”和“右半部分”这种空间模式。

另一个观察是注意力图的可视化。把最后一层第一个注意力头的打分输出出来,能明显看到 Class Token 并不是平均地看所有 Patch,而是把大部分注意力集中到图像边缘或者色彩突变区域,这些区域恰恰是区分 A/B 两类最关键的线索。这个现象帮助我真正理解了 Class Token 的“查询”角色:它不是分类头的简单替代品,而是整个 Transformer 信息汇聚的中枢。

6. 被论文背刺过的那些坑:阅读与实践经验

6.1 容易理解错的三个点

第一,认为 ViT 完全没有空间信息。这是最普遍的误解。ViT 确实没有卷积那种天然的局部连接,但它通过位置编码显式注入了空间关系。只不过这种关系不是先验固定的,而是可以从数据中学的。

第二,认为 patch 切得越小越好。我见过不少初学者把 Patch 尺寸从 16 改到 4,结果显存直接爆掉,准确率还未必提升。序列长度是平方级增长,不是线性增长。不要只看“小 Patch 保留更多细节”,还要算算计算量是否匹配你的资源。

第三,认为 ViT 一定能超过 CNN。这个结论只能在“大规模预训练”或者“足够数据从头训练”的前提下成立。在中小规模数据上,CNN 的归纳偏置依然是强大优势。如果项目数据量不大,与其随机初始化 ViT,不如用预训练权重微调。

下面把常见问题整理成一张排查表,方便你复现时对照:

现象可能原因排查方向
训练损失完全不降学习率过大或过小尝试降低学习率,检查数据是否归一化
位置编码维度和输入不匹配忘记加上 Class Token 位置确认 pos_embed 序列长度为 N+1
验证集波动很大数据量太小加强数据增强,或换 CNN 基座
显存不够Patch 太小或图分辨率太高增大 Patch,或降低输入分辨率
小数据集效果差没有预训练加载预训练权重替换随机初始化

6.2 一套适合多数人的读论文流程

我读任何论文都会用“三遍法”。第一遍只读标题、摘要、图表和结论,目标是在十分钟内回答三个问题:解决了什么问题、用什么方法、实验结果是什么。第二遍读公式和实验细节,把不清楚的符号标记出来,必要时自己推导一遍。第三遍动手复现,哪怕只复现一个小模块,都要让它跑起来。

读完一篇论文后,我会做一页纸笔记。内容包括:问题定义、核心方法、关键图表、局限性、可以借鉴到我自己项目的点。Vision Transformer 这篇论文特别适合做这种笔记,因为它本身就是一个“从质疑中生长出来”的工作:先质疑卷积先验,再提出更一般的建模方法,最后用实验证明数据规模是重要条件。

我的实际体会是,最花时间的不是看懂注意力公式,而是搞懂实验对照关系。看表不能只看谁分数高,还要看它是在什么数据规模、什么预训练设置下得出的。脱离场景比较模型能力,只会给你一堆没有意义的结论。

最后再分享一个小技巧:读论文时,拿出一张白纸,把“原文的结论”和“你的推导”分开写。例如原文说“在大规模数据上预训练后 ViT 超过 CNN”,你在下面写一句“所以在我的小数据集任务上,也许应该先用 CNN 或预训练模型”。这种写法能逼你把论文知识和自己的业务场景连接起来,而不是停留在复述原文。读完一篇论文,能多出一个可落地的判断,这篇论文才算真正读透了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询