资讯动态

视觉Transformer ViT原理与实战:从结构拆解到小样本分类

发布时间:2026/10/9 9:20:30 来源:尧图企业网站定制
最近翻看各大模型榜单一个很明显的趋势是几乎所有新发布的视觉模型骨架都换成了ViT。从CLIP到DINOv2从SAM到各种多模态大模型背后的视觉编码器清一色是Transformer结构。这篇科普想把这件事讲透为什么视觉模型会集体转向ViTViT到底解决了什么问题如果你正在做图像分类、目标检测或者想让模型在小样本场景下也能出效果这篇文章值得读完。我会尽量少讲数学公式多用类比和实际案例把ViT的原理、选型逻辑和实操经验拆开揉碎。看完之后你至少能回答三个问题ViT的核心结构是什么为什么它逐渐成为主流以及我在自己的任务里该怎么用、怎么调整。1. 为什么大家都在谈ViT——视觉模型的新常态1.1 现象观察视觉榜单上的名字悄悄换了如果你关注过ImageNet、COCO或者各种视觉挑战赛的榜单会注意到一个变化几年前占据榜首的还是各种深度卷积网络ResNet、DenseNet、EfficientNet但最近两年前排基本被ViT系列和它的变体包揽。不只是学术榜单。实际业务里很多算法团队的默认baseline也从ResNet换成了ViT系列。比如做图像检索、特征提取、多模态对齐直接拉一个预训练好的ViT出来用效果往往比精心调过的CNN还好。这种感觉就像前几年大家还在争论“卷积网络是不是视觉任务的终极答案”现在争论已经结束了Transformer赢下了这一轮。从我自己的体感来说最明显的变化是换模型之后的迁移成本变低了。之前用CNN做下游任务经常需要针对任务结构去设计不同的backbone和neck现在大家都是ViT很多模型的接口、预训练权重、微调策略都高度统一代码通用性反而好了很多。1.2 什么是ViT把图片当成句子来读的模型ViT的全称是Vision Transformer核心思想很简单把一张图片切成很多个小块patch每个小块当成一个“词”然后像处理文本一样用Transformer去建模这些“词”之间的关系。打个比方以前用CNN看一张图片相当于用一个小放大镜在图片上滑来滑去一开始只能看到局部细节然后通过一层层堆叠才能逐步拼凑出全貌而ViT更像是一眼把整张图摊开在桌面上把所有小块都排列好然后通过注意力机制直接让每个小块和整张图的其他小块对话。这种“全局建模”的能力是ViT和CNN最大的区别也是它能够快速占领视觉领域的关键原因。1.3 这篇科普适合谁读如果你是学生或者刚入行的算法工程师这篇文章能帮你少走弯路。你不需要先啃完Attention Is All You Need也不用手推反向传播我会把ViT从结构到实战一层层拆开。如果你已经在用CNN做项目正在犹豫要不要换ViT这篇文章会告诉你不同场景下的取舍数据量不够怎么办分类头要不要改1-shot场景下怎么用最划算如果你只是好奇“视觉模型为什么变成这样”也可以看。我会尽量少用名词轰炸把每个概念都掰开讲清楚。2. CNN到ViT视觉骨干网络的路线之争2.1 CNN的立身之本归纳偏置要理解ViT为什么能崛起得先回头看CNN曾经为什么能统治视觉任务。CNN的核心是卷积算子每次只看一个小邻域通过共享权重捕捉局部模式。这个设计天然带有所谓的“归纳偏置”图片具有局部相关性邻近像素往往有相似的含义以及平移等变性同一个物体出现在图片不同位置卷积核能照样识别。归纳偏置的好处是数据效率高。你用少量数据就能训练出一个像模像样的CNN因为卷积结构的先验假设和图片的自然属性高度吻合。但副作用也很明显CNN的感受野是局部的哪怕堆了很多层一个输出单元理论上能看到的区域也是有限的。要让模型真的做到“看一眼全局”通常需要很深的网络或者复杂的特征金字塔结构。这在图像分类任务上还能忍到了需要长距离依赖的任务比如全景分割、视频理解、图像修复CNN全局建模能力不足的问题就会被放大。2.2 Transformer凭什么跨界成功Transformer最初是为序列建模设计的靠的是自注意力机制。自注意力的意思是序列里的每个元素都会和其他所有元素计算相关性然后根据相关性加权聚合信息。这让它天然具备全局视野不需要层层堆叠第一层就能看到整个序列。视觉任务里的“长距离依赖”需求其实一直存在。比如要判断一张图里是不是有一辆卡车模型需要同时看到车头、车斗和周围环境光看局部像素块是不够的。CNN需要很深才能建立这种远距离联系Transformer注意力一步到位。ViT的做法是把图片切成patch后拉成序列。每个patch相当于一个“词”图像变成了类似句子的输入。然后Transformer encoder直接在这个序列上做全局注意力建模。这样ViT不再依赖卷积那种局部共享权重的方式而是“让数据自己找到该关注哪里”。2.3 弱归纳偏置大数据更强的上限ViT的头几个版本在ImageNet上其实并没有马上超过ResNet原因很简单Transformer没有CNN那种内置的局部先验因此在中小规模数据集上会学得很慢甚至过拟合。转折点在于尺度。当预训练数据够大时比如在JFT-300M、ImageNet-21k上训练ViT的表现就能反超CNN。这就引出一个关键结论ViT的归纳偏置更弱所以更能依靠数据本身去拟合真实规律CNN的归纳偏置强数据少的时候占便宜但数据大了之后反而被自己的先验假设限制住了。后来大家发现不需要几十亿的数据也能训练好ViT前提是引入合适的训练技巧比如DeiT提出的数据增强和蒸馏策略、Swin引入的局部注意力、MAE的表征自监督预训练。这些改进补上了ViT“吃数据”的短板也彻底点燃了视觉领域的Transformer热潮。2.4 为什么是“最近”爆发“最近发布的视觉模型多为ViT结构”这个现象其实有清晰的现实驱动力。第一多模态模型成为主流CLIP这种用图文对比学习的预训练范式天然依赖Transformer编码器第二大规模预训练基础设施成熟大家有条件在几十亿图文对上去训练一个弱归纳偏置但强上限的模型第三硬件和算子库升级无论是CUDA算子还是TensorRT都对Transformer结构做了大量优化推理部署不再是瓶颈。所以说ViT不是凭空出现的革命而是数据和算力发展到一定程度后自然选择的结果。维度CNNViT感受野局部需要深层次堆叠全局第一层即可跨patch建模归纳偏置强局部性、平移等变弱主要靠数据学习数据需求中小数据也容易拟合大数据下上限更高计算开销对高分辨率输入友好高分辨率下注意力矩阵开销大迁移性需针对任务设计结构结构统一通用性强3. ViT结构核心拆解从Patch到分类头3.1 Patch Embedding把图像切成“词”ViT的第一步是把图像切成小块。假设输入图片是224x224x3设置patch size16那么整张图会被切成(224/16)^2196个小patch每个patch的大小是16x16x3。然后把每个patch拉平成一维向量维度是16163768再通过一个线性投影层映射到D维向量空间。这个D通常就是Transformer的隐藏层维度比如ViT-Base取的是768。这一步叫Patch Embedding本质是把“像素块”变成“词向量”。可以这样理解之前CNN理解图像的最小单位是像素和卷积核ViT理解图像的最小单位是patch。patch size选多大决定了模型能保留多少局部细节。patch越小序列越长计算量也越大patch太大细节信息丢失严重分类效果会下降。3.2 位置编码给patch排好顺序Transformer本身不关心序列顺序所以ViT必须显式告诉模型每个patch原本在空间上的位置。ViT的做法是加一个可学习的位置编码向量维度与patch embedding相同直接相加。这就像给每个词发一个编号让模型知道哪些patch是相邻的。虽然从原理上看可学习位置编码不一定有严格的几何意义但实验证明它足够让模型学会“邻近关系”。有一个小坑如果你想用预训练好的ViT处理更大分辨率的输入patch数量会变化比如从196变成785位置编码就对不上了。常规做法是用插值把预训练好的位置编码扩展到新的长度然后在你的数据上微调一段时间让模型适应新的位置分布。3.3 Transformer Encoder自注意力与全局建模ViT的主干是标准的Transformer Encoder由多层组成每层主要包含多头自注意力机制、MLP、LayerNorm和残差连接。自注意力机制会为每个patch计算一个“注意力权重”。比如图像里有一个“猫头”patch和一个“猫耳朵”patch模型会让它们互相增强而背景patch和它们的关系就会被弱化。这种动态地构建patch与patch之间的依赖关系正是ViT强大的原因。和NLP有所不同的是图像patch之间的空间关系更多是二维的。早期的ViT直接使用一维位置编码加一个[CLS] token。这个[CLS] token的作用类似BERT里那个特殊token它会通过注意力机制汇总整张图的信息最后用于分类。所有patch的信息经过多层编码后都会注入到[CLS] token对应的表征中分类头本质上就是在读这个汇总向量。3.4 分类头的设计与预训练习惯ViT在预训练时会在[CLS] token的输出后面接一个线性分类头输出维度等于预训练数据集的类别数。比如在ImageNet-21k上预训练分类头就是21k维。这就引出了一个经常被问的问题用ViT做评估时分类头需要调整吗答案是要看你的评估场景。如果你的任务是直接用别人训好的模型做zero-shot推理比如测试模型在ImageNet-1k上的准确率那分类头当然要保留原始的样子。但如果你要在自己的数据集上做分类类别数量几乎不可能等于预训练类别数这时候就必须把分类头换掉。注意换分类头不是只改一个数字。ViT的[CLS] token输出维度通常是768或1024你的新分类头应该是一个线性层输入维度是768/1024输出维度是你的类别数。用timm或者Hugging Face加载模型时设置num_classes参数就能自动替换。3.5 ViT的常见变体DeiT、Swin、MAE、CLIP纯ViT有一个痛点在中小数据集上容易欠拟合或过拟合。后来涌现的变体各有解法。DeiT通过数据增强和蒸馏策略让ViT在小数据集上也能训得不错。Swin引入层次化结构和窗口注意力降低了计算复杂度同时保留了局部建模能力在检测和分割任务上表现优异。MAE则走了另一条路用自监督掩码重建的方式预训练ViT让模型在不需要海量标注的前提下学出高质量表征。CLIP把视觉编码器和文本编码器进行对比学习让ViT天然具备了zero-shot图像分类能力。这些变体本质上都是在“全局注意力”和“可学出来的局部性”之间做权衡目的都是让ViT更落地。4. 实操用ViT做图像分类的完整流程4.1 环境准备与模型加载目前加载ViT最常用的工具是Hugging Face Transformers和timm。前者接口统一适合快速测试后者以预训练权重丰富著称适合做实验对比。以timm为例加载一个ViT-Base模型只需要几行代码import timm model timm.create_model(vit_base_patch16_224, pretrainedTrue, num_classes1000)用Hugging Face的方式from transformers import ViTForImageClassification, ViTImageProcessor model ViTForImageClassification.from_pretrained(google/vit-base-patch16-224) processor ViTImageProcessor.from_pretrained(google/vit-base-patch16-224)这段代码跑通的前提是网络环境允许下载权重。权重文件通常几百兆第一次加载会慢一点之后有本地缓存就会快很多。4.2 用ViT评估时分类头到底要不要调整这个问题在很多技术社区里反复出现我直接给结论。场景一你在做零样本评估。也就是说你完全不做任何微调只测试预训练模型在目标数据集上的表现。这时候分类头必须保持原样因为它的输出维度对应预训练类别模型预测结果就是预训练类别标签的logits。你可以直接对logits做softmax看预测置信度。场景二你要在自己的数据集上做分类。这时候分类头必改。假设你自己的数据有10个类别而预训练模型分类头是1000维那就是一个维度不匹配的报错。正确做法是设置num_classes10。场景三你只想用ViT做特征提取后面接一个自定义分类器。这是最常见的用法。你只需要取出模型倒数第二层的[CLS] token输出作为特征向量然后自己训练一个小分类头比如LR、SVM或者一个简单的MLP。一个容易踩坑的细节在timm中如果想做特征提取通常需要调用forward_features方法而不是直接forward。因为直接forward返回的是分类结果而不是中间特征。import torch import timm model timm.create_model(vit_base_patch16_224, pretrainedTrue, num_classes0) model.eval() dummy_input torch.randn(1, 3, 224, 224) with torch.no_grad(): features model.forward_features(dummy_input) # shape: [1, 196, 768] # 如果只想用 [CLS] token一般取第一维或最后一维具体看模型实现 cls_token features[:, 0]注意不同库对ViT的forward_features实现略有差异有的返回所有patch token加[CLS] token有的直接返回[CLS] token建议打印一下shape确认。4.3 小样本图像分类1-shot的实践要点结合热搜词“小样本图像分类1-shot”我讲讲ViT在小样本场景下的使用心得。小样本分类的核心矛盾是数据量太少。如果只有每个类别1张或几张图直接全量微调ViT几乎必然过拟合。哪怕你用再强的数据增强几万甚至几亿参数的网络也不是几张图能撑起来的。我测试下来最稳的做法是把ViT当成固定的特征提取器不更新主干参数只对提取的特征做分类。具体流程可以这样设计加载预训练ViT模型设置num_classes0去掉分类头。读取所有支撑集图片提取特征向量得到每个类别的“原型向量”。比如1-shot时每个类别只有一张图那这个类的原型就是那张图的特征。读取查询集图片提取特征。用余弦相似度计算查询特征和每个类别原型的距离取最近的作为预测结果。import torch import torch.nn.functional as F # 假设 extracted_features 是从模型提取的 [N, D] 特征矩阵 # support_feats 是每个类别的原型特征shape [C, D] # query_feat 是查询图片特征shape [D] cosine_sim F.cosine_similarity(query_feat.unsqueeze(0), support_feats, dim1) pred_class cosine_sim.argmax().item()这种最近邻方法虽然简单但在ImageNet预训练ViT的特征空间里效果往往比直接微调全模型更好。原因是预训练特征已经具备很强的判别能力而小样本场景下你最缺的不是容量而是泛化能力。如果你非要微调建议遵循这三个原则第一冻结backbone的大部分层只解冻最后几层Transformer Block。第二只训练新加的分类头。第三学习率调到很小比如1e-5以下配合warmup。4.4 微调ViT的完整训练循环示例下面给一个简单的PyTorch微调代码思路适配自定义数据集import torch import torch.nn as nn from torch.utils.data import DataLoader from transformers import ViTForImageClassification model ViTForImageClassification.from_pretrained( google/vit-base-patch16-224, num_classes10, ignore_mismatched_sizesTrue # 自动替换分类头 ) # 冻结一部分层 for name, param in model.named_parameters(): if vit.encoder.layer in name: layer_idx int(name.split(layer.)[1].split(.)[0]) if layer_idx 8: # 前8层冻结 param.requires_grad False optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr5e-6) criterion nn.CrossEntropyLoss() for epoch in range(epochs): for images, labels in dataloader: outputs model(pixel_valuesimages).logits loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step()这里用ignore_mismatched_sizesTrue可以让库自动处理分类头维度变化非常省事。5. 常见问题与排查技巧实录5.1 ViT训练不稳定loss经常乱跳怎么办我自己踩过最大的坑是学习率给太高。ViT对学习率比CNN更敏感特别是微调阶段一个过大的学习率很容易让整个模型崩溃。如果你发现loss不降反升或准确率突然骤降先用一个小学习率测试比如1e-5然后慢慢往上调。另一个常见原因是LayerNorm的位置。ViT大多采用Pre-LN结构也就是在残差连接之前先做LayerNorm。这种结构的好处是训练更稳定但对初始学习率也有一定要求。实践中我建议配合warmup策略前5到10个epoch线性升高学习率然后再衰减能显著避免loss爆掉。5.2 patch size怎么选还是得看任务。patch size16是ViT最经典的配置适合大多数分类任务。patch size8会保留更多细节在小目标检测或细粒度分类里效果更好但序列长度变成196的四倍GPU显存消耗会剧增。patch size32速度更快适合特征粗度要求不高的场景比如场景分类。如果你用timm创建模型时可以选择patch size不同的权重比如vit_base_patch8_224、vit_base_patch16_224、vit_base_patch32_224。预训练权重不同实际效果差距不小不建议自己随意改patch size而不微调。5.3 推理速度慢显存占用高ViT在224x224输入下patch size16时序列长度是196显存压力并不大。但如果你要处理高分辨率输入比如384x384或640x640patch数量会变为原来2到3倍显存和延迟都会明显上升。排查思路有几条。第一确认是否真的需要那么高的输入分辨率很多时候224x224已经够用强行上384只会增加成本。第二考虑用Swin或小模型变体替代纯ViT比如Swin-Tiny、ViT-Tiny速度和质量往往能找到更好的平衡。第三如果部署环境支持INT8量化可以对注意力层或者整个encoder做量化实测下来精度损失可控速度提升明显。5.4 ViT API介绍Hugging Face、timm、TorchVision怎么选围绕热搜词“vit api介绍”我整理三个常用库的适用场景。Hugging Face Transformers接口非常统一从加载预处理到模型输出都封装好了适合快速原型、多模态模型调用。缺点是抽象层厚想深入改内部结构反而绕。timm更适合研究人员和算法工程师预训练权重极其丰富很多ViT变体都能找到现成的权重。timm的代码更贴近torch原生风格改起来灵活。缺点是自己要写更多预处理逻辑。TorchVision其实也内置了ViT模型比如torchvision.models.vit_b_16、vit_l_16加载方式简单适合不想引额外依赖的场景。但权重数量有限灵活性也低一些。三者的选择标准可以概括为图省事用Transformers做实验用timm基础使用用TorchVision。5.5 踩坑记录位置编码插值、混合精度、数据增强先说位置编码插值。我想把一个224x224预训练的ViT用到384x384输入上直接改输入尺寸会报位置编码长度不匹配的错。处理方法是用torch.nn.functional.interpolate对位置编码做双线性插值然后把插值后的向量加入模型。但要注意插值之后最好在自己的数据上微调几十步让位置编码适应新尺度否则效果会明显下降。再说混合精度训练。ViT的结构里有大量矩阵乘法开启AMP通常能显著省显存和加速训练。但有个小坑LayerNorm和Softmax这类操作在FP16下可能精度不够AMP会自动把它们切回FP32所以问题不大。真正需要注意的是loss scaling。如果发现早期loss变成nan多半是精度下溢可以调整scale或直接关闭AMP试试。数据增强方面ViT不像CNN那么依赖数据的局部扰动。DeiT中大量使用的RandAugment、Mixup、CutMix对ViT同样有效。小样本场景下光用翻转和裁剪不够建议加上更激进的增强比如随机擦除、颜色扰动。不过要注意增强要符合任务语义比如医学图像就不适合乱翻转。6. 一点个人体会我个人做过的几个项目里ViT在图像分类上确实给过我很强惊喜尤其是在有明显物体语义边界的场景下它的全局注意力比CNN更容易抓住关键部位。但我也遇到过翻车的时候比如小数据集上直接训ViT效果反而不如ResNet后来改成冻结主干加线性探针才稳住。现在的视觉模型生态确实已经被Transformer框架占据大半ViT不再只是“未来趋势”而是很多团队实际跑得通的默认选择。推荐的做法是先跑通一个预训练ViT的baseline别急着改结构把分类头、预处理、评估流程理清楚再根据任务特点决定要不要换Swin、加位置编码插值或者换更强的变体。另外如果是做1-shot这类小样本场景强烈建议先用特征提取加最近邻的方式做一个基线不要一上来就全量微调。很多时候特征质量决定了下限分类头只是锦上添花。最后再分享一个小技巧无论你用哪个库加载ViT都先打印一下输入输出的shape确认[CLS] token的位置和分类头结构。这个习惯帮我避开了很多人容易忽略的维度坑也让你对模型内部细节更有手感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑