﻿---
title: "Transformer 是怎么训练的：手算一层注意力的梯度下降"
description: "训练时屏幕上打出来的 loss 0.615、0.594、0.572……每一个数背后发生了什么？我们先用一个只有一个旋钮的机器，把损失、导数、梯度下降和学习率讲清楚；再打开一个只有 10 个参数的 Transformer，任务是“说出两个字里的第一个字”，把一步训练里的每个数都摊开算：前向怎样从输入算到损失，反向传播怎样从损失往回算出 10 个偏导数，更新之后为什么有的样本变好、有的变差，几十步里注意力怎样转向正确的位置，学习率太大时模型为什么会卡死；最后看真实的大模型多了什么，以及现实中找最优解那么难，为什么在 Transformer 里却这么顺。"
pubDate: 2026-10-26
tags: ["AI", "Transformer", "梯度下降", "入门"]
hero: "./hero.svg"
clc: "TP183"
refs:
  - id: vaswani
    title: "Attention Is All You Need"
    url: https://arxiv.org/abs/1706.03762
    author: Ashish Vaswani 等
    site: NeurIPS 2017
    note: "Transformer 的原始论文。注意力打分除以 √d_k 的做法见第 3.2.1 节。"
  - id: backprop
    title: "Learning representations by back-propagating errors"
    url: https://doi.org/10.1038/323533a0
    author: David E. Rumelhart, Geoffrey E. Hinton, Ronald J. Williams
    site: Nature 323
    date: "1986"
  - id: induction
    title: "In-context Learning and Induction Heads"
    url: https://transformer-circuits.pub/2022/in-context-learning-and-induction-heads/index.html
    author: Catherine Olsson 等
    site: Transformer Circuits Thread
    date: "2022"
    note: "训练好的 Transformer 里有专门“看前一个词元”的注意力头（previous token head），它和后面的归纳头配合完成复制。"
  - id: gpt3
    title: "Language Models are Few-Shot Learners"
    url: https://arxiv.org/abs/2005.14165
    author: Tom B. Brown 等
    site: NeurIPS 2020
    note: "表 2.1：175B 模型 96 层、d_model 12288、96 个头、每头 128 维，批大小 320 万词元，学习率 0.6×10⁻⁴，共训练 3000 亿词元。附录 B：Adam（β₁ = 0.9，β₂ = 0.95），梯度全局范数裁剪到 1.0，前 3.75 亿词元线性预热，之后 2600 亿词元内余弦衰减到 10%。表 D.1：训练总计算量 3.14×10²³ 次浮点运算；论文同时训练了从 1.25 亿到 1750 亿参数的 8 个模型。"
  - id: adam
    title: "Adam: A Method for Stochastic Optimization"
    url: https://arxiv.org/abs/1412.6980
    author: Diederik P. Kingma, Jimmy Ba
    site: ICLR 2015
  - id: notes
    title: "本文的实验脚本与日志"
    note: "项目共享目录 `gradient-descent-research/`：`experiment/tiny.py` 是模型和手写的反向传播，`walk.py` 打印 BA 一步的全部中间值，`batch.py` 是四个样本的梯度和多步轨迹，`torch_check.py`、`train_torch.py` 用 PyTorch 自动求导对账，`build_figs.py` 生成文中的图和表。"
  - id: micrograd
    title: "karpathy/micrograd"
    url: https://github.com/karpathy/micrograd
    author: Andrej Karpathy
    site: GitHub
    note: "一百来行 Python 的自动求导引擎，适合接着本文自己动手写一遍反向传播。"
    further: true
---

在[《Transformer 和 FPGA 都是电路》](/blog/transformer-vs-fpga/)里，我们用一句话概括过模型是怎么训练的：先算出“错误”对每个参数的坡度，再朝下坡的方向挪一小步。这句话没错，但它跳过了所有细节。真正训练一个模型时，屏幕上看到的是这样一串数：

```text
step  0  loss 0.615
step  1  loss 0.594
step  2  loss 0.572
step  3  loss 0.548
step 10  loss 0.336
step 20  loss 0.076
step 40  loss 0.030
```

step 是第几步，loss 叫作损失，可以先理解成“模型现在错得有多厉害”。它一步步变小，说明模型越来越对。可是 0.615 是怎么算出来的？电脑在每一步里到底做了什么，才让它变成 0.594？为什么前十步降得慢，第十步到第二十步又降得快？

这串数来自一个真的 Transformer，只不过它小到只有 10 个参数。小到这个程度，每一个中间结果都写得下。不过在打开它之前，我们先用一个更小的东西，把“训练”这件事本身弄清楚。

## 一、热身：一个旋钮

### 一台只有一个旋钮的机器

设想一台机器，输入一个数 $x$，输出 $y = w \cdot x$。$w$ 是机器上的一个旋钮，可以拧到任何读数。我们希望输入 2 的时候，输出是 6。

你当然一眼就看出 $w$ 应该是 3。但假装我们不知道，只能从某个读数开始，比如 $w = 1$，一点点去拧。神经网络的训练就是这件事，只不过旋钮有成千上亿个，没有人能一眼看出答案。

先要有一个数，说明“现在差多远”。$w = 1$ 时输出是 2，目标是 6，差了 −4。差值可正可负，我们把它平方，得到一个永远不小于 0 的数，叫作**损失**：

$$
L = (2w - 6)^2
$$

$w = 1$ 时 $L = 16$；拧对了，$w = 3$，$L = 0$。训练的目标就是把 $L$ 拧小。

### 导数告诉我们往哪边拧

$L$ 是 $w$ 的二次函数，画出来是一条开口向上的抛物线，谷底在 $w = 3$（图 1）。站在 $w = 1$ 这一点，该往左拧还是往右拧？看脚下的坡就知道了。

坡度就是**导数**。$L = (2w-6)^2$ 的导数是 $L' = 2(2w - 6) \times 2 = 8w - 24$，$w = 1$ 时等于 −16。负数表示往右是下坡：$w$ 调大一点，$L$ 会变小，而且 $w$ 每增加一点点，$L$ 大约减少这一点点的 16 倍。

于是规则很简单：**导数是负的就把 $w$ 调大，是正的就调小，调多少和导数成正比**。写成式子：

$$
w_{\text{新}} = w - \eta \times L'(w)
$$

$\eta$ 读作“伊塔”，叫作**学习率**，管的是一步迈多大。取 $\eta = 0.05$：$w$ 从 1 变成 $1 - 0.05 \times (-16) = 1.8$，损失从 16 降到 5.76。再算一次导数，$8 \times 1.8 - 24 = -9.6$，于是 $w$ 变成 2.28，损失 2.07。这样一步步走下去，$w$ 依次是 2.568、2.741、2.845……越来越接近 3。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-warm-title gd-warm-desc">
<title id="gd-warm-title">一个旋钮的梯度下降</title>
<desc id="gd-warm-desc">损失 L = (2w − 6)² 是一条开口向上的抛物线，谷底在 w = 3。从 w = 1 出发，每一步把 w 减去 0.05 乘以当前的斜率，依次走到 1.8、2.28、2.568……一步比一步小，逐渐靠近谷底。</desc>
<defs>
<marker id="gd-warm-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-warm-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="360" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="328" rx="8"/>
<text class="t-zone" x="32" y="44">一个旋钮 · 输入 2，希望输出 6，损失 L = (2w − 6)²</text>
<line class="dd-hair" x1="96" y1="292" x2="744" y2="292"/>
<text class="t-note" x="84" y="296" text-anchor="end">0</text>
<line class="dd-hair" x1="96" y1="182" x2="744" y2="182"/>
<text class="t-note" x="84" y="186" text-anchor="end">10</text>
<line class="dd-hair" x1="96" y1="72" x2="744" y2="72"/>
<text class="t-note" x="84" y="76" text-anchor="end">20</text>
<text class="t-note" x="96" y="312" text-anchor="middle">0</text>
<text class="t-note" x="204" y="312" text-anchor="middle">1</text>
<text class="t-note" x="312" y="312" text-anchor="middle">2</text>
<text class="t-note" x="420" y="312" text-anchor="middle">3</text>
<text class="t-note" x="528" y="312" text-anchor="middle">4</text>
<text class="t-note" x="636" y="312" text-anchor="middle">5</text>
<text class="t-note" x="744" y="312" text-anchor="middle">6</text>
<text class="t-note" x="420" y="334" text-anchor="middle">旋钮的读数 w</text>
<path class="dd-edge" style="stroke-width:2" d="M180.2 75.2 L183.5 81.0 L186.7 86.7 L190.0 92.4 L193.2 98.0 L196.4 103.5 L199.7 108.9 L202.9 114.2 L206.2 119.5 L209.4 124.7 L212.6 129.8 L215.9 134.8 L219.1 139.8 L222.4 144.6 L225.6 149.4 L228.8 154.2 L232.1 158.8 L235.3 163.3 L238.6 167.8 L241.8 172.2 L245.0 176.5 L248.3 180.8 L251.5 184.9 L254.8 189.0 L258.0 193.0 L261.2 196.9 L264.5 200.8 L267.7 204.5 L271.0 208.2 L274.2 211.8 L277.4 215.3 L280.7 218.8 L283.9 222.1 L287.2 225.4 L290.4 228.6 L293.6 231.8 L296.9 234.8 L300.1 237.8 L303.4 240.7 L306.6 243.5 L309.8 246.2 L313.1 248.9 L316.3 251.4 L319.6 253.9 L322.8 256.4 L326.0 258.7 L329.3 261.0 L332.5 263.1 L335.8 265.2 L339.0 267.2 L342.2 269.2 L345.5 271.1 L348.7 272.8 L352.0 274.5 L355.2 276.2 L358.4 277.7 L361.7 279.2 L364.9 280.6 L368.2 281.9 L371.4 283.1 L374.6 284.2 L377.9 285.3 L381.1 286.3 L384.4 287.2 L387.6 288.0 L390.8 288.8 L394.1 289.5 L397.3 290.1 L400.6 290.6 L403.8 291.0 L407.0 291.4 L410.3 291.6 L413.5 291.8 L416.8 292.0 L420.0 292.0 L423.2 292.0 L426.5 291.8 L429.7 291.6 L433.0 291.4 L436.2 291.0 L439.4 290.6 L442.7 290.1 L445.9 289.5 L449.2 288.8 L452.4 288.0 L455.6 287.2 L458.9 286.3 L462.1 285.3 L465.4 284.2 L468.6 283.1 L471.8 281.9 L475.1 280.6 L478.3 279.2 L481.6 277.7 L484.8 276.2 L488.0 274.5 L491.3 272.8 L494.5 271.1 L497.8 269.2 L501.0 267.2 L504.2 265.2 L507.5 263.1 L510.7 261.0 L514.0 258.7 L517.2 256.4 L520.4 253.9 L523.7 251.4 L526.9 248.9 L530.2 246.2 L533.4 243.5 L536.6 240.7 L539.9 237.8 L543.1 234.8 L546.4 231.8 L549.6 228.6 L552.8 225.4 L556.1 222.1 L559.3 218.8 L562.6 215.3 L565.8 211.8 L569.0 208.2 L572.3 204.5 L575.5 200.8 L578.8 196.9 L582.0 193.0 L585.2 189.0 L588.5 184.9 L591.7 180.8 L595.0 176.5 L598.2 172.2 L601.4 167.8 L604.7 163.3 L607.9 158.8 L611.2 154.2 L614.4 149.4 L617.6 144.6 L620.9 139.8 L624.1 134.8 L627.4 129.8 L630.6 124.7 L633.8 119.5 L637.1 114.2 L640.3 108.9 L643.6 103.5 L646.8 98.0 L650.0 92.4 L653.3 86.7 L656.5 81.0 L659.8 75.2"/>
<line class="dd-edge-acc dd-dash" x1="182.4" y1="80.8" x2="252.6" y2="195.2"/>
<circle class="dd-fill-acc" cx="204.0" cy="116.0" r="5"/>
<path class="dd-edge-acc" d="M204.0 116.0 Q247.2 102.0 290.4 228.6"/>
<circle class="dd-fill-acc" cx="290.4" cy="228.6" r="4"/>
<path class="dd-edge-acc" d="M290.4 228.6 Q316.3 214.6 342.2 269.2"/>
<circle class="dd-fill-acc" cx="342.2" cy="269.2" r="4"/>
<path class="dd-edge-acc" d="M342.2 269.2 Q357.8 255.2 373.3 283.8"/>
<circle class="dd-fill-acc" cx="373.3" cy="283.8" r="4"/>
<path class="dd-edge-acc" d="M373.3 283.8 Q382.7 269.8 392.0 289.0"/>
<circle class="dd-fill-acc" cx="392.0" cy="289.0" r="4"/>
<path class="dd-edge-acc" d="M392.0 289.0 Q397.6 275.0 403.2 290.9"/>
<circle class="dd-fill-acc" cx="403.2" cy="290.9" r="4"/>
<text class="t-acc t-mono" x="216" y="120">w = 1，L = 16，斜率 −16</text>
<text class="t-mono" x="302" y="233">w = 1.8，L = 5.76</text>
<text class="t-say" x="447" y="284">谷底 w = 3，L = 0</text>
<text class="t-say" x="560" y="138">每一步：w 减去 0.05 × 斜率</text>
<text class="t-say" x="560" y="158">越靠近谷底，坡越缓，步子越小</text>
</svg>
</div>
<figcaption>图 1 · 从 w = 1 出发：斜率是负的，就把 w 往大里调；离谷底越近，斜率越小，步子也越小</figcaption>
</figure>

这就是**梯度下降**：算坡度，往下坡挪一步，再算坡度，再挪一步。注意步子是越来越小的，因为越靠近谷底，坡越缓，导数越小。

### 步子迈多大

学习率不能随便取（表 1）。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-warmlr-title">同一个起点 w = 1，四种步长走 4 步</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="gd-warmlr-title">
<colgroup><col style="width:14%"><col><col style="width:26%"></colgroup>
<thead>
<tr><th scope="col">学习率</th><th scope="col">w 的变化</th><th scope="col">结果</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="mono">0.05</th><td class="mono">1 → 1.8 → 2.28 → 2.568 → 2.741</td><td class="good">一步步靠近 3</td></tr>
<tr><th scope="row" class="mono">0.125</th><td class="mono">1 → 3 → 3 → 3 → 3</td><td class="good">一步到位</td></tr>
<tr><th scope="row" class="mono">0.25</th><td class="mono">1 → 5 → 1 → 5 → 1</td><td class="warn">在 1 和 5 之间来回跳</td></tr>
<tr><th scope="row" class="mono">0.3</th><td class="mono">1 → 5.8 → −0.92 → 8.488 → −4.683</td><td class="bad">越跳越远</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">每一步离谷底的距离都乘上同一个数 1 − 8 × 学习率：0.05 时是 0.6，0.125 时是 0，0.25 时是 −1，0.3 时是 −1.4。</p>
</div>
<figcaption>表 1 · 步子太小走得慢，正好合适一步到位，太大就在谷底两边来回跳，再大就越跳越远</figcaption>
</figure>

这个例子简单到可以算清原因。新的 $w$ 减去 3，等于旧的 $w$ 减去 3 再乘上 $1 - 8\eta$。$\eta = 0.05$ 时乘的是 0.6，离谷底的距离每步缩到六成；$\eta = 0.125$ 时乘的是 0，一步就到；$\eta = 0.25$ 时乘的是 −1，从谷底一边跳到另一边，距离不变，永远在 1 和 5 之间来回；$\eta = 0.3$ 时乘的是 −1.4，每跳一次离得更远。导数只说明脚下这一点的坡，步子太大，就会跨过谷底。

### 两个旋钮：偏导数

机器再加一个旋钮 $b$，输出变成 $y = w \cdot x + b$。输入还是 2，目标还是 6，损失是 $L = (2w + b - 6)^2$，从 $w = 1$、$b = 0$ 出发。

现在 $L$ 同时取决于两个数。办法是一次只看一个：先假装 $b$ 不动，只对 $w$ 求导；再假装 $w$ 不动，只对 $b$ 求导。这样得到的叫**偏导数**，写作 $\partial L / \partial w$ 和 $\partial L / \partial b$，读作“L 对 w 的偏导”。

$$
\frac{\partial L}{\partial w} = 2(2w + b - 6) \times 2 = -16, \qquad \frac{\partial L}{\partial b} = 2(2w + b - 6) \times 1 = -8
$$

所有参数的偏导数排在一起，叫作**梯度**。这里的梯度是 $(-16, -8)$，两个旋钮各自往大里拧，$w$ 拧得多一些。

### 一条会反复用到的规律

把上面两个偏导数拆开看。两个式子前面都有 $2(2w + b - 6)$，它是损失对输出 $y$ 的导数：输出每变一点，损失变多少。$w = 1$、$b = 0$ 时它是 −8，意思是输出应该变大。后面再乘一个数：$w$ 乘的是 2，$b$ 乘的是 1。这两个数正是它们在 $y = w \cdot 2 + b \cdot 1$ 里各自乘着的那个输入。

所以**一个参数的偏导数 = 损失对输出的导数 × 这个参数乘着的输入**。输入大，这个旋钮对输出的影响就大，分到的“责任”也大；输入是 0，拧它什么也改变不了，偏导数就是 0。

这其实是复合函数求导的一个特例，叫**链式法则**：损失通过 $y$ 依赖 $w$，那么“$w$ 变一点，损失变多少”就等于“$w$ 变一点，$y$ 变多少”乘上“$y$ 变一点，损失变多少”。中间隔了好几层也是一样，把每一层的变化倍数依次乘起来。像一串齿轮，第一个转一圈，最后一个转几圈，就等于沿途各级传动比相乘。

有了这几样工具，就可以打开那个 10 个参数的 Transformer 了。

## 二、一个能手算的 Transformer

### 任务：说出第一个字

先说清楚 Transformer 在做什么。它是今天大语言模型的基本结构，干的事情是“看前面的字，猜下一个字”。我们给它一个小到不能再小的任务：字只有 A 和 B 两个，模型每次读两个字，读到第二个字时，说出**第一个**字是什么。一共只有四种输入：

- AA → A，AB → A，BA → B，BB → B。

为什么挑这个任务？因为模型站在第二个位置上做预测，手里直接拿着的是第二个字，要答对就必须回头看第一个字。“该看哪里”正是 Transformer 里**注意力**负责的事；训练好的大模型里，也确实有专门负责“看前一个字”的注意力[^induction]。

一个完整的 Transformer 还有很多别的部件[^vaswani]，我们只留下注意力和最后把结果变成答案的输出层，其余全部去掉。

### 字怎样变成数

电脑只能处理数，所以每个字先变成三个数。A 记作 $(1, 0, 0)$，B 记作 $(0, 1, 0)$：第一个数表示“是不是 A”，第二个数表示“是不是 B”。第三个数是一个位置标记，只有正在做预测的第二个位置上是 1，相当于告诉模型“这是我自己”。

所以输入 BA 变成两组数：第一个位置 $x_1 = (0, 1, 0)$，第二个位置 $x_2 = (1, 0, 1)$。这一步是固定的，不参与训练。

### 注意力：先打分，再分配比例

注意力要决定两个位置各看几成。它分三步。

**第一步，提问和亮牌。** 第二个位置提出一个“问题”，叫**查询**，记作 $q$；每个位置各亮出一张“牌”，叫**键**，记作 $k_1$、$k_2$。它们都是用参数和输入算出来的一个数：查询用三个参数 $w_{q1}, w_{q2}, w_{q3}$，键用另外三个参数 $w_{k1}, w_{k2}, w_{k3}$，算法都是“对应位置相乘，再加起来”。比如第二个位置的键：

$$
k_2 = w_{k1} \times 1 + w_{k2} \times 0 + w_{k3} \times 1
$$

**第二步，打分。** 查询和每张牌相乘，得到两个分数 $s_1 = q\,k_1$，$s_2 = q\,k_2$。分数越高，越该看那个位置。

**第三步，把分数变成比例。** 两个分数要变成两个加起来等于 1 的比例 $a_1$、$a_2$，表示第一个位置看几成、第二个位置看几成。用的办法叫 **softmax**：先对每个分数取 $e$ 的幂，变成正数，再各自除以总和。

$$
a_1 = \frac{e^{s_1}}{e^{s_1} + e^{s_2}}, \qquad a_2 = \frac{e^{s_2}}{e^{s_1} + e^{s_2}}
$$

为什么用 $e$ 的幂？因为它永远是正的，分数高的分到的比例就大，而且它的导数就是它自己，后面求导时很方便。

### 输出：看到了什么，就说什么

按比例把两个字混在一起：看到的东西里，A 占 $h_A$，B 占 $h_B$。BA 这个例子里，第一个字是 B、第二个字是 A，所以 $h_B = a_1$，$h_A = a_2$。

输出层用四个参数 $W_{AA}, W_{AB}, W_{BA}, W_{BB}$ 给 A、B 两个答案打分：

$$
z_A = W_{AA}\,h_A + W_{AB}\,h_B, \qquad z_B = W_{BA}\,h_A + W_{BB}\,h_B
$$

两个分数再用一次 softmax，变成“答 A”和“答 B”的概率 $p_A$、$p_B$。

要训练的参数就是 $w_q$ 的 3 个、$w_k$ 的 3 个、$W$ 的 4 个，一共 10 个。初值我们挑了好算的：$w_{q1} = w_{q2} = w_{q3} = 0.5$；$w_{k1} = w_{k2} = 0$，$w_{k3} = 0.5$；$W_{AA} = W_{BB} = 0.5$，另外两个是 0。真实的训练从随机的小数出发，我们手挑初值，是为了让数字整齐，并且让模型一开始是错的。

## 三、前向：从输入算到损失

### 一个样本走一遍

从输入一路算到答案，叫**前向计算**。拿 BA 来算，正确答案是 B。

查询：$q = 0.5 \times 1 + 0.5 \times 0 + 0.5 \times 1 = 1$。键：第一个位置是 B，$k_1 = w_{k2} = 0$；第二个位置是 A 加位置标记，$k_2 = w_{k1} + w_{k3} = 0.5$。分数 $s_1 = 0$，$s_2 = 0.5$。

softmax：$e^0 = 1$，$e^{0.5} = 1.649$，加起来 2.649，所以 $a_1 = 1 / 2.649 = 0.378$，$a_2 = 0.622$。模型把六成多的注意力放在了第二个字，也就是它自己身上，**看错了地方**。原因在 $w_{k3}$：它是 0.5，给带着“我自己”标记的那个位置加了分。

看到的东西：$h_A = a_2 = 0.622$，$h_B = a_1 = 0.378$，六成是 A。输出分数 $z_A = 0.5 \times 0.622 = 0.311$，$z_B = 0.5 \times 0.378 = 0.189$。再用 softmax：$p_A = 0.531$，$p_B = 0.469$。模型更倾向于说 A，答错了（图 2）。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 532" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-fwd-title gd-fwd-desc">
<title id="gd-fwd-title">前向：样本 BA 从输入一路算到损失</title>
<desc id="gd-fwd-desc">十个参数取初值，样本 BA 的第二个位置先算查询和键，得到注意力权重 0.378 和 0.622，多半看了自己；取出的内容偏向 A，输出 B 的概率只有 0.469，损失 0.756。</desc>
<defs>
<marker id="gd-fwd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-fwd-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="532" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="232" rx="8"/>
<text class="t-zone" x="32" y="44">看哪里：查询、键和注意力</text>
<rect class="dd-zone" x="16" y="264" width="768" height="252" rx="8"/>
<text class="t-zone" x="32" y="292">说什么：取值、输出和损失</text>
<line class="dd-edge" x1="260" y1="80" x2="282" y2="80" marker-end="url(#gd-fwd-arrow)"/>
<line class="dd-edge" x1="516" y1="80" x2="538" y2="80" marker-end="url(#gd-fwd-arrow)"/>
<line class="dd-edge" x1="656" y1="200" x2="656" y2="310" marker-end="url(#gd-fwd-arrow)"/>
<line class="dd-edge" x1="540" y1="328" x2="518" y2="328" marker-end="url(#gd-fwd-arrow)"/>
<line class="dd-edge" x1="284" y1="328" x2="262" y2="328" marker-end="url(#gd-fwd-arrow)"/>
<rect class="dd-card" x="28" y="64" width="232" height="110" rx="6"/>
<path class="dd-card-head" d="M28.6 96 V70 Q28.6 64.6 34 64.6 H254 Q259.4 64.6 259.4 70 V96 Z"/>
<text class="t-name" x="44" y="85">输入</text>
<text class="t-note" x="244" y="85" text-anchor="end">固定，不训练</text>
<text class="t-mono" x="44" y="113">x₁ = B，位置 1</text>
<text class="t-val" x="244" y="113" text-anchor="end">(0, 1, 0)</text>
<line class="dd-hair" x1="29" y1="122" x2="259" y2="122"/>
<text class="t-mono" x="44" y="139">x₂ = A，当前位置</text>
<text class="t-val" x="244" y="139" text-anchor="end">(1, 0, 1)</text>
<line class="dd-hair" x1="29" y1="148" x2="259" y2="148"/>
<text class="t-mono" x="44" y="165">目标</text>
<text class="t-val" x="244" y="165" text-anchor="end">B</text>
<rect class="dd-card" x="284" y="64" width="232" height="162" rx="6"/>
<path class="dd-card-head" d="M284.6 96 V70 Q284.6 64.6 290 64.6 H510 Q515.4 64.6 515.4 70 V96 Z"/>
<text class="t-name" x="300" y="85">查询与键</text>
<text class="t-mono t-acc" x="300" y="113">w_q₁,₂,₃</text>
<text class="t-val t-acc" x="500" y="113" text-anchor="end">0.5, 0.5, 0.5</text>
<line class="dd-hair" x1="285" y1="122" x2="515" y2="122"/>
<text class="t-mono t-acc" x="300" y="139">w_k₁,₂,₃</text>
<text class="t-val t-acc" x="500" y="139" text-anchor="end">0, 0, 0.5</text>
<line class="dd-hair" x1="285" y1="148" x2="515" y2="148"/>
<text class="t-mono" x="300" y="165">q（由 w_q 和 x₂ 算）</text>
<text class="t-val" x="500" y="165" text-anchor="end">1</text>
<line class="dd-hair" x1="285" y1="174" x2="515" y2="174"/>
<text class="t-mono" x="300" y="191">k₁ = w_k₂</text>
<text class="t-val" x="500" y="191" text-anchor="end">0</text>
<line class="dd-hair" x1="285" y1="200" x2="515" y2="200"/>
<text class="t-mono" x="300" y="217">k₂ = w_k₁ + w_k₃</text>
<text class="t-val" x="500" y="217" text-anchor="end">0.5</text>
<rect class="dd-card" x="540" y="64" width="232" height="136" rx="6"/>
<path class="dd-card-head" d="M540.6 96 V70 Q540.6 64.6 546 64.6 H766 Q771.4 64.6 771.4 70 V96 Z"/>
<text class="t-name" x="556" y="85">注意力比例</text>
<text class="t-note" x="756" y="85" text-anchor="end">softmax</text>
<text class="t-mono" x="556" y="113">s₁ = q·k₁</text>
<text class="t-val" x="756" y="113" text-anchor="end">0</text>
<line class="dd-hair" x1="541" y1="122" x2="771" y2="122"/>
<text class="t-mono" x="556" y="139">s₂ = q·k₂</text>
<text class="t-val" x="756" y="139" text-anchor="end">0.5</text>
<line class="dd-hair" x1="541" y1="148" x2="771" y2="148"/>
<text class="t-mono" x="556" y="165">a₁ 看 x₁</text>
<text class="t-val" x="756" y="165" text-anchor="end">0.378</text>
<line class="dd-hair" x1="541" y1="174" x2="771" y2="174"/>
<text class="t-mono" x="556" y="191">a₂ 看 x₂</text>
<text class="t-val" x="756" y="191" text-anchor="end">0.622</text>
<rect class="dd-card" x="540" y="312" width="232" height="84" rx="6"/>
<path class="dd-card-head" d="M540.6 344 V318 Q540.6 312.6 546 312.6 H766 Q771.4 312.6 771.4 318 V344 Z"/>
<text class="t-name" x="556" y="333">加权取值</text>
<text class="t-note" x="756" y="333" text-anchor="end">h = a₁·B + a₂·A</text>
<text class="t-mono" x="556" y="361">h_A</text>
<text class="t-val" x="756" y="361" text-anchor="end">0.622</text>
<line class="dd-hair" x1="541" y1="370" x2="771" y2="370"/>
<text class="t-mono" x="556" y="387">h_B</text>
<text class="t-val" x="756" y="387" text-anchor="end">0.378</text>
<rect class="dd-card" x="284" y="312" width="232" height="188" rx="6"/>
<path class="dd-card-head" d="M284.6 344 V318 Q284.6 312.6 290 312.6 H510 Q515.4 312.6 515.4 318 V344 Z"/>
<text class="t-name" x="300" y="333">输出</text>
<text class="t-note" x="500" y="333" text-anchor="end">softmax</text>
<text class="t-mono t-acc" x="300" y="361">W_AA, W_AB</text>
<text class="t-val t-acc" x="500" y="361" text-anchor="end">0.5, 0</text>
<line class="dd-hair" x1="285" y1="370" x2="515" y2="370"/>
<text class="t-mono t-acc" x="300" y="387">W_BA, W_BB</text>
<text class="t-val t-acc" x="500" y="387" text-anchor="end">0, 0.5</text>
<line class="dd-hair" x1="285" y1="396" x2="515" y2="396"/>
<text class="t-mono" x="300" y="413">z_A</text>
<text class="t-val" x="500" y="413" text-anchor="end">0.311</text>
<line class="dd-hair" x1="285" y1="422" x2="515" y2="422"/>
<text class="t-mono" x="300" y="439">z_B</text>
<text class="t-val" x="500" y="439" text-anchor="end">0.189</text>
<line class="dd-hair" x1="285" y1="448" x2="515" y2="448"/>
<text class="t-mono" x="300" y="465">p_A</text>
<text class="t-val" x="500" y="465" text-anchor="end">0.531</text>
<line class="dd-hair" x1="285" y1="474" x2="515" y2="474"/>
<text class="t-mono" x="300" y="491">p_B</text>
<text class="t-val" x="500" y="491" text-anchor="end">0.469</text>
<rect class="dd-card" x="28" y="312" width="232" height="58" rx="6"/>
<path class="dd-card-head" d="M28.6 344 V318 Q28.6 312.6 34 312.6 H254 Q259.4 312.6 259.4 318 V344 Z"/>
<text class="t-name" x="44" y="333">损失</text>
<text class="t-mono" x="44" y="361">L = −ln p_B</text>
<text class="t-val" x="244" y="361" text-anchor="end">0.756</text>
<text class="t-say" x="36" y="204">强调色是要训练的 10 个参数，</text>
<text class="t-say" x="36" y="224">其余数都由它们和输入算出来</text>
<text class="t-say" x="36" y="400">损失越小越好：p_B = 1 时 L = 0</text>
</svg>
</div>
<figcaption>图 2 · 前向计算：初始参数下，模型把 62% 的注意力放在了自己身上，于是更像在说 A，正确答案 B 只拿到 0.469 的概率</figcaption>
</figure>

### 损失：为什么用 −ln p

热身里的损失是“差值的平方”。这里的答案是一个字，没有“差多少”可言，用什么当损失？

最直觉的是数一数答对了几道。可这个数没法用来训练：把某个参数从 0.5 改成 0.51，$p_B$ 从 0.469 变成 0.470，答案还是 A，答对的题数一道没变，导数是 0。分数对小的改动毫无反应，就给不出“该往哪边拧”的信息。

所以实际用的是**交叉熵损失**：

$$
L = -\ln p_{\text{正确答案}}
$$

$\ln$ 是以 $e$ 为底的对数。BA 的损失是 $-\ln 0.469 = 0.756$。它有几个好记的点：正确答案的概率是 1 时，$L = 0$；是 0.5，也就是瞎猜时，$L = \ln 2 = 0.693$；概率越接近 0，$L$ 越大，没有上限。$p_B$ 每多一点，损失就少一点，哪怕答案还没翻过来。

另外三个样本也这样算。AA 和 BB 两个字相同，不管注意力放在哪里，看到的都是同一个字，正确答案的概率是 0.622，损失 0.474；AB 和 BA 情况对称，损失也是 0.756。四个数取平均：$(0.474 + 0.756 + 0.756 + 0.474) / 4 = 0.615$，这就是开头打出来的第一个数。

现在的问题和热身一样：**10 个旋钮，各该往哪边拧、拧多少，才能让 0.615 变小？**

## 四、梯度：每个参数往哪边调

### 第一次尝试：挨个动一下

不会求导也有办法：一个一个试。把 $W_{BB}$ 从 0.5 调到 0.51，其余不动，重新算一遍 BA 的损失，0.7563 变成了 0.7542，少了 0.0020。改动 0.01，损失变了 −0.0020，两者之比是 −0.200。这个比值就是 $\partial L / \partial W_{BB}$ 的近似值：负的，说明 $W_{BB}$ 该调大。

再试 $w_{k3}$：从 0.5 调到 0.51，损失多了约 0.00125，比值 +0.125，说明它该调小，和前面“它让模型看错了地方”的判断一致。把 $w_{k3}$ 在一段范围里来回扫，画出损失的样子（图 3），跟热身的抛物线是一回事：损失是一条光滑的曲线，偏导数就是当前这一点的斜率。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-slope-title gd-slope-desc">
<title id="gd-slope-title">只动一个参数时损失的形状</title>
<desc id="gd-slope-desc">固定其余九个参数，把 w_k 的第 3 个数从 −3 扫到 3，样本 BA 的损失从约 0.474 平滑地升到约 0.974；在当前值 0.5 处，切线斜率是 0.125。</desc>
<defs>
<marker id="gd-slope-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-slope-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="360" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="328" rx="8"/>
<text class="t-zone" x="32" y="44">样本 BA 的损失 · 只改 w_k 的第 3 个数，其余 9 个参数不动</text>
<line class="dd-hair" x1="96" y1="292" x2="744" y2="292"/>
<text class="t-note" x="84" y="296" text-anchor="end">0</text>
<line class="dd-hair" x1="96" y1="220" x2="744" y2="220"/>
<text class="t-note" x="84" y="224" text-anchor="end">0.4</text>
<line class="dd-hair" x1="96" y1="148" x2="744" y2="148"/>
<text class="t-note" x="84" y="152" text-anchor="end">0.8</text>
<line class="dd-hair" x1="96" y1="76" x2="744" y2="76"/>
<text class="t-note" x="84" y="80" text-anchor="end">1.2</text>
<line class="dd-hair" x1="96" y1="76" x2="96" y2="292"/>
<text class="t-note" x="96" y="312" text-anchor="middle">−3</text>
<line class="dd-hair" x1="204" y1="76" x2="204" y2="292"/>
<text class="t-note" x="204" y="312" text-anchor="middle">−2</text>
<line class="dd-hair" x1="312" y1="76" x2="312" y2="292"/>
<text class="t-note" x="312" y="312" text-anchor="middle">−1</text>
<line class="dd-hair" x1="420" y1="76" x2="420" y2="292"/>
<text class="t-note" x="420" y="312" text-anchor="middle">0</text>
<line class="dd-hair" x1="528" y1="76" x2="528" y2="292"/>
<text class="t-note" x="528" y="312" text-anchor="middle">1</text>
<line class="dd-hair" x1="636" y1="76" x2="636" y2="292"/>
<text class="t-note" x="636" y="312" text-anchor="middle">2</text>
<line class="dd-hair" x1="744" y1="76" x2="744" y2="292"/>
<text class="t-note" x="744" y="312" text-anchor="middle">3</text>
<text class="t-note" x="420" y="334" text-anchor="middle">w_k 第 3 个数（“当前位置”这一维在键里的权重）</text>
<path class="dd-edge" style="stroke-width:2" d="M96.0 203.4 L98.7 203.3 L101.4 203.2 L104.1 203.1 L106.8 203.1 L109.5 203.0 L112.2 202.9 L114.9 202.8 L117.6 202.7 L120.3 202.6 L123.0 202.5 L125.7 202.4 L128.4 202.3 L131.1 202.2 L133.8 202.1 L136.5 202.0 L139.2 201.9 L141.9 201.8 L144.6 201.6 L147.3 201.5 L150.0 201.4 L152.7 201.3 L155.4 201.1 L158.1 201.0 L160.8 200.9 L163.5 200.7 L166.2 200.6 L168.9 200.4 L171.6 200.3 L174.3 200.1 L177.0 200.0 L179.7 199.8 L182.4 199.7 L185.1 199.5 L187.8 199.3 L190.5 199.2 L193.2 199.0 L195.9 198.8 L198.6 198.6 L201.3 198.5 L204.0 198.3 L206.7 198.1 L209.4 197.9 L212.1 197.7 L214.8 197.5 L217.5 197.3 L220.2 197.0 L222.9 196.8 L225.6 196.6 L228.3 196.4 L231.0 196.1 L233.7 195.9 L236.4 195.7 L239.1 195.4 L241.8 195.2 L244.5 194.9 L247.2 194.6 L249.9 194.4 L252.6 194.1 L255.3 193.8 L258.0 193.6 L260.7 193.3 L263.4 193.0 L266.1 192.7 L268.8 192.4 L271.5 192.1 L274.2 191.8 L276.9 191.4 L279.6 191.1 L282.3 190.8 L285.0 190.5 L287.7 190.1 L290.4 189.8 L293.1 189.4 L295.8 189.1 L298.5 188.7 L301.2 188.4 L303.9 188.0 L306.6 187.6 L309.3 187.2 L312.0 186.8 L314.7 186.4 L317.4 186.0 L320.1 185.6 L322.8 185.2 L325.5 184.8 L328.2 184.4 L330.9 184.0 L333.6 183.5 L336.3 183.1 L339.0 182.6 L341.7 182.2 L344.4 181.7 L347.1 181.3 L349.8 180.8 L352.5 180.3 L355.2 179.9 L357.9 179.4 L360.6 178.9 L363.3 178.4 L366.0 177.9 L368.7 177.4 L371.4 176.9 L374.1 176.4 L376.8 175.9 L379.5 175.4 L382.2 174.9 L384.9 174.3 L387.6 173.8 L390.3 173.3 L393.0 172.7 L395.7 172.2 L398.4 171.7 L401.1 171.1 L403.8 170.6 L406.5 170.0 L409.2 169.5 L411.9 168.9 L414.6 168.4 L417.3 167.8 L420.0 167.2 L422.7 166.7 L425.4 166.1 L428.1 165.5 L430.8 165.0 L433.5 164.4 L436.2 163.8 L438.9 163.3 L441.6 162.7 L444.3 162.1 L447.0 161.6 L449.7 161.0 L452.4 160.4 L455.1 159.8 L457.8 159.3 L460.5 158.7 L463.2 158.1 L465.9 157.6 L468.6 157.0 L471.3 156.4 L474.0 155.9 L476.7 155.3 L479.4 154.8 L482.1 154.2 L484.8 153.6 L487.5 153.1 L490.2 152.5 L492.9 152.0 L495.6 151.5 L498.3 150.9 L501.0 150.4 L503.7 149.9 L506.4 149.3 L509.1 148.8 L511.8 148.3 L514.5 147.8 L517.2 147.2 L519.9 146.7 L522.6 146.2 L525.3 145.7 L528.0 145.2 L530.7 144.7 L533.4 144.3 L536.1 143.8 L538.8 143.3 L541.5 142.8 L544.2 142.4 L546.9 141.9 L549.6 141.4 L552.3 141.0 L555.0 140.6 L557.7 140.1 L560.4 139.7 L563.1 139.2 L565.8 138.8 L568.5 138.4 L571.2 138.0 L573.9 137.6 L576.6 137.2 L579.3 136.8 L582.0 136.4 L584.7 136.0 L587.4 135.6 L590.1 135.3 L592.8 134.9 L595.5 134.5 L598.2 134.2 L600.9 133.8 L603.6 133.5 L606.3 133.1 L609.0 132.8 L611.7 132.5 L614.4 132.1 L617.1 131.8 L619.8 131.5 L622.5 131.2 L625.2 130.9 L627.9 130.6 L630.6 130.3 L633.3 130.0 L636.0 129.7 L638.7 129.4 L641.4 129.2 L644.1 128.9 L646.8 128.6 L649.5 128.4 L652.2 128.1 L654.9 127.9 L657.6 127.6 L660.3 127.4 L663.0 127.2 L665.7 126.9 L668.4 126.7 L671.1 126.5 L673.8 126.3 L676.5 126.0 L679.2 125.8 L681.9 125.6 L684.6 125.4 L687.3 125.2 L690.0 125.0 L692.7 124.9 L695.4 124.7 L698.1 124.5 L700.8 124.3 L703.5 124.1 L706.2 124.0 L708.9 123.8 L711.6 123.6 L714.3 123.5 L717.0 123.3 L719.7 123.2 L722.4 123.0 L725.1 122.9 L727.8 122.7 L730.5 122.6 L733.2 122.5 L735.9 122.3 L738.6 122.2 L741.3 122.1 L744.0 121.9"/>
<line class="dd-edge-acc dd-dash" x1="322.8" y1="187.3" x2="625.2" y2="124.5"/>
<circle class="dd-fill-acc" cx="474.0" cy="155.9" r="5"/>
<text class="t-acc t-mono" x="462" y="108" text-anchor="end">现在：w_k₃ = 0.5，L = 0.756</text>
<text class="t-acc t-mono" x="462" y="126" text-anchor="end">切线斜率 ∂L/∂w_k₃ = 0.125</text>
<line class="dd-edge-acc" x1="474.0" y1="211.9" x2="366.0" y2="211.9" marker-end="url(#gd-slope-arrow-acc)"/>
<text class="t-say" x="356" y="217" text-anchor="end">斜率为正，往左（调小）是下坡</text>
<text class="t-say" x="107" y="256">调到很负：只看 x₁，损失也只降到 0.474</text>
<text class="t-say" x="733" y="166" text-anchor="end">调到很正：只看自己，损失升到 0.974</text>
</svg>
</div>
<figcaption>图 3 · 梯度就是脚下的坡度：在 0.5 这一点，w_k₃ 每调大 0.01，损失大约多 0.00125，所以该往小里调</figcaption>
</figure>

图 3 还说明了一件事：就算把 $w_{k3}$ 调到很负、注意力全放在第一个字上，BA 的损失也只降到 0.474，因为输出层的 0.5 太小，答得不够果断。要把损失降下去，“看哪里”和“说什么”两头都得改，10 个参数要一起动。

### 不足：参数一多就试不动

挨个试，每个参数都要多算一两次前向。10 个参数还算得过来；GPT-3 有 1750 亿个参数，挨个试一遍，就要算 3500 亿次前向，而这只够走一步。

**有没有办法算一趟，就得到所有参数的偏导数？**

## 五、反向传播：从损失往回算

### 思路：沿途的倍数相乘

热身里的链式法则说，隔了好几层，就把每层的变化倍数乘起来。$w_{k3}$ 影响损失的路线是：$w_{k3}$ 改变键 $k_2$，$k_2$ 改变分数 $s_2$，分数改变比例 $a$，比例改变看到的 $h$，$h$ 改变输出分数 $z$，最后 $z$ 改变损失。

关键在于，靠近损失的那几段路是很多参数共用的。10 个参数都要经过“$z$ 怎样影响 $L$”这一段；6 个注意力参数还要共同经过 $h$、$a$、$s$。所以从损失出发往回走，每走一层，就把“损失对这一层的导数”记下来，下一层直接拿来乘。一趟走完，所有参数的偏导数就都有了。这就是**反向传播**[^backprop]，它的计算量和一次前向差不多，大约是两倍，跟参数有多少个无关。

下面沿着图 2 的箭头倒着走一遍。每一步只用到热身里那两件事：求导，以及倍数相乘。

### 逐层往回

**第一步：输出分数。** 只有两个答案时，$p_B = \dfrac{e^{z_B}}{e^{z_A} + e^{z_B}}$。把损失 $L = -\ln p_B$ 写开，再分别对 $z_A$、$z_B$ 求导，能得到一个很干净的结果：

$$
\frac{\partial L}{\partial z_A} = p_A = 0.531, \qquad \frac{\partial L}{\partial z_B} = p_B - 1 = -0.531
$$

（推导：$L = -z_B + \ln(e^{z_A} + e^{z_B})$，对 $z_A$ 求导得 $\dfrac{e^{z_A}}{e^{z_A}+e^{z_B}} = p_A$，对 $z_B$ 求导得 $-1 + p_B$。）读法很直接：A 的分数应该降，B 的分数应该升，力度就是模型离正确答案差多远。

**第二步：输出层的四个参数。** $z_A = W_{AA}\,h_A + W_{AB}\,h_B$，正是热身里那种“参数乘输入”的形状。用那条规律，偏导数等于“传回来的导数 × 这个参数乘着的输入”：

- $W_{AA}$：$0.531 \times 0.622 = 0.330$；$W_{AB}$：$0.531 \times 0.378 = 0.200$；
- $W_{BA}$：$-0.531 \times 0.622 = -0.330$；$W_{BB}$：$-0.531 \times 0.378 = -0.200$。

$W_{BB}$ 的 −0.200，正好就是前面挨个试出来的那个数。

**第三步：看到的东西和注意力比例。** $h_A$ 同时出现在 $z_A$ 和 $z_B$ 里，两条路的影响要加起来：$\partial L / \partial h_A = 0.531 \times W_{AA} + (-0.531) \times W_{BA} = 0.531 \times 0.5 + 0 = 0.265$；同理 $\partial L / \partial h_B = -0.265$。看到的 A 多了不好，B 多了好。在 BA 里 $h_B = a_1$、$h_A = a_2$，所以 $\partial L / \partial a_1 = -0.265$，$\partial L / \partial a_2 = 0.265$：多看第一个字会让损失变小。

**第四步：穿过 softmax。** 两个比例加起来恒等于 1，一个涨另一个就跌，不能分开算。只有两个位置时，$a_1 = \dfrac{1}{1 + e^{s_2 - s_1}}$，求导可以得到：$s_1$ 每增加一点，$a_1$ 增加 $a_1 a_2$ 倍，$a_2$ 减少同样多。所以

$$
\frac{\partial L}{\partial s_1} = a_1 a_2 \times \Big(\frac{\partial L}{\partial a_1} - \frac{\partial L}{\partial a_2}\Big) = 0.235 \times (-0.265 - 0.265) = -0.125
$$

$\partial L / \partial s_2$ 正好相反，是 +0.125。$a_1 a_2$ 这个倍数值得记住：比例是 0.5 和 0.5 时它最大，是 0.25；如果注意力几乎全压在一边，比如 0.999 和 0.001，它就接近 0，梯度传到这里就过不去了。第九节会遇到这种情况。

**第五步：查询和键。** $s_1 = q\,k_1$，对 $q$ 求导是 $k_1$，对 $k_1$ 求导是 $q$。所以 $\partial L / \partial q = -0.125 \times k_1 + 0.125 \times k_2 = -0.125 \times 0 + 0.125 \times 0.5 = 0.062$；$\partial L / \partial k_1 = -0.125 \times q = -0.125$，$\partial L / \partial k_2 = 0.125$。

**第六步：落到 6 个注意力参数上。** 又是“传回来的导数 × 输入”。$q = w_{q1} \times 1 + w_{q2} \times 0 + w_{q3} \times 1$，所以 $w_{q1}$ 和 $w_{q3}$ 的偏导数都是 0.062，$w_{q2}$ 乘着的输入是 0，偏导数是 0。$k_1 = w_{k2}$，$k_2 = w_{k1} + w_{k3}$，所以 $w_{k2}$ 的偏导数是 −0.125，$w_{k1}$ 和 $w_{k3}$ 都是 0.125。$w_{k3}$ 的 0.125，正是前面试出来的那个坡度。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 480" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-bwd-title gd-bwd-desc">
<title id="gd-bwd-title">反向：从损失往回，逐层算出每个数的梯度</title>
<desc id="gd-bwd-desc">从损失出发往回走：先得到损失对两个输出分数的梯度 0.531 和 −0.531，再到输出矩阵、取值、注意力权重、打分，最后得到十个参数各自的梯度。</desc>
<defs>
<marker id="gd-bwd-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-bwd-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="480" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="232" rx="8"/>
<text class="t-zone" x="32" y="44">然后回到注意力：打分、查询和键</text>
<rect class="dd-zone" x="16" y="264" width="768" height="200" rx="8"/>
<text class="t-zone" x="32" y="292">先从损失出发：输出和取值</text>
<line class="dd-edge-acc" x1="260" y1="328" x2="282" y2="328" marker-end="url(#gd-bwd-arrow-acc)"/>
<line class="dd-edge-acc" x1="516" y1="328" x2="538" y2="328" marker-end="url(#gd-bwd-arrow-acc)"/>
<line class="dd-edge-acc" x1="656" y1="312" x2="656" y2="150" marker-end="url(#gd-bwd-arrow-acc)"/>
<line class="dd-edge-acc" x1="540" y1="80" x2="518" y2="80" marker-end="url(#gd-bwd-arrow-acc)"/>
<line class="dd-edge-acc" x1="284" y1="80" x2="262" y2="80" marker-end="url(#gd-bwd-arrow-acc)"/>
<rect class="dd-derived" x="28" y="64" width="232" height="58" rx="6"/>
<text class="t-name" x="44" y="85">输入</text>
<text class="t-mono" x="44" y="113">字变成数，这步固定</text>
<text class="t-val" x="244" y="113" text-anchor="end">—</text>
<rect class="dd-card" x="284" y="64" width="232" height="162" rx="6"/>
<path class="dd-card-head" d="M284.6 96 V70 Q284.6 64.6 290 64.6 H510 Q515.4 64.6 515.4 70 V96 Z"/>
<text class="t-name" x="300" y="85">查询与键</text>
<text class="t-mono" x="300" y="113">∂L/∂q</text>
<text class="t-val" x="500" y="113" text-anchor="end">0.062</text>
<line class="dd-hair" x1="285" y1="122" x2="515" y2="122"/>
<text class="t-mono" x="300" y="139">∂L/∂k₁</text>
<text class="t-val" x="500" y="139" text-anchor="end">−0.125</text>
<line class="dd-hair" x1="285" y1="148" x2="515" y2="148"/>
<text class="t-mono" x="300" y="165">∂L/∂k₂</text>
<text class="t-val" x="500" y="165" text-anchor="end">0.125</text>
<line class="dd-hair" x1="285" y1="174" x2="515" y2="174"/>
<text class="t-mono t-acc" x="300" y="191">∂L/∂w_q</text>
<text class="t-val t-acc" x="500" y="191" text-anchor="end">0.062, 0, 0.062</text>
<line class="dd-hair" x1="285" y1="200" x2="515" y2="200"/>
<text class="t-mono t-acc" x="300" y="217">∂L/∂w_k</text>
<text class="t-val t-acc" x="500" y="217" text-anchor="end">0.125, −0.125, 0.125</text>
<rect class="dd-card" x="540" y="64" width="232" height="84" rx="6"/>
<path class="dd-card-head" d="M540.6 96 V70 Q540.6 64.6 546 64.6 H766 Q771.4 64.6 771.4 70 V96 Z"/>
<text class="t-name" x="556" y="85">注意力比例</text>
<text class="t-mono" x="556" y="113">∂L/∂s₁</text>
<text class="t-val" x="756" y="113" text-anchor="end">−0.125</text>
<line class="dd-hair" x1="541" y1="122" x2="771" y2="122"/>
<text class="t-mono" x="556" y="139">∂L/∂s₂</text>
<text class="t-val" x="756" y="139" text-anchor="end">0.125</text>
<rect class="dd-card" x="540" y="312" width="232" height="84" rx="6"/>
<path class="dd-card-head" d="M540.6 344 V318 Q540.6 312.6 546 312.6 H766 Q771.4 312.6 771.4 318 V344 Z"/>
<text class="t-name" x="556" y="333">加权取值</text>
<text class="t-mono" x="556" y="361">∂L/∂a₁（= ∂L/∂h_B）</text>
<text class="t-val" x="756" y="361" text-anchor="end">−0.265</text>
<line class="dd-hair" x1="541" y1="370" x2="771" y2="370"/>
<text class="t-mono" x="556" y="387">∂L/∂a₂（= ∂L/∂h_A）</text>
<text class="t-val" x="756" y="387" text-anchor="end">0.265</text>
<rect class="dd-card" x="284" y="312" width="232" height="136" rx="6"/>
<path class="dd-card-head" d="M284.6 344 V318 Q284.6 312.6 290 312.6 H510 Q515.4 312.6 515.4 318 V344 Z"/>
<text class="t-name" x="300" y="333">输出</text>
<text class="t-mono t-acc" x="300" y="361">∂L/∂W_AA,AB</text>
<text class="t-val t-acc" x="500" y="361" text-anchor="end">0.330, 0.200</text>
<line class="dd-hair" x1="285" y1="370" x2="515" y2="370"/>
<text class="t-mono t-acc" x="300" y="387">∂L/∂W_BA,BB</text>
<text class="t-val t-acc" x="500" y="387" text-anchor="end">−0.330, −0.200</text>
<line class="dd-hair" x1="285" y1="396" x2="515" y2="396"/>
<text class="t-mono" x="300" y="413">∂L/∂h_A</text>
<text class="t-val" x="500" y="413" text-anchor="end">0.265</text>
<line class="dd-hair" x1="285" y1="422" x2="515" y2="422"/>
<text class="t-mono" x="300" y="439">∂L/∂h_B</text>
<text class="t-val" x="500" y="439" text-anchor="end">−0.265</text>
<rect class="dd-card" x="28" y="312" width="232" height="84" rx="6"/>
<path class="dd-card-head" d="M28.6 344 V318 Q28.6 312.6 34 312.6 H254 Q259.4 312.6 259.4 318 V344 Z"/>
<text class="t-name" x="44" y="333">损失</text>
<text class="t-mono" x="44" y="361">∂L/∂z_A = p_A − 0</text>
<text class="t-val" x="244" y="361" text-anchor="end">0.531</text>
<line class="dd-hair" x1="29" y1="370" x2="259" y2="370"/>
<text class="t-mono" x="44" y="387">∂L/∂z_B = p_B − 1</text>
<text class="t-val" x="244" y="387" text-anchor="end">−0.531</text>
<text class="t-say" x="36" y="156">梯度到这里就停下：</text>
<text class="t-say" x="36" y="176">这一步没有参数要训练</text>
<text class="t-say" x="36" y="426">正数：这个数调大，损失变大</text>
<text class="t-say" x="36" y="446">负数：调大，损失变小</text>
</svg>
</div>
<figcaption>图 4 · 反向传播：箭头和图 2 相反，每一格的梯度都由它下游那一格的梯度乘上本地的导数得到；强调色是十个参数的梯度</figcaption>
</figure>

一趟走下来，10 个偏导数全部到手（图 4），每一步都只是求导和乘法。

### 对账

手算难免出错，我们用两种办法核对[^notes]。一是前面那种“挨个动一下”，10 个数和手算的结果在四位小数上全部一致。二是交给 PyTorch，一个常用的深度学习工具，它能自动做反向传播。下面这段代码可以跳过，只看输出：

```python
import torch
x1 = torch.tensor([0., 1., 0.])          # B，第 1 个位置
x2 = torch.tensor([1., 0., 1.])          # A，第 2 个位置（带“我自己”标记）
wq = torch.tensor([0.5, 0.5, 0.5], requires_grad=True)
wk = torch.tensor([0.0, 0.0, 0.5], requires_grad=True)
Wo = torch.tensor([[0.5, 0.0], [0.0, 0.5]], requires_grad=True)

q = wq @ x2
k = torch.stack([wk @ x1, wk @ x2])
a = torch.softmax(q * k, dim=0)
h = a[0] * x1[:2] + a[1] * x2[:2]
z = Wo @ h
loss = torch.nn.functional.cross_entropy(z[None], torch.tensor([1]))
loss.backward()
print(loss.item(), wq.grad, wk.grad, Wo.grad, sep="\n")
```

```text
0.756250262260437
tensor([0.0623, 0.0000, 0.0623])
tensor([ 0.1247, -0.1247,  0.1247])
tensor([[ 0.3303,  0.2003],
        [-0.3303, -0.2003]])
```

和手算的一样。`loss.backward()` 那一行，做的就是第一步到第六步。

## 六、更新：沿着下坡走一步

### 一步更新

有了 10 个偏导数，更新和热身里一模一样：每个参数减去学习率乘它的偏导数。先只用 BA 这一个样本、取学习率 1 走一步（表 2）。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-step1-title">只用样本 BA 走一步 · 新值 = 原值 − 1 × 梯度</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="gd-step1-title">
<colgroup><col style="width:34%"><col><col><col class="hl"></colgroup>
<thead>
<tr><th scope="col">参数</th><th scope="col" class="num">原值</th><th scope="col" class="num">梯度</th><th scope="col" class="num acc">新值</th></tr>
</thead>
<tbody>
<tr><th scope="row">w_q₁（A）</th><td class="mono num">0.5</td><td class="mono num">0.062</td><td class="mono num acc">0.438</td></tr>
<tr><th scope="row">w_q₂（B）</th><td class="mono num">0.5</td><td class="mono num">0</td><td class="mono num acc">0.5</td></tr>
<tr><th scope="row">w_q₃（当前位置）</th><td class="mono num">0.5</td><td class="mono num">0.062</td><td class="mono num acc">0.438</td></tr>
<tr><th scope="row">w_k₁（A）</th><td class="mono num">0</td><td class="mono num">0.125</td><td class="mono num acc">−0.125</td></tr>
<tr><th scope="row">w_k₂（B）</th><td class="mono num">0</td><td class="mono num">−0.125</td><td class="mono num acc">0.125</td></tr>
<tr><th scope="row">w_k₃（当前位置）</th><td class="mono num">0.5</td><td class="mono num">0.125</td><td class="mono num acc">0.375</td></tr>
<tr><th scope="row">W_AA</th><td class="mono num">0.5</td><td class="mono num">0.330</td><td class="mono num acc">0.170</td></tr>
<tr><th scope="row">W_AB</th><td class="mono num">0</td><td class="mono num">0.200</td><td class="mono num acc">−0.200</td></tr>
<tr><th scope="row">W_BA</th><td class="mono num">0</td><td class="mono num">−0.330</td><td class="mono num acc">0.330</td></tr>
<tr><th scope="row">W_BB</th><td class="mono num">0.5</td><td class="mono num">−0.200</td><td class="mono num acc">0.700</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">梯度保留三位小数，新值按未舍入的梯度算。</p>
</div>
<figcaption>表 2 · 一步更新：十个参数里有九个动了，w_q₂ 的梯度是 0，原地不动</figcaption>
</figure>

用新参数把 BA 重新算一遍：注意力 $a_1$ 从 0.378 升到 0.473，$p_B$ 从 0.469 升到 0.625，损失从 0.756 降到 0.470。模型对 BA 的答案翻了过来，从说 A 变成了说 B。

### 现象：BA 变好了，别的样本变差了

但再看看另外三个样本（表 3）。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-side-title">按 BA 走了一步之后，四个样本的损失</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="gd-side-title">
<thead>
<tr><th scope="col">样本</th><th scope="col" class="num">走之前</th><th scope="col" class="num">走之后</th></tr>
</thead>
<tbody>
<tr><th scope="row">AA → A</th><td class="mono num">0.474</td><td class="mono num bad">0.777</td></tr>
<tr><th scope="row">AB → A</th><td class="mono num">0.756</td><td class="mono num bad">1.061</td></tr>
<tr><th scope="row">BA → B</th><td class="mono num">0.756</td><td class="mono num good">0.470</td></tr>
<tr><th scope="row">BB → B</th><td class="mono num">0.474</td><td class="mono num good">0.341</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">BA 自己：a₁ 从 0.378 升到 0.472，p_B 从 0.469 升到 0.625。</p>
</div>
<figcaption>表 3 · 只照顾一个样本：BA 的损失降了，AA 和 AB 反而变差，模型学到的主要是“多说 B”</figcaption>
</figure>

AA 的损失从 0.474 涨到 0.777，答案也从对变成了错。原因要回到表 2：变化最大的不是注意力的参数，而是输出层。对 BA 这一个样本来说，最快的下坡路是“不管看到什么，都多说 B”：B 那一行的两个参数调大，A 那一行调小。这条路比把注意力扭过来陡得多（输出层的偏导数是 0.330、0.200，$w_{k3}$ 只有 0.125），可它对 AA、AB 是坏事。

一个样本算出来的坡度，只是这一个样本的坡度。**怎样让一步对所有样本都有好处？**

## 七、一批样本：把梯度平均

### 四个样本各说各的

训练真正要降的是四个样本的平均损失。平均数的导数就是导数的平均，所以把四个样本的偏导数各自算出来，再取平均就行。把它们并排放在一起（表 4），能看出各个样本在“想”什么。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-batch-title">初始参数下，四个样本各自的梯度和它们的平均</div>
<div class="dt-scroll">
<table class="dt dt-sticky" aria-labelledby="gd-batch-title">
<colgroup><col style="width:24%"><col><col><col><col><col class="hl"></colgroup>
<thead>
<tr><th scope="col">参数</th><th scope="col" class="num">AA</th><th scope="col" class="num">AB</th><th scope="col" class="num">BA</th><th scope="col" class="num">BB</th><th scope="col" class="num acc">平均</th></tr>
</thead>
<tbody>
<tr><th scope="row">w_q₁（A）</th><td class="mono num">0</td><td class="mono num">0</td><td class="mono num">0.062</td><td class="mono num">0</td><td class="mono num acc">0.016</td></tr>
<tr><th scope="row">w_q₂（B）</th><td class="mono num">0</td><td class="mono num">0.062</td><td class="mono num">0</td><td class="mono num">0</td><td class="mono num acc">0.016</td></tr>
<tr><th scope="row">w_q₃（当前位置）</th><td class="mono num">0</td><td class="mono num">0.062</td><td class="mono num">0.062</td><td class="mono num">0</td><td class="mono num acc">0.031</td></tr>
<tr><th scope="row">w_k₁（A）</th><td class="mono num">0</td><td class="mono num">−0.125</td><td class="mono num">0.125</td><td class="mono num">0</td><td class="mono num acc">0</td></tr>
<tr><th scope="row">w_k₂（B）</th><td class="mono num">0</td><td class="mono num">0.125</td><td class="mono num">−0.125</td><td class="mono num">0</td><td class="mono num acc">0</td></tr>
<tr><th scope="row">w_k₃（当前位置）</th><td class="mono num">0</td><td class="mono num">0.125</td><td class="mono num">0.125</td><td class="mono num">0</td><td class="mono num acc">0.062</td></tr>
<tr><th scope="row">W_AA</th><td class="mono num">−0.378</td><td class="mono num">−0.200</td><td class="mono num">0.330</td><td class="mono num">0</td><td class="mono num acc">−0.062</td></tr>
<tr><th scope="row">W_AB</th><td class="mono num">0</td><td class="mono num">−0.330</td><td class="mono num">0.200</td><td class="mono num">0.378</td><td class="mono num acc">0.062</td></tr>
<tr><th scope="row">W_BA</th><td class="mono num">0.378</td><td class="mono num">0.200</td><td class="mono num">−0.330</td><td class="mono num">0</td><td class="mono num acc">0.062</td></tr>
<tr><th scope="row">W_BB</th><td class="mono num">0</td><td class="mono num">0.330</td><td class="mono num">−0.200</td><td class="mono num">−0.378</td><td class="mono num acc">−0.062</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">AA 和 BB 两个字一样，看哪个位置取到的都是同一个字，所以对 w_q、w_k 的梯度全是 0。</p>
</div>
<figcaption>表 4 · 四个样本各说各的：AB 和 BA 对 w_k₁、w_k₂ 的意见正好相反，平均后抵消；只有 w_k₃ 上大家意见一致</figcaption>
</figure>

AA 和 BB 对 6 个注意力参数的偏导数全是 0：两个字一样，看哪里都一样，它们对注意力没有意见，只希望输出层更果断地“看到什么就说什么”。

AB 和 BA 在 $w_{k1}$、$w_{k2}$ 上意见正好相反。BA 希望 A 的牌小一点、B 的牌大一点（这样会多看第一个字 B），AB 希望的正好反过来。平均后两者抵消成 0，这是好事：“A 的牌比 B 小”并不是这个任务的规律，只照顾一个样本才会学到它。

它们在 $w_{k3}$ 上意见一致，都是 +0.125，平均后是 0.062。四个样本唯一的共识是：**把“我自己”这张牌压低**，让模型少看自己。这正是这个任务真正的规律。

### 平均之后走一步

用平均梯度走一步（学习率还是 1）：$w_{k3}$ 从 0.5 降到 0.438，$W_{AA}$、$W_{BB}$ 从 0.5 升到 0.562，另外两个从 0 降到 −0.062。四个样本里，AA 和 BB 的损失从 0.474 降到 0.429，AB 和 BA 却从 0.756 微微涨到 0.759，平均损失从 0.615 降到 0.594，这就是开头的第二个数。

为什么难的两个样本反而略差？因为这一步主要加强的是输出层的“照抄”：看到什么就更果断地说什么。可 AB、BA 这时看的还主要是自己（$a_1$ 只有 0.397），照抄得越果断，错得越果断。**平均梯度保证的是平均损失下降，不保证每个样本都变好。**

真实训练的样本有成千上万，不可能每一步都全部算一遍，于是每一步随机抽一小批来算平均梯度，这叫随机梯度下降。一小批的平均只是全体平均的估计，有点偏差，但方向大体是对的，代价却小得多。

## 八、多步：从平台到收敛

### 现象：难的样本先停在原地，然后突然下降

一直这样走下去，前 40 步的情况画在图 5 里。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 440" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-curve-title gd-curve-desc">
<title id="gd-curve-title">四十步训练：损失和注意力</title>
<desc id="gd-curve-desc">学习率 1、四个样本一起训练。前 6 步 AA、BB 的损失先降，AB、BA 几乎不动；注意力 a₁ 越过 0.5 以后，AB、BA 的损失迅速下降，40 步时降到 0.032。</desc>
<defs>
<marker id="gd-curve-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-curve-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="440" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="196" rx="8"/>
<text class="t-zone" x="32" y="44">损失 · 学习率 1，四个样本一起算梯度</text>
<rect class="dd-zone" x="16" y="228" width="768" height="196" rx="8"/>
<text class="t-zone" x="32" y="256">样本 BA 的注意力 a₁：放在第一个字上的比例</text>
<line class="dd-hair" x1="96" y1="188" x2="744" y2="188"/>
<text class="t-note" x="84" y="192" text-anchor="end">0</text>
<line class="dd-hair" x1="96" y1="126" x2="744" y2="126"/>
<text class="t-note" x="84" y="130" text-anchor="end">0.4</text>
<line class="dd-hair" x1="96" y1="64" x2="744" y2="64"/>
<text class="t-note" x="84" y="68" text-anchor="end">0.8</text>
<line class="dd-hair" x1="96" y1="400" x2="744" y2="400"/>
<text class="t-note" x="84" y="404" text-anchor="end">0</text>
<line class="dd-hair" x1="96" y1="338" x2="744" y2="338"/>
<text class="t-note" x="84" y="342" text-anchor="end">0.5</text>
<line class="dd-hair" x1="96" y1="276" x2="744" y2="276"/>
<text class="t-note" x="84" y="280" text-anchor="end">1</text>
<text class="t-note" x="96" y="420" text-anchor="middle">0</text>
<text class="t-note" x="258" y="420" text-anchor="middle">10</text>
<text class="t-note" x="420" y="420" text-anchor="middle">20</text>
<text class="t-note" x="582" y="420" text-anchor="middle">30</text>
<text class="t-note" x="744" y="420" text-anchor="middle">40</text>
<text class="t-note" x="744" y="420" text-anchor="end" dx="40">步</text>
<line class="dd-edge dd-dash" x1="193.2" y1="64" x2="193.2" y2="400"/>
<path class="dd-edge" style="stroke-width:2" d="M96.0 114.5 L112.2 121.5 L128.4 127.7 L144.6 133.4 L160.8 138.6 L177.0 143.4 L193.2 147.7 L209.4 151.7 L225.6 155.3 L241.8 158.7 L258.0 161.8 L274.2 164.7 L290.4 167.4 L306.6 169.7 L322.8 171.7 L339.0 173.4 L355.2 174.7 L371.4 175.9 L387.6 176.8 L403.8 177.6 L420.0 178.3 L436.2 179.0 L452.4 179.5 L468.6 180.0 L484.8 180.4 L501.0 180.8 L517.2 181.1 L533.4 181.4 L549.6 181.7 L565.8 182.0 L582.0 182.2 L598.2 182.4 L614.4 182.6 L630.6 182.8 L646.8 183.0 L663.0 183.2 L679.2 183.3 L695.4 183.5 L711.6 183.6 L727.8 183.7 L744.0 183.9"/>
<path class="dd-edge-acc" style="stroke-width:2.4" d="M96.0 70.8 L112.2 70.3 L128.4 71.0 L144.6 72.6 L160.8 75.0 L177.0 77.9 L193.2 81.5 L209.4 86.0 L225.6 91.8 L241.8 99.6 L258.0 109.9 L274.2 122.6 L290.4 135.8 L306.6 147.1 L322.8 155.4 L339.0 161.3 L355.2 165.4 L371.4 168.5 L387.6 170.8 L403.8 172.6 L420.0 174.0 L436.2 175.2 L452.4 176.2 L468.6 177.0 L484.8 177.8 L501.0 178.4 L517.2 179.0 L533.4 179.5 L549.6 179.9 L565.8 180.3 L582.0 180.7 L598.2 181.0 L614.4 181.3 L630.6 181.6 L646.8 181.8 L663.0 182.1 L679.2 182.3 L695.4 182.5 L711.6 182.7 L727.8 182.8 L744.0 183.0"/>
<path class="dd-edge" style="stroke-width:2" d="M96.0 353.2 L112.2 350.7 L128.4 348.0 L144.6 345.2 L160.8 342.5 L177.0 339.9 L193.2 337.4 L209.4 334.7 L225.6 331.6 L241.8 327.6 L258.0 322.5 L274.2 316.0 L290.4 308.9 L306.6 302.3 L322.8 297.1 L339.0 293.2 L355.2 290.4 L371.4 288.4 L387.6 286.8 L403.8 285.7 L420.0 284.7 L436.2 283.9 L452.4 283.3 L468.6 282.8 L484.8 282.3 L501.0 281.9 L517.2 281.6 L533.4 281.3 L549.6 281.0 L565.8 280.8 L582.0 280.6 L598.2 280.4 L614.4 280.2 L630.6 280.1 L646.8 279.9 L663.0 279.8 L679.2 279.7 L695.4 279.6 L711.6 279.5 L727.8 279.4 L744.0 279.3"/>
<text class="t-acc t-mono" x="102" y="91">AB、BA</text>
<text class="t-mono" x="102" y="145">AA、BB</text>
<text class="t-acc t-mono" x="744" y="173" text-anchor="end">0.032</text>
<text class="t-mono" x="116" y="373">0.378</text>
<text class="t-mono" x="744" y="299" text-anchor="end">0.973</text>
<text class="t-say" x="203" y="362">第 6 步，a₁ 越过一半；此时 w_k₃ 已从 0.5 降到 −0.028</text>
</svg>
</div>
<figcaption>图 5 · 前几步只学会了“照抄看到的字”，等注意力转到第一个字上，难的两个样本才开始降</figcaption>
</figure>

前 6 步，AA、BB 的损失从 0.474 降到 0.260，AB、BA 却几乎原地不动，0.756、0.759、0.755……到第 6 步才到 0.687。这段时间里并非什么都没发生：$w_{k3}$ 每一步都在往下走，0.5、0.438、0.362、0.275……第 6 步降到 −0.028，变成负数；注意力 $a_1$ 跟着从 0.378 慢慢爬到 0.505，越过了一半。

越过一半之后，情况反过来了。模型看到的东西里第一个字占了多数，输出层那份“照抄”的果断从帮倒忙变成了帮忙，两件事互相放大：第 10 步 AB、BA 的损失是 0.504，第 16 步已经是 0.146。开头那串数里，第 10 步 0.336、第 20 步 0.076，降得最快的就是这一段。

再往后，曲线变得很平。10 个偏导数整体的大小（平方和再开方）在第 11 步最大，是 0.228；到第 50 步只有 0.023，第 200 步只剩 0.005。坡越来越缓，每一步挪得越来越少，就像热身里越靠近谷底步子越小。损失从 0.030（第 40 步）慢慢挪到 0.005（第 200 步）。走到这样一片坡度很小、每步几乎不动的地方，就叫**收敛**。

### 收敛到了什么

走了 200 步以后，$w_{k3} = -1.94$，$w_{q1}$、$w_{q2}$ 约 0.99，$w_{q3}$ 约 1.48，$W_{AA}$、$W_{BB}$ 是 2.91，另外两个是 −2.41。

把它们代回模型就能读出它学会的规则。查询 $q = 0.99 + 1.48 = 2.47$，是个正数；第一个字的牌是 0，第二个字因为带着“我自己”标记，牌是 −1.94。分数一个是 0、一个是 $2.47 \times (-1.94) = -4.8$，softmax 之后 $a_1 = 0.992$。也就是说，模型学会了**“别看自己，看前一个字”**，再用输出层把看到的字照抄出来。这条规则没有人写过，是四个样本的梯度一步步推出来的。

损失永远不会恰好是 0：要 $p$ 正好等于 1 才行，而 softmax 给不出正好的 1，参数只能越来越大地逼近它。真实训练也不会等到“完全收敛”，而是在预算用完、或者在没见过的数据上的成绩不再变好时停下。

走到的这个地方也不是唯一的。我们换掉手挑的初值，让 10 个参数从随机数出发，跑了 100 次，每次都在 20 到 37 步之内把平均损失降到 0.05 以下，一次也没有失败；可最后的参数各不相同，光是 $w_{k3}$ 就散布在 −1.82 到 1.77 之间。$w_{k3}$ 是正数的那些解，查询恰好是负数，负负得正，同样做到了“别看自己”。梯度下降找到的不是“那个”最优解，而是一大片同样好的解里离起点比较近的一个[^notes]。

**还有一个数是我们随手定的：学习率为什么取 1？**

## 九、学习率：步子迈多大

### 现象：大了更快，再大就卡死

同一个起点，换四种学习率各跑 60 步（图 6）。

<figure class="dd dd-figure">
<div class="dd-scroll">
<svg class="dd-svg" viewBox="0 0 800 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="gd-lr-title gd-lr-desc">
<title id="gd-lr-title">学习率对训练的影响</title>
<desc id="gd-lr-desc">学习率 0.1 时损失下降很慢，60 步后还有 0.469；学习率 1 约 20 步降到 0.08；学习率 10 三步就降到 0.031；学习率 30 第三步跳到 3.792，之后停在 3.7 左右不再下降。</desc>
<defs>
<marker id="gd-lr-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head" d="M1 1 L7 4 L1 7"/></marker>
<marker id="gd-lr-arrow-acc" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path class="dd-head-acc" d="M1 1 L7 4 L1 7"/></marker>
</defs>
<rect class="dd-frame" width="800" height="360" rx="10"/>
<rect class="dd-zone" x="16" y="16" width="768" height="328" rx="8"/>
<text class="t-zone" x="32" y="44">四个样本的平均损失 · 同一个起点，四种学习率</text>
<line class="dd-hair" x1="96" y1="284" x2="744" y2="284"/>
<text class="t-note" x="84" y="288" text-anchor="end">0</text>
<line class="dd-hair" x1="96" y1="178" x2="744" y2="178"/>
<text class="t-note" x="84" y="182" text-anchor="end">0.5</text>
<line class="dd-hair" x1="96" y1="72" x2="744" y2="72"/>
<text class="t-note" x="84" y="76" text-anchor="end">1</text>
<text class="t-note" x="96" y="304" text-anchor="middle">0</text>
<text class="t-note" x="204" y="304" text-anchor="middle">10</text>
<text class="t-note" x="312" y="304" text-anchor="middle">20</text>
<text class="t-note" x="420" y="304" text-anchor="middle">30</text>
<text class="t-note" x="528" y="304" text-anchor="middle">40</text>
<text class="t-note" x="636" y="304" text-anchor="middle">50</text>
<text class="t-note" x="744" y="304" text-anchor="middle">60</text>
<text class="t-note" x="420" y="326" text-anchor="middle">步</text>
<path class="dd-edge" style="stroke-width:2" d="M96.0 153.6 L106.8 154.0 L117.6 154.5 L128.4 154.9 L139.2 155.4 L150.0 155.8 L160.8 156.3 L171.6 156.7 L182.4 157.2 L193.2 157.6 L204.0 158.1 L214.8 158.6 L225.6 159.0 L236.4 159.5 L247.2 160.0 L258.0 160.5 L268.8 161.0 L279.6 161.4 L290.4 161.9 L301.2 162.4 L312.0 162.9 L322.8 163.4 L333.6 163.9 L344.4 164.4 L355.2 164.9 L366.0 165.4 L376.8 165.9 L387.6 166.5 L398.4 167.0 L409.2 167.5 L420.0 168.0 L430.8 168.5 L441.6 169.1 L452.4 169.6 L463.2 170.1 L474.0 170.6 L484.8 171.2 L495.6 171.7 L506.4 172.2 L517.2 172.8 L528.0 173.3 L538.8 173.8 L549.6 174.4 L560.4 174.9 L571.2 175.5 L582.0 176.0 L592.8 176.6 L603.6 177.1 L614.4 177.7 L625.2 178.2 L636.0 178.8 L646.8 179.3 L657.6 179.9 L668.4 180.5 L679.2 181.0 L690.0 181.6 L700.8 182.2 L711.6 182.8 L722.4 183.4 L733.2 184.0 L744.0 184.6"/>
<path class="dd-edge dd-dash" style="stroke-width:2" d="M96.0 153.6 L106.8 196.3 L117.6 251.3 L128.4 277.4 L139.2 279.1 L150.0 280.1 L160.8 280.7 L171.6 281.2 L182.4 281.5 L193.2 281.8 L204.0 282.0 L214.8 282.2 L225.6 282.3 L236.4 282.5 L247.2 282.6 L258.0 282.7 L268.8 282.7 L279.6 282.8 L290.4 282.9 L301.2 282.9 L312.0 283.0 L322.8 283.0 L333.6 283.1 L344.4 283.1 L355.2 283.2 L366.0 283.2 L376.8 283.2 L387.6 283.2 L398.4 283.3 L409.2 283.3 L420.0 283.3 L430.8 283.3 L441.6 283.4 L452.4 283.4 L463.2 283.4 L474.0 283.4 L484.8 283.4 L495.6 283.4 L506.4 283.5 L517.2 283.5 L528.0 283.5 L538.8 283.5 L549.6 283.5 L560.4 283.5 L571.2 283.5 L582.0 283.5 L592.8 283.6 L603.6 283.6 L614.4 283.6 L625.2 283.6 L636.0 283.6 L646.8 283.6 L657.6 283.6 L668.4 283.6 L679.2 283.6 L690.0 283.6 L700.8 283.6 L711.6 283.6 L722.4 283.6 L733.2 283.6 L744.0 283.7"/>
<path class="dd-edge-acc" style="stroke-width:2.4" d="M96.0 153.6 L106.8 158.0 L117.6 162.7 L128.4 167.7 L139.2 172.9 L150.0 178.2 L160.8 183.6 L171.6 189.4 L182.4 195.8 L193.2 203.5 L204.0 212.7 L214.8 223.3 L225.6 234.2 L236.4 243.5 L247.2 250.6 L258.0 255.7 L268.8 259.5 L279.6 262.3 L290.4 264.6 L301.2 266.4 L312.0 267.8 L322.8 269.1 L333.6 270.1 L344.4 271.0 L355.2 271.8 L366.0 272.5 L376.8 273.1 L387.6 273.7 L398.4 274.2 L409.2 274.6 L420.0 275.0 L430.8 275.4 L441.6 275.7 L452.4 276.1 L463.2 276.4 L474.0 276.6 L484.8 276.9 L495.6 277.1 L506.4 277.3 L517.2 277.5 L528.0 277.7 L538.8 277.9 L549.6 278.1 L560.4 278.3 L571.2 278.4 L582.0 278.6 L592.8 278.7 L603.6 278.8 L614.4 279.0 L625.2 279.1 L636.0 279.2 L646.8 279.3 L657.6 279.4 L668.4 279.5 L679.2 279.6 L690.0 279.7 L700.8 279.8 L711.6 279.9 L722.4 279.9 L733.2 280.0 L744.0 280.1"/>
<path class="dd-edge" style="stroke-width:1.6" d="M96.0 153.6 L106.8 132.7 L117.6 90.3 L128.4 72.0 L139.2 72.0 L150.0 72.0 L160.8 72.0 L171.6 72.0 L182.4 72.0 L193.2 72.0 L204.0 72.0 L214.8 72.0 L225.6 72.0 L236.4 72.0 L247.2 72.0 L258.0 72.0 L268.8 72.0 L279.6 72.0 L290.4 72.0 L301.2 72.0 L312.0 72.0 L322.8 72.0 L333.6 72.0 L344.4 72.0 L355.2 72.0 L366.0 72.0 L376.8 72.0 L387.6 72.0 L398.4 72.0 L409.2 72.0 L420.0 72.0 L430.8 72.0 L441.6 72.0 L452.4 72.0 L463.2 72.0 L474.0 72.0 L484.8 72.0 L495.6 72.0 L506.4 72.0 L517.2 72.0 L528.0 72.0 L538.8 72.0 L549.6 72.0 L560.4 72.0 L571.2 72.0 L582.0 72.0 L592.8 72.0 L603.6 72.0 L614.4 72.0 L625.2 72.0 L636.0 72.0 L646.8 72.0 L657.6 72.0 L668.4 72.0 L679.2 72.0 L690.0 72.0 L700.8 72.0 L711.6 72.0 L722.4 72.0 L733.2 72.0 L744.0 72.0"/>
<text class="t-mono" x="744" y="175" text-anchor="end">0.1：60 步时还有 0.469</text>
<text class="t-acc t-mono" x="226" y="238" dx="10">1：20 步到 0.076</text>
<text class="t-mono" x="118" y="242" dx="10">10</text>
<text class="t-mono" x="744" y="92" text-anchor="end">30：第 3 步跳到 3.792，之后一直卡在 3.7 上下（超出纵轴）</text>
</svg>
</div>
<figcaption>图 6 · 步子太小走得慢，步子太大会一步跨过山谷，落到坡度为 0 的平地上再也走不回来</figcaption>
</figure>

0.1 太小，60 步后平均损失还有 0.469，要两百步左右才能到 0.08。学习率 10 反而最快，3 步就降到 0.031。可一旦加到 30，事情就坏了：第 1 步损失从 0.615 涨到 0.714，第 2 步 0.914，第 3 步跳到 3.79，然后再也下不来。

### 原因：一步跨过了谷底

热身里已经见过前一半：步子太大，就会跨过谷底，跳到另一边去。这里还多了一件更糟的事。

学习率 30 的第 2 步，$w_{k3}$ 一下变成 7.56，三个查询参数变成 15.2、15.2、30.0。这时 BA 第二个位置的分数是 341，第一个位置是 0，softmax 给出的比例是 $(0, 1)$：模型只看自己。

还记得反向传播第四步里的那个倍数 $a_1 a_2$ 吗？现在它是 0。损失再怎么变，传到分数这里都乘上了 0，6 个注意力参数再也收不到任何信号，注意力永远转不回来。剩下的输出层只能看到第二个字，而第二个字和答案无关，它最好的做法是两个字各给一半概率；可是学习率太大，它每一步都冲过头，在两边来回翻，损失就停在 3.7 上下。

这也是大模型训练里很多做法的来由。GPT-3 的学习率只有 0.00006，开始时还要从 0 慢慢升上去，后面再逐渐降低；偏导数整体太大时，还会按比例缩小再用[^gpt3]。这些手段都是为了不让某一步迈得太远，掉进坡度为 0、再也走不出来的地方。

## 十、放大到真实的 Transformer

我们的模型和 GPT-3 走的是同一套流程：前向算损失，反向算梯度，减一步，重复。差别在下面这张表里。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-scale-title">本文的模型与 GPT-3</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="gd-scale-title">
<colgroup><col style="width:22%"><col><col class="hl"></colgroup>
<thead>
<tr><th scope="col">项目</th><th scope="col">本文</th><th scope="col" class="acc">GPT-3</th></tr>
</thead>
<tbody>
<tr><th scope="row">参数</th><td class="mono">10</td><td class="mono acc">1750 亿</td></tr>
<tr><th scope="row">结构</th><td>一层注意力，查询和键各是 1 个数</td><td>96 层，每层 96 组注意力，查询和键各是 128 个数</td></tr>
<tr><th scope="row">其他部件</th><td>没有</td><td>每层还有一个小网络和几处帮助训练稳定的结构；字怎样变成数也参与训练</td></tr>
<tr><th scope="row">一步用的题目</th><td class="mono">4 道</td><td class="mono">约 320 万道</td></tr>
<tr><th scope="row">训练数据</th><td>4 道题反复用</td><td>约 3000 亿个词元</td></tr>
<tr><th scope="row">求梯度</th><td>手算，再用工具核对</td><td>工具自动做反向传播</td></tr>
<tr><th scope="row">更新规则</th><td>参数减去学习率乘偏导数</td><td>同样的思路，但每个参数有自己的步长</td></tr>
<tr><th scope="row">学习率</th><td class="mono">1</td><td class="mono">0.00006</td></tr>
</tbody>
</table>
</div>
<p class="dt-note">GPT-3 的数字取自论文表 2.1 和附录 B。词元是模型处理文字的单位，一个词元大致是一个英文单词或一两个汉字。</p>
</div>
<figcaption>表 5 · 参数差了十个数量级，每一步做的事没有变</figcaption>
</figure>

几处差别值得多说一句。

**每个位置都是一道题。** 我们的模型只在第二个位置答题；语言模型在每个位置都猜下一个词元，一段 2048 个词元的文字就是 2048 道题，损失是所有题目的平均。所以“一步 320 万道题”就是一步平均了这么多道题的梯度。用 3000 亿除以 320 万粗算，GPT-3 一共走了九万多步。

**多出来的部件，大多是为了让梯度传得动。** 我们的梯度往回只走了六步，96 层的模型要往回走几百步，一路连乘，很容易乘到接近 0，或者大得离谱。所以真实的 Transformer 给梯度留了一条可以直通的近路，还在每层把数值拉回固定范围，FPGA 那篇讲过这一点。至于每一步反向传播本身，做的仍然是第五节那几件事：求导，乘上传回来的导数。

**每个参数有自己的步长。** 我们的做法里，10 个参数共用一个学习率，偏导数大的步子就大；输出层就是因此先跑在了注意力前面。GPT-3 用的方法叫 Adam，它记着每个参数最近的偏导数有多大，按这个调整各自的步长，让偏导数一直很小的参数也能走得动[^adam]。所以它的学习率和我们这里的“1”不能直接比大小。

## 十一、与现实中的优化问题相比

### 现象：现实里找最优解很难，这里却很顺

现实世界里，找一个问题的最优解往往很难。一个面包配方要烤出来尝了才知道好不好，一种药要做完实验才知道有没有效，一张课表的排法多到排不完。可在 Transformer 里，1750 亿个参数，靠热身里那个简单的规则，一步步就走到了很低的地方。为什么这里就这么顺？

先要纠正一点：Transformer 找到的并不是最优解。第八节的 100 次随机起点各自停在了不同的地方；GPT-3 训练结束时的损失也远不是 0。训练要的只是“足够低”，而足够低的地方有很多。不过现实中很多问题也只要求够好，就算只求够好依然很难。所以差别不在目标，而在找解的条件上。

### 原因：坡度是不是现成的

第四节那个“挨个动一下”的办法，其实就是现实中找解的常态。不知道配方和口味之间的公式，就只能改一点、烤一炉、尝一口，**只看结果，不知道坡度**，这叫黑箱优化。Transformer 的损失是我们自己写出来的公式，每一步运算都能求导，反向传播一趟就把所有参数的坡度一起算出来。

这个差别在小问题上看不出来。我们让一个只看损失值的随机搜索去解本文那个 10 个参数的问题：每次随机改几个参数，变好就留下，变差就退回。从同一个初值出发跑 20 次，中间那一次用了 77 次计算，就把平均损失降到 0.05 以下；梯度下降要 27 步，每步一次前向加一次反向，大约相当于 81 次前向。两者差不多。

把同一个任务放大，差距才显出来（表 6）。字从 2 个加到 64 个，序列也一样长，参数从 36 个增加到 5120 个。随机搜索这一边换成更聪明的版本，步子会根据成败自动调整；梯度这一边用 Adam。

<figure class="dd dd-figure dt-figure">
<div class="dt-panel">
<div class="dt-title" id="gd-scale5-title">把平均损失降到 0.05 以下要多少次计算 · 每格是不同随机种子的结果</div>
<div class="dt-scroll">
<table class="dt" aria-labelledby="gd-scale5-title">
<colgroup><col style="width:16%"><col><col><col class="hl"></colgroup>
<thead>
<tr><th scope="col">参数个数</th><th scope="col" class="num">梯度：步数</th><th scope="col" class="num">梯度：折合前向次数</th><th scope="col" class="num acc">黑箱：评估次数</th></tr>
</thead>
<tbody>
<tr><th scope="row" class="mono">36</th><td class="mono num">32 / 29 / 37</td><td class="mono num">96 / 87 / 111</td><td class="mono num acc">55 / 37 / 28</td></tr>
<tr><th scope="row" class="mono">80</th><td class="mono num">48 / 47 / 43</td><td class="mono num">144 / 141 / 129</td><td class="mono num acc">180 / 97 / 96</td></tr>
<tr><th scope="row" class="mono">192</th><td class="mono num">69 / 69 / 65</td><td class="mono num">207 / 207 / 195</td><td class="mono num acc">337 / 356 / 393</td></tr>
<tr><th scope="row" class="mono">512</th><td class="mono num">88 / 87 / 86</td><td class="mono num">264 / 261 / 258</td><td class="mono num acc">648 / 685 / 844</td></tr>
<tr><th scope="row" class="mono">1,536</th><td class="mono num">137 / 105 / 195</td><td class="mono num">411 / 315 / 585</td><td class="mono num acc">3,695 / 1,482 / <span class="bad">失败</span></td></tr>
<tr><th scope="row" class="mono">5,120</th><td class="mono num">368 / 243</td><td class="mono num">1,104 / 729</td><td class="mono num acc"><span class="bad">失败</span> / <span class="bad">失败</span></td></tr>
</tbody>
</table>
</div>
<p class="dt-note">梯度一步按 3 次前向折算。黑箱上限 30 万次评估，“失败”是用完 30 万次仍未达标。</p>
</div>
<figcaption>表 6 · 参数越多，只看结果的搜索越吃力；梯度一步拿到所有参数的方向，步数涨得很慢</figcaption>
</figure>

只有 36 个参数时，随机搜索反而比梯度省；到 192 个参数，它已经要多花一倍；到 5120 个参数时，梯度两次分别用了 368 步和 243 步，随机搜索两次都用完了 30 万次计算，损失停在 0.667 和 0.113，没有达标。梯度需要的步数随参数增加涨得很慢，因为每一步都同时拿到所有参数的方向；随机搜索每次只能知道一个“好还是不好”，参数越多，碰巧改对的机会越小，需要的次数大致跟着参数个数涨，还会卡在半路下不来。照这个趋势推到 1750 亿个参数，随机搜索毫无希望。现实中的问题还要再乘上一次试验的代价：这里一次前向不到一毫秒，可烤一炉面包要一小时，做一轮药物实验要几个月。

### 另外两个条件：试错便宜，参数连续

**试错便宜、可重复、能撤回。** 一次前向在电脑里算，结果完全确定，算错了把参数改回去就行，可以重复亿万次。现实中的试错有误差，要花时间和钱，有的还不可逆。这并不是说训练不花代价：GPT-3 的训练用了约 3×10²³ 次浮点运算，只是这些代价交给了机器，而且能拆成无数个可以同时进行的小步骤。

**参数是连续的，好解连成一片。** 课表、路线、电路开关这类问题，变量只能一格一格地换，换一格结果就跳一下，坡度无从谈起，FPGA 那篇讲的就是这件事。Transformer 的参数可以取任意小数，每一步运算都能求导，损失随参数平滑变化；参数又远多于“恰好够用”，前面 100 次随机起点都能走到低处，靠的就是这一点。

### 反过来看

现实中的问题一旦凑齐这些条件，同样可以用梯度来解。工程师设计机翼、桥梁结构或者光学镜片的形状时，会把物理过程写成能求导的计算，一次算出目标对成千上万个设计参数的导数，它和反向传播是同一类计算。

Transformer 里凑不齐这些条件的部分，也就和现实一样难。模型要多少层、学习率取多少、训练数据怎么搭配，这些都没有导数可求，只能一次一次地试，而试一次就是完整训练一个模型。本文的学习率 1、10、30 就是试出来的；GPT-3 的论文同时训练了 8 个大小不同的模型[^gpt3]，大模型团队普遍先在小模型上把这些选择试好，再放大到大模型。

所以，不是 Transformer 里的问题天生简单，而是人们把它造成了梯度能解的样子：损失写得出公式，每一步都能求导，计算便宜到可以重复亿万次。难的部分并没有消失，它转到了“怎样把问题造成这个样子”上。

## 十二、回到开头

现在可以把开头那串数逐行读一遍了。它就是下面这段代码跑出来的，同样可以跳过代码，只看后面的解释[^notes]：

```python
import torch
emb = {"A": [1., 0, 0], "B": [0, 1., 0]}
X = torch.tensor([[emb[s[0]], [a + b for a, b in zip(emb[s[1]], [0, 0, 1.])]]
                  for s in ["AA", "AB", "BA", "BB"]])
Y = torch.tensor([0, 0, 1, 1])                       # 目标：第一个字

wq = torch.tensor([0.5, 0.5, 0.5], requires_grad=True)
wk = torch.tensor([0.0, 0.0, 0.5], requires_grad=True)
Wo = torch.tensor([[0.5, 0.0], [0.0, 0.5]], requires_grad=True)
opt = torch.optim.SGD([wq, wk, Wo], lr=1.0)

def model(X):
    q = X[:, 1] @ wq                                 # 每个样本一个查询（第 2 个位置）
    k = X @ wk                                       # 每个位置一个键
    a = torch.softmax(q[:, None] * k, dim=1)         # 注意力比例
    h = (a[:, :, None] * X[:, :, :2]).sum(1)         # 按比例混合看到的字
    return h @ Wo.T                                  # 两个答案的分数

for step in range(41):
    loss = torch.nn.functional.cross_entropy(model(X), Y)
    if step in (0, 1, 2, 3, 10, 20, 40):
        print(f"step {step:2d}  loss {loss.item():.3f}")
    opt.zero_grad()
    loss.backward()
    opt.step()
```

第 0 步的 0.615，是四个样本损失 0.474、0.756、0.756、0.474 的平均。`loss.backward()` 算出的是表 4 最后一列那 10 个平均偏导数，`opt.step()` 把它们乘上学习率 1 从参数里减掉，于是第 1 步变成 0.594，其中 AA、BB 变好了，AB、BA 还略差一点。第 2、3 步的 0.572、0.548 降得慢，是因为注意力还在慢慢转，出力的主要是输出层。第 10 步的 0.336 和第 20 步的 0.076 之间降得最快，那是注意力越过一半之后，“看对地方”和“照抄看到的字”互相放大的阶段。第 40 步的 0.030 之后，坡已经很缓，模型基本学会了“看前一个字”。

## 小结

1. **训练在做什么？** 把一个叫损失的数拧小。损失要对参数的每一点变化都有反应，才算得出坡度：“答对几道”不行，交叉熵 $-\ln p_{\text{正确答案}}$ 可以。
2. **梯度下降是什么？** 算出损失对每个参数的偏导数，也就是坡度，每个参数减去学习率乘它的偏导数，重复。一个旋钮时，它就是沿着抛物线一步步走到谷底。
3. **反向传播做了什么？** 从损失往回，把沿途每一层的变化倍数依次相乘，一趟算出所有参数的偏导数，不用挨个去试。最常用的一条：参数的偏导数等于传回来的导数乘上它乘着的输入，输入是 0，偏导数就是 0。
4. **为什么要一批样本一起算？** 只按一个样本走，会学到只对它有用的捷径；按一批样本的平均走，互相矛盾的意见抵消，剩下的是共同的规律。
5. **为什么要走很多步，曲线为什么先平后陡？** 坡度只在脚下可靠，所以每次只走一小步。注意力没转过来之前，难的样本几乎不动；越过一半以后，“看哪里”和“说什么”互相放大，损失迅速下降；之后坡越来越缓，这就是收敛。
6. **学习率为什么要小心？** 太小走得慢，太大会跨过谷底，甚至跳进坡度为 0 的地方再也出不来。
7. **为什么这里找解比现实中容易？** 找到的不是唯一的最优解，而是一大片好解里的一个；更关键的是，损失是写出来的公式，一趟反向传播就拿到所有参数的坡度，试错又便宜。只看结果的随机搜索，代价随参数个数上涨，到大模型的规模就完全走不动。现实问题凑齐这些条件也能用梯度解；Transformer 里凑不齐的部分，比如层数和学习率，也只能靠试。

## 参考资料
