Advertisement

【论文阅读】ViT-Vision Transformer(简介+代码+面试常见问题)

阅读量:

已有较为深入的研究表明,在自然语言处理领域已有众多重要模型如BERT、GPT等;然而,在计算机视觉领域中如何有效地将图像数据输入到Transformer架构中仍是一个尚未解决的问题。

在论文正式介绍内容之前,请了解视觉任务中Transformer模型应用所面临的挑战。由于多头自注意力机制使得Transformer能够将序列模型的串行计算转换为高度并行的操作**(其中QKV向量通过矩阵乘法运算生成)** ,这些计算步骤在底层GPU架构上得到了高度优化从而显著提升了训练效率。

在视觉任务中,如果将图像中的每个像素视为一个token,并利用注意力机制去学习像素之间的相互作用关系时,则会面临显存资源的瓶颈(举个例子:一张128×128的图像包含了16,384个像素元素,在这种情况下两两之间计算全局注意力就需要处理一个16,384×16,384大小的矩阵乘法运算,在当前硬件条件下这是不可行的操作)。因此为了突破这一局限性,研究者们转而采用了一种新的方法——将图像划分为若干块状结构(即所谓的patch),然后将每个patch视为独立的token来进行全局注意力计算(举个例子:对于一张128×128的图像来说,在使用大小为16×16像素块的情况下总共可以提取出64个这样的patch tokens,在这种情况下只需要处理一个相对较小规模的64×64矩阵乘法运算即可完成全局注意力的学习)。值得注意的是这里的patch=16这一参数值直接来源于论文标题内容。

论文的亮点不仅在于将其应用于视觉任务之外, 还提出了大规模数据条件下视觉任务中无需依赖卷积操作的结论; 在仅微弱地引入归纳偏置(先验知识)的情况下, 在有足够的数据支持下, Transformers完全能够超越基于CNN的传统模型达到现有的SOTA水平

最初阅读这篇文章时感觉有些吃力难懂,在观看了李沐老师关于"模型精度"系列论文的视频后,在了解视觉任务中Transformer应用的核心难点后才逐渐豁然开朗

在博客中,在模型结构介绍完成、代码部分尚未展开时,我打算列举一些算法岗位常见的面试问题。这希望能够加深对ViT模型的理解。随后将开始正文部分

摘要

Transformer架构已成为自然语言处理领域的主流模型,在计算机视觉领域却尚未得到全面应用。早期研究主要集中在结合自注意力机制与卷积神经网络的方法上,并未对基础架构作出重大突破性创新。然而,在这篇论文的研究中发现,在图像分类等视觉任务中完全摒弃传统的卷积神经网络架构是可行的:研究人员直接将图像切片序列输入到原始transformer模型即可获得良好性能表现;值得注意的是,在这项研究中采用了大量数据进行预训练工作并成功迁移至多个不同规模的数据集(其中包括ImageNet、CIFAR-10以及VTAB),通过Vision Transformer(ViT)能够实现超越当时基于CNN的最大性能记录;此外研究表明,在图像分类等典型计算机视觉任务上ViT不仅达到了与传统方法相当的效果,并且所需的计算资源也更为节省(具体可参考论文中的TPUv3核心计算天数指标)

引言

基于自监督学习的方法(尤其是Transformer架构)在自然语言处理领域取得了显著成就。其主要应用于通过大量语料库进行预训练,并在小数据集上进行微调优化。Transformer架构以其高效的并行计算能力和可扩展性著称,在大规模网络训练方面展现出显著优势。随着模型与数据规模的增长趋势不断推进,在当前阶段尚未观察到模型能力达到饱和状态。

在视觉领域中,在那个时代,CNN依然是主流模型。尽管受自然语言处理技术的启发,当时的绝大多数研究也只能将卷积神经网络与自注意力机制简单结合,或者尝试完全替代传统的卷积结构。然而,这些研究大多专注于设计特定类型的局部化注意力机制,因为它们基于显存限制的原因无法采用全局化的自注意力方案.因此,在硬件资源有限的情况下,CNN-based网络仍然是该时代最优选择.

基于NLP领域中Transformer模型的成功案例与启发,在后续研究中作者仅进行了少量的调整就实现了对图像直接应用标准Transformer架构的目标

实验研究表明,在中等规模的数据集(如ImageNet)上训练ViT时,其性能并不如相同规模的ResNet模型。作者认为这可能源于Transformer架构缺乏CNN模型中的一些归纳偏置(即先验知识),例如平移不变性和局部性等特性)。然而,在处理大规模数据集时,实验结果表明基于大量数据的学习超越了这些归纳偏置的能力,在预训练任务以及少量样本学习任务中均展现出显著优势。

结论

该研究者提出了一种直接将Transformer模型应用于图像识别任务的方法。与基于自注意力机制的传统视觉模型不同的是,在其方法中,在对图像进行分块处理时(即Patch),并未引入额外的归纳偏置项以外的因素。相反地,则采用了另一种思路:将整个图像视为一个序列,并采用Transformer编码器对其进行处理。这一简洁而具扩展性的方法在大规模数据集上的预训练表现尤为出色,在多个图像分类任务中(包括但不限于)均展现了与当时最先进的方法相当或更好的性能水平。同时相比传统的预训练方法而言,在成本上更为经济。

尽管在分类任务方面表现出色,在实际应用中仍存在许多值得深入研究的方向。具体来说,在检测、分割等视觉任务中就可能面临挑战。此外还应探索自监督预训练策略这一重要方向。通过初始实验结果表明ViT具备自主学习能力这一特点后仍需改进以达到与有监督学习方法相比仍存在不足之处的情况。展望未来ViT的进一步扩展有望带来性能上的显著提升

相关工作

在NLP领域中,Transformer已成为当前最领先的模型架构之一。其中最著名的代表包括BERT、GPT等系列模型。这些模型能够在大规模语料库上进行预训练,并在此基础上进行微调优化。通过采用自监督学习的训练方法,在实验结果中表现出了显著的优势。

通过自注意力机制的基本概念,在不同像素之间计算关联性是一种思路。受限于显存资源的限制,在当时的实验中有一些研究采用局部化注意力替代全局性关注;此外还有研究将卷积神经网络(CNN)提取出的特征图用于自注意力计算;还有一些则仅在特定区域或块内进行关注。这些特殊的机制在视觉任务中表现出良好的效果,但同时计算效率相对较低。

在另一篇与ViT架构高度类似的论文中,在输入图像的不同位置随机抽取不同尺寸的小块(即Patch)并对其进行自注意力机制计算作为特征提取方法

此外还有相当数量的基于CNN与自注意力机制融合的工作(在后续实验中,作者对这种混合模型进行了比较分析)。

方法

ViT尽量遵循原始Transformer架构的设计理念,在改动上力求简洁明了。以下展示了模型的整体架构图。

在这里插入图片描述

标准Transformer主要处理一维序列数据,在实际应用中常需处理二维图像信息。为此方法将输入图像划分为多个大小一致的小块(patch),随后将这些块展开(P为patch的尺寸)。其输出维度的变化情况如下所述

\begin{equation} x\in R^{H*W*C} -> x\in R^{N*(P^2*C)} \end{equation}

考虑到模型中的潜在向量维度为D,在经过线性变换层后将每个patch被映射为D维空间中的表示,并将其赋值为patch embeddings

ViT主要应用于图像分类任务,在此基础之上通常会增添一个用于识别图像类别的分类头(classification head)。该分类头的输入端口既可以是编码器的所有输出结果。为了与Bert类似的处理方式,在这一架构中我们额外引入了一个可学习的class token(CT),将其与图像patch一并作为编码器的输入端口。在前向计算过程中,这个class token能够整合所有patch的信息,并且综合了图像中的所有特征信息作为其输入来源。值得注意的是,在预训练阶段该结构采用单隐藏层多层感知机(MLP)构建这一层次;而在微调阶段则简化为单线性层设计。

该领域中的位置编码机制主要采用标准的一维可学习位置编码方案(实验结果表明,在实际应用中二维可学习的位置编码并未展现出显著的优势),这一方案与原始的patch embedding技术相结合后作为输入被馈送给编码器进行处理。

编码器严格按照Transformer架构进行设计。其核心组件包括基于多头自注意力机制(MSA)、MLP模块、归一化层以及残差连接等关键组件。其中,在MLP模块中采用GELU激活函数以引入非线性计算。在前向传播过程中,首先将图像分割成patch并经过线性映射层处理;随后与类令牌(class token)进行拼接;最后与位置编码进行按元素相加的操作。

$\begin{equation} Z_0 = [\mathbf{X}c; \mathbf{X}{p1}; \mathbf{X}{p2}; \cdots; \mathbf{X}{pN}] + E_{pos}, \qquad E \in \mathbb{R}{(P2C)D}, E_{pos} \in \mathbb{R}^{(N+1)D} \end{equation}

接着数据经过多个多头自注意力和残差连接。

\begin{equation} z'_l=Multihead\;SelfAttention(LN(z_{l-1}))+z_{l-1},\qquad l=1..L \end{equation}

然后经过带有残差连接的线性层。

\begin{equation} z_l=MLP(LN(z'_{l}))+z'_{l-1},\qquad l=1..L \end{equation}

在经过多层编码处理后, class embedding token通过层归一化生成全局特征 y, 并作为分类器的输入

y=LN(z_L^0)

和CNN相比,在ViT中归纳偏差(先验信息)更为稀少。CNN通过卷积操作自然包含了局部位置特性等先验知识;而ViT仅限于对图像进行分块处理,并且其多层感知机(MLP)部分也保留了一定的局部位置特性;值得注意的是,在ViT架构中除了上述特征外还需要模型自行推导其他相关联关系(包括位置关联等细节)。

此外作者提到了一种混合架构模型——CNN执行特征图展平操作,并对其进行embedding处理并加入位置编码后输入到Transformer中。

作者的训练策略为:在大型数据集上预训练ViT,在下游任务上微调模型,微调时会删除预训练预测头,额外添加一个零初始化的前向层,得到下游任务类别的预测概率。有其他工作指出在微调阶段更适合使用高分辨率图像,如果这样做的话patch大小保持不变,那么就导致patch数量更多(序列长度更长)。Transformer的Encoder层输入输出尺寸保持一致,因此模型不必调整,但是预训练得到的位置编码不再适用。因此作者选择在预训练得到的位置编码中,根据其在原始图像中的位置进行2D插值 (这种做法会向模型中添加一些先验知识)。

实验

研究者借助多样化的实验数据深入考察了ViT在各预训练数据规模与不同类型下游任务中的应用表现,并对基于CNN的传统模型以及混合架构体系进行了系统性性能对比分析。综合分析结果表明

  • 经过适量的数据量预训练后发现,在相同参数配置下ViT的表现略逊于具有同样规模参数配置的基于CNN的方法;
    • 基于大量数据集的预训练研究表明,在多项微调任务以及小样本学习场景下ViT均展现出超越基于CNN架构的传统方法的优势,并且相比其具有更高的计算效率;
    • 对于ViT体系而言除了编码层结构的设计外图像划分成块的方式(即所谓的patch尺寸)同样会对整体性能产生重要影响;
    • 在分析特征空间时我们发现深层特征提取网络主要学习到了纹理图案边缘等基础性视觉特征;
    • 关于位置编码部分通过可视化分析可以看出相邻区域之间的相互作用权重更高这与直观认知高度契合;
    • 在关注注意力距离这一层面的研究表明该模型能够同时捕捉到不同尺度的空间关系(而仅依赖局部感受野机制的传统CNN架构则无法实现这一点)而随着网络层次深度的增长则更加注重大范围的空间关联性这正反映了当前深度学习框架在捕捉全局依赖方面的优势

另外作者指出Transformer的成功不仅源于其独特的架构设计( architectural innovation),还得益于有效的自监督预训练策略( effective self-supervised learning framework)。基于这一观察,我们在此基础上进行了进一步优化:借鉴图像领域的方法,在部分像素块上实施mask操作( mask operation),并通过重建任务实现预训练目标( pretraining objective)。实验结果表明,在与现有同类方法对比中( compared with other state-of-the-art self-supervised learning approaches)ViT展现出显著优势( superior performance),然而与有监督学习预训练方法相比( contrasted with fully supervised learning-based pretraining)仍存在差距( margin for improvement),这一发现值得进一步研究和探索( future exploration)。其中MAE取得了令人注目的效果( achieved remarkable results),但其在实际应用中仍需改进以达到更优性能水平( further optimization is needed to bridge the performance gap)。

这一部分重点阐述了ViT模型的设计思路。这一架构设计延续了原始框架的核心理念,并着重探讨了几大关键问题:如何对图像进行分块处理以及附加位置编码、引入类token机制和设计分类器等问题。通过详实的数据实验验证了ViT的有效性(建议读者参考原论文正文及附录中的详细实验数据),这些研究结果充分证明了该方法的优势所在(确实如谷歌所做的研究所示…)。值得注意的是作者对于Transformer模型机制的理解具有前瞻性,在深入探讨其有效性原因时不仅强调并行计算的优势还深入分析了自监督预训练策略在其中发挥的重要作用,并通过相对简单的自监督学习任务展示了该方法的强大潜力…

面试常见问题

ViT将图像分割为固定大小的patch后,如何实现这些patch与Transformer模型的有效对接?能否详细阐述预处理流程中的核心环节?

将patch进行线性嵌入至模型隐藏维度,并通过逐元素相加的方式将其与1D可学习的位置编码结合。

在预处理过程中需要考虑选择适当的 patch size。在嵌入之后,在其中加入一个新的 class token,并将其与 patch embeddings 进行拼接。以使其与 NLP 架构尽可能一致。最后将结果最终与一维可学习的位置编码参数直接相加,并作为编码层的输入使用。

对比之下,在位置编码机制方面存在显著差异。具体而言,在ViT中采用了基于二维坐标的定位方式,在此过程中未采用位置编码这一关键组件。这种方法相较于传统NLP模型具有独特的优势与特点

Transformer模型采用了基于位置信息计算正弦和余弦值的方法进行位置编码;相比之下,在ViT架构中,则采用了1维的位置编码参数,并且这些参数是可学习的;此外,在论文研究中设置了对照实验,在不采用位置编码的情况下观察了模型的表现。结果显示这种设置会导致模型性能略显不足(具体表现为约降低3个百分点)。

在模型架构中,ViT引入的'分类token'(class token)主要承担什么功能?这种做法背后的原因是什么?

在前馈计算过程中, 分类token会整合图像中的全局特征, 并将这些特征作为输入传递给分类器以完成图像分类任务.

为了实现与NLP(BERT模型)中设置尽量相似的目标,并将其与patches连接起来作为编码器的输入,在实验中发现如果不使用 class tokrn、而是直接将编码器输出传递给分类头也能获得类似的效果

它们之间的主要差异体现在哪些方面?其优势在于能够更高效地提取长距离空间特征。然而,在面对有限的数据时,模型可能会过度拟合训练数据。

在CNN架构中,特征提取主要依赖于卷积操作;这些操作基于局部性和平移不变性的基本假设。相比之下,在Vision Transformer(ViT)中仅引入了少量的图像先验假设,并完全依赖于模型对图像patches之间关系的学习机制。

自注意力机制在图像处理中可能会遇到什么计算效率问题?ViT如何通过patch划分来缓解这一问题?

将图像中的每一个像素视为token输入模型中,在QKV架构下计算时会遇到大规模矩阵相乘的挑战,从而降低了计算效率;而ViT则通过划分Patch并将每个Patch作为一个Token进行处理,有效地减少了Token序列的长度,从而缓解了上述的大规模矩阵相乘问题。

若输入图像的分辨率与预训练模型的设计分辨率不符时,请ViT采用何种策略来应对这一问题?请阐述位置编码插值的工作原理。

基于原始预训练位置编码的基础之上,在考虑图像位置之间的关系时执行二维插值操作

通过ViT的实验结果可以看出,在ImageNet-21k等大规模数据集上其表现超越了CNN。这种超越性特征能否延伸至小规模数据集?

基于全局自注意力机制设计的ViT架构,在无需依赖任何先验知识的前提下能够提取图像中普遍存在的视觉特征。从而在网络规模较大的情况下展现出超越卷积神经网络的能力。同时即使采用基于少量样本进行微调优化的方法其性能同样优于传统的CNN模型

在工业领域(包括医学影像分析与自动驾驶等)应用Vision Transformer(ViT)时可能会遇到哪些困难?分别提出三种优化方案,并简述其工作原理。

开放性问题,留给读者思考_

代码实现

从顶层到底层的角度分析模型的实现代码,并基于开源PyTorch框架的具体实现进行了详细说明

复制代码
    class ViT(nn.Module):
    def __init__(self, *, image_size, patch_size, num_classes, dim, depth, heads, mlp_dim, pool = 'cls', channels = 3, dim_head = 64, dropout = 0., emb_dropout = 0.):
        super().__init__()
        image_height, image_width = pair(image_size)
        patch_height, patch_width = pair(patch_size)
    	# 确保图像可以被正确划分为多个patch
        assert image_height % patch_height == 0 and image_width % patch_width == 0, 'Image dimensions must be divisible by the patch size.'
    	# 计算patch的数量
        num_patches = (image_height // patch_height) * (image_width // patch_width)
    	# 对patch进行展平后得到的数据维度(注意考虑了通道)
        patch_dim = channels * patch_height * patch_width
        assert pool in {'cls', 'mean'}, 'pool type must be either cls (cls token) or mean (mean pooling)'
    	# 初始化patch embedding层,首先打patch,接着进行层归一化和线性层(处理为model hidden dims),再经过一个层归一化
        self.to_patch_embedding = nn.Sequential(
            Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1 = patch_height, p2 = patch_width),
            nn.LayerNorm(patch_dim),
            nn.Linear(patch_dim, dim),
            nn.LayerNorm(dim),
        )
    	# 位置编码是可学习的参数,注意num_patches+1代表在patches中拼接了class token
        self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
    	# class token,可学习参数,维度和model hidden dims相同
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
        self.dropout = nn.Dropout(emb_dropout)
    	# transformer中的编码器层
        self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim, dropout)
    
        self.pool = pool
        self.to_latent = nn.Identity()			# 残差层
    
        self.mlp_head = nn.Linear(dim, num_classes)	# 分类头
    
    def forward(self, img):
        x = self.to_patch_embedding(img)		# 首先对图像进行patch
        b, n, _ = x.shape				# 得到batch_size, nums_patches
    
        cls_tokens = repeat(self.cls_token, '1 1 d -> b 1 d', b = b)	# batch_size个位置编码
        x = torch.cat((cls_tokens, x), dim=1)				# class token和图像patch进行拼接
        x += self.pos_embedding[:, :(n + 1)]				# 与位置编码进行element-wise add
        x = self.dropout(x)
    
        x = self.transformer(x)						# 经过编码器层
    
        x = x.mean(dim = 1) if self.pool == 'mean' else x[:, 0]
    
        x = self.to_latent(x)
        return self.mlp_head(x)				# 得到最终的分类结果
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/vwEls4DSCK25xPfTb9RXu3pkMmay.png)

接着来看一下这个实现版本中的tranfromer结构。

复制代码
    class Transformer(nn.Module):
    def __init__(self, dim, depth, heads, dim_head, mlp_dim, dropout = 0.):
        super().__init__()
        self.norm = nn.LayerNorm(dim)
        self.layers = nn.ModuleList([])
        for _ in range(depth):
            self.layers.append(nn.ModuleList([
                Attention(dim, heads = heads, dim_head = dim_head, dropout = dropout),		# 多头自注意力
                FeedForward(dim, mlp_dim, dropout = dropout)					# 前向计算层
            ]))
    
    def forward(self, x):
        for attn, ff in self.layers:
            x = attn(x) + x	# 多头注意力+残差
            x = ff(x) + x	# 前向计算+残差
    
        return self.norm(x)	# 最后进行层归一化
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/1ocLr5TnMKJvp2QNEC80kPtZ9dFe.png)

其中的多头注意力见下:

复制代码
    class Attention(nn.Module):
    def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.):
        super().__init__()
        inner_dim = dim_head *  heads
        project_out = not (heads == 1 and dim_head == dim)
    
        self.heads = heads
        self.scale = dim_head ** -0.5
    
        self.norm = nn.LayerNorm(dim)
    
        self.attend = nn.Softmax(dim = -1)
        self.dropout = nn.Dropout(dropout)
    
        self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)
    
        self.to_out = nn.Sequential(
            nn.Linear(inner_dim, dim),
            nn.Dropout(dropout)
        ) if project_out else nn.Identity()
    
    def forward(self, x):
        x = self.norm(x)
    
        qkv = self.to_qkv(x).chunk(3, dim = -1)
        q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv)	# 将[batch_size, lens, (heads * dim/heads)] -> [batch_size, heads, lens, heads*dim]
    
        dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale	# 计算注意力分数
    
        attn = self.attend(dots)
        attn = self.dropout(attn)
    
        out = torch.matmul(attn, v)					# 得到注意力的结果
        out = rearrange(out, 'b h n d -> b n (h d)')				# 恢复尺寸
        return self.to_out(out)
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/84C3A9taoTW12jkOezi6QKwLMxqN.png)

另外MLP层的实现比较简单,因此这里不再做赘述。

全部评论 (0)

还没有任何评论哟~