Vision Transformer论文精读:核心机制、实验结论与代码复现指南
发布时间:2026/10/22 20:13:15 锦皓数字建站

看到“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×14196个 Patch。每个 Patch 展平后的长度是16×16×3768再经过一个可学习的线性投影压缩或映射成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是 LayerNormMSA是多头自注意力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 层数嵌入维度注意力头数参数量约Base127681286MLarge24102416307MHuge32128016632M这些数字之间的关系不是随便定的。层数加深意味着模型能建模更复杂的关系嵌入维度加宽意味着每个 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×864。类别一共有两类A 类图像左半部分偏亮、右半部分偏暗B 类图像反过来。每张图再叠加一点随机噪声。任务看起来简单但模型必须区分左右方向这天然依赖位置信息。如果位置编码被去掉模型很难稳定收敛这个设计正好用来验证位置编码的作用。模型配置我故意缩小嵌入维度D32注意力头数4Encoder 层数2MLP 中间层维数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], axis1) # [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 序列长度为 N1验证集波动很大数据量太小加强数据增强或换 CNN 基座显存不够Patch 太小或图分辨率太高增大 Patch或降低输入分辨率小数据集效果差没有预训练加载预训练权重替换随机初始化6.2 一套适合多数人的读论文流程我读任何论文都会用“三遍法”。第一遍只读标题、摘要、图表和结论目标是在十分钟内回答三个问题解决了什么问题、用什么方法、实验结果是什么。第二遍读公式和实验细节把不清楚的符号标记出来必要时自己推导一遍。第三遍动手复现哪怕只复现一个小模块都要让它跑起来。读完一篇论文后我会做一页纸笔记。内容包括问题定义、核心方法、关键图表、局限性、可以借鉴到我自己项目的点。Vision Transformer 这篇论文特别适合做这种笔记因为它本身就是一个“从质疑中生长出来”的工作先质疑卷积先验再提出更一般的建模方法最后用实验证明数据规模是重要条件。我的实际体会是最花时间的不是看懂注意力公式而是搞懂实验对照关系。看表不能只看谁分数高还要看它是在什么数据规模、什么预训练设置下得出的。脱离场景比较模型能力只会给你一堆没有意义的结论。最后再分享一个小技巧读论文时拿出一张白纸把“原文的结论”和“你的推导”分开写。例如原文说“在大规模数据上预训练后 ViT 超过 CNN”你在下面写一句“所以在我的小数据集任务上也许应该先用 CNN 或预训练模型”。这种写法能逼你把论文知识和自己的业务场景连接起来而不是停留在复述原文。读完一篇论文能多出一个可落地的判断这篇论文才算真正读透了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。