作者从"the trophy didn't fit in the suitcase because it was too big"这句经典歧义句出发,用真实代码逐步拆解Self-Attention如何解决指代消解问题,深入到矩阵运算和权重计算细节。
新篇章由此开启。上一系列文章完全围绕在 LLM 之上构建各种应用展开——RAG、Agent、Eval、生产环境,所有内容都把模型当作一个通过 API 调用的黑盒。对于构建产品来说,这是一种合理的做法,对大多数工作而言也是正确的抽象层级。但一直有读者问同一个问题:好,那这个黑盒内部到底在发生什么?
这一系列文章就是答案。我们要从根本上深入探索,从那个使现代 LLM 成为可能的唯一核心理念开始——Attention(注意力机制),它是 Transformer 架构的核心机制,贯穿整个系列的所有模型实际上一直在运行着它。
在讲数学之前,先看一个现实生活中的例子
读一下这个句子:"the trophy didn't fit in the suitcase because it was too big."(奖杯放不进箱子,因为它太大了。)"it"指的是谁,奖杯还是箱子?
你瞬间就给出了答案,甚至没意识到自己做了什么。你的大脑看到"it"这个词,然后扫描回整个句子,权衡每个词与判断"it"指代什么之间的相关性,最后锁定"trophy",因为"big"与奖杯放不进去这件事搭配是合理的,而"didn't"或"the"这类词几乎不会被考虑——它们在语法上必要,但对确定"it"的指代毫无关系。
这就是 Attention 的全部直觉。对于句子中的每个词,模型都会问:"句子中的哪些其他词对理解这个词真正重要?"然后给每个其他词分配一个相关性分数,对重要的词给予更多关注,对不重要的词少关注。换一个词:"the trophy didn't fit in the suitcase because it was too small"(奖杯放不进箱子,因为它太小了),"it"突然就指向箱子了,同样的句子结构,完全不同的答案,因为实际含义发生了变化。如果一个模型做不到这一点——不能单独处理词语、不能衡量它们之间的相互关系——就不可能正确回答这个问题。
为什么它出现时是一件大事
在 Attention 出现之前,处理文本序列的主流方式是一个词接一个词、按照顺序地阅读,并在阅读过程中携带一个不断运行的摘要,类似于从左到右读一个句子,同时试图将所有重要的内容记住为一条连续的笔记。这种方法在处理长句子时确实很吃力——当你读到第三十个词时,那个不断运行的摘要已经被反复压缩和覆写太多次了,以至于第二个词的细节往往已经消失了。
真正的突破,来自 2017 年一篇标题 literally 就是"Attention Is All You Need"的论文——它意识到你根本不需要那个顺序运行的摘要。每个词可以直接同时查看句子中的所有其他词,不受距离限制,并自行决定每个词的相关性。第二百个词可以直接 attend to 第一个词,中间没有任何信息瓶颈。这种直接的、一次性的连接能力,正是 Transformer 能够如此良好地扩展、并成为 2017 年以来几乎所有主要 LLM 背后架构的真正原因。
真正的机制:Query、Key 和 Value
这里是变得具体的地方。对于每个词——抱歉,是每个 token,因为之前一篇文章已经讲过,文本被分解成 token,不一定是完整的词——模型从它创建三个不同的向量:query、key 和 value。
把它想象成图书馆检索。你的 query 是你要找的东西。书架上每本书都有一个 key,一个描述这本书内容简介的标签。你把你的 query 与每本书的 key 进行比较,以弄清楚每本书与你想要的内容的相关程度,而 key 匹配最好的那些书,其实际内容——也就是 value——是你最需要关注的。
在 Transformer 中,每个 token 同时为序列中的每个其他 token 做这件事。Token 第五个生成一个 query,问"什么与我相关",将这个 query 与所有其他 token(包括它自己)的 key 进行比较,得到每个的相关性分数,然后用这些相关性分数对所有 token 的 value 进行加权组合,得到一个全新的、上下文感知的自身表示。

让我们用代码真正实现它
我将完全使用 numpy 从头实现一个真实的、简化版的 self-attention,这样你看到的是实际的数学运算,而不仅仅是概念。这是刻意精简的机制,真正的 Transformer 在此基础上添加了更多东西,但这就是运行在每个模型内部的实际核心计算。
第一步,将一个小序列表示为向量
import numpy as np
# pretend these are embeddings for the words "the", "cat", "sat"
# in reality these come from a learned embedding table, here
# we're just making up small numbers to see the mechanism clearly
np.random.seed(42)
embeddings = np.random.randn(3, 4) # 3 tokens, 4 dimensional embeddings each
print("Token embeddings:")
print(embeddings)
第二步,生成 query、key 和 value 向量
在真实模型中,这些来自通过整个训练过程学习到的权重矩阵(我们在这个系列后续会讲到)。这里我们使用小的随机矩阵,只是为了看清实际的运作机制。
d_model = 4 # embedding dimension
W_query = np.random.randn(d_model, d_model) * 0.5
W_key = np.random.randn(d_model, d_model) * 0.5
W_value = np.random.randn(d_model, d_model) * 0.5
queries = embeddings @ W_query
keys = embeddings @ W_key
values = embeddings @ W_value
print("Queries shape:", queries.shape) # 3 tokens, each with a query vector
第三步,计算注意力分数——每个 token 对每个其他 token 的相关性
# dot product between every query and every key gives a relevance score
attention_scores = queries @ keys.T
print("Raw attention scores:")
print(attention_scores)
# scale down, this keeps the numbers from getting too large as
# dimensions grow, a detail the original paper found mattered
attention_scores = attention_scores / np.sqrt(d_model)
# turn scores into proper weights that sum to 1 per row, using softmax
def softmax(x):
exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
attention_weights = softmax(attention_scores)
print("\nAttention weights, each row sums to 1:")
print(attention_weights)
那个 attention_weights 矩阵确实是整个机制的核心。第一行告诉你 token 一(即"the")对自身、对"cat"和对"sat"的注意力分配;第二行告诉你"cat"的情况,以此类推。这些不是固定的,而是对每个输入重新计算的,基于那个特定序列的实际内容。
第四步,用这些权重组合 value
# each token's new representation is a weighted blend of every
# token's value vector, weighted by how much attention it paid
output = attention_weights @ values
print("\nNew context aware representations:")
print(output)
那个 output 就是 self-attention 的实际结果——每个 token 原始的 embedding 现在已经与每个相关 token 的信息混合在一起,按照它们实际的重要程度加权。这就是在整个系列中你用过的每个基于 Transformer 的模型内部,真实发生的事情——在更大规模、更多维度、更多 token 的情况下。
为什么需要"Multi-Head"Attention,以及为什么要多此一举
真实的 Transformer 不只做一次,而是并行做几次,称为多个 attention heads,每个 head 有自己独立的 query、key 和 value 权重矩阵,每个学习关注不同类型的关系。
def single_attention_head(embeddings, d_model):
W_q = np.random.randn(d_model, d_model) * 0.5
W_k = np.random.randn(d_model, d_model) * 0.5
W_v = np.random.randn(d_model, d_model) * 0.5
q = embeddings @ W_q
k = embeddings @ W_k
v = embeddings @ W_v
scores = (q @ k.T) / np.sqrt(d_model)
weights = softmax(scores)
return weights @ v
def multi_head_attention(embeddings, num_heads=3):
d_model = embeddings.shape[1]
head_outputs = [single_attention_head(embeddings, d_model) for _ in range(num_heads)]
# in a real model these get concatenated and projected back down,
# here we're just showing that each head produces its own view
return head_outputs
heads = multi_head_attention(embeddings, num_heads=3)
for i, head_output in enumerate(heads):
print(f"Head {i} output:\n{head_output}\n")
直觉上,一个 head 可能会真正学习关注语法关系,比如将代词与它们指代的名词匹配。另一个可能关注相邻词之间的关系。还有一个可能专门处理训练过程中发现有用的其他东西。没有人明确编程指定每个 head 关注什么——这是从训练中涌现出来的,而这正是这个系列下一篇文章要深入探讨的内容。
这在完整的 Transformer 中处于什么位置
Attention 是那个脱颖而出的想法,但真实的 Transformer 层在它周围还包裹着一些其他东西。在 self-attention 之后,每个 token 的新表示会通过一个小的 feedforward neural network,它对每个 token identical 地应用,在 token 间信息混合之外增加了额外的处理能力。Layer normalization 使数字在流经许多堆叠层时保持在稳定、行为良好的范围内。而 residual connections——将层的输入加回到其输出上——帮助信息和梯度在非常深的层堆叠中干净地流动,不会消失。
堆叠几十层这些结构——Attention,然后是 Feedforward,重复——每一层根据之前的内容进一步细化 token 表示——你就得到了 GPT、Claude 以及 2017 年以来构建的几乎所有主要 LLM 背后的真实架构。
真实的生产级 Transformer 比这个简化版本有更多工程细节:positional encoding 让模型知道 token 顺序(因为 Attention 本身没有内在的序列感);causal masking 使得生成文本的 token 无法偷看尚未生成的未来 token;还有各种优化,使这一切能够大规模高效运行。但核心计算——query、key、value,加权组合——确实就是这样,在海量规模上运行,不是什么隐藏在下面的本质不同的秘密机制。
这个系列的下一步
本文覆盖了让模型能够在单次前向传播中将 token 相互关联的机制。没有覆盖的是模型实际上如何学习我们这里随机初始化的那些权重矩阵,也没有覆盖训练过程中发生了什么——将随机数字变成产生连贯语言的东西。这确实是下一个部分,也是这个系列下一步要去的地方。
如果你自己运行这段代码,并打印出你真正关心的句子的 attention weights,我真的很想听听你注意到了什么——这通常是让这个概念不再感觉抽象的最快方式。