01 / 本课学习路线
本课学习路线
阅读与推演约 120 分钟,练习约 80 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
先确认自己能否完成打分、掩码、归一化和加权计算,再用下面的先修自测检查基础。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 写出 Q、K、V 到输出的维度变化 (n,d)→(n,n)→(n,d),说清每个下标从哪里开始 | 第 03 节 | 自查第 3 条 |
| 按「打分 → 掩码 → softmax → 加权求和」手算一个 3×2 的例子,每个中间值都能复算 | 第 05 节 | 练习 1 |
| 不查看参考代码写出因果注意力完整程序并通过(AC)AI006 | 第 06 节 | 必做任务 2 |
| 写对第 g 头的列区间与缩放 √hd,处理整行被屏蔽的行,通过 AI031 | 第 07 节 | 必做任务 3 |
| 说出四种错误做法各错在哪一步、错误输出是什么 | 第 08 节 | 练习 2 至 4 |
先修自测:下面 5 题请先自己写答案,再展开对照。任何一题答不出,按括号里的位置补看 10 分钟再回来——这些能力在本课每一步都会用到。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | A 是 2×3 矩阵,B 是 3×2 矩阵,A·B 是几行几列?B·A 呢? | 模块 6 · 第 1 课(矩阵乘法与维度核对) |
| 自测 2 | K 是 n 行 d 列的矩阵,它的转置 Kᵀ 是几行几列?Q·Kᵀ 的第 i 行第 j 列等于什么? | 模块 6 · 第 1 课 |
| 自测 3 | 手算 softmax([1, 2]),保留四位小数。 | 模块 6 · 第 2 课(浮点与稳定 softmax) |
| 自测 4 | 一行输入是 1.5 2 -0.25,用 Python 把它读成三个浮点数的列表。 | 模块 1 · 第 1 课(标准输入输出) |
| 自测 5 | √2 约等于多少(四位小数)?Python 里怎么算? | 本节答案(math.sqrt);模块 6 · 第 2 课 只讲 softmax,不讲开方 |
补看入口(本站内,可直接点开):矩阵乘法与维度核对(矩阵乘法与转置)、浮点与稳定 softmax、标准输入输出与首次独立提交(读入实数)。
展开先修自测答案
自测 1:A·B 是 2×2(左矩阵的行数 × 右矩阵的列数);B·A 是 3×3。两个乘积不相等,矩阵乘法不满足交换律。
自测 2:Kᵀ 是 d 行 n 列。Q·Kᵀ 的第 i 行第 j 列 = Q 的第 i 行与 K 的第 j 行的点积,也就是 Σ Q[i][c]·K[j][c],c 从 0 到 d−1。本课把这个数叫「第 i 个词元对第 j 个词元的分数」。
自测 3:先减最大值 2 得 [−1, 0],指数为 [0.3679, 1.0000],和为 1.3679,权重 = [0.2689, 0.7311]。不减最大值直接算指数 [2.7183, 7.3891] 除以和 10.1073,结果相同;减最大值只是为了防止大数溢出,不改变结果。
自测 4:[float(x) for x in input().split()],得到 [1.5, 2.0, -0.25]。本课的参考程序用 sys.stdin.read().split() 一次读完全部数字再按位置切,两种写法都可以。
自测 5:√2 ≈ 1.4142;math.sqrt(2),需要先 import math。
03 / 概念与符号
注意力解决什么问题,Q、K、V 各是什么
模块 6 · 第 3 课 的 k 近邻用「距离」决定谁对预测有影响;注意力换成一种可训练的写法:每个位置的输出,是所有位置的值向量按「匹配程度」加权平均的结果,权重由内容本身算出来。
先看要解决的问题。一个长度为 n 的序列(比如一句话里的 n 个词元)中,第 i 个位置想从其它位置「取信息」,但不同位置的重要程度不同:与当前位置匹配的应当权重大,不相关的权重小,且所有权重加起来等于 1,这样输出仍与 V 同一量级。注意力就是「算一组和为 1 的权重,再用它们对 V 加权平均」这件事的标准做法。
| 符号 | 含义 | 形状 / 取值 | 下标约定 |
|---|---|---|---|
| n | 序列长度,词元个数 | AI006 中 1≤n≤80;AI031 中 1≤n≤40 | 位置 i、j 从 0 数到 n−1 |
| d | 每个向量的维度 | 1≤d≤16 | 分量 c 从 0 数到 d−1 |
| Q | 查询矩阵,第 i 行 Q[i] 是第 i 个位置「想找什么」 | n 行 d 列,题目直接给出 | Q[i][c] |
| K | 键矩阵,第 j 行 K[j] 是第 j 个位置「能被匹配的特征」 | n 行 d 列 | K[j][c] |
| V | 值矩阵,第 j 行 V[j] 是第 j 个位置「被取走的内容」 | n 行 d 列 | V[j][c] |
| S | 分数矩阵,S[i][j] = Q[i]·K[j] / √d(单头;多头时每个头各有一张,只用自己的列) | n 行 n 列 | 行是查询位置 i,列是被看的位置 j |
| mask | 掩码(mask),标记 i 能否看到 j | AI031 中 n×n 的 0/1;AI006 由「j ≤ i」隐含给出 | AI031:1 表示可见,0 表示屏蔽 |
| W | 权重矩阵,每行是可见分数的 softmax,行和为 1(单头;多头时每个头各有一张) | n 行 n 列(不可见处为 0) | W[i][j] |
| O | 输出矩阵,O[i] = Σ W[i][j]·V[j] | n 行 d 列,与 V 同形状 | O[i][c] |
| h、hd | 多头数与每头维度,hd = d/h | AI031 保证 h 整除 d | 第 g 头取列 [g·hd, (g+1)·hd),g 从 0 数到 h−1 |
题目里 Q、K、V 都是直接给出的数字矩阵。真实模型里它们由输入向量乘以三个可训练矩阵得到,本课不涉及那一步(第 07 节末尾的补充学习有一段说明)。
掩码的 1 和 0 没有通用含义
AI031 规定 mask[i][j]=1 表示位置 i 可以看到位置 j。别的题可能规定 1 表示屏蔽。动手前用题目样例核对一次:AI031 样例第二行掩码是 0 1,对应的输出第二行等于 V 的第二行,说明它只看到了 j=1——这与「1 表示可见」一致。
维度变化是自查第 3 条的内容,请在纸上写一遍:Q、K、V 各是 (n,d);S = Q·Kᵀ/√d 是 (n,n);W 与 S 同形状 (n,n);O = W·V 是 (n,d)。检查方法:矩阵乘法要求左矩阵列数等于右矩阵行数——Q (n,d) 乘 Kᵀ (d,n) 合法,W (n,n) 乘 V (n,d) 合法。
04 / 从直觉到公式
为什么用点积、为什么除以 √d、为什么先掩码后 softmax
四步公式里每一步都有原因。下面用能手算的小数字逐条说明,再给出规则。
第一步,用点积衡量匹配程度。q=(2,1) 与 k=(1,3) 的点积是 2×1 + 1×3 = 5;与 k′=(1,0) 的点积是 2。点积越大,两个向量方向越接近(在长度相近时),所以把它当作「i 想找的」与「j 能提供的」之间的匹配分数。整张分数表就是 Q·Kᵀ:第 i 行第 j 列是 Q[i]·K[j]。
第二步,除以 √d。点积是 d 个乘积的和,d 越大,分数的量级通常越大,softmax 之后权重会过度集中到最大分数上。除以 √d 是缓和这种增长的约定:当 q、k 的各分量相互独立、均值为 0 且各维方差相同(通常按单位方差建模)时,Σ q_c·k_c 的方差正比于 d,除以 √d 后方差就不再随 d 变化;它不是对任意输入的保证——q=k=全 1 向量时,d=1、4、16 的缩放后分数分别是 1、2、4,仍随 d 增长。用第 05 节的 3×2 小例(d=2)对比:第 3 行不缩放时权重为 [0.2119, 0.2119, 0.5761],除以 √2 后为 [0.2483, 0.2483, 0.5035]——缩放让分布更平缓。题目明确写了缩放因子,按题目写:AI006 用 √d,AI031 每个头用 √hd。
第三步,softmax 把一行分数变成和为 1 的权重:每个分数取指数,再除以本行指数之和。指数保证权重为正,且分数大的权重大。实现时先减本行最大值:Python 的指数函数(math.exp)算到 math.exp(710) 直接溢出抛出 OverflowError(math.exp(709) 已约 8.2×10³⁰⁷)。会不会碰到这条线,看的是传给指数函数的分数——也就是除以 √d(或 √hd)之后的值:AI006 缩放后的分数可达 4×10⁶,必然溢出;AI031 缩放后最多 256,不会溢出(第 08 节的表给了两题的上界)。负的大分数不会上溢,只会下溢成 0。减最大值后每个指数都在 (0, 1] 之间,结果与不减时在数学上相同(分子分母同乘一个常数)。浮点上,比最大值小很多的分数(AI006 里差值可达 4×10⁶)取指数会下溢成 0,对应权重就是 0——与数学上「接近 0」的结果一致,不影响答案。
第四步的顺序规则:先按掩码筛出可见集合,只在可见集合上做 softmax
softmax 的分母是「参与归一的分数」的指数之和。如果先对整行做 softmax、再把不可见位置的权重置 0,剩下的权重之和小于 1,加权平均就不再是平均——第 05 节会算出这个错误输出。正确做法只有一种:先由掩码决定第 i 行能看的集合,只对这个集合里的分数做 softmax。常见写法「把不可见位置置成 −1e9 再对整行 softmax」在集合非空时与之等价,但整行被屏蔽时会得到均匀分布(两个 −1e9 归一后各 0.5),而 AI031 规定这一行输出全 0——所以先判空,再决定是否执行 softmax。
四步规则(第 i 行)
1. 可见集合 vis = { j : 第 i 行能看到 j } AI006:j ≤ i AI031:mask[i][j] = 1
2. 若 vis 为空 → 本行输出全 0,结束(AI031 规定)
3. 分数 s_j = Q[i]·K[j] / 缩放因子,j ∈ vis AI006:√d AI031:每头 √hd
4. m = vis 中最大分数;w_j = e^(s_j − m) / Σ e^(s_k − m),k ∈ vis
5. 输出 O[i] = Σ w_j · V[j],j ∈ vis05 / 完整手算例
同一组输入走完四步:全量注意力,然后加因果掩码
n=3、d=2,Q=K=[[1,0],[0,1],[1,1]],V=[[1,2],[3,4],[5,6]],缩放因子 √2≈1.4142。所有中间值保留四位小数显示,但计算时保留完整精度(用第 06 节的检查脚本可以逐项复现)。
| 行 i | Q[i]·K[0] | Q[i]·K[1] | Q[i]·K[2] | 除以 √2 后的 S[i] |
|---|---|---|---|---|
| 0 | 1×1+0×0 = 1 | 1×0+0×1 = 0 | 1×1+0×1 = 1 | [0.7071, 0.0000, 0.7071] |
| 1 | 0 | 1 | 1 | [0.0000, 0.7071, 0.7071] |
| 2 | 1 | 1 | 2 | [0.7071, 0.7071, 1.4142] |
行下标从 0 开始,与程序一致。第 2 行的 Q[2]·K[2] = 1×1+1×1 = 2 是整张表的最大分数。
| 行 i | 本行最大值 m | e^(s−m) 三项 | 三项之和 | 权重 W[i] |
|---|---|---|---|---|
| 0 | 0.7071 | [1.0000, 0.4931, 1.0000] | 2.4931 | [0.4011, 0.1978, 0.4011] |
| 1 | 0.7071 | [0.4931, 1.0000, 1.0000] | 2.4931 | [0.1978, 0.4011, 0.4011] |
| 2 | 1.4142 | [0.4931, 0.4931, 1.0000] | 1.9861 | [0.2483, 0.2483, 0.5035] |
e^(0−0.7071) = e^(−0.7071) ≈ 0.4931。每行权重之和为 1;把表里四位小数的权重直接相加,第 2 行会得到 1.0001(0.2483 + 0.2483 + 0.5035),这是显示舍入,不是计算误差。
| 行 i | 第 0 列的加权和 | 第 1 列的加权和 | 输出 O[i] |
|---|---|---|---|
| 0 | 0.4011×1 + 0.1978×3 + 0.4011×5 | 0.4011×2 + 0.1978×4 + 0.4011×6 | [3.0000, 4.0000] |
| 1 | 0.1978×1 + 0.4011×3 + 0.4011×5 | 0.1978×2 + 0.4011×4 + 0.4011×6 | [3.4067, 4.4067] |
| 2 | 0.2483×1 + 0.2483×3 + 0.5035×5 | 0.2483×2 + 0.2483×4 + 0.5035×6 | [3.5105, 4.5105] |
第 0 行两端权重相等(0.4011),V[0] 与 V[2] 的平均正好是 V[1]=[3,4],所以输出恰好是 [3, 4]。第 2 行分数最高的自身位置得到权重 0.5035,但其余位置的权重仍大于 0——softmax 不是「只取最大」。
| 行 | 打分 S(除以 √2 后) | softmax 权重 | 输出 |
|---|---|---|---|
| 1 | [0.7071, 0.0000, 0.7071] | [0.4011, 0.1978, 0.4011] | [3.0000, 4.0000] |
| 2 | [0.0000, 0.7071, 0.7071] | [0.1978, 0.4011, 0.4011] | [3.4067, 4.4067] |
| 3 | [0.7071, 0.7071, 1.4142] | [0.2483, 0.2483, 0.5035] | [3.5105, 4.5105] |
三张分步表合成一张汇总表(这里的行号从 1 数起,与上面从 0 数起的行 i 相差 1)。复习时先遮住后两列,自己从分数列推出权重和输出,再对照。
现在加因果掩码(AI006 的规则:第 i 行只能看 j ≤ i)。公式一个字不变,变的只是每行参与 softmax 的集合。
| 行 i | 可见集合 | 可见分数 | 权重 | 输出 O[i] |
|---|---|---|---|---|
| 0 | {0} | [0.7071] | [1.0000] | [1.0000, 2.0000] = V[0] |
| 1 | {0, 1} | [0.0000, 0.7071] | [0.3302, 0.6698] | [2.3395, 3.3395] |
| 2 | {0, 1, 2} | [0.7071, 0.7071, 1.4142] | [0.2483, 0.2483, 0.5035] | [3.5105, 4.5105] |
第 1 行:m=0.7071,指数 [0.4931, 1.0000],和 1.4931,权重 [0.3302, 0.6698];输出第 0 列 = 0.3302×1 + 0.6698×3 = 2.3395。第 2 行能看到全部位置,与全量结果相同。
| 行 i | 保留下来的权重 | 权重之和 | 错误输出 | 正确输出 |
|---|---|---|---|---|
| 1 | [0.1978, 0.4011](来自全量的第 1 行) | 0.5989 | [1.4011, 2.0000] | [2.3395, 3.3395] |
权重之和 0.5989 不等于 1,输出被整体缩小。如果在置 0 之后再除以 0.5989 重新归一,结果与正确做法相同——这说明「先掩码再 softmax」和「整行 softmax 后重新归一」在数学上等价,但前者少一步、也不会在整行被屏蔽时出问题。
06 / 从步骤到程序
把四步规则写成代码:核心片段与完整参考程序
先对照步骤表看核心循环里每个变量对应第 04 节规则的哪一行,再自己写完整程序;两份完整参考程序在展开区里,写完再展开对照。
| 规则 | 代码里的名字 | 说明 |
|---|---|---|
| 可见集合 vis | visible(AI006 是 range(0, i + 1),AI031 是列表推导) | 决定本行哪些 j 参与后面三步 |
| 缩放因子 | scale = math.sqrt(d) 或 math.sqrt(hd) | 循环外算一次 |
| 分数 s_j | scores 列表,与 visible 等长、顺序一致 | 只算可见的 j |
| 最大值 m 与权重 w_j | softmax_row(scores) | 减最大值 → 指数 → 除以和 |
| 输出 O[i] | row[p] += w * V[j][p] | zip(weights, visible) 保证权重和位置一一对应 |
核心片段(不是完整程序):注意力核心循环,可见集合函数 visible(i) 由掩码含义决定
Pythonscale = math.sqrt(d)
out = []
for i in range(n):
vis = visible(i) # 掩码:哪些 j 可见,以题目要求为准(因果 = 只看 j <= i)
row = [0.0] * d
if not vis: # 整行被屏蔽:按题目要求输出全 0(AI031),不执行 softmax
out.append(row)
continue
scores = [dot(Q[i], K[j]) / scale for j in vis]
mx = max(scores) # 稳定 softmax:先减本行(可见集合内)最大值
ws = [math.exp(s - mx) for s in scores]
total = sum(ws)
for j, w in zip(vis, ws):
w /= total # 归一成权重
for p in range(d):
row[p] += w * V[j][p] # 按权重加权求和 V
out.append(row)这段代码依赖外部的 dot、visible、已读入的 Q/K/V/n/d,不能直接提交。它只展示四步规则如何落到循环里。完整程序还要做三件事:读入并按 n、d 切分矩阵,判断可见集合,按题目精度格式化输出。
检查脚本:复现第 05 节全量注意力的每一个中间值
Pythonimport math
Q = [[1, 0], [0, 1], [1, 1]]
K = [[1, 0], [0, 1], [1, 1]]
V = [[1, 2], [3, 4], [5, 6]]
d = 2
# 固定例子的预期输出(与第 05 节表格一致)。改了 Q、K、V 之后:把它换成你手算的结果,或设为 None 只看打印不核对
EXPECTED = [[3.0, 4.0], [3.4067, 4.4067], [3.5105, 4.5105]]
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
for i in range(3):
scores = [dot(Q[i], K[j]) / math.sqrt(d) for j in range(3)]
m = max(scores)
exps = [math.exp(s - m) for s in scores]
total = sum(exps)
weights = [e / total for e in exps]
out = [sum(w * V[j][c] for w, j in zip(weights, range(3))) for c in range(2)]
print(i + 1, "分数", [round(s, 4) for s in scores], "最大值", round(m, 4), "指数", [round(e, 4) for e in exps], "和", round(total, 4))
print(" 权重", [round(w, 4) for w in weights], "输出", [round(o, 4) for o in out])
assert abs(sum(weights) - 1.0) < 1e-9 # 通用检查:权重和为 1,任何输入都成立
if EXPECTED is not None:
assert [round(o, 4) for o in out] == EXPECTED[i] # 固定例子核对;改了数据就更新 EXPECTED
# 期望输出(固定例子):
# 1 分数 [0.7071, 0.0, 0.7071] 最大值 0.7071 指数 [1.0, 0.4931, 1.0] 和 2.4931
# 权重 [0.4011, 0.1978, 0.4011] 输出 [3.0, 4.0]
# 2 分数 [0.0, 0.7071, 0.7071] 最大值 0.7071 指数 [0.4931, 1.0, 1.0] 和 2.4931
# 权重 [0.1978, 0.4011, 0.4011] 输出 [3.4067, 4.4067]
# 3 分数 [0.7071, 0.7071, 1.4142] 最大值 1.4142 指数 [0.4931, 0.4931, 1.0] 和 1.9861
# 权重 [0.2483, 0.2483, 0.5035] 输出 [3.5105, 4.5105]把它保存成文件运行,输出应与注释里的六行完全一致:每行先打印分数、本行最大值、指数和指数之和,再打印权重与输出,与第 05 节三张表逐格对应。想改 Q、K、V 得到自己的新例子(第 09 节的练习 1 就是这样出的):把 EXPECTED 换成你手算的输出,或设为 None 只看打印;「权重和为 1」那条检查对任何输入都成立,不用改。
展开完整参考程序 1:AI006 因果注意力(先自己写完并提交一次,再展开对照)
完整程序:AI006(标准输入 → 标准输出)
Pythonimport math
import sys
def read_matrix(tokens, start, n, d):
"""从 tokens[start] 起连续读 n 行、每行 d 个实数,返回矩阵和新的读取位置。"""
rows = []
pos = start
for _ in range(n):
rows.append([float(x) for x in tokens[pos:pos + d]])
pos += d
return rows, pos
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def softmax_row(scores):
"""数值稳定的 softmax:先减本行最大值,再取指数并归一化。"""
mx = max(scores)
exps = [math.exp(s - mx) for s in scores]
total = sum(exps)
return [e / total for e in exps]
def causal_attention(Q, K, V, n, d):
scale = math.sqrt(d)
out = []
for i in range(n):
visible = range(0, i + 1) # 因果:第 i 行只看 j <= i
scores = [dot(Q[i], K[j]) / scale for j in visible]
weights = softmax_row(scores)
row = [0.0] * d
for w, j in zip(weights, visible):
for p in range(d):
row[p] += w * V[j][p]
out.append(row)
return out
def fmt4(x):
"""保留四位小数;绝对值小于 0.00005 时输出 0.0000(避免 -0.0000)。"""
if abs(x) < 0.00005:
return "0.0000"
return f"{x:.4f}"
def main():
tokens = sys.stdin.read().split()
n, d = int(tokens[0]), int(tokens[1])
Q, pos = read_matrix(tokens, 2, n, d)
K, pos = read_matrix(tokens, pos, n, d)
V, pos = read_matrix(tokens, pos, n, d)
for row in causal_attention(Q, K, V, n, d):
print(" ".join(fmt4(x) for x in row))
main()逐段对应:read_matrix 完成读入与切分;causal_attention 是第 04 节的四步规则;fmt4 实现「四位小数、绝对值小于 0.00005 输出 0.0000」。运行后先用第 09 节练习 5 的样例(n=3、d=1)核对:输出应为 1.0000、1.5000、99.9911。
读代码时请回答两个问题:① visible = range(0, i + 1) 为什么不是 range(0, i)?——因为 j ≤ i 包含自己,而 range 的右端不含。② 为什么 fmt4 不用 round?——round(-0.00001, 4) 得到 -0.0,格式化后是 -0.0000,题目要求输出 0.0000。
展开完整参考程序 2:AI031 多头掩码注意力(先做完第 07 节再展开)
完整程序:AI031(标准输入 → 标准输出)
Pythonimport math
import sys
def read_matrix(tokens, start, n, d, cast=float):
rows = []
pos = start
for _ in range(n):
rows.append([cast(x) for x in tokens[pos:pos + d]])
pos += d
return rows, pos
def softmax_row(scores):
mx = max(scores)
exps = [math.exp(s - mx) for s in scores]
total = sum(exps)
return [e / total for e in exps]
def multi_head_attention(Q, K, V, mask, n, d, h):
hd = d // h
scale = math.sqrt(hd) # 每个头按自己的维度 hd 缩放
out = [[0.0] * d for _ in range(n)]
for i in range(n):
visible = [j for j in range(n) if mask[i][j] == 1]
if not visible: # 整行被屏蔽:输出全 0,不做 softmax
continue
for g in range(h):
lo, hi = g * hd, (g + 1) * hd # 第 g 头的列区间 [lo, hi)
scores = []
for j in visible:
s = sum(Q[i][c] * K[j][c] for c in range(lo, hi)) / scale
scores.append(s)
weights = softmax_row(scores)
for w, j in zip(weights, visible):
for c in range(lo, hi):
out[i][c] += w * V[j][c]
return out
def fmt2(x):
s = f"{x:.2f}"
return "0.00" if s == "-0.00" else s
def main():
tokens = sys.stdin.read().split()
n, d, h = int(tokens[0]), int(tokens[1]), int(tokens[2])
Q, pos = read_matrix(tokens, 3, n, d)
K, pos = read_matrix(tokens, pos, n, d)
V, pos = read_matrix(tokens, pos, n, d)
mask, pos = read_matrix(tokens, pos, n, n, cast=int)
for row in multi_head_attention(Q, K, V, mask, n, d, h):
print(" ".join(fmt2(x) for x in row))
main()与程序 1 的差别只有三处:可见集合来自掩码矩阵并判空;外层多一个头循环 for g in range(h),每头只用列区间 [lo, hi);缩放用 √hd。用题目样例核对:输出应为 1.34 0.66 2.00 2.00 / 0.00 2.00 4.00 0.00(第 07 节的拼接表)。
out[i][c] += w * V[j][c] 只写入本头的列区间,各头的结果自然「按原顺序拼接」——不需要额外的拼接步骤。整行被屏蔽时 continue 跳过,该行保持初始化的全 0。
07 / 多头拆分与拼接
第 g 头取哪些列、按什么缩放、怎么拼回去
多头注意力把 d 维按列切成 h 段,每段 hd=d/h 维,各头独立完成四步,再按头的顺序拼回 d 维。AI031 直接给出 Q、K、V,考查的就是拆分与拼接的下标。
列区间的规则:第 g 头(g 从 0 数到 h−1)取列 [g·hd, (g+1)·hd),左闭右开。d=4、h=2 时 hd=2:头 0 取列 0、1,头 1 取列 2、3。缩放因子是本头的维度 √hd,不是总维度 √d——因为每个头的点积只累加 hd 个乘积。同一行的掩码对所有头相同。
| 行 i · 头 g | 列区间 | 可见集合 | 可见分数(除以 √2 后) | 权重 | 本头输出 |
|---|---|---|---|---|---|
| 0 · 头 0 | [0, 2) | {0, 1} | [0.7071, 0.0000] | [0.6698, 0.3302] | [1.3395, 0.6605] |
| 0 · 头 1 | [2, 4) | {0, 1} | [0.3536, 0.3536] | [0.5000, 0.5000] | [2.0000, 2.0000] |
| 1 · 头 0 | [0, 2) | {1} | [0.7071] | [1.0000] | [0.0000, 2.0000] |
| 1 · 头 1 | [2, 4) | {1} | [0.3536] | [1.0000] | [4.0000, 0.0000] |
样例输入:Q=[[1,0,0.5,0.5],[0,1,0.5,0.5]],K=[[1,0,1,0],[0,1,0,1]],V=[[2,0,0,4],[0,2,4,0]],掩码 [[1,1],[0,1]]。行 0 头 0 的分数:Q[0] 前两列 (1,0) 与 K[0] 前两列 (1,0) 点积 1,与 K[1] 前两列 (0,1) 点积 0,各除以 √2。行 0 头 1 的分数:(0.5,0.5)·(1,0)=0.5 与 (0.5,0.5)·(0,1)=0.5,除以 √2 得 0.3536,两个相等所以权重各 0.5。
拼接:按头的顺序把本头输出放回各自列区间
行 0 = 头 0 的 [1.3395, 0.6605] ‖ 头 1 的 [2.0000, 2.0000] → 保留两位 → 1.34 0.66 2.00 2.00 行 1 = 头 0 的 [0.0000, 2.0000] ‖ 头 1 的 [4.0000, 0.0000] → 0.00 2.00 4.00 0.00 与题目样例输出逐位一致。
整行被屏蔽的行
如果某一行掩码全为 0,可见集合为空:没有分数可做 softmax(程序里 max([]) 会抛出 ValueError,用 −1e9 填充则得到均匀分布)。AI031 规定这一行输出全 0,所以必须在打分之前判空。这条规则只对 AI031 成立——AI006 的因果集合至少包含自己,永远非空。
补充学习(选学)多头注意力如何关注不同的信息约 5 分钟不同头可以注意不同对象;真实模型先投影再拆头
两个头打分不同,注意的对象就可以不同——不同头可以学到不同的关系模式(比如语法、指代),但具体语义并不保证稳定可解释。真实模型不是机械拆原始向量:先经可学习矩阵投影出 Q/K/V,再把投影后的维度拆头。例如把 4096 维拆成 32 个 128 维的头——头数与维度取决于架构,机制和本课的小例一致。
AI031 直接给出 Q/K/V,要求按注意力头拆分列并按原顺序拼接输出,考查的是拆分与拼接的下标处理:第 g 头取列 [g·hd, (g+1)·hd),输出按 g=0…h−1 原序拼回。
补充学习(选学)Transformer 主要模块概览约 4 分钟注意力、前馈网络与残差连接如何组成一层
一个 Transformer 层通常包含注意力模块和前馈网络(FFN)两部分:注意力模块计算词元之间的关系(本课算的这张表),FFN 分别处理每个词元;两个模块都使用残差连接。由于点积不包含位置信息,还需要位置编码:加法式的绝对位置嵌入在进入第一层前加到输入上,RoPE(旋转位置编码)则在各注意力层里对 Q、K 做旋转(模块 7 · 第 3 课 会完整计算一遍,模块 7 · 阶段测验中也会考查)。重复堆叠这样的层就构成模型主体。
08 / 边界、反例与复杂度
四种错误做法各输出什么:三种用 AI031 样例,一种用样例的掩码变式
下面每一行都是一个具体的错误实现。前三行的输入是 AI031 的样例(第 07 节那组数);第四行必须把样例第 0 行的掩码改成 0 0 才能暴露错误——原样例没有整行被屏蔽的行。你可以把参考程序改成对应写法自己验证。
| 错误做法 | 错在哪一步 | 错误输出(第 0 行) | 正确输出(第 0 行) |
|---|---|---|---|
| 缩放用 √d=2 而不是 √hd=√2 | 第 3 步分数量级偏小,权重偏平 | 1.24 0.76 2.00 2.00 | 1.34 0.66 2.00 2.00 |
| 把掩码 1 当成屏蔽、0 当成可见 | 第 1 步可见集合取反:行 0 变成整行被屏蔽、行 1 只看 j=0 | 0.00 0.00 0.00 0.00(第 1 行变成 2.00 0.00 0.00 4.00) | 1.34 0.66 2.00 2.00 |
| 头 1 的列区间写成 [1, 3) | 第 g 头起始列算错:列 1 被两头各算一次、列 3 从未写入 | 1.34 1.49 1.65 0.00(第 1 行 0.00 4.00 4.00 0.00) | 1.34 0.66 2.00 2.00 |
| 整行被屏蔽时不判空,用 −1e9 填充(把样例第 0 行掩码改成 0 0 再测) | 第 2 步:空集合上 softmax 得均匀分布 | 1.00 1.00 2.00 2.00(两行 V 的平均) | 0.00 0.00 0.00 0.00 |
第一行的错误输出计算:头 0 分数 [1/2, 0/2]=[0.5, 0],权重 [0.6225, 0.3775],输出 0.6225×[2,0] + 0.3775×[0,2] = [1.2449, 0.7551]。最后一行的错误在题目样例上看不出来——样例没有整行被屏蔽的行,错误程序照样输出 1.34 0.66 2.00 2.00;必须自拟一个含全 0 行的输入才能暴露它。
| 情况 | AI006 的规则 | AI031 的规则 |
|---|---|---|
| 输出精度 | 四位小数 | 两位小数 |
| 负零 | 绝对值小于 0.00005 时输出 0.0000(判断在格式化之前) | 格式化后得到 -0.00 时改为 0.00 |
| 溢出 | |元素| ≤ 1000、d ≤ 16 时点积可达 1.6×10⁷,除以 √16 后分数可达 4×10⁶,不减最大值必然溢出 | |分量| ≤ 8、hd ≤ 16 时每头点积最大 8×8×16 = 1024,除以 √16 后分数最大 256,e^256 ≈ 1.5×10¹¹¹ 仍在双精度范围内——AI031 不减最大值也不会溢出;减最大值是零成本的统一写法,两题都按它写 |
| 空集合 | 不会出现(j ≤ i 至少含自己) | 可能出现,输出全 0 |
复杂度:每行对每个可见位置做一次 d 维点积,再做一次 d 维加权,总量是 O(n²·d);多头版本是 O(h·n²·hd) = O(n²·d)。按完整矩阵估算,AI006 打分阶段不超过 80×80×16 ≈ 1.0×10⁵ 次乘加,AI031 不超过 40×40×16 ≈ 2.6×10⁴ 次;加权阶段另有同量级运算。空间 O(n·d) 存三个矩阵和输出,加 O(n²) 存掩码。避免在内层重复构造矩阵或计算缩放系数;耗时还取决于实现与评测环境,提交前需在目标语言核对。
09 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。答案给出计算过程或代码,以及做错时最可能错在哪一步。
练习 1(跟做):仍用第 05 节的 Q=K=[[1,0],[0,1],[1,1]],把 V 换成 [[1,0],[0,1],[2,2]],计算全量注意力(不带掩码)第 0 行的输出。提示:权重不变,只有第四步的加权和要重算。
展开练习 1 答案
Q、K 没变,第 0 行权重仍是 [0.4011, 0.1978, 0.4011]。第 0 列:0.4011×1 + 0.1978×0 + 0.4011×2 = 1.2033;第 1 列:0.4011×0 + 0.1978×1 + 0.4011×2 = 1.0000。输出 [1.2033, 1.0000]。
常见错误:把权重也重算了——权重只取决于 Q 和 K;或者把 V 按列而不是按行取值——V[j] 是第 j 行。
练习 2(改一个条件):AI031 样例的掩码改成全 1(两行都能看到 j=0 和 j=1),其它不变。计算第 1 行的输出,保留两位小数。
展开练习 2 答案
第 1 行头 0:Q[1] 前两列 (0,1) 与 K[0] 前两列 (1,0) 点积 0,与 K[1] 前两列 (0,1) 点积 1;除以 √2 得 [0, 0.7071];权重 [0.3302, 0.6698];输出 0.3302×[2,0] + 0.6698×[0,2] = [0.6605, 1.3395]。头 1:(0.5,0.5) 与 (1,0)、(0,1) 的点积都是 0.5,权重各 0.5,输出 0.5×[0,4] + 0.5×[4,0] = [2, 2]。拼接:0.66 1.34 2.00 2.00。
对照:原样例第 1 行只看 j=1,输出是 0.00 2.00 4.00 0.00;放开掩码后第 1 行的头 0 与第 0 行的头 0 恰好对称(权重互换)。如果你算出 1.34 0.66,说明把 K[0]、K[1] 的顺序弄反了。
练习 3(改一个条件):AI031 样例改成 h=1(单头,hd=4,缩放 √4=2),掩码不变。计算第 0 行的输出,直接保留两位小数。不要先保留四位再取两位:两次舍入可能与一次舍入不同,例如 1.23496 直接取两位是 1.23,先取四位得 1.2350 再取两位就成了 1.24。
展开练习 3 答案
单头时用全部 4 列:Q[0]·K[0] = 1×1 + 0×0 + 0.5×1 + 0.5×0 = 1.5,Q[0]·K[1] = 0 + 0 + 0 + 0.5 = 0.5;除以 2 得 [0.75, 0.25];权重 [0.6225, 0.3775];输出 0.6225×[2,0,0,4] + 0.3775×[0,2,4,0],精确值约为 1.2449、0.7551、1.5102、2.4898,直接保留两位:1.24 0.76 1.51 2.49。
这题与第 08 节第一行的错误做法对比:前两列的数值相同(因为那种错误做法在头 0 上恰好等价于用 2 缩放),后两列不同——说明头数会真实改变结果,h=1 与 h=2 不是同一道题。
练习 4(独立实现):写出 AI031 需要的两个辅助函数并通过断言——visible_from_mask(row) 由一行 0/1 掩码返回可见位置列表;fmt2(x) 保留两位小数并把 -0.00 归一为 0.00。
练习 4 的断言(待填写练习,把 ... 换成你的实现)
Pythondef visible_from_mask(row):
# 待完成:row 是一行 0/1 掩码,返回可见的 j 列表(升序);1 表示可见
...
def fmt2(x):
# 待完成:保留两位小数,"-0.00" 归一为 "0.00"
...
assert visible_from_mask([1, 1]) == [0, 1]
assert visible_from_mask([0, 1]) == [1]
assert visible_from_mask([0, 0]) == []
assert fmt2(1.339) == "1.34" and fmt2(-0.004) == "0.00" and fmt2(-0.006) == "-0.01"展开练习 4 答案
参考实现
Pythondef visible_from_mask(row):
return [j for j, m in enumerate(row) if m == 1]
def fmt2(x):
s = f"{x:.2f}"
return "0.00" if s == "-0.00" else sfmt2(-0.006) 必须得到 -0.01,不能把所有负数小值都改成 0.00——只有格式化结果恰好是 -0.00 时才归一。
练习 5(迁移):AI006 的第二组样例是 n=3、d=1,Q=[1,1,1],K=[0,0,10],V=[1,2,100],期望输出 1.0000、1.5000、99.9911。不运行程序,解释第 2 行(下标 2)为什么是 99.9911 而不是 100.0000,并算出它的三个权重。
展开练习 5 答案
d=1 时缩放因子 √1=1。第 2 行可见 {0,1,2},分数 [0, 0, 10]。减最大值 10 得 [−10, −10, 0],指数 [4.54×10⁻⁵, 4.54×10⁻⁵, 1],和 1.0000908,权重 [0.0000454, 0.0000454, 0.9999092]。输出 = 0.0000454×1 + 0.0000454×2 + 0.9999092×100 = 99.9911。前两个位置的权重虽小但不为 0,所以输出略小于 100。
第 1 行(下标 1)可见 {0,1},分数 [0, 0],权重各 0.5,输出 (1+2)/2 = 1.5;第 0 行只看自己,输出 V[0]=1。这组样例同时验证了「后面的位置不影响前面的输出」:K[2]=10 再大也不会进入第 0、1 行的可见集合。
10 / 读题要求、题解入口与复习自评
两道题的输入输出对照,以及完成本课之后怎么复习
提交前逐项核对输入布局、掩码规则与输出精度;具体要求以题目页为准。
| 项目 | AI006 因果注意力计算 | AI031 多头掩码注意力 |
|---|---|---|
| 第一行 | n d | n d h(h 整除 d) |
| 随后 | n 行 Q、n 行 K、n 行 V,每行 d 个实数 | n 行 Q、n 行 K、n 行 V(每行 d 个实数,最多两位小数),再 n 行、每行 n 个 0/1 的掩码 |
| 可见规则 | 第 i 个位置只看编号不大于 i 的位置 | mask[i][j]=1 可见、0 屏蔽;所有头共用同一份掩码 |
| 缩放 | √d | 每头 √hd |
| 整行被屏蔽 | 不会出现 | 该行输出全 0 |
| 输出 | n 行、每行 d 个数,四位小数,绝对值小于 0.00005 输出 0.0000 | n 行、每行 d 个数,两位小数,单个空格分隔,-0.00 输出 0.00 |
| 数据范围 | 1≤n≤80,1≤d≤16,|元素|≤1000 | 1≤n≤40,1≤d≤16,|分量|≤8 |
| 时限 | 3 秒 | 4 秒 |
| 舍入保证 | 输出不落在四位小数的舍入中点 | 用语言标准两位小数格式化即可,无需自行实现舍入 |
参考程序与题解:需要对照解法时,展开第 06 节的两份完整参考程序(AI006、AI031)。AI006 的题目页另有思路与 Python / Java / C++ 参考代码。
复习与自评:「学习完成检查」六条是自评,勾选表示你认为自己能做到,它不会改变题目的通过(AC)状态。本课记为完成的条件:必做题 AI006、AI031 都通过,并勾选全部六条学习完成检查;复习题不计入完成状态。复习时用下面三个问题自测:① 不看正文,写出第 04 节的五行规则;② 不看表格,重算第 05 节因果掩码下第 1 行的权重 [0.3302, 0.6698];③ 说出 √hd 与 √d 在 AI031 样例上分别给出什么输出(第 08 节第一行)。答不出哪一条,就回到对应的节重读,再做第 09 节对应的练习。
11 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道
softmax_row),再做 AI006。断言只能提前发现溢出(第二条断言给了 1000 的分数);掩码与 softmax 的顺序错误断言查不出来,要用第 05 节「因果掩码下的重算」那组数自测:第 1 行应得 [2.3395, 3.3395],得到 [1.4011, 2.0000] 就是顺序错了。AI031 在 AI006 全部正确之后做,再处理多头拆列、逐头计算、全屏蔽行和按序拼接。参考程序在第 06 节的展开区里:先自己写完并提交一次,再展开对照。稳定 softmax 函数(softmax_row)
必做任务 1自主练习练习重点:先减本行最大值再取指数、归一化;较大分数不上溢;预计用时:10 分钟
完成标准:能说出不减最大值时指数函数(exp)在哪个量级溢出
需要时查看提示
m = max(row),每个分量算 exp(x - m) 再除以总和。第二条断言给了 1000 的分数——不减最大值时这一行会抛出溢出错误(OverflowError)。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
import math
def softmax_row(row):
# 待完成:每项减去本行最大值后计算指数并归一化
...
got = softmax_row([0.7071, 0.0, 0.7071])
assert [round(x, 4) for x in got] == [0.4011, 0.1978, 0.4011]
# 较大输入不上溢:不减最大值时 math.exp(1000) 会抛出 OverflowError
assert abs(sum(softmax_row([1000.0, 999.0])) - 1.0) < 1e-9AI006 · 因果注意力计算
必做任务 2练习重点:按 n、d 读入 Q/K/V,逐行做打分 → 掩码 → softmax → 加权求和;预计用时:35 分钟
完成标准:能解释第 i 行为什么只用 j ≤ i 的列,以及稳定 softmax 防的是什么
需要时查看提示
分数除以 √d 不要漏;权重先减本行最大分再取指数;输出保留四位小数,绝对值小于 0.00005 时写 0.0000,避免出现 -0.0000。超过 15 分钟仍未通过时,展开第 06 节的完整参考程序 1 逐段对照。
AI031 · 多头掩码注意力
必做任务 3练习重点:拆列、每头独立四步、按序拼接;掩码 1=可见,同一行的掩码应用于该行的所有注意力头;预计用时:35 分钟
完成标准:能写对第 g 头的列区间,并说出全屏蔽行输出什么
需要时查看提示
缩放因子是 √hd(每头维度)不是 √d;mask[i][j]=1 表示可见、所有头共用;整行被屏蔽时该行输出全 0——在 softmax 之前判空,不要用 −1e9 填充后直接计算(会得到均匀分布);保留两位小数,-0.00 归一成 0.00。第 08 节的错误做法表可以对照你的错误输出。
提交结果
提交结果说明与处理方法
- WA
答案错误
四查:掩码含义(AI031 是 1=可见)、掩码是否在 softmax 之前、缩放是 √d 还是 √hd、多头列区间——第 08 节的表给出了每种错误在样例上的输出
- PE
格式错误
AI006 四位小数、AI031 两位小数——两题精度不同;-0.0000/-0.00 都要归一成正零
- RE
运行错误
读入严格按 n、d、h 数个数;全屏蔽行不判空会在空列表上取最大值或除以 0
- TLE
超时
核对复杂度是否为 O(n²d),并检查是否在每行重复构造完整的 K 矩阵
- AC
通过
用 n=1 的单词元输入再测两次:mask[0][0]=1 时输出等于 V 的第 1 行;mask[0][0]=0 时整行被屏蔽,输出必须是全 0
12 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。