Skip to content

第7章 · Attention 与 Transformer Block

因果性和 shape 测试不自动推出学习者掌握。

先修:第6章的 shape、normalization 和梯度对拍。

本章目标

  • 从 Q/K/V 推导 scaled dot-product attention、causal mask 和多头拆分。
  • 手写 pre-norm 残差、FFN、位置表示和最小 decoder-only Transformer Block。
  • 用因果性、head reshape、消融和参考实现对拍证明每个边界,而不是只看输出形状。

公式与 shape

单层 attention:

Attn(Q,K,V)=softmax(QKdk+M)V,

其中未来位置的 M-∞(或等价的布尔屏蔽)。典型 batch/head 形状如下:

对象shape说明
hidden X(B, T, D)D = H × d_head
Q/K/V(B, H, T, d_head)head 拆分后保持 token 顺序
score(B, H, T, T)第三个轴 query,第四个轴 key
mask(T, T) 或可广播形状位置 i 只能看到 j ≤ i
attention output(B, H, T, d_head)(B, T, D)concat 后过 output projection
block output(B, T, D)residual 不改变 shape

数学桥接:第2章 → 第7章

第2章 §4 的 dot_product(a, b) = Σ a[i]·b[i] 就是 attention score 的核心:QK 的每个元素都是两个向量的点积,衡量相似度。第2章的直觉"纯白和纯黑点积为 0(正交),纯白和纯红在中间"直接对应 attention:query 和 key 越相似,score 越大。

scoreij=qikjdk

除以 dk 是因为点积的绝对值随维度线性增长——如果不缩放,大维度下 softmax 的输入会非常大,梯度接近 0(饱和)。第2章 §6 的 `self_information(p) = -\log_2(p)$ 解释了为什么 softmax 后的权重可以看作"信息量加权":概率越低的 token,对其梯度的影响越大。

第2章 §8 的 `relu(x) = \max(0, x)$ 是 Transformer FFN 的基础激活函数(虽然 LLaMA 用 SwiGLU,但 ReLU 是最小理解单元)。

前端类比:attention = Array.map(query => keys.map(key => dot(query, key) / sqrt(d_k)))——每个 query 和所有 keys 做点积(CSS 颜色相似度),然后 softmax 归一化为权重,最后加权求和 V。

位置从哪里来:sinusoidal 与学习式位置编码

到目前为止的 attention 只依赖 token 内容:把输入序列打乱,QK 的数值多重集合不变(只有 causal mask 依赖下标)。但“狗咬人”和“人咬狗”不是同一句话,block 必须额外注入位置信号。两条经典路线:

1. Sinusoidal(正弦位置编码,Attention Is All You Need:零新增参数,把位置 p 编码成一组不同频率的正弦/余弦:

PE(p,2i)=sin(pωi),PE(p,2i+1)=cos(pωi),ωi=100002i/D.

低频通道随位置缓慢变化、高频通道快速振荡,合起来像位置的“多进制计数”。它有一条可直接验证的代数性质:同一间距的内积与绝对位置无关,

PE(p)PE(p+Δ)=icos(Δωi),

即位置内积天然携带“相距多远”的信息。仓库已有实现并被 python/tests/test_blocks_opt.py 覆盖,可直接对拍:

python
from llm_core.blocks import sinusoidal_positional_encoding

pe = sinusoidal_positional_encoding(seq_len=16, d_model=32)   # (16, 32)
d1 = [pe[p] @ pe[p + 1] for p in (0, 5, 10)]   # 间距 Δ=1,三个不同起点
d5 = [pe[p] @ pe[p + 5] for p in (0, 5, 10)]   # 间距 Δ=5
# 实测:d1 三个值均为 15.313649(spread < 2e-15),d5 均为 11.777383(spread 0)
# → 同距内积与起点无关;不同 Δ 给出不同值

2. 学习式位置编码(learned PE,GPT-2 路线):直接学一个位置表 EposRTmax×D,与 token embedding 相加(blocks.pyapply_token_and_position 就是这个加法)。好处是位置模式可以按任务学习;代价是上下文上限被 Tmax 写死,超长外推没有保证。

为什么第7章的 block 暂时不含它:本章要把 QKV、causal mask、residual 的契约隔离出来单独对拍;fixture 用互异 token,加入位置向量只会稀释 mask 写反、head 轴混淆这类故障的信号。但要明确记账:没有位置编码的模型对顺序不敏感(mask 除外)——这是刻意留白,不是遗漏。

衔接第9章:sinusoidal 的“内积只依赖间距”暗示了更彻底的做法——不把位置加进 embedding,而是直接旋转 Q/K,让相对位置成为 attention 的内建性质。那就是第9章的 RoPE。

交互观察

交互:因果 Attention 权重

用玩具向量演示「当前 token 该看谁」。真实模型里 Q/K/V 是学出来的。

the
32.6%
cat
33.6%
sat
33.8%

完整权重矩阵(行=query,列=key)

the
cat
sat
the
1.0
cat
0.5
0.5
sat
0.3
0.3
0.3

把 query 位置移到句子中间,观察未来列始终为零权重;再用 SoftmaxDemo 解释缩放和温度如何改变权重尖锐度。

交互:Softmax + Temperature

把 logits 看成「未归一化分数」。Temperature 越高分布越平,越低越尖。

tok0
8.5%
tok1
23.2%
tok2
5.2%
tok3
63.1%

从零实践

  1. 先实现单头 attention,输出 score、mask 后 score、softmax 权重和加权和,保存中间张量。

  2. D 拆成 H × d_head,用小随机矩阵对拍 head reshape 前后元素顺序。

  3. 组装 pre-norm x + Attention(Norm(x))x + FFN(Norm(x));明确激活和 projection shape。

  4. 运行当前入口:

    bash
    PYTHONPATH=python python -m pytest python/tests/test_attention_gpt.py -q
    PYTHONPATH=python python -m pytest python/tests/test_transformer_contract.py -q
    PYTHONPATH=python python -m pytest python/tests/test_attention_ablation.py -q

当前 attention.pyblocks.pygpt.pytransformer.py 已存在,attention_ablation.pytest_attention_ablation.py 补齐了 scale、mask、residual 消融和 future-token 扰动。固定合成 fixture 的实测值为:causal prefix future perturbation 0.0、去 mask 后 10.799219943491902,去 scale 的最大权重差 0.2811741490082953,去 residual 的最大输出差 1.7321348424395848。这些是本地 NumPy 契约证据;learner 的独立 clean-room 对拍仍是本章 gate。

L3 clean-room handoff

本章的目标不是在现有模块上补几行代码。关闭参考实现后,从 clean_room/transformer_block.py 的冻结接口重写 QKV projection、causal attention、head 合并、pre-norm residual 和 FFN;不得导入课程参考实现。第19章的行为 runner 会检查 token → logits → loss/update → generate 的完整主链,因此这里必须保留 shape 草图、一次 future-leak 或 head-axis 故障的失败前/修复后记录,以及与逐头参考的对拍。接口合同可先检查:

bash
pnpm clean-room:contract

故障注入与预期信号

本表是本章唯一的故障注入权威清单;「动手实验」一节不再另列第二份。

注入预期失败信号修复后证据
residual 分支 shape 或顺序错误block 输出维度错或训练信号断裂residual/Norm contract 和消融报告
忘记除以 dksoftmax 输出趋近 one-hot、梯度消失点积后立刻缩放,scale 消融对拍通过
mask 加 0 而不是负无穷未来位置概率非零、未来信息泄漏遮罩位置填极小负值再相加,future-token 扰动差回到 0
mask 上下三角写反当前 token 被未来位置主导因果遮罩取上三角为不可见区域,因果性测试通过
多头 reshape 时 head 与序列维混淆拼回 shape 报错或注意力加权错位head 维先移到 batch 维再做 matmul,reshape 对拍通过

论文与延伸

前端/Agent 迁移

attention 是受上下文窗口约束的动态路由:每个 query 选择哪些 key/value,类似组件依赖图中的加权读取,但它本身不保证可解释。Agent 的上下文组装应明确“可见消息集合”和“被禁止读取的未来/未授权状态”。

口述与自测(不看资料,5–10 分钟)

  • (B, T, D) 为起点,逐步口述 Q/K/V、score、mask、softmax、head concat 和 output projection 的 shape;说明 1/√d_k 的作用。
  • 结合 Attention Is All You Need,解释 causal mask、residual、normalization 和 FFN 分别解决什么;为什么”未来扰动不改变 prefix”是必要但不充分的实现证据。
  • 用第2章的 dot_product([1,2,3], [4,5,6]) = 32 解释 attention score 的几何含义:两个向量方向相同时点积最大,正交时为 0。
  • 说明为什么 1/√d_k 是必需的:如果 dk=64,两个随机向量的点积期望约为多少?不缩放时 softmax 的梯度会怎样?

实验与参考

动手实验

pytest 把缩放点积注意力的三个核心性质钉死:每行 softmax 权重之和为 1;改动未来位置 token 不影响当前位置输出;多头拆分再拼回 shape 一致、语义不变。

环境准备

bash
cd <仓库>
export PYTHONPATH="$PWD/python"

命令与预期输出

以下三条命令已在「从零实践」跑过(test_attention_gpt.pytest_transformer_contract.pytest_attention_ablation.py),预期输出的判定信号补充如下:

text
attention 行权重之和 ≈ 1.0(atol=1e-6)
替换未来位置 token,当前位 logits 差值 < 1e-6
多头 reshape/permute/contiguous 后拼回 shape == (B, T, n_embd)

概念图

图:第7章 Scaled Dot-Product Attention 全链路 — 从第2章的点积和 softmax,经第5章 embedding 和第6章线性投影,到本章的 Q/K/V → 缩放点积 → causal mask → softmax → 加权求和 → 多头 → 输出投影。后续第8章把 attention 封装为 Transformer Block 的子层,第9章用 KV Cache 避免重复计算,第11章把 attention 机制用于检索文档的语义匹配。

故障注入清单

故障注入以正文「故障注入与预期信号」一节为唯一权威清单(5 项,覆盖 residual、scale、两种 mask 写法和 head 轴混淆),此处不再另列第二份;做故障题时逐项对照该表的预期信号与修复后证据。

资源 / 成本 / 隐私

小 batch 的 NumPy attention/Transformer 在本地 CPU 即可,预计 gross cost 为 0;浏览器交互不发第三方请求。只使用随机或合成 token,禁止把真实对话放入 fixture。

Evidence

仓库当前机器证据(只读快照)

evidence/module-manifest-v1.json07.evidence 指向当前文件:evidence/07-runtime-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。

补充的本地消融实现也记录在 python/llm_core/attention_ablation.py 和对应测试中;这些补充文件不改变 manifest 的 canonical evidence 指向。

学习者提交模板(待填写,不是当前机器证据)

复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actualartifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。

yaml
schema: learn-llm.evidence.v1
module: 07-attention-transformer
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 5
commands:
  - PYTHONPATH=python python -m pytest python/tests/test_attention_gpt.py -q
  - PYTHONPATH=python python -m pytest python/tests/test_transformer_contract.py -q
  - PYTHONPATH=python python -m pytest python/tests/test_attention_ablation.py -q
metrics:
  - name: future_token_invariance
    expected: <versioned-tolerance>
    actual: <recorded-value>
  - name: reference_output_max_error
    expected: <versioned-tolerance>
    actual: <recorded-value>
  - name: no_mask_future_prefix_influence
    expected: >1e-5
    actual: <recorded-value>
  - name: scale_ablation_max_weight_delta
    expected: >0.01
    actual: <recorded-value>
  - name: residual_ablation_max_output_delta
    expected: >1e-5
    actual: <recorded-value>
artifacts:
  - <learner-repo-relative-artifact-path>
cost:
  gross_usd: 0
  credit_usd: 0
licenses:
  - source: <source>
    version: <version>
    license: <license>
    attribution: <attribution>
    redistribution: <redistribution>
known_failures:
  - <sanitized-failure-or-none>

只有输出中间张量、因果扰动、scale/mask/residual 消融和独立 clean-room 对拍都可追溯时,第7章才能离开 gate

私有学习站 · 原理从零构建 · 勿提交个人隐私或密钥