从零开始的LLM 1.Attention机制
前言
从零开始学习ai文章系列已完成《动手学深度学习》和《磨菇书》两本书的学习,新开的LLM系列课本只要来自《Happy-LLM》,但是内容和排版等个人重新进行整理,因此不会按照原来课本中的章节来写。如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
《Happy-LLM》原文(课本):https://datawhalechina.github.io/happy-llm/#/
1. 图结构(Graph Structure)
在介绍 Attention 机制之前,我们先来认识图结构的基本概念。
图结构由两个核心元素构成:节点(Node) 和 边(Edge)。节点代表实体或信息单元,边则表示节点之间的关系或连接。边是有方向的——例如节点 A 指向节点 B,记作 A → B,表示信息或影响从 A 流向 B。每条边还带有一个数值,称为权重(Weight),用来表示两个节点之间关系的强弱程度。
以 "我爱你" 字符串中,每个字为一个节点为例:

节点之间两两相连,每条有向边都带有一个数值权重。但这些数字本身没有任何含义——意义是人赋予的。
图只是图。节点是节点,边是边,权重是权重。
当我们观察到某幅图中,「我」→「爱」的权重接近 1,其余边的权重都趋近于 0 时,我们可以说:这幅图描述的是主谓关系。
当「爱」→「你」的权重接近 1,其余边趋近于 0 时,我们可以说:这幅图描述的是动宾关系。

我们所说的 Attention,本质上就是一种图结构的学习。
它并不预设任何语法规则,而是自动为节点之间的每条边学习出一套权重分布——构建出多张图,每张图的权重分布各不相同。
这些权重究竟代表什么?只有 Attention 自己知道。
我们能做的,只是事后拿着某张权重图,凭借自己的语言直觉去猜测:这张图大概捕捉了主谓关系,那张图大概捕捉了修饰关系……但这终究是人的解读,不是模型的"意图"。
模型从不声明自己在做什么。它只是学到了一组权重,让最终的预测结果变得更准确。至于那些权重背后藏着什么语义,是我们强加的理解,而非模型内在的知识。
这里有一个更深的含义值得关注:Attention 构建的不是一张图,而是多张图同时并存(即多头注意力,Multi-Head Attention)。每个头独立学习一套权重分布,有的头可能学到了主谓关系,有的头可能学到了指代关系,有的头学到的东西我们完全无法命名。它们共同作用,才构成了模型对语言的"理解"。
2. attention机制
注意力机制有三个核心变量:Query(查询)、Key(键)、Value(值)。根据这三者的来源,Attention 分为两种类型:Cross-Attention 和 Self-Attention。
2.1 Cross-Attention
Query 来自一个输入,Key 与 Value 来自另一个输入。
以新闻阅读为例:假设你面前有一篇报道,你想知道这件事发生在什么时候。你脑中浮现出一个问题——"时间是什么?"——这就是 Query,它来自你的意图,而非新闻本身。新闻中的每一个词,就是 Key。Query 与每个 Key 逐一比对,一旦某个 Key 高度吻合(比如"昨日"、"3月5日"),这个词所携带的信息就会被提取出来——这就是 Value。

可以理解为 Q节点是边的起始端,K节点是边的终止端,即节点间关系为 Q->K。
如图所示,Query "时间是什么" 与新闻中每个词的 Key 逐一比对后,"昨日"和"3月5日"获得了最高的注意力权重——它们的 Value 将被重点提取。
2.2 Self-Attention
Query、Key、Value 均来自同一个输入。每个词既是"提问者",也是"被查询者"——每个节点都会去关注序列中的所有其他节点,形成我们之前介绍的全连接图结构。

横纵轴都是同一个序列"我爱你",这正是 Self-Attention 和 Cross-Attention 最直观的区别。
从这张图里可以读出几个有意思的信息:
- 「我」作为 Query,对"爱"的权重最高(0.90)——"我"在理解自身时,最依赖紧跟其后的动词。
- 「爱」作为 Query,对"我"和"你"的权重相当(各 0.45)——谓语同时关注主语和宾语,符合直觉。
- 「你」作为 Query,对"爱"的权重最高(0.80)——宾语回望动词,确认自己在句中的语义角色。
每一行的权重之和为 1,这是 Softmax 归一化的结果
3. attention计算流程
这里我以self-attention为例,说明attention的计算流程(实际上cross-attention也是一样的,但是2个输入X不太方便写)
3.1 输入
\[ X∈\mathbb{R}^{B\times N\times D} \]
其中
| 符号 | 含义 |
|---|---|
| B | Batch size,一次并行处理的样本数 |
| N | Token 数量,即序列长度 |
| D | Embedding 维度,每个 token 的向量维度 |
X 并不是原始文本,而是文本经过两步处理后得到的向量表达:

第一步:Tokenizer
原始文本被切分为一个个 token,并映射为对应的 token
ID。例如"我爱大象"中,"大象"可能被映射为 ID 114514。
第二步:Embedding 层
每个 token ID 经过 Embedding 层,转换为一个 D 维的稠密向量。例如
114514 → [0.2, 1.7, −0.5, …](共 D 个值)。
将序列中所有 token 的向量拼在一起,就得到了形状为 \(N \times D\) 的矩阵;加上批次维度 B,最终输入 X 的形状为 B×N×D。
Tokenizer 与 Embedding
Tokenizer 做的事情很单纯:将原始文本切分并映射为 token ID,仅此而已,没有任何"理解"可言。
Embedding 层则不同——它是一个可学习的网络结构,本质上是一张巨大的查找表(形状为 V×D,V 为词表大小),负责将离散的 token ID 转换为连续的向量。你可以把这个向量空间理解为一个新的世界,Embedding 的作用就是为每个 token 在这个世界里分配一个坐标。
比如"猫"单独出现时,它在向量空间中对应某个坐标 a;而"猫在追狗"里的"猫",描述的是一个具体的行为主体,语义更丰富,对应另一个坐标 b。(注:纯 Embedding 层本身是静态的,即只能获得类似a的向量 ;让同一个词在不同上下文中获得不同表达,是 Attention 的职责,即获得类似b的向量。这里用两个坐标只是为了说明向量空间能够表达语义差异的潜力。)
如果觉得抽象,可以这样类比:电脑里所有东西归根结底都是 0 和 1,但我们通过不同的结构和规则,赋予了它们图像、声音、文字等不同的意义。向量空间也是同理——D 维坐标本身只是一串数字,但经过训练后,这串数字承载了语义。
Embedding 层没有独立的训练目标,它和整个模型一起,通过反向传播端到端地学习。
以语言模型为例,训练目标是"预测下一个词"。模型拿到"我爱__",需要预测出"你"。如果预测错了,损失函数会产生一个误差信号,这个信号一路反向传播,一直传回 Embedding 层,更新"我"和"爱"对应的向量坐标。
训练过程中,模型反复经历"预测→出错→更新"的循环。慢慢地,语义相近的词(比如"猫"和"狗")会在向量空间中聚拢到一起,语义相反的词(比如"爱"和"恨")也会呈现出有规律的相对位置关系。这些都不是人为规定的,而是模型从大量文本中自己归纳出来的。
这也解释了为什么 Embedding 如此强大:它把人类语言这种离散的、符号化的系统,压缩进了一个连续的几何空间——在这个空间里,语义可以用距离和方向来度量,这为后续 Attention 的计算奠定了基础。
3.2 线性变换得到 Q, K, V
权重矩阵(可学习参数,随模型一起训练) \[ W_Q,W_K,W_V∈\mathbb{R}^{D\times D} \] 计算 \[ \begin{align*} Q &=X\cdot W_Q \\ K &=X\cdot W_K \\ V &=X\cdot W_V \\ \end{align*} \] 维度结果如下 \[ Q,K,V \in \mathbb{R}^{B \times N \times D} \] 形状没有变化,但含义完全不同——X 是原始语义坐标,而 Q、K、V 是针对各自用途重新投影后的表达。
我们已经有了输入 \(X \in \mathbb{R}^{B \times N \times D}\) ,它是每个 token 在向量空间中的坐标。但我们不能直接拿 X 去做 Attention 计算——因为每个 token 在不同场合扮演不同的角色:有时它是"提问者",有时是"被查询者",有时是"信息提供者"。同一个向量无法同时胜任三种角色。
因此,我们用三个独立的权重矩阵,将 XX X 分别投影到三个不同的子空间,得到各司其职的 Q、K、V:
- Q(Query):我想关注什么? 每个 token 作为提问者时的表达。
- K(Key):我能提供什么线索? 每个 token 作为被查询者时亮出的标签。
- V(Value):我实际携带的信息是什么? 一旦被选中,真正输出的内容。
为什么不直接用 X 本身作为 Q、K、V?
因为 X 是 Embedding 给出的原始坐标,它只有一种表达。而 Q、K、V 需要分别回答三个完全不同的问题——"我该关注谁"、"我能被谁关注"、"我能贡献什么"——这三件事对同一个 token 而言往往需要不同的侧重。引入三个独立的权重矩阵,就是给模型足够的自由度,让它自己学会如何为每种用途找到最合适的投影方向。
3.3 拆分多头(Multi-Head)
回顾之前的结论:Attention 学到的权重分布代表某种语义关系,但具体是什么关系只有模型自己知道。一张权重图只能捕捉一种关系——主谓、动宾、指代、修饰……这些关系同时存在于一句话中,单张图顾此失彼。
Multi-Head 的思路很直接:与其用一个 D 维的大空间做一次 Attention,不如把它拆成 H 个 d 维的小空间,并行做 H 次 Attention。每个头独立学习一套权重分布,捕捉不同类型的语义关系。
设:
• 头数:H
• 每个头的维度:\(d=D/H\)
拆分维度
将 Q、K、V 从 D 维拆分为 H 个 d 维: \[ Q,K,V \in \mathbb{R}^{B \times N\times H \times d} \] 转置维度顺序
将第 2、3 维(N 与 H)对调,让每个 head 能独立拿到完整的序列: \[ Q,K,V \in \mathbb{R}^{B \times H\times N \times d} \]
3.4 每个 Head 计算 Attention
有了 \(Q,K,V \in \mathbb{R}^{B \times H\times N \times d}\) ,每个 head 独立执行以下四步:
第一步:点积,度量相关性 \[ Q\cdot K^{\mathrm{T}} \] 对最后两维做矩阵乘法:$(N, d) (d, N) (N, N) $ ,完整结果: \[ Q\cdot K^{\mathrm{T}} \in \mathbb{R}^{B \times H\times N \times N} \] 这一步的结果,就是我们在图结构中所说的边。
输出矩阵的形状是 \(N \times N\) 。 行是 Query(提问的节点),列是 Key(被查询的节点)。第 \((i,j)\) 个位置的数值,就是节点 \(i\) 指向节点 \(j\) 这条有向边的原始权重,代表"第 \(i\) 个 token 对第 \(j\) 个 token 有多感兴趣"。
回想我们最初画的"我爱你"全连接图——每个节点两两相连,每条边都有一个权重。\(Q \cdot K^T\) 做的,正是一次性计算出图中所有边的权重,填满这张 N×N 的表。

此时的权重还是原始的点积分数,尚未归一化——后续的缩放和 Softmax 才会将它们转化为我们在热力图中看到的概率分布。
第二步:缩放,稳定数值 \[ \frac{Q\cdot K^{\mathrm{T}}}{\sqrt{d}} \] 点积的结果会随维度 d 增大而数值膨胀——d 越大,向量内积的量级越大,导致 Softmax 输入过大,梯度趋近于零,训练变得困难。除以 \(\sqrt{d}\) 将数值拉回合理范围,与 d 的大小无关。
第三步:Softmax,归一化为概率分布 \[ Softmax(\frac{Q\cdot K^{\mathrm{T}}}{\sqrt{d}}) \] 对最后一维(即每一行)做 Softmax,将原始分数压缩到 (0,1) 区间,且每行之和为 1。此时得到的就是注意力权重矩阵——对应我们在第 2 章看到的热力图,每个数值代表"第 i 个 token 应该分配多少注意力给第 j 个 token"。
第四步:加权求和,提取 Value \[ Softmax(\frac{Q\cdot K^{\mathrm{T}}}{\sqrt{d}})\cdot V \] 用注意力权重对 V 做加权求和:$(N, N) (N, d) (N, d) $ ,完整结果: \[ Attention(Q,K,V)\in \mathbb{R}^{B \times H\times N \times d} \] 每个 token 的输出,是整个序列所有 token 的 Value 的加权组合——权重越高的 token 贡献越多。输出维度与输入一致,仍为 (B,H,N,d)。
四步合并,即标准的 Attention 公式: \[ Attention(Q,K,V)=Softmax(\frac{Q\cdot K^{\mathrm{T}}}{\sqrt{d}})\cdot V \] 输出维度和输入一致,但含义已经完全不同:输入时每个 token 只有自己的信息,输出时每个 token 已经融合了整个序列中其他所有 token 的信息——融合的比例,正是由注意力权重决定的。这就是 Attention 的本质:一次有选择的信息聚合。
我们可以借用向量空间的概念来理解。
回想之前说的:向量空间是一个新的世界,每个词都有自己的坐标。Embedding 给出的是词典意义上的坐标——"猫"就是那只猫,不管它出现在哪里,坐标都一样。
但语言里的词从来不是孤立存在的。"猫追狗"里的"猫"和"猫很可爱"里的"猫",虽然是同一个字,承载的语义却完全不同——前者是一个正在行动的主体,后者只是一个被描述的对象。
Attention 做的事情,就是把上下文揉进坐标里。
经过 Attention 之后,"猫追狗"里"猫"的坐标,已经从词典意义上的"猫",悄悄漂移到了向量空间中一个新的位置——这个新位置离"猫"很近,但也带着"追"的动势、"狗"的存在。你可以想象它在向量空间里向着"追狗的猫"这个方向移动了一步。
"狗"的坐标同理,漂移向了"被追的狗"。"追"的坐标则同时受到"猫"和"狗"的牵引,落在了"猫对狗施加的追逐"这个语义位置上。
所以,Attention 的输出不是三个孤立的词,而是三个描述同一件事的坐标。(注意:一个token是一个坐标)
输入时,向量空间里住着三个居民:"猫"、"追"、"狗"——它们各自待在自己的位置,互不相知。
输出时,这三个坐标都发生了漂移。但它们漂移的目的地并不是三个新的孤立的词,而是同一件事情的三个侧面:
- "猫"的坐标漂移到了"猫追狗"这件事里猫所处的位置
- "追"的坐标漂移到了"猫追狗"这件事里追这个动作所处的位置
- "狗"的坐标漂移到了"猫追狗"这件事里狗所处的位置
三个坐标,描述的是同一件事,只是视角不同。
换句话说,Attention 把三个词的独立坐标,变成了对"猫追狗"这个事件的三个向量表达。这个事件现在有了坐标,可以被后续的网络层继续处理、理解、推理。
3.5 拼接多头结果
每个 head 独立完成 Attention 计算后,得到输出 $Attention(Q,K,V)^{B H N d} $。现在需要把它们合并回去。
第一步:转置还原
将维度顺序从 \(B×H×N×d\) 换回 \(B×N×H×d\) ——和 3.3 节的转置互为逆操作。
第二步:拼接
将 H 个 head 的结果沿最后一维拼接,\(H×d=D\): \[ B×N×H×d→B×N×D \] 即: \[ Concat(heads)\in \mathbb{R}^{B \times N \times D} \]
至此,每个 token 的向量重新回到了 D 维——但现在这个向量是 H 个 head 各自视角的拼合,同时携带了多种语义关系的信息。
3.6 输出线性层
拼接之后,还有最后一步线性变换。 \[ Output=Concat(heads)⋅W_O \] 其中 \(W_O \in \mathbb{R}^{D \times D}\) 是可学习的权重矩阵。
这一步的作用是对多头的拼接结果做一次融合投影——H 个 head 的输出直接拼在一起,彼此之间还是独立的;乘以 \(W_O\) 之后,不同 head 捕捉到的语义关系才真正被混合、整合成一个统一的表达。
最终输出 \[ Output=\in \mathbb{R}^{B \times N \times D} \]
形状与最初的输入 X 完全一致。从输入到输出,维度没有变化,但每个 token 的向量已经从孤立的词义坐标,变成了充分感知上下文之后的语义表达——这正是整个 Attention 计算流程的意义所在。
4. 位置编码(Positional Encoding)
理解了 Attention 的计算之后,我们需要补上一个前面章节刻意跳过的细节——位置编码。
回想 \(Q \cdot K^T\) 的计算过程:它学习的是节点与节点之间的关系,但这张图里没有任何"顺序"的概念。"猫追狗"和"狗追猫"送进 Attention,得到的权重矩阵是完全一样的——因为两句话的词相同,只是位置不同,而 Attention 对位置视而不见。
这显然是个问题。语言的意义很大程度上依赖顺序。为此,Transformer 在 Embedding 之后、进入 Attention 之前,给每个 token 的向量叠加一个位置编码,把序列中的位置信息注入进去。
4.1 Sinusoidal 位置编码
原始 Transformer 使用正余弦函数进行位置编码,公式如下:

其中:
- pos:token 在序列中的位置(从 0 开始)
- i:向量的维度索引(0≤i<D/2)
- D:Embedding 维度
偶数维度用 sin,奇数维度用 cos。不同维度使用不同频率的波,频率随维度 i 增大而降低——低维度捕捉细粒度的位置差异,高维度捕捉粗粒度的长程位置关系。
这样设计的好处是:任意两个位置之间的相对距离,可以通过线性变换表达出来——模型不仅知道每个 token 在哪,还能推断两个 token 之间相差多远。
4.2 计算示例
以"I like to code"为例,设 D=4,词向量矩阵 X 为:

计算各位置的位置编码(D=4,故 i∈{0,1}):

比如pos=1("like"),对应四个维度:

同理,代入 pos=0,1,2,3,得到位置编码矩阵 PE:

然后将位置编码直接加到词向量上: \[
X'=X+PE
\]

X' 就是最终送入 Attention 的输入——每个 token 的语义信息与位置信息已经融为一体。
4.3 RoPE(旋转位置编码)
Sinusoidal 是位置编码的起点,但现代大模型普遍采用了一种更优雅的方案——RoPE(Rotary Position Embedding,旋转位置编码),LLaMA、Claude、GPT-NeoX 等主流模型均使用这一方案。
两者最直观的区别在于作用位置不同:
- Sinusoidal 在进入第一个 Block 之前,把位置向量直接加到词向量 X 上,一次性完成,后续不再处理。
- RoPE 则在每一层 Block 内部,在 \(Q \cdot K^\top\) 计算之前,把位置信息以旋转变换的形式注入 Q 和 K。
更关键的区别在于编码的内容不同:Sinusoidal 编码的是每个 token 的绝对位置,而 RoPE 编码的是两个 token 之间的相对距离——这使得模型在面对训练时未见过的长序列时,泛化能力更强。
实现上计算完Q,K后,先对Q,K做旋转位置编码,然后才进行计算。传入的输入X无需做任何处理。

RoPE 的数学原理涉及复数空间中的旋转矩阵,由于原文第五章才正式提及其原理,因此这里先放着,后续总结到的时候再回来写。
如果采用旋转编码的话,attention计算流程变为如下

5. Mask Self-Attention(掩码自注意力)
忘记了这东西,这里补上。由于是LLM的核心结构,因此单独一小节,不合在前面了。
掩码自注意力,即 Mask Self-Attention,是指使用注意力掩码的自注意力机制。掩码的作用是遮蔽一些特定位置的 token,模型在学习的过程中,会忽略掉被遮蔽的 token。
5.1 为什么需要掩码?
语言模型的训练目标是:给定一段文本序列,不断根据前面的 token 预测下一个 token。
例如,待学习的序列是
【BOS】 I like you 【EOS】,理想的训练过程是:
1 | Step 1:输入 【BOS】 → 预测 I |
这个过程有一个关键约束:每一步预测时,模型只能看到当前位置之前的 token,不能看到未来的内容。否则模型直接"抄答案",学不到任何语言规律。
然而上述过程是串行的——必须完成 Step 1 才能进行 Step 2。这与 Transformer 相较于 RNN 的核心优势——并行计算——直接冲突。如果每条训练语料都需要串行完成上述步骤,计算效率将极为低下。
5.2 掩码如何解决并行问题?
Masked Self-Attention 的核心思路是:用一个掩码矩阵,在一次前向传播中同时模拟所有步骤。
还是以 【BOS】 I like you 【EOS】
为例,构造如下并行输入:
1 | 样本 1:【BOS】 [M] [M] [M] [M] → 预测 I |
这四行可以同时并行地送入模型,每行只能看到未被遮蔽的 token,预测下一个位置——效果与串行完全等价,但只需一次前向传播。
5.3 掩码矩阵的形态
观察上述掩码规律,可以发现它本质上是一个下三角矩阵(对角线及以下为 1,以上为 0):
1 | 位置 【BOS】 I like you 【EOS】 |
其中 1 表示可以关注,0 表示被掩蔽。矩阵维度为
(seq_len, seq_len),实际使用时维度为
(1, seq_len, seq_len),通过广播机制适配同一 batch
中的所有样本。
5.4 掩码在注意力计算中的具体作用
还是以 【BOS】 I like you 【EOS】 为例,重点看
like 这个位置的计算过程。(理解为当前Q为like)
第一步:计算 QKᵀ 得分矩阵
每个 token 都有自己的 Q 向量和 K 向量(由输入向量乘以参数矩阵得到)。将所有 token 的 Q 与所有 token 的 K 做点积,得到一个 5×5 的得分矩阵,表示"每个 token 对其他所有 token 的关注程度":
1 | 【BOS】 I like you 【EOS】 |
此时 like 这一行的含义是:like 对 【BOS】的得分是
3.8,对 I 是 5.2,对自身是 6.4,对 you 是 4.9,对 【EOS】是
2.3。没有任何限制,like 能看到整个序列包括后面的 you 和
【EOS】。
第二步:施加掩码
将 like 行中,位置在 like
之后的得分全部替换为 -∞:
1 | 【BOS】 I like you 【EOS】 |
第三步:Softmax
对这一行做 Softmax,将得分转化为概率。-∞ 经过 Softmax
后变为精确的 0:
1 | 【BOS】 I like you 【EOS】 |
like 的注意力完全集中在
【BOS】、I、like 三个位置,you 和
【EOS】的权重精确为 0,不贡献任何信息。
5.5 小结
\(QK^T\) 的结果在图结构中,可以理解为一个有向图:序列中每个 token 作为一个节点,每个节点都向其他所有节点连出一条有向边,代表"我关注你的程度"。未加掩码时,这是一个全连接的有向图
加入掩码后,这张图发生了变化——每个节点只保留连向自己和之前节点的边,连向后续节点的边权重归零,相当于这些边被切断:

此时输出中每个token位置的向量也有了新的意义变化,在self-attention中,由于能够看到后面的词,因此每个token位置的输出向量实际上是浓缩了整个句子中所有token(包括自己)的分析。而在masked
self-attention中,还是以like这个token的位置举例,他仅能够与前面【BOS】、I和自己like
三个位置建立联系,因此此位置的token是分析了自己与前面所有token后,所产生的token,也意为下一token(虽然我们也还需要将他映射到词汇表中)
这一结构变化直接影响了每个 token 位置输出向量的语义含义:
普通 Self-Attention(无掩码,如 BERT)中,每个 token
可以看到序列中所有位置,因此其输出向量是对整个句子所有 token
信息的聚合——like 的输出向量里同时融合了
I、you
乃至整句的语境,代表的是"在完整句子中,like
所处位置的语义状态"。
Masked Self-Attention(如 GPT)中,以
like 为例,它只能与 【BOS】、I
和自身建立联系,因此其输出向量是对"到目前为止已出现的所有
token"的信息聚合,代表的是"在看过 【BOS】 和
I 之后,当前位置是 like 的语境状态"。
这个输出向量随后经过线性层映射到词表维度,再经 Softmax 转化为概率分布,才最终得到对下一个 token 的预测。输出向量本身不是下一个词,而是预测下一个词所依赖的上下文表示。

