AI 技术演进与核心算法实战 | 第四篇:注意力机制解密:Self-Attention 的矩阵运算图解与 Q,K,V 的物理意义



Transformer 架构之所以能一统 NLP 和 CV,本质就在于它赋予了模型“注意力”。而 Q、K、V 就是理解注意力的钥匙。
在上一篇中,我们探讨了 Embedding 原理,了解到词汇是如何被映射到高维连续空间的。模型通过 Embedding 获得了词汇本身的“孤立语义”。
然而,人类语言是极其复杂的,一个词的意思往往取决于它的上下文。 例如:“苹果”这个词,在“我吃了一个苹果”和“苹果发布了新手机”中,意思截然不同。如果仅仅使用静态的 Embedding,模型是无法区分这两种情况的。
为了解决这个问题,2017 年 Google 在其划时代论文《Attention Is All You Need》中提出了 Self-Attention(自注意力机制)。它使得模型能够在处理每个词时,动态地去“关注”句子中的其他词,从而获得包含上下文信息的动态 Embedding。
本篇是 《AI 技术演进与核心算法实战》第一模块的第四篇。我们将剥开 Self-Attention 的数学外衣,通过直观的图解和极简的 Python 代码,带你真正搞懂 Q、K、V 的物理意义以及矩阵运算背后的直觉。
1. 什么是注意力?从“找对象”说起
为了通俗地理解 Q、K、V 的概念,我们不妨想象一个“相亲/找对象”的场景:
假设你是一个正在寻找伴侣的人:
- Query (Q,查询):你对理想伴侣的要求。比如“我想要一个性格开朗、喜欢旅游、会做饭的人”。这代表了你主动寻找信息的意图。
- Key (K,键):其他候选人身上的标签和特质。比如候选人 A 的标签是“性格内向、宅、会写代码”,候选人 B 的标签是“性格开朗、热爱户外、厨艺精湛”。这代表了被检索对象的特征。
- Value (V,值):候选人真正的内在价值或详细信息。比如候选人 B 除了上述标签,他的真实性格、工作情况、生活细节等完整内容。
注意力机制的计算过程就是:
- 拿你的要求 Q 去和所有候选人的标签 K 进行匹配(点积计算)。
- 匹配度越高(点积越大),你分配给那个人的“注意力”(Attention Score)就越多。
- 最后,你根据分配的注意力权重,综合所有候选人的真实价值 V,得到一个最终的判断(输出)。
在语言模型中,一句话中的每个词都同时扮演着寻找者(Q)、被寻找的标签(K)和真实的价值(V)三个角色。每个词都在看句子里的其他词,看看谁和自己最相关,然后把相关词的信息吸收过来,丰富自己的语义。
2. Self-Attention 核心步骤与通俗图解
为了让大家彻底摆脱对公式的恐惧,我们放弃冷冰冰的矩阵符号,用具体的例子来看看 Self-Attention 到底在干什么。
假设我们有一句话:“吃 苹果”。这两个词在进入模型前,各自拥有一个初始的词向量(Embedding)。接下来,它们要经历一场“信息交流大会”。
步骤 1:分身有术 —— 提取 Q、K、V
每个词在交流前,都会通过三个不同的“滤镜”(模型学到的权重),变出三个不同的身份:
- Q (Query/查询):拿着大喇叭,喊出“我在找什么”。
- K (Key/胸牌):挂在胸前,写着“我是谁,我有什么特征”。
- V (Value/内涵):揣在兜里,装着“我真实的语义内容”。
步骤 2:确认眼神 —— 计算匹配度 (Attention Scores)
接下来,“吃” 这个词想要丰富自己的含义,它举起自己的大喇叭(Q),去和句子里所有词的胸牌(K)进行配对。 在数学上,这个配对过程就是点积运算。点积结果越大,说明两个词越“来电”。
- “吃”的 Q(找食物) 配对 “吃”自己的 K(动词):匹配度一般(比如得分 10)。
- “吃”的 Q(找食物) 配对 “苹果”的 K(食物):完美契合!(比如得分 90)。
步骤 3:分配注意力比例 —— Softmax 归一化
刚才算出的得分(10分、90分)是绝对数值,为了方便后续计算,模型会施加一个叫做 Softmax 的魔法漏斗。 它的作用很简单:把所有的得分变成百分比,并且保证加起来等于 100%。(在此之前还会稍微除以一个数“冷静”一下,防止极端值)。
经过 Softmax 后:
- “吃”对“吃”自己的注意力比例:10%
- “吃”对“苹果”的注意力比例:90%
这就意味着,“吃”这个词在理解自身当前语境时,决定花 90% 的精力去关注“苹果”。
步骤 4:取长补短,融为一体 —— 加权求和 (Weighted Sum)
终于到了最后一步!“吃”这个词根据刚才定好的比例,去拿大家兜里的内涵(V):
- 从“吃”自己的 V 里抽取 10%。
- 从“苹果”的 V 里抽取 90%。
把这些抽出来的内涵(向量)全部加起来揉碎,就得到了“吃”这个词全新的词向量(Output)!
此时的“吃”,已经不再是一个干巴巴的动作,而是变成了“吃苹果”这个具体情境下的“吃”。同理,“苹果”也会以类似的方式融合“吃”的信息,变成“被吃的苹果”。
这就是 Self-Attention 的核心魔力:它让孤立的词汇相互交流,根据语境动态地改变彼此的含义!
3. 极简代码实现:手写 Self-Attention
理解了理论后,我们用 PyTorch 来把这个过程变成可运行的代码。在写代码之前,我们先理清思路,并弄明白我们的“原材料”从哪里来。
代码实现思路梳理
- 输入 是什么?
在上一篇文章中,我们学到了 Embedding。这里的输入矩阵 ,其实就是一句话被分词后,查表得到的初始词向量的集合。
比如句子长度为 3(“我”, “吃”, “苹果”),每个词的向量维度是 4,那么 就是一个
3×4的矩阵。在深度学习中,为了并行计算,我们通常还会加一个批次(Batch)维度,所以实际的形状是(batch_size, seq_len, embed_size)。 - 如何生成 Q、K、V?
在 PyTorch 中,我们可以使用
nn.Linear(线性层)来代表我们上文提到的权重矩阵 、、。输入 经过这三个线性层,就会分别变成 Q、K、V。 - 计算公式的对应:
- 点积匹配度:利用
torch.matmul(矩阵乘法)计算 。 - 缩放与 Softmax:除以维度的平方根,并调用
F.softmax。 - 最终融合:再次利用
torch.matmul将概率矩阵与 相乘。
- 点积匹配度:利用
环境准备与运行方法
在运行代码之前,你需要确保本地安装了 Python 和 PyTorch 库。
-
安装 PyTorch:打开你的终端(Terminal)或命令行,输入以下命令安装
torch:pip install torch(注:如果你的网络环境较慢,可以添加国内镜像源,例如
pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple) -
保存并运行代码:将下面的代码保存为
self_attention_demo.py文件,然后在终端中运行:python self_attention_demo.py
核心代码实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class SelfAttention(nn.Module):
def __init__(self, embed_size):
super(SelfAttention, self).__init__()
self.embed_size = embed_size
# 定义生成 Q, K, V 的线性映射
self.W_q = nn.Linear(embed_size, embed_size, bias=False)
self.W_k = nn.Linear(embed_size, embed_size, bias=False)
self.W_v = nn.Linear(embed_size, embed_size, bias=False)
def forward(self, x):
# x shape: (batch_size, seq_len, embed_size)
# 1. 计算 Q, K, V
Q = self.W_q(x) # (batch_size, seq_len, embed_size)
K = self.W_k(x) # (batch_size, seq_len, embed_size)
V = self.W_v(x) # (batch_size, seq_len, embed_size)
# 2. 计算注意力分数: Q 和 K^T 的点积
# K.transpose(1, 2) 将 seq_len 和 embed_size 维度互换
attention_scores = torch.matmul(Q, K.transpose(1, 2))
# 3. 缩放 (Scaling)
d_k = self.embed_size
scaled_attention_scores = attention_scores / (d_k ** 0.5)
# 4. Softmax 归一化,得到注意力权重
attention_weights = F.softmax(scaled_attention_scores, dim=-1)
# 5. 加权求和得到输出
output = torch.matmul(attention_weights, V)
return output, attention_weights
if __name__ == "__main__":
# 模拟输入:批次大小 1,序列长度 3(3个词),词向量维度 4
x = torch.rand(1, 3, 4)
print("输入 X:\n", x)
attention = SelfAttention(embed_size=4)
output, weights = attention(x)
print("\n注意力权重 (Attention Weights):\n", weights)
print("\n输出 Output:\n", output)
运行结果解析
运行上述代码后,你会看到类似如下的输出(由于权重初始化和 torch.rand 是随机的,每次运行的具体数字会不同,但结构一致):
输入 X:
tensor([[[0.4862, 0.0456, 0.9413, 0.7616],
[0.0483, 0.1623, 0.6971, 0.2962],
[0.6240, 0.8581, 0.4798, 0.0640]]])
注意力权重 (Attention Weights):
tensor([[[0.3379, 0.3476, 0.3146],
[0.3362, 0.3416, 0.3222],
[0.3465, 0.3393, 0.3142]]], grad_fn=<SoftmaxBackward0>)
输出 Output:
tensor([[[ 0.7261, -0.1013, 0.2098, 0.0596],
[ 0.7279, -0.1009, 0.2071, 0.0592],
[ 0.7295, -0.1022, 0.2118, 0.0608]]], grad_fn=<UnsafeViewBackward0>)
- 输入 X:代表了 3 个词,每个词是一个 4 维的向量(初始的 Embedding)。
- 注意力权重:这是一个 的矩阵。你可以看到第一行的三个值加起来刚好是 1(例如 0.3379 + 0.3476 + 0.3146 ≈ 1),这就是 Softmax 的功劳,代表第一个词对这 3 个词分别分配了多少“注意力比例”。
- 输出 Output:这是融合了上下文信息后的全新矩阵,形状依然是 。但此时每一行的向量,都已经包含了整个句子的综合语义!
4. 总结
Self-Attention 的精妙之处在于,它打破了传统 RNN/LSTM 必须按顺序处理文本的限制。在 Self-Attention 中,句子里的每一个词都可以同时“看”到其他所有的词,并通过点积运算自动计算出它们之间的关联度。
这种机制不仅让模型拥有了极强的捕获长距离依赖(Long-term Dependency)的能力,而且矩阵乘法天生适合 GPU 并行计算,这正是 Transformer 能够高效训练海量数据的根本原因。
📚 参考文献与延伸阅读
- Attention Is All You Need (Vaswani et al., 2017) - 深度学习历史上最重要的论文之一,正式提出了 Transformer 架构和 Self-Attention 机制。
- The Illustrated Transformer (Jay Alammar) - 极度推荐的可视化博客,用生动的图解详细剖析了 Transformer 的内部结构。
- Neural Machine Translation by Jointly Learning to Align and Translate (Bahdanau et al., 2014) - 最早将注意力机制引入 NLP(Seq2Seq 模型)的奠基之作。
下一篇预告: Transformer 架构深潜:Positional Encoding、Layer Norm 与前馈网络的手写实现(NanoGPT 复现)