1. 视觉Transformer为什么值得重新梳理
如果你这几年一直在做计算机视觉相关的工作,应该能明显感觉到视觉Transformer(Vision Transformer,常简称ViT)已经从一篇论文变成了一条绕不开的技术主线。我的感受很直接:2020年第一次看到“图像等于16×16个单词”这个说法时,第一反应是惊讶——图像不是规则排列的二维网格吗,怎么就能套用NLP里的序列建模思路?后来的实验一步步证明,这条路不但走得通,而且能走得比CNN更远,只是它需要的数据规模和训练技巧,比我们习惯的那套卷积思路苛刻得多。
这篇博客本质上是我给自己做的一次知识抽题式整理,没有打算把Transformer的所有细节都复述一遍,而是挑出几个自己反复用到、也反复踩坑的知识点,写成既能当复习提纲、又能当工程查表的笔记。内容会覆盖ViT的主干结构、自注意力机制的数学细节、位置编码的作用与坑、从ViT到Swin等变体的演进动机,以及训练部署阶段的一些实战经验。适合正在入门视觉Transformer的读者,也适合已经用过这类模型但想回头把原理补扎实的从业者。
1.1 从“图像等于16×16个单词”说起
ViT的核心思想可以用一句话概括:把一张图切成固定大小的小块(patch),把每个小块当作一个“单词”,再把这一串“单词”交给标准的Transformer编码器处理。以224×224的输入图像为例,如果patch size取16,那么每边切成14份,一共得到196个patch;每个patch的三通道数据展平后是16×16×3=768维的向量,这就像是把一张图片“翻译”成了196个长度为768的token序列。
这个做法放在今天看着已经很自然了,但在当时非常反直觉。CNN是靠在图像上滑动的卷积核起家的,天然拥有局部连接、平移不变性这些先验;而Transformer靠注意力机制做全局依赖建模,硬套到图像上,等于放弃了视觉领域积攒了几十年的归纳偏置。现在回头想,ViT真正的贡献并不是简单地把自注意力搬到了图像上,而是用最朴素的实验证明了一个事实:只要数据量足够大,模型完全可以通过学习自行获得类似“感受野”“空间秩序”的能力,而不需要靠卷积结构手动注入。
我习惯这样类比:CNN是不断通过加深网络和扩大卷积核去“够到”远处像素,上一层看到局部,下一层看到更大局部,层层递进;ViT则是一上来就在所有patch之间建立了“直通车”,任意两个位置可以直接交换信息。这个差异是理解后面很多变体设计的钥匙。
1.2 全局建模带来的新可能
图像里有很多长距离依赖的语义关系。比如说,一张大象的全身照中,象头可能在左上角,象腿在右下角,但它们在语义上是高度相关的。传统CNN要建立这种跨区域关联,必须堆叠足够多的卷积层,让感受野一层层扩大;而且即便到了深层,一个神经元的感受野也不是均匀覆盖全图的,信息经过多次下采样和池化会被稀释。
自注意力机制则在任意两个patch之间直接计算关联度,一步到位。这种全局建模在面对大目标、复杂语义、上下文理解等任务时优势很明显,因为模型不再需要依赖网络的深度去“间接拼接”信息。这也是为什么ViT在分类任务上表现好的原因之一:它不需要手工设计特别大的卷积核,也不需要依赖整个网络堆得很深才能看到全图。
但“全局建模”并不全是优点,它带来一个明显的问题:参数规模大、容易过拟合。CNN里那套所谓的“先验”虽然有时会限制模型上限,但也帮模型在数据不足时更快收敛。ViT既然放弃了这些先验,就要用更大的数据去补偿。所以我们会看到,原始ViT在百万级数据集上从零训练时打不过同级别的ResNet,而一旦把预训练数据规模推到上亿级别,ViT的优势就彻底释放出来了。
1.3 这次梳理的内容范围与适用人群
这篇笔记没有按照论文顺序从头讲到尾,我刻意把它拆成了几个“独立但互相咬合”的知识块:主干结构维度流转、自注意力的数学拆解、位置编码的语义与实现细节、从ViT到Swin的演进动机,以及训练部署时的实操经验。每一块都会尽量配上代码片段、参数表或者踩坑记录,让读完之后能直接迁移到自己的实验里。
如果你是完全没接触过Transformer的读者,建议先粗略看一遍NLP领域那篇注意力机制论文的整体框架图,理解Q、K、V的大致作用;如果你已经用ViT或Swin跑过一些实验,可以直接跳到位置编码和训练经验两节,那里有不少我自己掉过的坑。接下来就按这个顺序,一个一个拆开聊。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ViT主干结构拆解:Patch Embedding与编码器流程
2.1 Patch Embedding:切块、展平与线性投影
代码里实现Patch Embedding的方式,比我第一次想象的简单得多。很多人以为需要先把图像用Python切成196个小块,再逐个展平,最后放到全连接层里。实际上,工程实现普遍直接用卷积一步到位:把卷积核大小设置成patch size,把步长也设置成patch size,一次Conv2d就把切块、展平、投影三件事全部完成了。
我用近乎于主流的框架接口习惯写个伪代码,方便对照维度:
python复制# 伪代码:使用卷积实现Patch Embedding
class PatchEmbedding:
def __init__(self, patch_size=16, in_chans=3, embed_dim=768):
# 卷积核大小 == 步长 == patch_size
self.proj = Conv2d(in_chans, embed_dim,
kernel_size=patch_size,
stride=patch_size)
def forward(self, x):
# x: (B, 3, 224, 224)
x = self.proj(x) # (B, 768, 14, 14)
x = x.flatten(start_dim=2) # (B, 768, 196)
x = x.transpose(1, 2) # (B, 196, 768)
return x
这段代码里最值得理解的是两次维度变换的含义。flatten(start_dim=2)把宽高两个维度合并成196,得到(B, 768, 196);紧接着transpose(1, 2)把embedding维度放到最后,得到(B, 196, 768)。因为Transformer模块期望的输入格式一般是(B, seq_len, embed_dim),也就是“196个token,每个token是768维”。如果你在调试时发现维度不匹配,基本都出在这两步的先后顺序上。
2.2 Class Token:一个会“集邮”的可学习向量
有了196个patch token之后,ViT还会在序列最前面拼接一个额外的可学习向量,这个向量被称为class token。它本身不携带任何像素信息,初始值来自随机初始化,但经过Transformer编码器的反复计算后,它会通过自注意力不断从所有patch token中收集信息,最终变成一个包含整张图像“全局摘要”的表示。做分类时,只取这个token对应的输出向量,接一个全连接分类头就够了。
不少入门读者会问:为什么不用全局平均池化?平均池化把所有patch的信息等权相加,容易稀释掉关键位置的判别性信息。class token则是一个可学习的“信息收集器”,它自己决定该把注意力重点放在哪些patch上、每个patch吸收多少信息。我在可视化注意力图时经常看到,class token的注意力分布并不均匀,它会有选择地倾向于那些语义上图别性较强的区域,这种自由度显然比无差别的平均池化高。
还有一点容易忽略:class token在每一层Transformer Encoder中都会参与注意力计算,并不是“只进不出”的摆设。可以把每一层编码器想象成一场圆桌会议,196个patch是与会代表,class token是会议记录员。记录员听完所有代表的发言,写出一份总结,这份总结又会带入下一场会议继续参与讨论。
2.3 Transformer Encoder里的附属组件:Norm、MLP、残差与Dropout
ViT把196个patch token和class token拼成的序列,送进L层Transformer Encoder。每一层并不只有多头自注意力模块,它的标准活跃组件还包括层归一化(Layer Norm)、全连接前馈网络(MLP)、残差连接和Dropout。只盯着QKV看是不够的,后几个部件对训练稳定性的影响同样巨大。
大多数视觉Transformer实现采用的是Pre-LN结构,也就是在注意力层之前先做Layer Norm,再做多头注意力,然后接残差连接;在MLP之前再做一次Layer Norm,最后再接残差。这种“先归一化再计算”的结构比Post-LN更好训练,也是目前几乎所有视觉Transformer变体默认的写法。如果你动手从零搭过ViT,遇到loss震荡不下降,第一个要检查的就是Layer Norm的位置和是否启用了残差连接。
前馈网络通常是一个两层的MLP,中间维度会扩展到4倍embedding维度(比如768→3072→768),中间夹一个GELU激活函数。MLP占了ViT很大一部分参数量,它在做“逐token的特征变换”,而注意力模块在做“token之间的信息交换”,两者各司其职。Dropout一般加在注意力输出的投影之后以及MLP输出之后,训练时会掉精度正常,推理时记得关闭。
2.4 一张图从输入到输出的维度变化
把整条数据流放在一张表里,能解决新手90%的维度困惑。我把常见配置(输入224×224,patch size 16,embed_dim 768)下的张量形态整理如下:
| 阶段 | 张量形状 | 说明 |
|---|---|---|
| 输入图像 | (B, 3, 224, 224) | B为批量大小 |
| Patch Embedding输出 | (B, 196, 768) | 196个patch token |
| 拼接Class Token | (B, 197, 768) | 序列最前面新增可学习向量 |
| 叠加位置编码 | (B, 197, 768) | 与位置编码逐元素相加 |
| Transformer Encoder输出 | (B, 197, 768) | 延续L层 |
| 分类头输入 | (B, 768) | 取第0个token对应的向量 |
ViT的Base配置通常使用12层Encoder、12个注意力头、embedding维度768,参数量在8600万左右。理解了这个维度流转,再去看任何视觉Transformer变体都会轻松很多,因为后续出现的种种新模型,本质上都是在这条主线上调整“序列长度”“注意力范围”“多尺度结构”这三个旋钮。
3. 自注意力机制:Q、K、V在图像里算什么
3.1 自注意力的数学形式:从Q到K再到V
自注意力是整个Transformer家族的发动机。对于输入序列X ∈ R^(n×d),它先通过三个线性投影分别得到Query、Key和Value,然后按照缩放点积的形式计算注意力权重,再对Value做加权求和。核心公式如下:
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V
其中Q = XW_Q,K = XW_K,V = XW_V,d_k是每个注意力头的维度。直观理解是:Q(Query)代表当前token“想找什么”,K(Key)代表每个token“能提供什么标签”,V(Value)代表每个token“真正给出的信息内容”。Q和K做点积可以给出当前token与其他所有token的匹配度分数,softmax把这些分数转成和为1的权重,最后按权重去取V中的信息。
一个生活化的类比是开会时的讨论分工:每个人手里拿着一张名片,写着“我擅长什么”,这就是K;同时每个人心里有一个问题“我想请教谁”,这就是Q;大家扫一眼名片,找到与自己问题匹配的人,接下来才真正去听对方发言,听到的内容就是V。注意力权重的大小,本质上反映了“这个问题和这个回答之间的匹配程度”。
公式里的除以sqrt(d_k)看似不起眼,但它对训练稳定性影响很大。当d_k变大时,点积结果也会随之变大,softmax会进入饱和区,出现梯度趋近于零的问题。缩放因子让点积的方差保持在合适范围,保证梯度能顺畅流动。这也是为什么不管网络宽度怎么变,注意力计算里始终要保留这个缩放因子的原因。
3.2 多头注意力为什么会“不谋而合”
多头注意力(Multi-Head Attention,MHA)并不是只算一组QKV,而是把embedding维度切成h份,通常h取12,每个头独立计算一组注意力,最后把所有头的结果拼接起来,再做一次线性投影输出。公式可以写作:
MHA(X) = Concat(head_1, ..., head_h) · W_O
其中每个head_i = Attention(X · W_Q_i, X · W_K_i, X · W_V_i)。
为什么要拆成多个头?因为单个头只能表达一种“关系倾向”,而图像中的语义关系是多维的:有些关系由空间邻接决定,比如相邻patch之间的边缘连续性;有些关系由颜色相似性决定;有些则由语义类别决定,比如某只猫的脸和另一只猫的脸虽然距离很远,但语义高度相关。多个头就相当于多个记者同时采访同一个会议,每个记者的关注点不同,有人盯着发言人,有人盯着投影屏幕,有人盯着在场听众的反应,最后把不同角度的信息汇总起来,模型对场景的理解才够全面。
我在实际的可视化中经常看到这种分工:浅层的头更关注局部纹理,分布比较随机;深层的头开始变得“有立场”,一些头会集中关注图像中具有判别性的目标区域,另一些头则均匀关注背景上下文。这个现象说明,注意力机制虽然从一开始就被设计成“让模型自己决定关注哪里”,但模型确实从数据中学会了类似语义聚焦的行为。
3.3 计算复杂度:二次方问题的由来
自注意力的计算复杂度是O(n²·d),其中n是token数量。原因在于QK^T这一步需要计算一个n×n的相似度矩阵,矩阵里的每个元素都是一对token之间的点积分数。在标准ViT中,n只有197(196个patch加1个class token),这个规模在现代GPU上可以轻松处理,所以ViT-B在224×224输入下并没有想象中那么慢。
但token数量一旦涨起来,计算量会迅速失控。举个简单的例子:把输入分辨率从224提升到448,patch size保持16不变,token数量就从196增加到784,注意力矩阵的规模从196×196膨胀到784×784,计算量直接翻了16倍。如果再往1024分辨率推,n变成4096,原始的自注意力基本就退化到没法用的程度。
这也是后来Swin等模型引入窗口注意力最直接的原因。把全局自注意力限制在局部窗口内(比如7×7的窗口),计算复杂度就从O(n²)降到了近似线性的水平。你可以把窗口注意力理解成“只邀请同一个小区的邻居开会”,而不是每次开会都把全城的人都请来。这样省下的算力可以花在更大的分辨率或多尺度结构上,精度和效率的平衡点更好找。
3.4 可视化注意力图的实践观察
注意力可视化是理解ViT最直观的手段之一。做法并不复杂:加载一个预训练好的ViT模型,在中间层把attention map保存下来,平均或挑某个头,把它画成热力图再叠到原图上。
我多次做过类似实验后,有一个朴素但重要的结论:不要指望每个头都有清晰的语义解释。大约只有一部分深层头会表现出与目标区域一致的高响应,其他头可能关注的是背景、阴影、边缘等相对“不可名状”的部分。可视化更适合用来验证模型是否学到了某种全局结构,而不是用来逐头“审问”。另外,不同数据集上训练出来的模型,其注意力分布差异很大,换了数据集后最好重新可视化,不要复用旧结果做判断。
4. 位置编码:Transformer的空间秩序感知
4.1 置换不变性:为什么没有位置编码不行
自注意力机制本身是一种置换等变操作:把输入token的顺序任意打乱,得到的输出只会以相同顺序重新排列,而不会发生内容变化。这意味着,如果把一张图的196个patch随机重排后送进一个没有任何位置信息的ViT,模型看到的“内容”在数学上和原图完全等价。这显然不行,因为在图像识别里,“有什么”和“在哪儿”同样重要。
好比一句话“猫在沙发上”和“沙发上在猫”,单词都一样,顺序不同,含义完全不同。图像里的空间排布也是如此,眼睛在嘴巴上方、轮子在车身下方,这些位置关系本身就承载着大量判别信息。位置编码的作用,就是给每个token补上“空间身份”,打破置换不变性,让模型能够感知patch的坐标和相对位置。
4.2 可学习位置编码与正弦位置编码的取舍
在NLP领域的Transformer中,常见的位置编码是正弦编码,通过不同频率的正弦和余弦函数组合出每个位置的向量。它的好处是理论上可以外推到任意长度,因为每个位置的编码可以由函数直接推算出来,不需要额外训练。ViT最早也尝试过正弦编码,但后来发现,在固定尺寸的图像输入下,可学习位置编码效果更好,也更简洁。
原因其实不难理解。图像的输入尺寸通常是固定的,比如224×224对应196个patch,训练时位置数量不变,可学习编码就能充分发挥数据拟合的优势;而NLP句子长度变化很大,预设的三角编码在“外推到未见长度”时更有价值。可学习位置编码的代价是它和训练分辨率强耦合:一旦换了更大的分辨率,位置编码矩阵就要做插值,而插值本身会带来信息损失和分布偏移,处理不得当会明显掉点。
4.3 分辨率变化时位置编码的插值坑
这里说的坑,是很多人在工程实践里真正踩过的地方。当你想把在224×224上预训练好的ViT模型用于384×384的输入时,patch数量会从196增长到576,原本形状为(197, 768)的位置编码矩阵就和新的token序列对不上了。主流做法是把可学习位置编码中属于空间token的部分还原成2D形状,比如(14, 14, 768),用双三次插值放大到(24, 24, 768),再把class token对应的那一个位置单独拷贝过来,不参与插值。
实操中我推荐一个三步走的流程:先用双三次插值得到新的位置编码,然后固定位置编码部分,在目标数据集上微调若干个epoch,让它适应新的网格分布;微调稳定后再放开全部参数训练。这个“先固定位置编码再全量微调”的顺序,是我在多次实验中比较有效的方案。另外,如果任务是检测或分割,往往还需要引入相对位置偏差(relative position bias),它比绝对位置插值更鲁棒,也更容易适应不同分辨率。
很多入门者换分辨率后精度下降,第一反应是怪数据增强或者学习率,其实真正的元凶往往是位置编码插值没有单独适配。下次你在调试这类问题时,可以从打印位置编码的前几个元素分布开始排查,对比插值前后的数值范围是否发生了明显漂移。
5. 从ViT到Swin等变体:演进脉络与动机
5.1 DeiT:用蒸馏把归纳偏置搬进Transformer
普通ViT对训练数据量的要求高得惊人,这让很多想在中小规模数据集上使用Transformer的人望而却步。DeiT就是为了解决这个问题出现的。它的核心改动是在class token旁边增加了一个distillation token,也就是“蒸馏token”。训练时,模型不仅学真实标签,还学一个来自强CNN教师网络的软标签,相当于把CNN多年积累的归纳偏置通过软标签蒸馏到了Transformer学生网络中。
这个思路给我的启发很大:归纳偏置不一定非要靠结构去写死,也可以通过知识和标签的形式迁移。DeiT展示了一个实际可用的路径:在ImageNet-1k这样百万级的数据集上,不依赖外部超大数据集,同样能训练出超越传统CNN的Transformer模型。对于算力和数据都有限的中小团队来说,蒸馏方案是降低视觉Transformer使用门槛的有效手段。
5.2 Swin:层级金字塔与移动窗口注意力
如果说ViT确立了“图像序列化”的框架,那Swin则补上了它缺失的两块拼图:多尺度层级结构和高效局部注意力。Swin把整个网络划分成多个阶段,每个阶段结束后用类似于Patch Merging的操作把token数量减半、通道数翻倍,逐渐形成类似CNN从高分辨率到低分辨率的特征金字塔。这个结构让模型天然适合目标检测、语义分割等需要多尺度特征的密集预测任务,而原始ViT那种单一分辨率的全局token序列在这个方向上很吃亏。
窗口注意力则是Swin的另一大创新。它把原本全局的自注意力限制在固定大小的局部窗口内计算,比如7×7个token的小窗口,这样注意力矩阵的规模大幅缩小,计算复杂度从O(n²)降到了近似线性。但问题也来了:同一个窗口内的token只能互相交流,窗口与窗口之间就断了联系。Swin的解法是移动窗口,每一层窗口偏移半个窗口大小,让原本不在同一个窗口内的token有机会在下一层交互。这种“局部计算、全局流通”的设计,让Swin在效率和精度上都取得了很好的平衡。
5.3 面向检测、分割的架构调整思路
除了Swin,视觉Transformer在密集预测方向上还有一系列针对性调整。PVT采用了空间缩减注意力:在计算注意力之前,先把Key和Value做空间下采样,从而降低计算量;它同时保留了金字塔结构,方便直接接进检测分割框架里。还有不少工作聚焦于如何让注意力范围随层级变化,从浅层的局部注意力逐步过渡到深层的全局注意力,试图同时在精度和效率间找平衡。
看多了这些变体之后,你会发现它们的共性非常清晰:本质上都是在ViT这条骨架上,根据任务需求调整“注意力范围”和“尺度结构”。分类任务通常偏好大范围甚至全局注意力,因为语义类别往往依赖全图上下文;检测分割任务则更看重多尺度层级和可控的计算量。理解了这条动机线,面对层出不穷的新模型时就不会迷茫了。
6. 训练与选型中的实际经验
6.1 数据规模和增强策略是第一条命脉
我在小规模数据集上从零训练ViT时,得到过非常明显的教训。数据集只有几万张图,模型是ViT-B,训练过程里验证集准确率始终低于同参数量的CNN,而且曲线震荡明显。那个阶段我一度怀疑是代码写错了,后来才意识到,问题的根源在于归纳偏置不足:ViT缺少CNN那种局部性和平移不变先验,所有空间关系都要靠数据去“喂”出来,数据不够,它就学不稳。
对应策略主要有两条。第一是把数据增强用满,尤其是Mixup、CutMix、RandAugment、随机裁剪翻转这些手段。在ViT上,增强的作用比CNN更明显,因为它们在制造更多的空间多样性,逼迫模型学会更鲁棒的特征。第二是优先使用预训练权重。基于千万级甚至上亿级图像预训练过的ViT,拿到中小数据集上微调,效果远好于从零训练。这也是我在现有开源预训练生态相对成熟后,一直推荐大家走的路线。
6.2 优化器、学习率调度与其他超参
ViT训练通常用AdamW而不是SGD,这一点和CNN时代很不一样。Adam类优化器在面对Transformer这种注意力密集的网络时,收敛过程更稳定。预训练阶段学习率常用0.001左右,配合warmup策略,在前几千步把学习率从0线性升到目标值,避免模型一开局就剧烈震荡;之后一般接cosine衰减,让训练在后期稳步收敛。
权重衰减对视觉Transformer尤其敏感。预训练阶段会用比较大的权重衰减,比如0.05;但微调阶段如果直接沿用这个值,反而可能压制可学习位置编码等参数的更新,导致收敛变慢或精度下降。我在一个小数据集微调时试过把权重衰减从0.05调大到0.3,验证集准确率直接掉了两个多点。以下是一份我验证过的中小数据集微调参考配置,复现性比较稳:
| 超参数 | 推荐值 | 备注 |
|---|---|---|
| 优化器 | AdamW | 比SGD收敛稳定 |
| 基础学习率 | 5e-5 ~ 2e-4 | 微调阶段不宜过大 |
| Warmup步数 | 总步数的5% ~ 10% | 防止早期震荡 |
| 学习率调度 | cosine decay | 收敛曲线更平滑 |
| 权重衰减 | 0.01 ~ 0.05 | 小数据集优先选0.01 |
| Drop path | 0.1 ~ 0.2 | 有效缓解过拟合 |
还有一个容易忽略的细节是随机深度(drop path)。ViT训练时加上随机深度,相当于让网络在训练中以一定概率“跳过”某些层的残差连接,这在深层Transformer里很常见。很多人只调学习率不调drop path,结果过拟合始终压不住;把drop path从0调到0.1或0.2后,往往会有明显改善。
6.3 任务场景与模型选择的权衡
选型没有银弹,我在不同项目里会用不同策略。如果是图像分类任务,数据量足够大且计算资源充裕,优先考虑ViT或者全局注意力建模能力强的改进型模型,它们在上限上确实更强。如果数据集只有几万张甚至更少,老老实实用CNN类的预训练模型,或者选择在超大图文数据上预训练过的Transformer权重做微调,不要轻易从零开始训练。
目标检测、语义分割这类密集预测任务,Swin这类金字塔结构通常比原始ViT更顺手,因为特征金字塔对多尺度目标更友好。部署到移动端或者推理速度要求极高的场景,CNN依然有明显优势,很多成熟推理引擎对卷积网络的优化也更完善。我的原则很简单:算力充足、数据充足、追求上限选Transformer;资源紧张、数据有限、追求稳定选CNN,或者用参数更小的Swin加预训练权重做折中。
最后分享一个自己养成的小习惯:在每次微调实验里,先打印并检查位置编码、class token和attention weight的数值尺度,如果在合理范围内再继续训练。这样做虽然多花几分钟,但能省下很多最后发现“跑了一整晚但方向从一开始就错了”的时间。视觉Transformer的坑,很多都藏在那些不起眼的中间张量里。
