一字一诗
LESSON 10 / 26

注意力:汇总前文的信息

这一课完成什么

这一课先把“注意力”当成一项明确的计算:根据分数,给前面各个位置分配权重,再按权重汇总它们携带的信息。权重是一组非负数,总和为 1。它们会随当前输入和训练得到的参数而变化。这里的名字并不意味着程序有人的意识。

注意力,是一次有权重的信息汇总

不是把某个字简单挑出来。模型先算各位置的参考权重,再把各位置带来的信息按这些权重加起来。

注意力,是一次有权重的信息汇总
用文字逐步读这张图
  1. 当前正在读的字:提出本次查询;称为 Q
  2. 各位置的线索:用来与查询比较;称为 K
  3. 算相似分数:Q 与 K 做点积;再做缩放
  4. 变成参考权重:softmax 后和为 1;高权重多参考
  5. 各位置的信息:将要被汇总的数字;称为 V
  6. 加权相加:权重 × V,再求和;得到新的表示
注意力权重高,就证明这个字是语义原因吗?

不能直接这样解释。权重描述这层这组计算怎样混合信息,不是完整的因果解释。

假设当前已经读到“春江花”,三个位置携带的两项信息分别是 [1,0][0,2][2,2]。这些小向量是为了手算而设的示例,不给每一维强行命名为“季节”或“情绪”。真实模型中的表示由训练学出,通常无法给每一维贴上清晰的人工标签。

一次具体的汇总

假设三个位置的匹配分数为 [2,1,0]。先减去最大分数 2,得到 [0,-1,-2],再分别取指数,得到大约 [1,0.368,0.135]。除以总和 1.503,得到 [0.665,0.245,0.090]。这就是第 6 课介绍过的 softmax 运算。

text
汇总的第一项 = 0.665 × 1 + 0.245 × 0 + 0.090 × 2 = 0.845
汇总的第二项 = 0.665 × 0 + 0.245 × 2 + 0.090 × 2 = 0.670

这个结果保留了三个位置的信息,贡献比例不同。与“只选择分数最高的位置”相比,加权汇总允许多处信息共同参与。注意力权重也不等于完整的因果解释:后面还有多层加工,不能只凭某个格子较亮就断言模型为什么写出了某个字。

演示中的滑块用于观察分数分布变尖或变平时,汇总如何变化。它是本页的教学控制;正式模型注意力使用代码中的缩放公式,生成时的 temperature 则控制最后选字的分布,二者作用位置不同。

动手计算

bash
./poet lesson 10
.venv/bin/python lessons/10/experiment.py

本课完整计算只有三个关键步骤:

python
scores = torch.tensor([2., 1., 0.])
weights = scores.softmax(-1)
values = torch.tensor([[1., 0.], [0., 2.], [2., 2.]])
summary = weights @ values

@ 表示矩阵乘法。左侧有 3 项权重,右侧有 3 行、每行 2 项信息,所以结果只有 2 项。输出应接近 [0.845,0.670],小数末尾会受显示精度影响。把全部分数改为 0,三个位置的权重都会成为三分之一,结果接近 [1.000,1.333]

从一个位置走向一整句

真实 GPT 会为每一个预测位置各算一组权重。若句子有 3 个字,就有一个 3 行、3 列的分数表:行表示“当前是谁在汇总”,列表示“从谁那里取信息”。第 11 课会把分数来源展开,第 12 课会限制哪些位置可以参与,第 13 课会同时做多组这样的汇总。

先记住本节的边界:我们暂时假定分数已经给定,还没有学习怎样生成分数。这样可以把“分配贡献比例”和“计算匹配程度”分开理解。

验收与排错

小练习与答案

把分数改为 [0,0,0],先手算再运行。再把第一项变为 10,判断输出会靠近哪个向量。

查看答案

分数相同意味着三个权重都为 1/3,汇总是 [1,4/3]。第一项远高于其他项时,权重集中到第一个位置,输出会靠近 [1,0],但有限分数下其他位置权重仍可能大于 0。可以直接修改本课 experiment.py 的 scores,然后重新运行。

对应正式模型

poetry_gpt/model.pyCausalAttention.forward 中,weights @ v 就是本课的汇总运算。使用 inspect=True 可以拿到权重。正常训练走 PyTorch 的优化计算路径,意义一致;项目的测试会检查两条路径的结果。

动手补全一小段

先运行上面的完整实验,再复制本课起始文件为自己的练习。starter 有意留空 solve 函数;补全后运行文件,底部检查会告诉你是否符合本课要求。卡住时打开参考答案,比较每一步。

bash
cp lessons/10/starter.py lessons/10/my_exercise.py
# 编辑 my_exercise.py 中的 solve 函数,然后运行:
.venv/bin/python lessons/10/my_exercise.py
# 对照完整答案:
.venv/bin/python lessons/10/solution.py

下载起始代码 · 下载参考答案

展开本课补全练习的完整参考答案
python
"""第 10 课补全练习:按权重汇总。修改 solve,保持下方检查不变。"""
import math, io, argparse
import torch
from torch import nn
from torch.nn import functional as F
torch.set_num_threads(2)
torch.manual_seed(26)

def solve(scores, values):
    return scores.softmax(dim=-1) @ values

y=solve(torch.zeros(3),torch.tensor([[1.,0.],[0.,2.],[2.,2.]]))
torch.testing.assert_close(y,torch.tensor([1.,4/3]))
print("本课补全练习通过。")
展开本课完整、可独立运行的实验代码
python
"""本课独立实验;在仓库根目录执行 .venv/bin/python lessons/10/experiment.py。"""
import sys, json, math
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import torch
from torch import nn
from torch.nn import functional as F
from poetry_gpt.common import ROOT, DEFAULT_DATA, read_json, read_jsonl
from poetry_gpt.model import ModelConfig, PoetryGPT, CausalAttention, Block
from poetry_gpt.data import Tokenizer, SPECIAL, clean_record, keywords_for
from poetry_gpt.labs import show
torch.set_num_threads(2)
torch.manual_seed(26)

scores = torch.tensor([2.0, 1.0, 0.0])
weights = scores.softmax(-1)
values = torch.tensor([[1.0, 0.0], [0.0, 2.0], [2.0, 2.0]])
show('分数', scores.tolist())
show('权重', weights.tolist())
show('权重之和', weights.sum().item())
show('汇总后的信息', (weights @ values).tolist())
下载本课实验