如果谈到近几年视觉领域最让人睡不着觉的技术Vision Transformer绝对排得上号。一个把自然语言处理里的Transformer结构硬搬到图像上的想法居然在分类、检测、分割任务上跟CNN打得有来有回甚至在大规模数据上直接反超。这个标题本身就像一记闷拳——原来图像不止能用卷积来理解还能切成小块当序列喂给注意力机制。这篇文章不打算复述论文而是从一个实际使用者的视角把ViT的来龙去脉、原理细节、代码实现、训练踩坑一次讲透。无论你是刚接触Transformer的新手还是想在项目里评估是否换用ViT的从业者这份笔记都能给你一套可以直接落地参考的思路。先说结论ViT不是简单地把Transformer搬到图像上而是重新定义了“像素之间怎么交互”。传统CNN假设邻近像素之间关系最紧密离得远的就得靠层层堆叠慢慢扩大感受野ViT则一开始就让每个图像块Patch都能跟全图其他块直接计算相关度这种长程依赖建模能力是它最大的本钱。1. Vision Transformer是什么一次范式迁移1.1 从卷积到序列图像不再是网格而是词CNN的起点是局部感受野和权值共享它天生觉得图像是二维网格特征应该有空间层次。这种归纳偏置让CNN在小数据集上很占便宜——不需要太多数据就能学会边缘、纹理、形状。但它也有天花板想要让两个距离很远的像素发生关系必须靠深层网络的逐步堆叠信息一路传递过去路径越长损失越多。ViT的做法非常激进把一张图切成固定大小的Patch比如16x16然后把每个Patch展平成一个向量加上位置编码组成一串“图像词”送进标准Transformer Encoder。这里不再有卷积的局部性假设每个Patch从第一层起就能全局聚合信息。换句话说ViT把“看一张图”变成了“读一句话”每个词都是图像的一小块。这种转变最直接的影响是当训练数据足够大比如上亿张图ViT学到特征的表现力往往优于同规模的CNN。原因在于CNN先验“邻近相关”在部分场景下会限制模型发现更远、更抽象的关系而Transformer只依赖数据自己说话。1.2 记忆中的关键节点那张里程碑式的图我第一次看到那张部分块被随机遮住的图时很受冲击模型对遮挡区块的注意力散布在全图多个不相邻的位置比如遮住一只狗的头它会很大程度去关注狗的腿和背景草地这在卷积模型里很难出现——因为卷积每层感受野有限深层也很难同时覆盖那么多不相邻的语义区域。这篇开山之作的贡献不在于模型结构有多复杂恰恰在于它足够简单没有用任何卷积只有Patch Embedding 标准Transformer Encoder 分类头。这个结构在那之前被认为需要大量数据和强正则才能训练稳定但论文作者通过在大规模数据上预训练把ViT做到了在多个视觉任务上媲美甚至超过当时的SOTA CNN。从那之后ViT家族不断壮大。有在训练中引入蒸馏来保留CNN优势的变体有通过金字塔结构让Transformer适配多尺度任务的也有把卷积和注意力融合到一起的混合结构。但所有的后续工作都得回到这个基本思想图像是序列注意力是核心。2. 核心原理拆解ViT的四个关键零件2.1 Patch Embedding如何把图片切成“词”输入图像是三通道的张量shape为(H, W, C)。ViT第一步是把它切成N个小块每个Patch的尺寸为P×P那么块数量NHW/P²。每个Patch经过一个可学习的线性投影映射到维度D的向量这就是Patch Embedding。实际实现里线性投影通常用二维卷积来完成卷积核大小等于P步长等于P输出通道数为D。这样一整张图经过一次卷积直接得到N个D维向量比循环切Patch再Linear要高效得多。这里有两个细节值得注意Patch的尺寸决定了序列长度。P越小序列越长计算量越大P越大每个Patch信息越粗序列越短。16是经典的默认值。线性投影之后的特征向量本质上是这个Patch内容和语义的编码。事实证明Patch Embedding学到的特征分布非常清晰同一物体不同位置的Patch往往会聚类。2.2 位置编码与分类标记给序列加上顺序和入口Transformer本身没有顺序概念对所有Patch一视同仁。为了让模型知道每个Patch在图像中的位置必须加入位置编码。ViT使用可学习的绝对位置编码初始化成可训练参数随着模型一起更新。它跟语言Transformer的三角编码不同因为图像没有时间方向位置关系是二维的但直接展平成一维序列也能学得很好这让实现变得非常简单。另一个重要零件是 [CLS] Token。每次在Patch序列最前面额外拼接一个可学习向量它会经过所有Transformer层最后取出对应位置的输出接一个全连接层做分类。为什么用这个标记因为注意力机制里[CLS] Token可以看成全局信息汇聚的“池子”它通过各层注意力均匀地从所有Patch那里收集信息最终表示整个图像。在训练时我们也只用这个向量计算损失省去对所有Patch输出做全局池化的步骤。2.3 Multi-Head Self-Attention信息交互的核心每个图像词会生成Query、Key、Value三个向量分别表达“我想找什么”、“我就是什么”、“我实际上有什么”。注意力得分通过Query和Key的点积计算再Softmax归一化表示每个词应该关注其他词的程度。多头注意力则把D维空间分成h个子空间每个子空间独立计算注意力最后拼接在一起。这样多头可以分别关注不同的关系——可能一个头关注颜色纹理另一个头关注空间结构还有一个头关注跨区域的物体部件。这个机制带来的复杂度是O(N²D)N是Patch数量。对224x224输入、16x16 Patch来说N196还能接受但如果输入是1024x1024、Patch还是16x16N4096二次复杂度会爆炸。这也是为什么后续出现了一系列降低复杂度的工作比如窗口注意力、稀疏注意力、缩放注意力等。2.4 MLP和残差连接稳住训练深度必备每个Transformer Block里注意力之后接一个两层MLP中间用GELU激活最后用残差连接和LayerNorm把各个操作串起来。结构几乎是标准Transformer Encoder没有特别复杂的改动。真正影响训练体验的是初始化方式和Learning Rate。我实测下来ViT对初始化非常敏感。用较小的标准差初始化最后一层线性层比如标准差为1e-6能让训练初期更稳定。残差连接使得梯度能够贯穿深层但注意力层的梯度幅值波动很大需要配合Warmup 高学习率 AdamW才能跑得起来。这些细节在后面训练技巧里再展开。3. ViT凭什么能赢优势与局限的真实对比3.1 全局感受野与适应性注意力CNN用固定大小的卷积核感受野通过深度逐层扩大但这种扩大是有代价的当网络足够深反向传播梯度经过很多层很容易衰减。ViT从第一层起就拥有全局感受野每个Patch可以关注任意距离的其他块而且这种关注是基于数据自适应学出来的不是结构强制的。这个能力在需要大范围上下文的任务上特别明显比如图片分类中的物体遮挡、分割实例的边界、检测时的目标关系建模。我用ViT做图像分类时对比过它跟ResNet的特征可视化。ViT的注意力图往往能贯穿整个物体即使物体被部分遮蔽注意力也会跳到背景中的常见特征区域。这在医学影像、遥感图像这类全局上下文重要的场景下效果显著。而在纹理复杂的自然图像里CNN的局部归纳偏置反而更有优势——近邻像素确实高度相关卷积高效且不易过拟合。3.2 数据胃口为什么小数据集上容易翻车ViT没有CNN的归纳偏置这意味着它需要在训练中自己学习“邻近像素相关”这个规律。如果训练集只有一两万张图ViT很容易学到局部噪声或错误的相关性表现远不如同参数量的ResNet。这个现象在早期备受质疑后来业界总结出共识ViT需要在大规模数据千万级以上上预训练再在下游任务上微调才能发挥潜力。具体数据量门槛很难一概而论但我的经验是当分类训练集小于10万张图时ViT的收益一般抵不过它对数据的额外需求。此时可以考虑混合模型比如用卷积做前几层特征提取后面接Transformer或者加强数据增强、正则化来硬堆性能。盲目把小模型换成ViT大概率会得到一个更慢、更差的结果。3.3 计算成本与扩展性优点和缺点一体两面全局注意力带来了表达力也带来了计算开销。ViT的FLOPs跟Patch数量的平方成正比这就导致它天然偏“胖”——增大维度D的边际成本比增大分辨率低得多。所以ViT普遍用很大的Embedding维度比如768、1024配合较少的层数。反过来说如果我们想要模型同时处理高分辨率和高维度计算量会迅速突破单卡极限必须靠分布式训练或注意力变体。这也是为什么实际工程项目里很少直接用原始ViT处理高清图。常见做法是输入先用卷积下采样再进ViT或者干脆用层次化ViT。如果你只是因为“Transformer先进”而替换现有CNN一定要先算清楚你的硬件和时延预算能不能扛得住O(N²)的Attention。4. 手把手从零实现一个极简ViT4.1 搭建Patch Embedding和Transformer Block我这里用PyTorch写一个精简版ViT不含预训练的完整版本但结构完整、可以直接在这基础上扩展。代码里省去了很多细节装饰用于理解核心流程。先引入需要的库。import torch import torch.nn as nn class PatchEmbedding(nn.Module): def __init__(self, in_channels3, patch_size16, embed_dim768): super().__init__() self.proj nn.Conv2d(in_channels, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): # x: (B, 3, H, W) - (B, N, D) x self.proj(x) # (B, D, H/P, W/P) x x.flatten(2) # (B, D, N) x x.transpose(1, 2) # (B, N, D) return x这里用卷积实现投影一个操作搞定切块和线性映射。输入经过proj之后形状变为(B, embed_dim, H/P, W/P)然后flatten最后两维得到(B, embed_dim, N)再转置成(B, N, embed_dim)方便后续输入Transformer。Transformer Block就是标准encoder层包含LayerNorm、自注意力、MLP和残差。实现如下class Attention(nn.Module): def __init__(self, dim, heads8): super().__init__() self.heads heads self.scale dim ** -0.5 self.qkv nn.Linear(dim, dim * 3, biasFalse) self.proj nn.Linear(dim, dim) def forward(self, x): B, N, C x.shape qkv self.qkv(x).reshape(B, N, 3, self.heads, C // self.heads) q, k, v qkv.permute(2, 0, 3, 1, 4).unbind(0) attn (q k.transpose(-2, -1)) * self.scale attn attn.softmax(dim-1) x (attn v).transpose(1, 2).reshape(B, N, C) return self.proj(x) class Block(nn.Module): def __init__(self, dim, heads8): super().__init__() self.norm1 nn.LayerNorm(dim) self.attn Attention(dim, headsheads) self.norm2 nn.LayerNorm(dim) self.mlp nn.Sequential( nn.Linear(dim, dim * 4), nn.GELU(), nn.Linear(dim * 4, dim) ) def forward(self, x): x x self.attn(self.norm1(x)) x x self.mlp(self.norm2(x)) return xAttention实现里把Q、K、V放在一个Linear里一次算出来再拆成多头。scale取dim的负0.5次方用来稳定点积结果。Softmax前乘scale这是标准做法。4.2 组装完整的ViT模型有了PatchEmbedding和Block我们只需要拼装整体。先定义分类Token和位置编码然后堆叠多个Block最后通过分类头输出类别概率。class ViT(nn.Module): def __init__(self, image_size224, patch_size16, num_classes1000, dim768, depth12, heads12, mlp_dim3072): super().__init__() self.patch_embed PatchEmbedding(3, patch_size, dim) n_patches (image_size // patch_size) ** 2 self.pos_embed nn.Parameter(torch.randn(1, n_patches 1, dim)) self.cls_token nn.Parameter(torch.randn(1, 1, dim)) self.blocks nn.ModuleList([Block(dim, heads) for _ in range(depth)]) self.norm nn.LayerNorm(dim) self.head nn.Linear(dim, num_classes) def forward(self, x): B x.shape[0] x self.patch_embed(x) # (B, N, D) cls self.cls_token.expand(B, -1, -1) # (B, 1, D) x torch.cat([cls, x], dim1) # (B, N1, D) x x self.pos_embed for blk in self.blocks: x blk(x) x self.norm(x) cls_out x[:, 0] return self.head(cls_out)初始化位置编码使用randn实际训练时模型会自己调整。如果用随机初始化建议加上适当的标准差缩放。depths、dim、heads三个参数直接决定模型大小。文中这段代码只做演示数据增强、dropout都省略了验证模型能否跑通可以用一个小随机张量试一下ViT(image_size64, patch_size16, dim192, depth4, heads4)(torch.randn(2,3,64,64))。4.3 如何加载预训练权重而不是从零训上面的代码训出来的效果通常很差因为随机初始化ViT对数据要求很高。实际项目里应该从公开的预训练权重开始。用某个深度学习库自带的VisionTransformer接口可以方便地导入在大规模数据上训练好的模型然后修改分类头适配自己的类别数。加载时需要注意源码和官方权重结构是否一致比如Norm位置、是否有额外Dropout层等不一致的话权重名称对不上需要自己写转换脚本。如果找不到现成接口也可以拿官方仓库分发的权重文件通过state_dict逐层加载。关键代码就一句话model.load_state_dict(torch.load(weight.pth), strictFalse)如果结构差异较大先用torch.load读出键名再手动建立新旧映射。这一步很容易因为命名差异翻车我在实际项目中踩过好几次。5. 把ViT训好经验、技巧和常见坑5.1 优化器、学习率与WarmupViT在ImageNet上的常用配置是优化器AdamW基础学习率0.003weight decay设0.3batch size 4096配合线性缩放学习率。我们做小数据集迁移时不可能用这么大的batch我的经验是默认Learing Rate从5e-4开始如果数据量小就降到1e-4batch size每翻一倍学习率线性乘以2。Warmup不能省。直接全量训练会发现损失在开始几百步内有明显波动尤其是前1-2个epoch因为Attention容易在训练初期被极端的梯度推入差的局部区域。建议warmup步数占总步数的5%~10%学习率从接近0线性增到目标值之后再用余弦退火逐渐衰减。这个组合在ViT上几乎稳定有效。5.2 数据增强和正则决定小数据上的成败ViT比CNN更需要正则。我用过的有效策略包括RandAugment随机组合多种颜色、旋转、平移等操作强度控制在中小幅度。Mixup和CutMix这两招对ViT特别有用混合样本能够打乱空间局部性防止模型依赖“周围像素必然相关”的捷径。Random Erasing / Patch Dropout模拟遮挡强迫模型建立全局依赖。特别是Patch Dropout随机丢弃部分输入Patch与ViT的序列输入非常契合。Label Smoothing将软标签引入防止过度自信稳定训练。Dropout也不可少通常加在Attention的Softmax之后和MLP中数值在0.1~0.3量级。但要注意预训练模型已经带了一部分正则微调时Dropout建议调小或不加否则因为数据量小很容易欠拟合。5.3 位置编码与分辨率微调变着花样用ViT对输入分辨率的变化相对敏感。因为预训练时位置编码是固定长度如果直接输入更高分辨率图像Patch数量增加会导致位置编码长度不够。一个常用办法是二维插值把预训练的位置编码矩阵重排回sqrt(N)×sqrt(N)的形状然后插值到新尺寸再展平。这个操作在检测、分割中常用因为这类任务需要更高的输入分辨率。但插值后的位置编码会略改变空间语义最好在目标数据上微调几个epoch让模型重新适应。如果新分辨率跟预训练差异太大比如从224到896直接插值效果差建议先用224微调再逐步升分辨率逐步微调。我在一个细粒度分类项目里试过从384微调效果比直接跳到448好得多。5.4 常见问题速查训练走了弯路怎么办现象可能原因处理建议训练损失不降学习率过大/过小没有warmup调低到1e-4增加warmup验证集精度远低于训练集数据增强不够过拟合增加Mixup、CutMix、Dropout注意力图全是背景位置编码没学好训练不充分延长训练步数检查初始化高分辨率微调崩溃位置编码插值不合理分阶段升分辨率或使用相对位置编码变体显存不足Patch数量太多Attention开销二次增长减小Patch尺寸使用窗口注意力或降采样我遇到最多的问题是“训练稳定好用但泛化差”。这往往是因为在ImageNet预训练上微调时学习率没有降到足够低且数据增强不够激进。对ViT来说微调时的关键是把预训练的知识保留住而不是从头学新数据所以有效学习率通常比CNN微调低不少。6. 什么时候选ViT项目里的决策参考6.1 适合ViT的场景数据量充足至少几十万张或者有强预训练模型可迁移任务需要长程依赖比如目标检测中识别相互覆盖的实例、医学影像里的病变区关联图像分辨率中等但类别语义复杂比如细粒度识别团队算力够能承受训练和推理时的计算开销在这些场景里ViT的成绩通常高于同规模CNN尤其是大规模预训练之后微调收益稳定明显。6.2 不适合ViT的场景只有几万张图甚至几千张图的垂直任务对推理延迟要求严格需要在边缘设备上跑输入图片非常大且无法下采样Attention开销会失控你已经有一套调试好的CNN推理链路换模型带来的收益未必高对于这些情况老老实实选ResNet、EfficientNet或混合结构更划算。我见过有人因为“Transformer高级”硬换成ViT结果精度低了5个点训练时间还翻倍。技术选型从来不是选最新的而是选最合适的。6.3 一条务实的折中路径卷积与注意力的融合纠结选CNN还是ViT其实还有一种解用CNN的前几层做降采样和局部特征提取后面接Transformer做全局建模。这种混合结构同时保留了局部先验和全局交互能力在中小数据集上比纯ViT更稳在大数据上也不逊色。实现时可以把PatchEmbedding替换成若干卷积层分辨率降到合适大小后再进入Transformer块这样既省显存又补足了纯ViT在纹理细节上的弱项。我在一个遥感图像分类项目中就用了类似方案前四层用卷积后面三个Transformer Block做全局融合。数据只有8万张混合结构比纯ViT提升了约3个点训练速度还快了接近一半。如果项目初期无法确定数据量是否撑得起纯ViT混合结构是更稳妥的起点。写在最后的实操体会ViT没有想象中那么高冷但也没有营销号吹得那么万能。我自己的实践感受是如果你已经有了一套表现不错的CNN方案先用它做baseline再考虑要不要加ViT如果你刚开始一个视觉项目数据量又不小直接上预训练ViT微调往往能带来惊喜但一定要重视优化器和数据增强的细节。多试试把注意力图可视化出来你会在上面看到许多CNN不太会表现出的全局语义交互这种理解对排错和调参都有帮助。这篇文章写到这里剩下的就是动手去跑一个最小模型哪怕先跑通一次穷举也不是白费功夫。