news 2026/10/24 6:58:04

视觉Transformer(ViT)原理详解:从图像分块到注意力机制与实操

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
视觉Transformer(ViT)原理详解:从图像分块到注意力机制与实操

两年前我第一次认真研究视觉Transformer时,心里其实是有点不以为然的——把图片切成小块,再当成“句子”去读,这听起来太像一个花活。可是当我真正跑通一条推理流程,又用它解决了一个实际项目里的细粒度分类问题之后,我发现自己原有那套“图像识别必须靠卷积”的直觉,确实需要被重新审视。这篇文章就是写给“完全没有基础”的读者的:不堆公式、不讲复杂数学,而是用人话把ViT(视觉Transformer)拆开,讲清楚它到底在做什么、为什么这么做、以及你想亲手跑一个该注意什么。如果你只听说过“Transformer很强,现在连图像也能做了”,但对它的原理一头雾水,这篇文章就是给你写的。

1. 先从CNN说起:为什么图像识别不能一直靠卷积

1.1 图像识别到底在做一件什么事

图像分类的本质,是让模型回答一个问题:这张图里是什么?为了做到这一点,模型必须从像素里提取出“有意义的模式”——边缘、颜色、纹理、形状,再到物体的一部分、乃至整个物体的结构。传统上统治这个领域的是卷积神经网络,也就是我们常说的CNN。

CNN的做法可以这样理解:它拿着一小块“放大镜”(卷积核)在整张图上从左到右、从上到下地滑动,每滑到一个位置就做一次局部运算,把这个小区域的特征记录下来。第一层卷积可能只能看到边缘和颜色块,第二层能看到纹理和拐角,层数越深,看到的模式越高级。这个过程很像一个保安在大厅里拿着手电筒一点点扫描,每一圈都比上一圈看得更细、看得更远。

1.2 卷积的核心假设

CNN能这么高效,靠的是两个天生的“预设”:第一,相邻像素之间的关系最重要——这叫局部性;第二,一个物体出现在图片左上角还是右下角,本质上应该是同一个物体——这叫平移不变性。这两个预设让CNN不需要看完整张图,就能把特征学得很好,参数量也被压得比较低。

但问题也随之而来:因为每个卷积核每次只看一小块,模型若想理解“左上角的车轮和右下角的车门之间是什么关系”,就必须通过一层一层堆叠,把感受野慢慢扩大。为了捕捉这种全局关系,你得把网络做得非常深,做得很深之后又得处理梯度消失、训练不稳等一系列麻烦事。更麻烦的是,信息从底层传到高层,经过的层数越多,中间的细节越容易被稀释。

1.3 另一边厢的Transformer

在CNN统治图像的同时,自然语言处理领域杀出了一个叫Transformer的架构,它彻底改变了局面。Transformer的核心思路和CNN完全不同:它不依赖局部性,也不依赖位置,而是让序列里的每个元素直接去和所有其他元素建立联系。比如在一句话里,“苹果”这个词到底是指水果还是手机品牌,光看自己是不够的,得结合整个句子里的其他词来理解。Transformer的注意力机制能一次性看到整句话的所有词,然后根据任务动态决定“我该多看看谁”。

这给图像领域的人带来一个很自然的联想:既然语言里词与词之间的全局关系这么重要,那图像里的“像素块”之间不也有类似的全局关系吗?如果干脆把图片当成一句话来处理,是不是就不用辛辛苦苦堆几十层卷积了?这就是ViT出现的契机。

2. 一张图变成一句话:ViT的整体思路与patch拆分

2.1 把图片切成Patch

ViT的第一步,是把一张图片切成固定大小的正方形小块,这些小块叫Patch。拿最常用的配置来说:一张224×224的彩色图片,切成16×16的小块,会得到(224÷16)的平方,也就是196个Patch。你可以把这步理解成把一幅拼图拆成一堆拼图块,每一块都自带一小片局部图案。

切完之后,每个Patch的形状是16×16×3,也就是一共768个数值。这768个数值里,有红绿蓝三个通道,每个通道16×16。可是这样的原始像素数值太“原始”了,直接拿去做注意力运算不合适。于是ViT加了一个线性投影层,也就是一个矩阵W,把这768个数值映射成另一个768维的向量。这个向量就叫Patch Embedding,相当于把“像素拼图块”翻译成了“模型能理解的语言单位”。

2.2 Token这个词从哪来的

如果你接触过自然语言处理,一定听过Token这个词。在语言模型里,一段话会被切成一个个Token,每个Token就是一个单位词。ViT借用了这个概念:一个Patch就是一个Token,196个Patch就是196个Token的序列。整张224×224的图,最终变成一句话,这句话的长度就是196个“词”。

这里有一个关键点:Patch不是随机乱切的,它保留了原始位置。左上角的Patch和右下角的Patch在序列里的编号是不同的,这为后面的位置编码留了伏笔。线性投影层本身的参数是全体Patch共享的,也就是说,不管Patch出现在哪里,都使用同一个“翻译器”,这保证了翻译规则的一致性。

2.3 为什么要加一个CLS Token

把Patch序列交给Transformer之前,还差一个特殊的Token,叫[CLS]。这个Token没有对应的图像区域,它就像一张空白的“总结卡片”,被放在序列的最前面。Transformer处理完整段序列之后,[CLS]对应的输出向量,会作为这张图片的整体表示,再送进分类头做最终的类别判断。

你可能会问:为什么不直接把196个Patch的输出平均一下当作整图表示?理论上可以,但平均操作会丢掉很多信息,而且没有一个可学习的“总结者”。[CLS]的特殊之处在于,它从头到尾都参与注意力计算,它有权决定“自己需要从哪个Patch多收集一些信息”。训练完成之后,[CLS]就学会了如何从整张图里提炼出最有判别力的信号。这个设计是从语言模型里继承的,也是ViT保持“一个序列走到底”风格的关键。

3. 位置编码:Transformer没有眼睛,全靠编号

3.1 自注意力天然不区分先后顺序

我在前面提到,Transformer的核心是注意力:每个Token都可以直接看所有Token。这带来一个副作用——如果把一句话里的词全部打乱顺序,注意力计算的结果是一模一样的。因为注意力只看“内容之间的相似度”,并不关心“谁在前谁在后”。这对文本已经是个问题了,对图像更是灾难级的。

想想看,一张人脸图片,眼睛的Patch和嘴巴的Patch内容是有固定空间关系的。如果模型不知道“眼睛在上面、嘴巴在下面”这个布局,它就很难理解这是一张脸。为了让模型拥有空间概念,ViT必须主动把位置信息“喂”进去,这就是位置编码存在的理由。

3.2 位置编码长什么样

ViT用的是可学习的位置编码,简单来说,就是为每个位置准备一组可训练的参数,形状和Patch Embedding一样。假设你有196个Patch,那么就准备196个位置向量。每个Patch的向量,会加上对应位置的位置向量。这里的加法不是拼接,而是逐元素相加,也就是说,每个Patch的每个维度上,都混入了“我在第几号位置”的信息。

这个设计很像给每个拼图块发一张“座位号”。模型读到一个Patch时,不仅能看出这块拼图的内容,还能看出它坐在第几排第几列。至于“第14号和第15号是水平相邻的,第1号和第15号相隔很远”这种更复杂的空间关系,是模型在训练过程中自己慢慢摸索出来的。

3.3 如果不加位置编码会怎样

如果你动手做过实验,把位置编码去掉,训练一个ViT,你会发现它在验证集上的表现会明显下降。原因很直观:模型看一张图时,196个Patch的内容完全一样,只是排列顺序不同,但它给出的结果会几乎相同——这显然不符合图像识别的直觉。加上位置编码之后,模型才能区分“一辆车从左往右开”和“一辆车从右往左开”这样的空间差异。所以有句话我很认同:位置编码不是ViT的装饰品,而是它的眼睛。

4. 注意力机制:模型怎么知道该看哪里

4.1 从“查字典”理解Q、K、V

注意力机制里最劝退初学者的就是Q、K、V这三个字母,但它们的本质一点都不复杂。你可以想象自己在图书馆找书:你手里有一张便利贴,上面写着“我要找的主题”,这叫Query,简称Q;每本书的书脊上都贴着标签,写着这本书是什么,这叫Key,简称K;真正打开书才能看到的详细内容,这叫Value,简称V。

注意力做的事就是:拿着你的Q,去和每个位置的K做对比,看哪个K和你的Q最匹配,匹配度越高的位置,你就越要认真去看它的V。放到图片场景里,就是每一个Patch都会做一个“我该关注谁”的判断。比如一个猫头的PatCh,它的Q可能是“我要找耳朵和胡须”,它去和其他Patch的K对比,发现“耳朵区域的K”匹配度很高,于是它就会重点吸收耳朵Patch的信息。

4.2 打分、缩放、归一化、加权求和

注意力的计算可以拆成四步。第一步,给每个Token生成三份向量Q、K、V,它们是通过三个不同的权重矩阵分别对输入向量做线性变换得到的。第二步,计算某个Query和所有Key的点积相似度,得到一个分数。第三步,把分数除以一个缩放因子,这个因子通常是向量维度的平方根,目的是防止分数太大,导致后面的Softmax进入梯度消失区间。第四步,用Softmax把这些分数变成和为1的权重,再用这些权重对所有Value做加权平均,得到这个Token更新后的新向量。

整个过程可以用一句话概括:每个Token在看完别人之后,把自己变成了“基于全局信息重新表达”的样子。这也是为什么ViT能在一个模块里就获得全图感受野——它根本不需要像CNN那样一层一层扩大视野,第一层注意力就已经把左上角和右下角直接连起来了。

4.3 多头注意力:一个头不够,就看几个角度

一个注意力头学习一种“关系模式”可能不太够,所以ViT采用了多头注意力。它把Q、K、V切成多个维度更小的子空间,每个子空间独立做一次注意力运算。你可以理解成,一组人围着一张图,各看各的:一个头专门关注颜色连续性,一个头专门关注边缘对齐,还有一个头关注物体的整体轮廓。最后把所有头的结果拼在一起,再过一次线性层,得到多头注意力的最终输出。

多头的好处是让模型有能力同时从多个角度理解图像,而且每个头可以各司其职。实际使用中,一个16×16Patch的ViT通常用12个头,每个头学出来的关注模式都不太一样。做注意力可视化的时候你会发现,有的头关注前景物体,有的头关注背景纹理,非常有意思。

5. Encoder内部:自注意力之外的三个关键搭档

5.1 前馈网络:注意力负责看,MLP负责想

单个Transformer Block里,除了多头注意力,还有一个前馈网络,一般叫MLP。它的结构通常是两个全连接层中间夹一个激活函数,隐藏层维度通常是Token向量的4倍。为什么在注意力之后还要接MLP?因为注意力本质上是在做“信息挑选和融合”,它告诉模型“该把哪些信息凑到一起”;但凑完之后,还需要一层很强的非线性变换,把这些融合结果变成一个更高级的表达。

你可以这样理解:注意力像是一个善于社交的人,把所有朋友聚到了一起;MLP则是把这个聚会产生的讨论,整理成一份深度的会议纪要。没有MLP,堆再多的注意力层也无法表达足够复杂的关系。

5.2 残差连接:深层网络的“高速公路”

每个模块的输出会和一个模块的输入相加,这就是残差连接。它看起来只是把输入和输出做个加法,但它解决了一个大问题:深度网络的梯度回传。有了残差连接,梯度可以直接从最后一层“抄近路”回到最前面,不会因为经过太多非线性变换而消失。你可以把它想成书里夹着书签,随时可以翻回先前那页,不必从头一页页找起。

ViT的每个Block都是“输入 → 注意力 → 加上原始输入 → MLP → 再加上前一步结果”的结构。这种设计让几十层甚至上百层的Encoder也能稳定训练。实际训练里,如果去掉这些残差连接,深层的ViT几乎不可能收敛,所以它重要性怎么强调都不过分。

5.3 层归一化:把数值拉回稳定区间

在注意力之前和MLP之前,ViT还会各加一次层归一化,也就是LayerNorm。它的作用是:对每个Token的向量,把均值拽回0、方差拉回1,然后再学一组缩放和平移参数,让模型可以决定“归一化之后如何重新表达”。简单说,它相当于给数据做了一次“体温调节”,让训练过程更稳定。

这里有个实践细节:不同版本的Transformer对归一化放的位置不一样,有的放在模块之后,有的放在模块之前。现在的主流ViT基本都采用“先归一化再计算”的方式,也就是Pre-LN,它在训练超大模型时明显更稳,收敛也更快。

5.4 一次完整流程走一遍

把上面说的组合起来,就是一个完整的Transformer Block:输入向量先过层归一化,进入多头注意力,得到的结果和输入相加;然后再过层归一化,进入MLP,结果再和输入相加。这样一块的输出,就是下一块的输入。ViT一般堆12层或者24层这样的Block。每一层都会让Token变得更“全局”——前面几层可能还在看局部边缘,后面几层已经能表达“这个物体的头部在哪里、腿在哪里”这种结构。

整个流程走完,最后取[CLS]位置的输出向量,接上一个简单的分类头(通常是线性层加Softmax),就得到了这张图片属于每个类别的概率。这就是ViT完整的前向过程。

6. 预训练与微调:ViT为什么这么“挑食”

6.1 CNN自带先验,ViT得全凭数据

CNN为什么在小数据集上也能勉强训练?因为它天生就自带“相邻像素相关”和“平移不变性”这两个先验,等于一出生就有人告诉它“你该怎么理解图片”。ViT没有这些先验,它初始时完全不知道相邻Patch之间的关系更紧密,一切都要靠数据自己学。

这就导致一个现象:在中等规模的数据集上,从零训练的ViT往往打不过同规模CNN。但在超大规模的数据集上,ViT的优势就出来了——它能学习比CNN更灵活、更复杂的图像关系,上限更高。这个现象是ViT早期论文里反复强调的。所以ViT不是“天生更强”,而是“数据足够多时更强”。

6.2 预训练加微调:先读万卷书,再学专业课

为了让ViT在中小数据集上也能用,实践里的标准做法是预训练加微调。先在包含上千万张图片的超大规模分类数据集上,把ViT训练一遍,让Encoder学会通用的视觉特征——边缘、纹理、形状、物体部件,这些东西和具体任务无关,具有很好的泛化性。这一步相当于让模型“读万卷书”,建立语感。然后,把最后的分类头换掉,用你自己任务的几千张或几万张图片去微调,学习率开得很小,让模型在已有特征的基础上做轻微调整。

形象点说,预训练是“通识教育”,微调是“专业课”,两者结合,让ViT在中小数据集上的表现立刻追上甚至超过CNN。实际项目中,几乎没有人会从随机权重开始训练一个ViT,除非你手里有几百万张图外加强大的算力。

6.3 训练策略比CNN更娇气

ViT的训练策略也比CNN更讲究。数据增强方面,随机裁剪、翻转、色彩扰动是标配,MixUp和CutMix这类“混合样本”增强对ViT尤其重要。优化器方面,ViT通常用带动量的Adam类优化器,学习率需要配合Warmup——也就是先用很小的学习率热身几百步,再升到目标学习率,最后用余弦退火慢慢降下来。如果你把训练CNN的SGD学习率直接搬到ViT上,大概率会踩坑。

我的实际体验是:同一批数据,CNN用一套“粗糙”的训练参数也能跑到不错的精度,ViT要是训练参数没调好,精度可能会差好几个点。它像一个味觉极其灵敏的美食家,你给它一点点不合适的调味,它都能尝出来。

7. 亲手跑一个ViT:加载预训练模型的实操流程

7.1 准备工作:用现成库,别自己造轮子

如果你只是想体验一下ViT,完全没有必要自己从零实现多头注意力和Encoder。主流深度学习框架里基本都有公开的预训练ViT实现,加载一个模型加上配套的图片预处理工具,只需要十几行代码。这里的关键不是模型怎么拼,而是图片预处理要和预训练时保持一致,否则输入分布不一致,模型性能会明显下降。

硬件方面,如果你有GPU当然好;没有GPU,用一个基础规模的ViT(比如Patch16的小模型)在CPU上做单张图片的推理,一般几秒钟也能跑完。微调大规模模型就需要GPU了,但只是跑通流程看效果,CPU完全够用。

7.2 推理流程的代码骨架

下面这段代码是示意性的,接口名称取决于你用的具体库,但整体流程是通用的:

# 用你手头顺手的开源库加载预训练ViT,这里接口是示意 model = load_pretrained_vit(name="vit_base_patch16_224", num_classes=1000) processor = load_processor(name="vit_base_patch16_224") # 读取一张图片,并转换成模型需要的输入张量 image = read_image("cat.jpg") pixels = processor(image) # resize到224x224,做标准化,变成[1, 3, 224, 224] # 前向推理,得到每个类别的得分 logits = model(pixels) # 形状是[1, 1000] probs = softmax(logits) # 打印概率最高的前5个类别 print(top_k(probs, k=5))

整个过程里,最容易被忽略的是标准化参数。预训练模型在训练时,通常把每个通道减均值、除以标准差,用的是一组固定的数值。如果你加载了官方预训练权重,一定也要加载配套的预处理流程,不要自己瞎写标准化的数值。差之毫厘,精度就会明显掉。

7.3 在自建小数据集上微调的要点

如果要在自己的图片数据集上微调,你需要把最后的分类头替换成自己的类别个数。然后加载预训练权重,把学习率设小,大概是1e-5到1e-4这个量级,用Adam类优化器,batch size根据显存能塞多大就多大。训练集如果只有几千张图,建议训练轮次不要太多,配合轻量级的数据增强,避免过拟合。

我个人的经验是:微调时先冻结除分类头之外的所有层,跑几个epoch观察分类头的收敛情况,再解冻后面几层一起微调,这种分阶段策略比一上来就全套微调更稳定,也更容易定位问题。

7.4 把注意力可视化出来

训练好模型之后,强烈建议做一个注意力可视化:从模型中间层取出某几个头的注意力权重,把注意力权重折算成图片尺寸,叠加到原图上。你会发现,模型在判断一张猫的图片时,会把高权重集中在猫的耳朵、眼睛和胡须上,而不是背景沙发。

这一步对理解ViT帮助极大,因为你能直观看到“模型该看哪里”到底是什么意思。实现起来也不难,很多开源库都提供了抽取中间层注意力的钩子函数,打印一个热力图叠加图,几十行代码就能搞定。我第一次看到自己的ViT注意力图时,真的有一种“原来它和我看图的逻辑这么像”的感觉。

8. 回头再看:ViT的局限、误区和我的体会

8.1 几个流传很广的误区

第一个误区是“ViT完全不用卷积”。其实现在很多高性能视觉模型都是卷积和Transformer的混合体,比如在输入端先用卷积做降采样,再接Transformer,效果往往比纯ViT更好。第二个误区是“位置编码就是把坐标直接拼到图片上”,实际是在Patch向量上做加法,而且可以是可学习的参数。第三个误区是“注意力头数越多越好”,头数的选择更多是精度和计算量的权衡,不是越多越灵,某些场景下小头数反而更稳。

还有不少人以为ViT一定比CNN慢。实际上,在GPU上,ViT的并行效率很高,常用尺寸的ViT推理速度并没有想象中那么差,在适配了高效算子的情况下,甚至可以和同类CNN打得有来有回。

8.2 落地时我踩过的坑

我最初在项目里用ViT,踩过最大的坑是没有做合适的数据增强,导致模型在验证集上过拟合得厉害。后来把MixUp和随机裁剪加上,并把训练轮次从30轮压到15轮,情况立刻改善。另一个坑是学习率没做Warmup,导致早期的几个epoch出现Loss飙升,后面虽然能恢复,但浪费了不少训练时间。

还有一个容易被忽视的点是Patch大小的选择。用16×16的Patch,序列长度是196,计算量适中;改成8×8的Patch,序列长度变成784,注意力计算量会涨好几倍,但细节保留更好。如果你的任务非常依赖细微纹理,比如分类不同品种的鸟,小Patch会有帮助;如果你的任务偏整体结构,大Patch更划算。这个选择需要根据你的数据特点实际测试,没有绝对的最优解。

8.3 我的真实体会

如果你问我现在做图像分类,是不是一定要用ViT?我的答案是:不一定。更准确的说法是,把ViT当作一个强悍的“全局关系提取器”,用来补充CNN和传统方法的短板,是一条很顺畅的路。在我实际处理的项目里,纯ViT和纯CNN各有输赢,但“卷积做浅层特征提取、ViT做全局建模”的组合几乎总是效果最稳的。

最后分享一个小技巧:如果你手里有一个中等规模的数据集,想用ViT又担心数据不够,可以先在一个与你任务相近的大数据集上做一轮粗预训练,然后再迁移到你的任务上微调。这一步操作能让最终精度提升不少,比直接在任务数据上硬着头皮多跑几十个epoch更有效。

说到底,ViT真正教会我的不是“该用哪个模型”,而是“如何从不同角度看待同一张图片”。这种思维方式的转变,比任何一个具体模型都更有价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 6:55:47

SpringBoot+Vue前后端分离校园管理系统设计与实践指南

年初接了个活儿,帮一位临近毕业的学弟梳理他的课设选题。他在某高校学软件工程,手头有个“前后端分离的校园管理系统”题目,技术栈锁定在SpringBootVue,数据库用MySQL。按理说这类题目在网上随便一搜就是一大把源码和教程&#xf…

作者头像 李华
网站建设 2026/10/24 6:53:29

推箱子算法实战:自动求解与逆向关卡生成全解析

1. 项目拆解:自动求解与关卡生成到底在解决什么问题推箱子(Sokoban)这个游戏,规则简单到一句话就能说清:把箱子推到目标点上,就算过关。但就是这么一个“简单”的游戏,背后却藏着两道让开发者又…

作者头像 李华
网站建设 2026/10/24 6:52:52

Python 3.13 下 pip 安装报 metadata-generation-failed 的处理

新版本 Python 推出后的头几个月,装包时最容易撞上的不是编译错误,而是连元数据都生不出来的失败。这类报错看起来指向 numpy,实际上和本地已装的 numpy 关系不大。 问题现象 Windows 11 + Python 3.13.7,执行: pip install pytidycensus报错: Preparing metadata (p…

作者头像 李华
网站建设 2026/10/24 6:52:45

欧姆龙CP1H与台达VFD-M变频器自由口Modbus RTU通讯实战

在项目里用欧姆龙CP1H去跟台达VFD-M变频器做自由口通讯,很多人第一反应是“这还要写教程”?但真到了现场,你会发现值得踩的坑一个都不少:自由口模式下报文怎么组织、CRC高低字节往哪儿放、变频器参数有没有切到通讯模式、RS485正负…

作者头像 李华
网站建设 2026/10/24 6:52:42

别把AI智能体做成下一个仪表盘:数据团队的转型路径

数据团队这两年应该都有一种共同感受:公司突然开始聊AI智能体,但聊着聊着,又变成了“做一个智能驾驶舱”“搞一个AI助手看板”“让机器人帮我们自动生成周报”。每次听到这种需求,我都有点坐不住——大家把AI智能体当成下一个仪表…

作者头像 李华