从零构建大模型(第三章)
3.3 无可训练权重的自注意力机制
🧠 核心思想:每个词都要“看”一遍所有词
想象你在读一句话:
“The journey of a thousand miles begins with a single step.”
当你读到 “journey” 这个词时,你并不是只看它本身,而是会联系 “The”、“of”、“miles”、“begins” 等所有词,理解整句话的意思。
自注意力机制就是让计算机也这么做:
👉 为每个词,根据它和句子中所有词的关系,重新生成一个“融合了上下文信息”的新向量,这个新向量就叫 上下文向量。
📦 这一节用到的“简化版自注意力”
真正的自注意力有可学习的“权重矩阵”(第3.4节才会讲)。
这一节先用最简单、没有任何可训练参数的版本,只演示计算过程。
输入是什么?
假设一句话被转换成了 6 个词的词嵌入(每个词是一个 3 维向量)。
例如:x⁽¹⁾ 是 “The” 的向量,x⁽²⁾ 是 “journey” 的向量……
我们要干什么?
以第二个词 x⁽²⁾ 为例,计算它的 上下文向量 z⁽²⁾。
🔧 三步计算法(以 x⁽²⁾ 为例)
1️⃣ 计算“注意力分数”
拿 x⁽²⁾ 作为 查询(Query)。
拿它和 所有词(包括自己)分别做 点积(dot product)。
得到 6 个分数:
attn_scores_2 = [score_with_x1, score_with_x2, score_with_x3, …]
分数越高,代表这两个词在当前语境下越相关。
💡 类比:你问“journey”和句子里的每个词“有多像”,用一个数字表示。
2️⃣ 把分数变成“权重”
用 Softmax 把 6 个分数归一化,使它们加起来 = 1。
得到 注意力权重:
attn_weights_2 = [0.14, 0.24, 0.23, 0.12, 0.11, 0.16]
权重越大,代表这个词对“journey”的贡献越大。
💡 类比:把“像的程度”转化为“给多少注意力”。
3️⃣ 加权求和 → 上下文向量
把每个词的原始向量 xⁱ 乘以对应的注意力权重。
再把所有结果相加。
得到 z⁽²⁾,这就是“journey”在整句话中的上下文向量。
🔁 把同样步骤应用到所有词
对 x⁽¹⁾(“The”)也做一遍,得到 z⁽¹⁾。
对 x⁽³⁾(“of”)也做一遍,得到 z⁽³⁾……
最终得到 6 个上下文向量,它们就包含了整句话的相互信息。
📊 关键输出:注意力权重矩阵
把所有词的注意力权重按顺序排成一个 6×6 的矩阵。
第 i 行就是计算第 i 个词时使用的 6 个权重。
🧩 总结
自注意力就是“每个词根据自己的查询,给所有词打分,然后加权求和,得到自己的新表示”。
📝 3.4 带可训练权重的自注意力机制
🎯 这一节在学什么?
上一节的简化版自注意力,我们用的是“原始向量直接点积”——这就像是没有学习能力的机械计算。而真正的GPT模型用的是一种可以学习的自注意力机制,叫做缩放点积注意力。
💡 概括:这一节就是在上一节的基础上,给自注意力装上“可训练的参数”,让模型能在训练过程中学会如何分配注意力。
对比表格
| 对比维度 | 3.3节(简化版) | 3.4节(真实版) |
|---|---|---|
| 是否可训练 | ❌ 无可训练参数 | ✅ 有可训练权重矩阵 |
| 输入处理 | 直接用原始向量计算 | 先通过权重矩阵“投影”再计算 |
| 核心公式 | 注意力分数 = xᵢ · xⱼ | 注意力分数 = (xᵢ·W_q) · (xⱼ·W_k)ᵀ / √d_k |
| 名称 | 简单自注意力 | 缩放点积注意力 |
🧩 三个“翻译官”:
W_q —— 查询权重矩阵(Query weights)
W_k —— 键权重矩阵(Key weights)
W_v —— 值权重矩阵(Value weights)
打个比方:
想象你在开会,桌上有一堆文件。你想找跟“项目进度”相关的内容:
W_q(你的“问题”):你心里有个问题——“项目进度在哪儿?”这个问题决定了你关注什么。
W_k(文件的“标签”):每份文件上有个标签,写着它的主题(“预算”“进度”“人员”)。
W_v(文件的“内容”):标签背后,文件里实际写着什么内容。
🔑 核心思想:通过训练这三个矩阵,模型学会如何提问(W_q)、如何打标签(W_k)、以及如何提取有价值的内容(W_v)。
查询(Query) 当前词元“问”其他词元的问题 搜索框里输入的关键词
键(Key) 每个词元用来被匹配的“标签” 数据库的索引字段
值(Value) 每个词元的实际内容 数据库中存储的数据
📐 三步计算法(完整版)
现在,让我们用“三个翻译官”重做一遍第3.3节的计算。
第1步:投影 → 生成 Q、K、V
对于每个输入向量 x⁽ⁱ⁾,我们通过三个矩阵把它“投影”成三个新向量:
q⁽ⁱ⁾ = x⁽ⁱ⁾ · W_q (查询向量)
k⁽ⁱ⁾ = x⁽ⁱ⁾ · W_k (键向量)
v⁽ⁱ⁾ = x⁽ⁱ⁾ · W_v (值向量)
💡 理解“投影”:就像用不同的滤镜看同一张照片——W_q让你看到“重点信息”,W_k让你看到“分类标签”,W_v让你看到“原始内容”。
第2步:计算注意力分数(带缩放)
当计算第 i 个词元对第 j 个词元的注意力时:
注意力分数 = (q⁽ⁱ⁾ · k⁽ʲ⁾) / √d_k
为什么要除以 √d_k(缩放)?
d_k 是键向量的维度(通常比较大,比如512或768)
如果不缩放,点积结果会非常大
大的分数经过Softmax后,会变得极度集中(接近one-hot),导致梯度消失,模型学不动
除以 √d_k 可以把数值拉回到一个合理的范围
🔑 这就是为什么叫 “缩放点积注意力” (Scaled Dot-Product Attention)。
第3步:Softmax归一化 → 注意力权重
注意力权重 = Softmax(注意力分数)
和上一节一样,让所有权重加起来等于1。
第4步:加权求和 → 上下文向量
z⁽ⁱ⁾ = Σ (注意力权重_ij × v⁽ʲ⁾)
注意:这里用的是 值向量 v,而不是原始输入 x!
假设输入:6个词元,每个是3维向量
inputs = torch.tensor([
[0.43, 0.15, 0.89], # x¹
[0.55, 0.87, 0.66], # x²
[0.57, 0.85, 0.64], # x³
[0.22, 0.58, 0.33], # x⁴
[0.77, 0.25, 0.10], # x⁵
[0.05, 0.80, 0.55], # x⁶
])
d_in = 3 # 输入维度
d_out = 2 # 输出维度(投影后的维度)
1️⃣ 定义三个可训练的权重矩阵
W_q = nn.Parameter(torch.rand(d_in, d_out))
W_k = nn.Parameter(torch.rand(d_in, d_out))
W_v = nn.Parameter(torch.rand(d_in, d_out))
2️⃣ 计算 Q、K、V
Q = inputs @ W_q # shape: (6, 2)
K = inputs @ W_k # shape: (6, 2)
V = inputs @ W_v # shape: (6, 2)
3️⃣ 计算注意力分数(带缩放)
attn_scores = Q @ K.T # shape: (6, 6)
d_k = K.shape[-1] # d_k = 2
attn_weights = F.softmax(attn_scores / d_k**0.5, dim=-1)
4️⃣ 计算上下文向量
context_vectors = attn_weights @ V # shape: (6, 2)
🧠 核心概念总结
可训练权重矩阵 W_q、W_k、W_v——模型通过学习它们来“学会”如何分配注意力
投影 把原始向量映射到一个新的空间,让模型能提取不同层面的信息
缩放 除以√d_k,防止点积过大导致Softmax梯度消失
Q/K/V的由来 借用数据库术语——查询、键、值,描述“提问-匹配-提取”的过程
与3.3节的关系 结构相同,但加了“投影”这一步,让注意力变得可学习
🎯 3.3节 vs 3.4节:
【3.3节】简化版
输入 x ──► 直接点积 ──► Softmax ──► 加权求和(用x) ──► 上下文向量
【3.4节】真实版
输入 x ──► W_q ──► Q ──┐
├──► W_k ──► K ──┼──► Q·Kᵀ/√d ──► Softmax ──► 加权求和(用V) ──► 上下文向量
└──► W_v ──► V ──┘
💡 为什么需要可训练的权重?
上一节的简化版自注意力是固定的——同样的输入永远算出同样的注意力分布。但在真实世界中,不同的任务、不同的语境下,“注意力”应该有不同的分配方式。
通过引入可训练的W_q、W_k、W_v,模型能够在训练过程中:
学会什么时候该关注哪里
学会从什么角度提取信息
学会如何表达上下文
这就像人类学习阅读——一开始你不知道哪些词重要,读多了你就知道该重点关注什么了。
📝 3.5 因果注意力——让模型“看不到未来”
🎯 核心思想:只能看过去,不能看未来
在上一节(3.4)中,我们实现的注意力机制是双向的——每个词元可以看到句子中所有的词元(包括它后面的词)。
但GPT这类自回归语言模型在生成文本时,是逐词生成的:
生成第1个词时,只知道输入(或什么都没有)
生成第2个词时,只知道第1个词
生成第3个词时,只知道第1、2个词
💡因果注意力就是给自注意力加一个“单向约束”——每个位置只能关注它自己和它之前的词元,不能看到未来的词元。
🔒 为什么叫“因果”?
“因果”这个名字很形象:
原因(过去) → 结果(现在)
未来的词元不应该影响现在的表示
这就像现实中,你不能用明天的新闻来写今天的报道
在机器学习领域,这也被称为掩码自注意力或自回归注意力。
🎭 如何实现“看不见未来”?
实现方式非常简单——用一个“遮罩”把未来位置的注意力分数遮住。
具体步骤
第1步: 像3.4节一样,先计算完整的注意力分数矩阵
attn_scores = Q @ K.T / √d_k # shape: (seq_len, seq_len)
第2步: 创建一个上三角遮罩矩阵
对于序列长度=6的情况,遮罩矩阵长这样:
位置: 0 1 2 3 4 5
0: 0 -∞ -∞ -∞ -∞ -∞
1: 0 0 -∞ -∞ -∞ -∞
2: 0 0 0 -∞ -∞ -∞
3: 0 0 0 0 -∞ -∞
4: 0 0 0 0 0 -∞
5: 0 0 0 0 0 0
🔑 关键:遮罩矩阵中,未来位置填 负无穷大,当前及之前位置填0。
第3步: 把遮罩加到注意力分数上
加上0的位置 → 分数不变
加上负无穷大的位置 → 分数变成负无穷大
第4步: Softmax归一化
当某个位置的分数是 负无穷大时,Softmax 计算出来的结果就是 0。
所以未来位置的注意力权重就被“清零”了——模型完全看不到它们!
假设我们已经有了 attn_scores: (batch, seq_len, seq_len)
📊 效果演示
假设输入序列有6个词元,遮罩前后的注意力权重矩阵对比:
不加遮罩(3.4节的双向注意力):
第0行: [0.14, 0.16, 0.18, 0.15, 0.19, 0.18] # 看到了所有6个词
第1行: [0.15, 0.17, 0.16, 0.18, 0.17, 0.17] # 看到了所有6个词
加上因果遮罩后:
第0行: [1.00, 0.00, 0.00, 0.00, 0.00, 0.00] # 只能看自己
第1行: [0.45, 0.55, 0.00, 0.00, 0.00, 0.00] # 只能看位置0和1
第2行: [0.30, 0.35, 0.35, 0.00, 0.00, 0.00] # 只能看位置0、1、2
…
第5行: [0.14, 0.16, 0.18, 0.15, 0.19, 0.18] # 能看到所有词(最后一位置)
🔑 注意:最后一个位置可以看到所有词,因为它是“当前”位置,没有未来。
🧠 与训练过程的关联
在训练GPT模型时,因果注意力发挥着关键作用:
训练阶段:
我们一次性输入整句话
但通过因果遮罩,模型在预测每个位置时,只能看到前面的词
这让我们可以并行计算所有位置的损失,而不需要逐个位置循环
推理阶段:
生成文本时,每次只输入已生成的词
不需要遮罩(因为输入本身就不包含未来词)
🆚 3.4节 vs 3.5节:对比总结
| 对比维度 | 3.4 节(标准自注意力) | 3.5 节(因果注意力) |
|---|---|---|
| 可见范围 | 所有位置(包括未来) | 只看当前及之前位置 |
| 矩阵形状 | 全连接 | 下三角(权重矩阵) |
| 实现方式 | 直接计算 | 加遮罩 + Softmax |
| 适用模型 | BERT、编码器 | GPT、解码器 |
| 训练方式 | 双向上下文 | 单向(自回归) |
| 能否并行 | 完全并行 | 并行(但遮罩约束) |
🧩 关键点速记
| 问题 | 答案 |
|---|---|
| 为什么需要因果注意力? | GPT逐词生成时不能看到未来词 |
| 怎么实现的? | 用上三角遮罩(-∞)遮住未来位置 |
| 遮罩加在哪里? | Softmax之前,加到注意力分数上 |
| 遮罩后Softmax会怎样? | -∞位置 → 权重=0 |
| 训练时用遮罩吗? | ✅ 用,可以并行计算所有位置 |
| 推理时用遮罩吗? | ❌ 不用,输入本身不含未来词 |
关键说明
- 上三角遮罩:矩阵的上三角部分(含对角线以外)被设为负无穷(-∞),确保模型仅关注当前位置及历史信息。
- Softmax效果:数学上,e−∞=0e^{-\infty}=0e−∞=0,因此遮罩位置的注意力权重被强制归零。
- 训练与推理差异:训练时使用遮罩实现并行化;推理时自回归生成已自然屏蔽未来词,无需额外操作。
💡 总结一句话
因果注意力 = 标准自注意力 + 上三角遮罩,让每个位置只能关注它自己和之前的词元——这是GPT能逐词生成文本的核心机制。
📝 3.6 多头注意力
🎯 核心思想
用多组独立的 W_q, W_k, W_v 并行计算注意力,每组称为一个“头”。每个头在独立的子空间里关注不同的特征,最后拼接输出,让模型从多个角度理解输入。
⚙️ 实现方式
方式1(直观):堆叠 h 个单头,各自计算后拼接。
方式2(高效):将 h 个头的权重拼成一个大矩阵,一次矩阵乘法算出所有头的 Q, K, V,再拆分、并行计算。
📐 形状变化(以高效实现为例)
假设 batch=2, seq=4, d_model=8, h=2, d_k=4
输入 (2,4,8)
乘以大矩阵 W_q_all(形状 8×8)得到 (2,4,8)
view(2,4,2,4) → transpose(1,2) → (2,2,4,4)(batch, heads, seq, d_k)
并行计算注意力,输出 (2,2,4,4)
transpose + view → (2,4,8) 拼接完成
💡 为什么有效?
多视角:不同头能关注句法、语义、长距离依赖等不同模式。
参数共享:通过拆分特征维度,总参数量与单头相同,但表达能力更强。
高效并行:合并计算充分利用 GPU 矩阵运算。
🔑 总结
多头注意力 = 多组单头并行计算,在独立子空间里捕捉不同关系,最后融合,是 Transformer 强大表达能力的基石。
3.7 小结
注意力机制可以将输入元素转换为增强的上下文向量表示,这些表示涵盖了关于所有输
入的信息。
自注意力机制通过对输入进行加权求和来计算上下文向量表示。
在简化的注意力机制中,注意力权重是通过点积计算得出的。
点积是两个向量的元素逐个相乘并将这些乘积相加的一种简洁计算方法。
尽管矩阵乘法不是必需的,但它可以通过替代嵌套的 for 循环使计算更高效、更紧凑。
在用于大语言模型的自注意力机制(也被称为“缩放点积注意力”)中,我们引入了可训
练的权重矩阵来计算输入的中间变换:查询矩阵、值矩阵和键矩阵。
在处理从左到右读取和生成文本的大语言模型时,我们会添加一个因果注意力掩码,以
防止模型访问未来的词元。
除了使用因果注意力掩码将注意力权重置 0,还可以添加 dropout 掩码来减少大语言模型
中的过拟合。
基于 Transformer 的大语言模型中的注意力模块涉及多个因果注意力实例,这被称为“多
头注意力”。
可以通过堆叠多个因果注意力模块实例来创建多头注意力模块。
创建多头注意力模块的一种更高效的方法是使用批量矩阵乘法
更多推荐

所有评论(0)