ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

注意力机制入门:从动态加权到自注意力与多头注意力

注意力机制入门:从动态加权到自注意力与多头注意力 如果你刚开始接触深度学习序列模型一定绕不开一个词注意力机制。从 Seq2Seq 机器翻译、Transformer 到 GPT 家族它几乎成了现代大模型的公共底座。但奇怪的是很多资料一上来就甩出 Q、K、V 三个矩阵让人背了一堆术语却仍然说不清“注意力”到底是什么。先给一个判断注意力机制不是一个新增的神经网络层也不是只能用在 Transformer 里的技巧它是一种“根据当前目标动态选择重要信息”的信息处理方式。一旦你抓住“动态加权”这四个字会发现序列注意力、自注意力、多头注意力、通道注意力都是在同一个思想上的不同变体。本文是 LMCC 注意力机制系列的第 01 篇专注从概念上把注意力讲透。读完你会理解注意力解决的是什么问题它的计算本质是什么为什么自注意力和多头注意力能应用在各类模型中以及视觉和时间序列任务里的注意力长什么样。1. 从 Seq2Seq 的瓶颈看起为什么需要注意力机制注意力机制之所以被提出不是因为研究者凭空想了一个华丽的结构而是因为经典的 seq2seq 模型有一个很明显的问题信息瓶颈。在机器翻译等任务中传统做法是用一个编码器把源句子压缩成一个固定长度的语义向量然后再让解码器根据这个向量逐步生成目标句子。流程大致是这样的编码器逐词读入源句子更新隐状态。编码结束后取最后一个隐状态作为整个句子的“语义向量”。解码器每一步都依赖这个语义向量和上一步输出生成新的词语。这段流程在短句子上表现尚可但句子一旦变长问题就出现了。最后一个隐状态相当于一个人只看完全文后必须把整篇文章的核心信息浓缩在一句话里说出来。源句子越长中间信息被压缩得越严重开头部分的信息很容易在传递过程中逐渐丢失。这就是典型的“固定向量瓶颈”。解决思路也很直观既然把整个句子塞进一个向量会丢信息那解码的时候就不要只依赖那个最后的语义向量而是让模型在每一步生成时都能重新回顾源句子中每一个位置的隐状态并选择对自己最相关的那些位置。换句话说模型应该学会“对齐”。翻译“I love you”时生成“爱”应该重点关注源句中的“love”而不是机械地引用整个句子的压缩表示。这种“动态选择源序列中相关位置”的机制就是最早的注意力机制由 Bahdanau 等人在 2014 年的神经机器翻译工作中明确提出。所以注意力机制要解决的第一个具体问题很简单让模型在生成每个输出时都能回头去看输入序列的各个位置并按重要程度加权汇总信息。2. 注意力机制的核心思想从“静态加权”到“动态加权”理解了动机之后再看注意力的定义就会清楚很多。我推荐把它理解为一种通用的信息聚合方式动态加权求和。我们早就用过各种加权求和。比如卷积神经网络中每个输出位置都是对局部区域像素的加权组合卷积核就是一组固定的权重又比如平均池化把区域内的特征做等权平均。这些操作的共同点是权重在学习完成后就固定了不随输入内容变化。注意力机制最大的不同在于它的权重不是固定参数而是根据输入动态计算出来的。同样是聚合三个词的信息当句子内容是“猫喜欢鱼”时模型可能会给“鱼”更高的权重当句子内容是“猫喜欢睡觉”时负责生成行为相关词的步骤又可能更关注“睡觉”。权重会随内容、随当前解码目标发生变化。为了统一描述这种动态选择过程注意力机制引入了一组常见的称呼查询 Query、键 Key、值 Value。可以这样理解Query 表示“我现在想要什么”。Key 表示“我这里有什么可提供的”。Value 是实际要取用的信息内容。注意力计算的第一步是计算 Query 和每个 Key 之间的匹配程度。匹配程度越高说明这个 Key 指向的信息越值得被提取。第二步把匹配分数归一化成权重。第三步用这些权重对 Value 做加权求和得到最终的输出。这里有一个容易混淆的点Key 和 Value 在很多场景下是同一个东西但它俩本身不是同一个概念。Key 负责参与匹配Value 负责提供内容。比如在做检索时Key 是文章的标题Value 是文章正文。标题匹配度决定了正文要不要被引用正文才是实际输出的内容。在注意力机制里你可以让 Key 和 Value 来自不同来源也可以让它们相同完全取决于任务设计。如果用一句话概括注意力机制的核心思想那就是通过一个动态计算的相关性权重从一组信息中聚合出适合当前目标的内容。3. 注意力的计算流程打分、归一化、加权求和从计算角度看注意力机制几乎都是三段式流程。第一段打分。给定一个 Query 和一组 Key计算 Query 与每个 Key 的相似度分数。相似度越高说明这个位置和当前需求越相关。第二段归一化。把打分结果通过 softmax 转成概率分布让所有权重之和为 1。这一步能让模型把“相对重要性”表达得更清楚也方便梯度传播。第三段加权求和。将归一化后的注意力权重与对应的 Value 相乘并累加得到最终输出。3.1 常见的相似度打分方式名称计算公式特点点积注意力score q · k简单直观但 q 和 k 维度很大时分数方差会变大训练容易不稳定缩放点积注意力score q · k / sqrt(d_k)Transformer 中使用通过除以向量维度的平方根来稳定梯度加性注意力score v^T tanh(W[q; k])表达能力更强但参数更多早期 seq2seq 中常用一般注意力score q^T W k引入可学习矩阵 W在点积的基础上增加灵活性缩放点积注意力能成为主流一个重要原因是它把点积结果用 sqrt(d_k) 做了归一化。当 Key 的维度 d_k 变大时点积结果的范围会随之变大softmax 后的概率分布很容易走向极端靠近 0 或 1梯度不明显。除以 sqrt(d_k) 后方差保持在可控范围训练更稳定。3.2 一个极简 Python 实现为了直观感受“打分-归一化-加权求和”的过程先写一个不依赖深度框架的最小版本。import numpy as np def naive_attention(query, keys, values): 极简注意力实现用点积打分softmax 归一化再做加权求和。 只用来帮助理解流程不做任何优化。 # 1. 打分query 与每个 key 做点积 scores [np.dot(query, key) for key in keys] # 2. 归一化先减最大值防止指数溢出再 softmax exp_scores np.exp(scores - np.max(scores)) weights exp_scores / np.sum(exp_scores) # 3. 加权求和 output sum(w * v for w, v in zip(weights, values)) return output, weights query np.array([1.0, 0.5, -0.2]) keys [ np.array([1.0, 0.0, 0.0]), np.array([0.0, 1.0, 0.5]), np.array([-0.5, 0.2, 1.0]), ] values keys[:] # 演示场景直接复用 key 对应的向量 output, weights naive_attention(query, keys, values) print(输出向量:, output) print(注意力权重:, weights) print(权重之和:, weights.sum())在这个例子中query 和第一个 key 的几何方向最接近所以得到的注意力权重最高。weights 之和等于 1说明权重被归一化成了概率分布。这个最简单的版本已经包含了注意力机制的全部核心逻辑。需要注意真实项目中 query、key、value 通常不是手工构造的而是通过线性变换从输入特征中学习得到的。线性变换的作用是让模型能够根据任务自动决定“什么算相关”。4. 自注意力与多头注意力同一思想的两个重要变体前面讲的注意力都是有一个外部 Query 去查询一组 Key 和 Value。比如机器翻译中解码器的隐状态作为 Query编码器的所有隐状态作为 Key 和 Value。这种模式叫作“交叉注意力”Query 来自一个序列Key 和 Value 来自另一个序列。自注意力则把问题换了一个角度Query、Key、Value 全部来自同一个序列。换句话说序列中的每个位置都去和其他位置计算相关性然后根据相关性聚合整个序列的信息。这样做的好处是每个位置都能直接看到全局上下文而不必像 RNN 那样一步步传递信息。自注意力的计算流程和前文完全一样输入序列 x 通过三个可学习矩阵 W_q、W_k、W_v 分别得到 Q、K、V。Q 和所有 K 计算相似度分数。softmax 归一化得到注意力权重。权重和 V 做加权求和得到新的向量表示。如果用一个通俗的类比自注意力就像是让一群人开会讨论问题。每个人发言时心里都会给其他人的观点打分更认同谁的观点就会更重视谁提出的信息。经过一轮交流后每个人的表态都不再只代表自己而是糅合了全场信息。多头注意力则是把自注意力这个“单线程”过程扩展成了“多线程”。具体做法是把 Q、K、V 分别拆成多个子空间每个子空间独立做一次注意力计算最后把所有子空间的结果拼接起来再经过一层线性映射输出。为什么要拆成多个头因为一次注意力往往只能关注一种相关模式。比如在文本里一个词的语法角色、语义类型、位置关系都可能是别的词产生关联的原因。多个头可以让模型从不同角度去看问题一个头关注指代关系一个头关注修饰关系一个头关注距离比较近的上下文。最后把不同角度的结果拼接起来得到更丰富的表示。这里给出一份可以在 PyTorch 中运行的多头自注意力最小实现方便你对照理解import math import torch import torch.nn as nn class MultiHeadSelfAttention(nn.Module): 多头自注意力的最小实现。 输入 x: [batch, seq_len, d_model] 输出: [batch, seq_len, d_model] def __init__(self, d_model64, num_heads4): super().__init__() assert d_model % num_heads 0, d_model 必须能被 num_heads 整除 self.num_heads num_heads self.d_model d_model self.head_dim d_model // num_heads self.wq nn.Linear(d_model, d_model) self.wk nn.Linear(d_model, d_model) self.wv nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) def forward(self, x): batch, seq_len, d_model x.shape # 1. 分别生成 Q、K、V Q self.wq(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) K self.wk(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) V self.wv(x).view(batch, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # 2. 缩放点积注意力 scores Q K.transpose(-2, -1) / math.sqrt(self.head_dim) attn torch.softmax(scores, dim-1) # 3. 加权求和 context attn V # 4. 拼接多个头的结果并线性映射 context context.transpose(1, 2).contiguous().view(batch, seq_len, d_model) return self.out_proj(context) if __name__ __main__: torch.manual_seed(0) model MultiHeadSelfAttention(d_model64, num_heads4) x torch.randn(2, 10, 64) y model(x) print(y.shape) # torch.Size([2, 10, 64])代码中需要注意几个细节d_model 必须能被 num_heads 整除否则拆分维度时会报错。transpose(1, 2)的目的是把头的维度从序列维度中拆出来方便批量并行计算。第 2 步计算得到的scores形状为[batch, num_heads, seq_len, seq_len]表示每个位置对所有位置的注意力权重。最终输出形状和输入一致因为多头结果经过拼接后又做了一次线性映射。这份代码跑通后你可以做一个小实验把attn权重打印出来观察句子中哪些位置之间的相关性更强。这个权重矩阵就是自注意力机制产生的“注意力图”。5. 注意力机制在视觉和时间序列任务中的常见形态很多初学者会误以为注意力机制只适用于文本任务实际上它是一个通用的信息加权思路。只要数据中存在着“哪些部分更重要”的问题注意力机制几乎都能借助合适的形式参与进来。5.1 通道注意力SE 模块的挤压与激励在图像任务中特征图除了空间维度之外还有通道维度。不同通道往往对应着不同的语义模式比如边缘、纹理、物体部件。通道注意力的目标就是让模型知道哪些通道在当前图像中更重要。SESqueeze-and-Excitation模块是通道注意力的经典实现它的流程分两步第一步是挤压Squeeze。对每个通道的特征图做全局平均池化把一个H * W的特征图压缩成一个数值。这个数值可以粗略理解为该通道的全局响应强度。第二步是激励Excitation。用一个带激活函数的全连接结构把通道维度的信息映射成一组权重。通常先用一个降维层把通道数压缩再经过 ReLU然后再恢复成原始通道数最后通过 sigmoid 把权重限制到 0 到 1 之间。第三步是把学习到的通道权重逐通道乘回原始特征图。这样一来重要的通道被放大不重要的通道被抑制。整个 SE 模块可以看作是一种轻量的注意力机制它引入的参数量很小却能让网络在通道维度上具备动态选择能力。5.2 时序注意力让模型关注关键时间步时间序列预测任务也大量使用注意力机制。给定过去一段时间的观测数据模型需要预测未来时刻的值。但并不是每一个历史时间点都同样重要可能某个时间窗口对应了业务高峰另一个时间窗口只是平静期。时序注意力的做法是在 LSTM 或 Transformer 等模型的基础上计算每个历史时间步对当前预测目标的重要性权重然后用加权求和的方式对历史信息进行聚合。这和前面讲的计算流程本质相同只是 Query、Key、Value 被定义在时间维度上。相比传统 LSTM 把全部历史信息压缩进最后一个隐状态时序注意力能够让模型直接“回看”更早的趋势变化。对于那些包含明显周期、突变或长依赖的时间序列任务注意力机制通常能带来明显收益。5.3 从 YOLOv8 引入多头注意力看注意力落地方案在目标检测领域网上经常能看到“YOLOv8 引入多头注意力机制 MHSA”的改进方案。需要澄清的是YOLOv8 默认的主干网络和检测头并不是以多头注意力为主体结构的这类改进通常是指把 MHSA 作为可选模块插入到 YOLOv8 的特定特征层之前用来增强全局上下文建模能力。目标检测模型长期依赖卷积提取局部特征感受野虽然可以逐步扩大但每个输出位置看到的信息终究是受限的。在特征图某个位置插入多头注意力可以让该位置直接和更多其他位置建立联系从而捕捉上下文关系。这类做法的价值是思路性的注意力能补足卷积模型在长距离依赖上的短板。但它不是免费的MHSA 的显存开销和计算量明显高于卷积插入位置不当反而会拖慢训练速度、降低收敛稳定性。实际项目里如果要尝试建议先在小数据集上做对照实验再决定是否用在生产环境。5.4 注意力不是固定模块而是一种设计思路从序列注意力、自注意力、多头注意力到通道注意力和时序注意力你会发现它们共享同一个骨架先判断相关性再归一化再加权聚合。区别只在于Query 从哪里来。Key 和 Value 从哪里来。注意力作用在哪个维度上是序列维度、通道维度、时间维度还是空间维度。理解了这一点再看各种改进模型时会轻松很多。所谓某种注意力变体不过是在定义“什么和什么算相关”、以及“在哪个维度上做加权”这两个问题上做了不同设计。6. 常见误区与学习建议注意力机制概念被广泛使用的过程中也产生了一些刻板印象。搞清楚这些误区能帮你少走弯路。误区实际情况注意力机制 Transformer注意力机制在 Transformer 之前就存在Transformer 是“大量使用自注意力”的架构不是注意力本身自注意力是唯一的注意力还有编码器-解码器注意力、通道注意力、时序注意力等自注意力只是其中一种重要形式注意力权重可以直接当解释性证据注意力权重反映的是模型内部的加权行为不一定等于人类理解的因果解释需要谨慎下结论注意力机制能解决所有长序列问题自注意力的计算复杂度随序列长度平方增长长序列场景通常需要稀疏注意力或其他优化多头越多效果越好头数太多会带来冗余和计算开销需要根据模型规模和任务做实验调整学习注意力机制时我的建议是不要一上来就读大模型源码。先从概念入手再实现一个最小版本观察权重怎么变化最后再回到 Transformer、YOLOv8 等具体模型里看工程化实现。这样做的好处是每一次看源码时你都能把眼前的矩阵运算映射回“打分、归一化、加权求和”这三步理解成本会低很多。在学习过程中还有一个容易被忽略的点注意力的价值高度依赖任务。同一个注意力模块在长文本、图像、时间序列上的表现可能差异很大。不要因为某篇论文用 SE 模块涨了点就认为所有任务都需要加 SE也不要因为 Transformer 效果强就把 MHSA 硬塞进所有网络。更好的做法是先明确任务中“哪些信息是动态相关的”再选择对应的注意力形式。7. LMCC 注意力机制系列的学习路线与下一步本文是 LMCC 注意力机制系列的第 01 篇核心是把注意力机制的概念讲清楚它从 seq2seq 的固定向量瓶颈中诞生本质是动态加权求和计算流程统一为打分、归一化、加权求和并由此延伸出自注意力、多头注意力、通道注意力和时序注意力等不同变体。后续内容建议按下面的路线推进第 02 篇回到代码层面手写一份完整的多头自注意力模块用真实文本和向量验证注意力权重的变化。第 03 篇进入 Transformer 结构把注意力放到整个编码器-解码器框架中理解搞清楚残差连接、层归一化和位置编码各自的作用。第 04 篇聚焦视觉任务实现 SE 模块并在一个简单分类网络里对比有无通道注意力的效果。第 05 篇聚焦时间序列预测用一个真实数据集对比 LSTM 和 LSTM 注意力机制的表现。最后再回到检测、生成、多模态等场景看注意力机制如何被工程化嵌入不同任务。学习注意力机制时最重要的不是记住某个图或某个公式而是拥有一种能力看到一个模型时能立刻问出两个问题——它的 Query 是什么Key 和 Value 是什么注意力作用在哪个维度上。能把这两个问题回答清楚你对注意力机制的理解就已经超过很多人了。下一节开始我们不再停留在概念而是直接用代码拆解多头自自注意力的完整实现。可以先把本文末尾的“打分、归一化、加权求和”三步内化再去写代码思路会清晰很多。
返回列表