从注意力机制到Vision Transformer:自注意力原理与ViT图像处理实战
2026/8/22 4:55:37 网站建设 项目流程

1. 从“注意力”到“自注意力”:一个直觉的起点

如果你接触过深度学习,尤其是自然语言处理或者计算机视觉,那么“注意力机制”这个词你一定不陌生。它就像一个智能的聚光灯,让模型在处理信息时,能够有选择地聚焦于最重要的部分。但“自注意力”听起来就有点玄乎了——自己对自己注意?这有什么用?

我第一次深入理解自注意力,是在尝试复现一个翻译模型的时候。传统的循环神经网络在处理长句子时,会面临“遗忘”开头信息的问题。而注意力机制的出现,让模型在生成每一个目标词时,都能“回头看”一遍源句子的所有词,并决定哪个词更重要。这已经很厉害了,但自注意力更进了一步:它让句子中的每个词,都能和句子中的所有其他词(包括它自己)建立关联,从而动态地计算出一个新的、富含上下文信息的词表示。

举个生活中的例子:你读一段技术文档。理解一个专业术语“Transformer”,你不仅需要看这个词本身,还需要看它前面出现的“Vision”,以及后面出现的“模型架构”。你的大脑在瞬间完成了对这个词在当前上下文中意义的重新评估和加权理解。自注意力机制要做的,就是让机器模拟这个过程。它不是简单地给每个词一个固定的向量,而是让词与词之间“开会讨论”,根据彼此的相关性,动态地调整各自的“发言权重”,最终形成更能代表其在当前语境下含义的新表示。

而Vision Transformer,则是将这个在文本领域大放异彩的“自注意力”机制,以一种非常大胆和直接的方式,应用到了图像领域。它彻底抛弃了卷积神经网络(CNN)的归纳偏置(如局部性、平移不变性),将图像视为一系列“图像块”的序列,然后直接用Transformer架构来处理。这个想法在2020年提出时,堪称石破天惊。今天,我们就来彻底拆解这个链条上的核心环节:注意力公式的每一步在做什么、Self-Attention如何运作,以及ViT是如何用这些模块“看懂”图片的。

2. 拆解注意力公式:不只是矩阵乘法

很多人看到注意力公式就头疼,觉得是一堆符号的堆砌。但如果我们把它当成一个清晰的、分步骤的数据处理流水线,一切就豁然开朗了。最经典的缩放点积注意力公式如下:

$$ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$

这里,Q(Query-查询)、K(Key-键)、V(Value-值) 是三个矩阵。别被名字吓到,我们可以把它们理解成一场信息检索大会的准备材料。

2.1 第一步:Q与K的匹配度计算 (QK^T)

这是整个机制的核心。Q矩阵代表了我们当前想要查询的问题或焦点。例如,在翻译任务中,当模型要生成下一个目标词时,这个“生成意图”就体现在Q中。K矩阵则代表了所有可供查询的“关键信息”或“索引标签”,通常是输入序列本身的另一种表示。

QK^T这个矩阵乘法,做的就是计算每一个查询(Q的每一行)与所有键(K的每一列)之间的相关性或匹配分数。结果是一个分数矩阵,其中第i行第j列的元素,表示第i个查询与第j个键的匹配程度。

注意:为什么是点积?点积在几何上可以衡量两个向量的方向相似性。方向越接近,点积值越大(假设向量已归一化)。在这里,它衡量的是查询向量和键向量的“内容相似度”。一个查询与某个键越相关,它们的向量方向就越接近,点积得分就越高。

2.2 第二步:缩放与稳定化 (/ sqrt(d_k))

这一步非常关键,但常常被初学者忽略。d_k是键向量K的维度。随着维度d_k增大,点积的结果的方差也会增大。想象一下,高维空间中的向量点积更容易产生极端大的值。

如果不进行缩放,将这些大的原始分数直接输入softmax函数,会导致梯度非常小(因为softmax会将几乎所有的概率质量都集中到分数最大的那一项上),这在训练时被称为进入“饱和区”,梯度消失,模型难以更新。除以sqrt(d_k)就是为了将点积得分的方差拉回到1左右,确保softmax函数有更平缓的梯度,训练更稳定。

2.3 第三步:归一化为概率分布 (softmax)

经过缩放后的分数矩阵,数值范围可能仍然很广,有正有负。softmax函数的作用是沿着最后一个维度(通常是键的维度)进行归一化,将每一行的分数转换成一个概率分布。

这个概率分布就是“注意力权重”。对于第i个查询,它给出了一个概率向量,其中第j个值表示“在回答第i个查询时,应该给予第j个输入信息多少注意力”。softmax的指数特性确保了所有权重为正且和为1,同时会放大高分值、抑制低分值,使得注意力分布更加尖锐。

2.4 第四步:加权求和生成输出 (权重 * V)

这是最后一步,也是产生实际价值的一步。V(Value-值) 矩阵代表了与每个键相关联的“实际内容”或“信息本体”。你可以把K想象成书的目录标题,而V就是标题下的具体章节内容。

我们用上一步得到的注意力权重矩阵(概率分布)对V矩阵进行加权求和。具体来说,输出矩阵的第i行,就是第i个查询对应的注意力权重,与所有值向量 (V的所有行) 的加权和。

结果是什么?输出矩阵的每一行,都是一个全新的表示。它不再是孤立的,而是融合了输入序列中所有位置的信息,且融合的权重是由该位置与序列中其他位置的相关性动态决定的。这就是“上下文感知”的表示。

我踩过的一个坑:早期自己实现时,我曾忘记除以sqrt(d_k),模型在训练初期损失下降非常缓慢,且不稳定。加上缩放后,训练曲线立刻平滑了许多。这个细节虽小,却是保证Transformer模型能够成功训练的关键trick之一。

3. Self-Attention:让序列内部“充分沟通”

理解了基础注意力,Self-Attention(自注意力)就很好理解了。它的特殊之处在于,Q,K,V这三个矩阵都来自同一个输入序列X

具体过程是:输入序列X(比如一个句子的词向量序列)分别乘以三个不同的可学习权重矩阵W^Q,W^K,W^V,得到Q,K,V

$$ Q = X W^Q, \quad K = X W^K, \quad V = X W^V $$

然后,将得到的Q, K, V代入我们刚才拆解的那个注意力公式中。

这意味着什么?这意味着序列中的每个元素(例如一个词),都在同时扮演三种角色:

  1. 查询者 (Query): “我想知道我和其他所有人的关系如何?”
  2. 被查询者 (Key): “其他人可以来查询我,看看我有多相关。”
  3. 信息提供者 (Value): “我身上携带的实际信息内容在这里。”

通过这样的操作,序列中任意两个位置之间都可以直接建立联系,无论它们相距多远。这完美解决了RNN的长距离依赖问题。一个句子开头的词可以直接影响句子结尾词的表示,中间不需要经过任何递归或卷积的逐步传递,信息流通路径是“一步到位”的。

一个生动的类比:把Self-Attention看作一个“词议会”。每个词(输入向量)进入议会前,先准备三份材料:一份是“我的问题清单”(Q),一份是“我的身份标签”(K),一份是“我的观点陈述”(V)。在议会中,每个词用自己的“问题清单”(Q)去比对所有人的“身份标签”(K),看看谁跟自己的问题最相关,从而决定听谁的“观点陈述”(V)时更认真(分配高权重)。最后,每个词综合听取所有人的观点后,形成了自己新的、更全面的立场(输出向量)。

4. 多头注意力:并行化的多视角洞察

如果Self-Attention是一个强大的特征提取器,那么多头注意力(Multi-Head Attention)就是给它装上了多组不同的“滤镜”,让它能从多个不同的子空间(或理解为不同的角度)同时进行观察和学习。

具体实现上,不是只用一组W^Q, W^K, W^V来生成单一的Q, K, V,而是用h组(h就是头的数量)不同的权重矩阵,并行地做h次Self-Attention操作。

假设我们有h个头,模型维度是d_model。那么:

  1. 对每个头i,我们使用维度为d_model x d_kW_i^Q,W_i^K和维度为d_model x d_vW_i^V,其中通常d_k = d_v = d_model / h。这样做的目的是将高维模型空间投影到多个低维子空间,每个头在低维空间内计算注意力,降低计算复杂度,同时鼓励学习多样化的关系。
  2. 并行计算得到h个输出矩阵head_i
  3. 将这hhead_i在特征维度上拼接(Concat)起来,形成一个大的矩阵。
  4. 将这个拼接后的矩阵通过一个可学习的线性投影矩阵W^O映射回d_model维,得到最终的输出。

公式表示为: $$ \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O $$ $$ \text{where head}_i = \text{Attention}(Q W_i^Q, K W_i^K, V W_i^V) $$

为什么需要多头?单一的自注意力机制可能只擅长捕捉一种类型的依赖关系(比如语法依赖)。而多头机制允许模型同时关注来自不同位置的不同类型的依赖关系。例如,在同一个句子中,一个头可能主要关注“主谓一致”这种语法关系,另一个头可能关注“指代消解”(比如“它”指代什么),再一个头可能关注“情感修饰”关系。多个头的结果最后综合起来,使得最终的表示更加丰富和鲁棒。

实操心得:头的数量h是一个超参数。并不是头越多越好。实践中,d_model必须能被h整除。常见配置如d_model=512, h=8,则每个头的维度d_k = d_v = 64。增加头数可以提升模型容量,但也会增加计算量。在一些轻量化模型中,减少头数是常见的压缩手段。我在一些下游任务微调时发现,对于特定任务,适当减少预训练模型的头数(通过剪枝或直接选择更小模型)有时效果反而更优,因为避免了过参数化和过拟合。

5. Vision Transformer (ViT):当图像遇见序列

到了这里,我们已经掌握了Self-Attention和多头注意力的精髓。但如何把它们用到图像上?卷积神经网络(CNN)通过卷积核滑动来捕捉局部特征,天然具有平移不变性和局部性假设。ViT的做法是“离经叛道”的:它认为这些假设不是必须的,全局注意力本身就能学好。

5.1 图像分块嵌入:从2D到1D的序列化

ViT的第一步是将一张标准图像(例如224x224x3)打散成一系列固定大小的图像块(Patches)。假设每个块大小为16x16,那么一张图就会被分成(224/16) * (224/16) = 14 * 14 = 196个块。

每个块(16x16x3=768维)被展平成一个向量,然后通过一个可训练的线性投影(全连接层)映射到模型维度D(例如768)。这个投影后的向量,就相当于NLP中的一个“词向量”。

但是,Transformer本身不具备位置信息。为了保留图像块之间的空间顺序,ViT引入了位置编码(Positional Encoding)。这些编码也是D维的向量,与块嵌入向量相加。位置编码可以是可学习的参数,也可以是固定的正弦/余弦函数。ViT论文中采用的是可学习的位置编码。

此外,和BERT中的[CLS]标记类似,ViT在序列的开头添加了一个额外的可学习的分类标记。这个标记经过Transformer编码后对应的输出向量,就用于最终的图像分类。

至此,图像的表示变成了一个形状为(197, 768)的矩阵(196个图像块 + 1个分类标记),完全符合Transformer Encoder的输入要求:一个序列。

5.2 Transformer Encoder 在 ViT 中的工作流

ViT直接使用了标准Transformer的Encoder部分,由交替的多头自注意力层(MSA)和前馈网络层(FFN)构成,每层前后有层归一化(LayerNorm)和残差连接。

对于一个输入序列z_0(包含分类标记和图像块嵌入+位置编码):

  1. 层归一化1:z'_l = LayerNorm(z_{l-1})
  2. 多头自注意力: 在z'_l上计算自注意力。关键在这里:对于图像块序列,自注意力机制允许任何一个图像块所有其他图像块(包括它自己)直接交互。这意味着,角落里的一个块可以直接关注到图像中心的一个块,并建立联系。这是CNN通过堆叠多层卷积才能实现的“感受野覆盖全局”,ViT在一层内就做到了。
  3. 残差连接1:z''_l = MSA(z'_l) + z_{l-1}(注意是加回原始的z_{l-1}
  4. 层归一化2:z'''_l = LayerNorm(z''_l)
  5. 前馈网络: 一个简单的两层MLP,通常中间层维度扩展4倍(如768 -> 3072 -> 768),并带有GELU激活函数和Dropout。它为每个位置的特征进行独立的非线性变换。
  6. 残差连接2:z_l = MLP(z'''_l) + z''_l

这样的一个“块”会堆叠L次(如ViT-Base是12层)。

5.3 分类头与训练策略

经过L层Transformer编码后,我们取序列第一个位置(即分类标记)对应的输出向量z_L^0,通过一个小的MLP(通常就是一个线性层)映射到类别数量上,得到最终的分类logits。

ViT的成功严重依赖于大规模预训练。论文指出,在中等规模的数据集(如ImageNet)上从头训练,ViT的性能不如同等计算量的CNN。这是因为Transformer缺少CNN的归纳偏置,需要更多的数据来学习图像固有的空间结构。但当在超大规模数据集(如JFT-300M,包含3亿张图像)上预训练后,再迁移到ImageNet等下游任务进行微调,ViT就能展现出超越当时SOTA CNN的性能。

我复现ViT时遇到的典型问题

  1. 计算资源:即使是最小的ViT模型,对内存的需求也很大,因为自注意力的计算复杂度是序列长度的平方。196个块的序列长度已经不小了。处理更高分辨率图像时,需要更激进的分块或采用分层、稀疏注意力等优化。
  2. 数据增强与正则化:训练ViT需要非常强的数据增强(如RandAugment, MixUp, CutMix)和正则化(如Dropout, Stochastic Depth),以防止过拟合,尤其是在数据量不是特别巨大的情况下。
  3. 学习率调度:通常使用带有热身的余弦衰减学习率调度器,热身阶段对Transformer的稳定训练至关重要。

6. 核心模块的代码级理解

光说不练假把式。我们结合PyTorch风格的伪代码,来具体感受一下这些模块是如何实现的。这能帮你把前面的数学公式和理论描述落到实处。

6.1 缩放点积注意力实现

import torch import torch.nn as nn import torch.nn.functional as F class ScaledDotProductAttention(nn.Module): def __init__(self, dropout=0.0): super().__init__() self.dropout = nn.Dropout(dropout) def forward(self, q, k, v, mask=None): # q, k, v: [batch_size, num_heads, seq_len, d_k] d_k = k.size(-1) # 获取键向量的维度 # 1. 计算匹配分数 scores = torch.matmul(q, k.transpose(-2, -1)) # [..., seq_len_q, seq_len_k] # 2. 缩放 scores = scores / (d_k ** 0.5) # 3. 可选:应用掩码(如解码器的因果掩码) if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) # 将mask为0的位置填充为负无穷 # 4. 归一化为注意力权重 attn_weights = F.softmax(scores, dim=-1) # [..., seq_len_q, seq_len_k] attn_weights = self.dropout(attn_weights) # 训练时使用Dropout # 5. 加权求和得到输出 output = torch.matmul(attn_weights, v) # [..., seq_len_q, d_v] return output, attn_weights # 返回输出和注意力权重(可用于可视化)

关键点解析

  • torch.matmul进行的是批次化的矩阵乘法。
  • transpose(-2, -1)是为了将k的最后两个维度(seq_len, d_k)转置为(d_k, seq_len),以便与q相乘。
  • 掩码操作通常在解码器中使用,防止当前位置关注到未来的信息(因果掩码),或者在处理变长序列时屏蔽填充位置。
  • 对注意力权重应用Dropout是一种非常有效的正则化方法,称为“注意力Dropout”。

6.2 多头注意力模块实现

class MultiHeadAttention(nn.Module): def __init__(self, d_model=512, num_heads=8, dropout=0.0): super().__init__() assert d_model % num_heads == 0, "d_model must be divisible by num_heads" self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads self.d_v = d_model // num_heads # 通常 d_v = d_k # 定义线性投影层 self.w_q = nn.Linear(d_model, d_model) # 实际实现中,通常直接投影到 num_heads * d_k self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_o = nn.Linear(d_model, d_model) self.attention = ScaledDotProductAttention(dropout) self.dropout = nn.Dropout(dropout) self.layer_norm = nn.LayerNorm(d_model) def forward(self, q, k, v, mask=None): batch_size = q.size(0) # 1. 线性投影并分头 # 线性投影后形状: [batch_size, seq_len, d_model] # 然后重塑为: [batch_size, seq_len, num_heads, d_k] # 最后转置为: [batch_size, num_heads, seq_len, d_k] 方便批次计算 q = self.w_q(q).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) k = self.w_k(k).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) v = self.w_v(v).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) # 2. 应用缩放点积注意力(所有头并行计算) x, attn = self.attention(q, k, v, mask=mask) # x: [batch_size, num_heads, seq_len, d_v] # 3. 合并多头 # 转置回: [batch_size, seq_len, num_heads, d_v] # 重塑为: [batch_size, seq_len, d_model] (因为 num_heads * d_v = d_model) x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) # 4. 输出投影 output = self.w_o(x) output = self.dropout(output) return output, attn

注意:在实际的Transformer/ViT实现中,多头注意力模块通常被整合进一个包含残差连接和层归一化的“块”中,如上文第5.2节所述。这里的forward方法通常返回的是需要与残差相加的部分。

6.3 ViT 图像块嵌入实现

class PatchEmbedding(nn.Module): """将图像分割为块并嵌入到向量空间。""" def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768): super().__init__() self.img_size = (img_size, img_size) self.patch_size = (patch_size, patch_size) self.num_patches = (img_size // patch_size) ** 2 # 使用一个卷积层来实现“分块+投影”两步操作,效率更高 # 卷积核大小=步长=patch_size,输出通道数=embed_dim self.projection = nn.Conv2d(in_channels, embed_dim, kernel_size=patch_size, stride=patch_size) def forward(self, x): # x: [B, C, H, W] B, C, H, W = x.shape # 确保输入图像尺寸正确 assert H == self.img_size[0] and W == self.img_size[1], \ f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})." # 投影: [B, C, H, W] -> [B, embed_dim, H/patch_size, W/patch_size] x = self.projection(x) # 展平空间维度: [B, embed_dim, num_patches_h, num_patches_w] -> [B, embed_dim, num_patches] x = x.flatten(2) # 调整维度顺序: [B, embed_dim, num_patches] -> [B, num_patches, embed_dim] (Transformer期望的序列格式) x = x.transpose(1, 2) return x

技巧:使用nn.Conv2d来实现分块嵌入是非常巧妙且高效的做法。一个kernel_size=patch_size, stride=patch_size的卷积操作,恰好等价于将图像不重叠地切割成patch_size x patch_size的小块,并对每个小块的所有像素进行线性投影(卷积核的权重就是投影矩阵)。这比手动切片再全连接要快得多。

7. 自注意力与CNN的对比思考

理解了ViT,一个自然的问题是:自注意力机制和卷积,到底谁更好?它们本质的区别是什么?

特性卷积神经网络 (CNN)自注意力/Transformer (ViT)
归纳偏置。具有局部性(卷积核只关注邻域)和平移等变性(物体移动,特征也移动)。。几乎没有先验假设,将图像视为一组无序的块序列。必须从数据中学习所有空间关系。
感受野局部到全局。浅层卷积感受野小,捕捉局部特征(边缘、纹理);深层通过堆叠,感受野增大,捕捉全局语义。全局。从第一层开始,每个块就能看到所有其他块,具有全局建模能力。
计算复杂度相对于输入图像大小是线性对数线性的(取决于架构)。相对于序列长度(图像块数量)是平方级的。对于高分辨率图像,计算开销巨大。
数据效率。得益于强的归纳偏置,在中小规模数据集上也能表现良好。。需要海量数据(数亿级别)进行预训练,才能学习到有效的视觉表示。
解释性中等。可以通过可视化卷积核或特征图来理解。注意力权重可高度可视化。可以直观看到图像分类时,模型关注了哪些区域(注意力图)。
擅长任务广泛的视觉任务,尤其是数据量有限、需要强空间先验的任务。在大规模预训练后,在图像分类、目标检测、分割等多个任务上达到SOTA。特别适合需要长距离依赖建模的任务。

我的实践体会:这两者并非取代关系,而是互补。CNN像是一个经验丰富的“老师傅”,带着对图像结构的深刻理解(归纳偏置)上手,在小数据上就能干得不错。Transformer则像一个极度聪明的“天才学生”,没有先入为主的观念,但如果有足够多的“教材”(大数据)供其学习,它能发现甚至超越“老师傅”的模式。目前一个明显的趋势是混合架构(Hybrid),例如在ViT的早期层使用卷积进行下采样和局部特征提取(Convolutional Stem),或者设计局部注意力与全局注意力结合的模块(如Swin Transformer的窗口注意力)。这种结合往往能兼顾数据效率与模型性能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询