尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从注意力机制到Vision Transformer:自注意力原理与ViT图像处理实战

从注意力机制到Vision Transformer:自注意力原理与ViT图像处理实战 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代入我们刚才拆解的那个注意力公式中。这意味着什么这意味着序列中的每个元素例如一个词都在同时扮演三种角色查询者 (Query): “我想知道我和其他所有人的关系如何”被查询者 (Key): “其他人可以来查询我看看我有多相关。”信息提供者 (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。那么对每个头i我们使用维度为d_model x d_k的W_i^Q,W_i^K和维度为d_model x d_v的W_i^V其中通常d_k d_v d_model / h。这样做的目的是将高维模型空间投影到多个低维子空间每个头在低维空间内计算注意力降低计算复杂度同时鼓励学习多样化的关系。并行计算得到h个输出矩阵head_i。将这h个head_i在特征维度上拼接Concat起来形成一个大的矩阵。将这个拼接后的矩阵通过一个可学习的线性投影矩阵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_model512, h8则每个头的维度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个块。每个块16x16x3768维被展平成一个向量然后通过一个可训练的线性投影全连接层映射到模型维度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:z_l LayerNorm(z_{l-1})多头自注意力: 在z_l上计算自注意力。关键在这里对于图像块序列自注意力机制允许任何一个图像块与所有其他图像块包括它自己直接交互。这意味着角落里的一个块可以直接关注到图像中心的一个块并建立联系。这是CNN通过堆叠多层卷积才能实现的“感受野覆盖全局”ViT在一层内就做到了。残差连接1:z_l MSA(z_l) z_{l-1}注意是加回原始的z_{l-1}层归一化2:z_l LayerNorm(z_l)前馈网络: 一个简单的两层MLP通常中间层维度扩展4倍如768 - 3072 - 768并带有GELU激活函数和Dropout。它为每个位置的特征进行独立的非线性变换。残差连接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时遇到的典型问题计算资源即使是最小的ViT模型对内存的需求也很大因为自注意力的计算复杂度是序列长度的平方。196个块的序列长度已经不小了。处理更高分辨率图像时需要更激进的分块或采用分层、稀疏注意力等优化。数据增强与正则化训练ViT需要非常强的数据增强如RandAugment, MixUp, CutMix和正则化如Dropout, Stochastic Depth以防止过拟合尤其是在数据量不是特别巨大的情况下。学习率调度通常使用带有热身的余弦衰减学习率调度器热身阶段对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, dropout0.0): super().__init__() self.dropout nn.Dropout(dropout) def forward(self, q, k, v, maskNone): # 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_model512, num_heads8, dropout0.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, maskNone): 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, maskmask) # 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_size224, patch_size16, in_channels3, embed_dim768): 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_sizepatch_size, stridepatch_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], \ fInput image size ({H}*{W}) doesnt 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_sizepatch_size, stridepatch_size的卷积操作恰好等价于将图像不重叠地切割成patch_size x patch_size的小块并对每个小块的所有像素进行线性投影卷积核的权重就是投影矩阵。这比手动切片再全连接要快得多。7. 自注意力与CNN的对比思考理解了ViT一个自然的问题是自注意力机制和卷积到底谁更好它们本质的区别是什么特性卷积神经网络 (CNN)自注意力/Transformer (ViT)归纳偏置强。具有局部性卷积核只关注邻域和平移等变性物体移动特征也移动。弱。几乎没有先验假设将图像视为一组无序的块序列。必须从数据中学习所有空间关系。感受野局部到全局。浅层卷积感受野小捕捉局部特征边缘、纹理深层通过堆叠感受野增大捕捉全局语义。全局。从第一层开始每个块就能看到所有其他块具有全局建模能力。计算复杂度相对于输入图像大小是线性或对数线性的取决于架构。相对于序列长度图像块数量是平方级的。对于高分辨率图像计算开销巨大。数据效率高。得益于强的归纳偏置在中小规模数据集上也能表现良好。低。需要海量数据数亿级别进行预训练才能学习到有效的视觉表示。解释性中等。可以通过可视化卷积核或特征图来理解。注意力权重可高度可视化。可以直观看到图像分类时模型关注了哪些区域注意力图。擅长任务广泛的视觉任务尤其是数据量有限、需要强空间先验的任务。在大规模预训练后在图像分类、目标检测、分割等多个任务上达到SOTA。特别适合需要长距离依赖建模的任务。我的实践体会这两者并非取代关系而是互补。CNN像是一个经验丰富的“老师傅”带着对图像结构的深刻理解归纳偏置上手在小数据上就能干得不错。Transformer则像一个极度聪明的“天才学生”没有先入为主的观念但如果有足够多的“教材”大数据供其学习它能发现甚至超越“老师傅”的模式。目前一个明显的趋势是混合架构Hybrid例如在ViT的早期层使用卷积进行下采样和局部特征提取Convolutional Stem或者设计局部注意力与全局注意力结合的模块如Swin Transformer的窗口注意力。这种结合往往能兼顾数据效率与模型性能。
返回列表