吃透 Transformer

交互式教程 · 从零到完全掌握

吃透 Transformer

从「下一个字的概率」出发,一步步推到注意力、训练、推理成本,以及 Transformer 之后的新结构。每个公式都完整推导,每个结论都能在页面里动手验证。

$$P(x_{t+1}\mid x_1,\dots,x_t)$$

整个系列只围绕这一个条件概率:怎么算、怎么学、怎么算得更省。

第 1 课 / 共 7 课

从文字到概率:最小的语言模型

学完这一课,你能从零推导并亲手实现一个完整的 bigram 语言模型:嵌入 → logits → softmax → 交叉熵 → 梯度 → 参数更新;也会看清它为什么离不开上下文,从而需要注意力。

需要的数学:高中数学,加上大一的微积分(偏导数、链式法则)和线性代数(向量、矩阵乘法)。忘了也没关系,§0 末尾有速查。每节末尾有练习,答案默认收起。

文本 → 概率分布

§0全景:语言模型到底在算什么

你可能听过这样的说法:大模型对每个 token 都要「全量计算」,所以特别吃显存、吃电。这个系列会把这句话拆到每一次乘法上。第一步,先弄清楚模型到底在算什么。

一个语言模型只做一件事:读入前面的 token $x_1,\dots,x_t$,输出下一个 token 的概率分布

$$P(x_{t+1}\mid x_1,\dots,x_t).$$

「分布」的意思是:词表里的每个 token 都分到一个概率,全部加起来等于 1。生成文字时,从这个分布里抽一个 token,把它接到输入末尾,再预测下一个,如此循环。

整段文字的概率也由它决定。根据概率的乘法公式(链式法则),

$$P(x_1,x_2,\dots,x_T)=P(x_1)\,P(x_2\mid x_1)\,P(x_3\mid x_1,x_2)\cdots=\prod_{t=1}^{T}P(x_t\mid x_{\lt t}),$$

其中 $x_{\lt t}$ 是 $x_1,\dots,x_{t-1}$ 的简写($t=1$ 时为空)。所以「会预测下一个 token」就等于「会给任意一段文字打分」。

生成:把采到的 token 接到输入末尾,再算一遍(自回归,第 6 课细讲) 文本「我吃苹」字符串 token id0 1 2T = 3 个整数 嵌入向量X3 × d Transformer层 × N3×d → 3×d logitsZ3 × V 概率分布P,每行和为 13 × V 下一个 token「果」id 3 分词 查表 E × W_U softmax 采样 第 1 课:输入端 第 2–4 课 本课先让它直通 第 1 课:输出端 生成:接回输入,再算一遍 文本字符串「我吃苹」 token idT = 30 1 2 嵌入向量3 × dX Transformer3×d → 3×d层 × N logits3 × VZ 概率分布3 × VP,每行和为 1 下一个 tokenid 3「果」 分词 查表 E × W_U softmax 采样 第 1 课 第 2–4 课 直通 第 1 课
图 0-1 一次前向计算。方框是数据和它的形状(T:token 个数;d:每个向量的维数;V:词表大小),箭头上是对数据做的运算。蓝色部分由本课实现;虚线框是第 2–4 课的内容,本课先让它「直通」(原样输出),得到能训练的最小模型。外侧的回环就是生成文字的方式。图中的编号取自 §7 训练器的词表。

数学速查

向量
本系列把一个 token 的向量写成行向量 $x\in\mathbb{R}^{1\times d}$(和代码里的一维数组一致),也简写为 $x\in\mathbb{R}^d$;$x_k$ 是它的第 $k$ 个分量。
矩阵与形状
$A\in\mathbb{R}^{m\times n}$ 有 $m$ 行 $n$ 列,$A_{ij}$ 是第 $i$ 行第 $j$ 列的元素,$A_{i,:}$ 是第 $i$ 行,$A_{:,j}$ 是第 $j$ 列。转置 $(A^\top)_{ij}=A_{ji}$。乘积 $AB$ 只在 $A$ 的列数等于 $B$ 的行数时有定义:$(m\times n)(n\times p)\to m\times p$(§4 细讲)。
偏导数
多元函数 $f(x_1,\dots,x_n)$ 对 $x_j$ 的偏导数 $\partial f/\partial x_j$:把其他变量都当常数,只对 $x_j$ 求导。
链式法则
若 $L$ 依赖 $z_1,\dots,z_m$,而每个 $z_i$ 又依赖 $x_j$,则
$$\frac{\partial L}{\partial x_j}=\sum_{i}\frac{\partial L}{\partial z_i}\,\frac{\partial z_i}{\partial x_j}.$$
即 $x_j$ 的影响沿每一条路径传到 $L$,各路径的贡献相加。本课所有梯度都只用这一条公式。
记号
$\delta_{ij}$(克罗内克 δ)在 $i=j$ 时为 1,否则为 0;$\mathbf 1$ 是全 1 向量;$\log$ 一律指自然对数 $\ln$。token 的编号(id)从 0 开始,和代码一致,所以「$E$ 的第 $i$ 行」就是 id 为 $i$ 的那一行。

练习 §0

  1. 词表大小为 $V$ 时,模型每一步输出多少个数?这些数满足什么约束?

    看答案

    $V$ 个,每个 token 一个概率。每个数都大于 0(§5 会看到 softmax 的输出严格为正),且全部加起来等于 1。

  2. 用链式法则写出 $P(\text{我},\text{吃},\text{饭})$ 的分解。

    看答案
    $$P(\text{我},\text{吃},\text{饭})=P(\text{我})\,P(\text{吃}\mid\text{我})\,P(\text{饭}\mid\text{我},\text{吃})$$

    语言模型的每一步正好提供其中一个因子。

  3. 设 $L=(3z_1+z_2)^2$,$z_1=2x$,$z_2=x^2$。用链式法则求 $x=1$ 处的 $\dfrac{dL}{dx}$,再直接代入求导验证。

    看答案

    $x=1$ 时 $z_1=2,\ z_2=1$,记 $u=3z_1+z_2=7$。

    $$\frac{\partial L}{\partial z_1}=2u\cdot3=42,\quad \frac{\partial L}{\partial z_2}=2u=14,\quad \frac{dz_1}{dx}=2,\quad \frac{dz_2}{dx}=2x=2,$$
    $$\frac{dL}{dx}=42\cdot2+14\cdot2=112.$$

    验证:$L=(6x+x^2)^2$,$\dfrac{dL}{dx}=2(6x+x^2)(6+2x)$,在 $x=1$ 处为 $2\cdot7\cdot8=112$。

字符串 → int[T]

§1分词:把文字变成编号

神经网络只会处理数字。分词器(tokenizer)把一段文字切成一个个片段,叫 token,再按一张固定的词表把每个片段换成编号(token id)。词表里的 token 个数记为 $V$。切多粗,是第一个要做的取舍:

粒度词表大小 V同一段文字的 token 数主要问题
字符级小。中文常用字约 3500 个(《通用规范汉字表》一级字表)多,一个字一个 token序列长,而注意力的计算量随长度增长(第 6 课);单个 token 携带的信息少
词级极大,要收录所有词、人名、新词少词表外的新词只能记作「未知」;中文还得先做分词,切法本身就有歧义
子词(BPE)可以指定,GPT-2 为 50257适中常见词是一个 token,罕见词拆成几块,不会出现「未知词」

$V$ 越大,同一段文字切出的 token 越少;代价是嵌入矩阵和输出层的大小都正比于 $V$(§2、§7 会算到)。子词是两者之间的折中,目前的大模型基本都用它。

BPE:反复合并最常见的相邻对

BPE(Byte Pair Encoding,字节对编码)这样构造词表:

  1. 从最小单位开始(这里是单个汉字),把每个词拆成字的序列。
  2. 统计语料中所有相邻单位对出现的次数(每个词出现几次,就按几次算)。
  3. 把出现最多的那一对合并成一个新单位,加入词表。
  4. 重复第 2、3 步,直到词表达到目标大小,或者已经没有可以合并的对。

看一个小例子。假设语料只有 5 个词(已经切好),出现次数为:机器学习 5 次,深度学习 3 次,学习 2 次,机器人 2 次,深度 1 次。起点词表是用到的 7 个字:机、器、学、习、深、度、人。语料一共 $4\times5+4\times3+2\times2+3\times2+2\times1=44$ 个 token。

第 1 轮统计相邻字对:「学习」出现在机器学习、深度学习、学习三个词里,共 $5+3+2=10$ 次;「机器」$5+2=7$ 次;「器学」5 次;「深度」$3+1=4$ 次;「度学」3 次;「器人」2 次。最多的是「学习」,把它合并成一个 token,词表变成 8 个,语料 token 数从 44 降到 34(「学习」出现 10 次,每次少 1 个 token)。

继续重复,合并顺序依次是:学习(10)→ 机器(7)→ 机器学习(5)→ 深度(4)→ 深度学习(3)→ 机器人(2)。6 轮之后每个词都成了一个 token,总数降到 13。下面的面板可以一步步看。

BPE 合并过程交互 · 第 0 轮

语料当前的切分(× 出现次数)

相邻对计数

词表(上标 = 第几轮合并得到)

对新文本分词时,先拆成字,再按学到的先后顺序依次套用这些合并规则。

真实系统有两点不同。第一,GPT-2 的分词器在字节上做 BPE:起点是 256 个字节值,学了 50000 次合并,再加 1 个表示「文本结束」的特殊 token,所以 $V=256+50000+1=50257$。一个常用汉字在 UTF-8 编码里占 3 个字节,字节级 BPE 得先把字节合并回汉字,因此在以英文语料为主训练出的词表里,不少汉字要用 2 到 3 个 token 表示。第二,训练前会先按空格和标点粗切一遍,合并不会跨过这些边界。

练习 §1

  1. 用上面学到的 6 条合并规则,按顺序对新词「学习机器」分词。结果是几个 token?

    看答案

    起点:学 习 机 器。规则 1(学+习)→ 学习 机 器;规则 2(机+器)→ 学习 机器;规则 3 要求相邻对按「机器, 学习」的顺序出现,这里是「学习, 机器」,不匹配;规则 4–6 也用不上。

    结果是 2 个 token:学习 | 机器。合并规则是有方向的。

  2. 词表从 30000 扩到 100000(嵌入维数 $d=4096$),同一段文字的 token 数和嵌入矩阵的参数量分别怎么变?

    看答案

    token 数变少,因为更多常见片段被合成了单个 token。嵌入矩阵有 $V\times d$ 个参数(§2),从 $30000\times4096=122{,}880{,}000$ 增加到 $100000\times4096=409{,}600{,}000$。

int → ℝd

§2one-hot 与嵌入

token id 只是编号:3 号和 4 号挨着,不代表它们意思相近。最直接的数字化方式是 one-hot 向量:

$$e_i\in\{0,1\}^V,\qquad (e_i)_k=\delta_{ik}=\begin{cases}1,&k=i\\0,&k\ne i\end{cases}$$

例如 $V=4$ 时,$e_2=(0,0,1,0)$。它有两个毛病:

  • 太大。GPT-2 的 $V=50257$,每个 token 都是 50257 维的向量,其中只有一个位置非零。
  • 没有相似度。对任意 $i\ne j$,$e_i\cdot e_j=\sum_k\delta_{ik}\delta_{jk}=0$,而且 $\lVert e_i-e_j\rVert=\sqrt2$。「猫」和「狗」的距离与「猫」和「的」完全一样,向量本身不携带任何意思。

改进办法是给每个 token 一个可学习的 $d$ 维向量($d\ll V$),把它们按行叠起来,就是嵌入矩阵:

$$E\in\mathbb{R}^{V\times d},$$

它的第 $i$ 行 $E_{i,:}$ 就是 token $i$ 的嵌入向量。

训练开始时 $E$ 里是随机数;训练过程会调整它,让用法相近的 token 得到方向相近的向量。

查表就是矩阵乘法

把 $e_i$ 看成 $1\times V$ 的行向量(记作 $e_i^\top$),它乘 $V\times d$ 的 $E$,结果是 $1\times d$。逐个分量看,第 $k$ 个分量是:

$$(e_i^\top E)_k\overset{(1)}{=}\sum_{m}(e_i)_m\,E_{mk}\overset{(2)}{=}\sum_m\delta_{im}\,E_{mk}\overset{(3)}{=}E_{ik}.$$
  1. (1)矩阵乘法的定义:行向量和 $E$ 的第 $k$ 列做点积。
  2. (2)one-hot 的定义,$(e_i)_m=\delta_{im}$。
  3. (3)求和里只有 $m=i$ 那一项不为 0。

对每个 $k$ 都成立,所以 $e_i^\top E=E_{i,:}$:用 one-hot 向量乘嵌入矩阵,恰好取出第 $i$ 行。代码里不会真的去乘一个几万维的稀疏向量,而是直接按下标取行(E[i]);但数学上它就是一次矩阵乘法,所以求梯度时可以把它当作普通的线性层,§7 会用到这一点。

one-hot × E = 取出一行交互 · 点一个 token

这里的 $E$ 是示意数值(真实的嵌入是训练出来的)。可以先记住:「猫」「狗」两行方向相近,「的」那一行方向很不一样,§3 会用点积量化这一点。

参数量

$E$ 一共有 $V\times d$ 个参数。GPT-2 small 的 $V=50257$,$d=768$:

$$V\times d=50257\times768=38{,}597{,}376,$$

约 3860 万个参数,占 GPT-2 small 全部约 1.24 亿参数的三成左右。光是「把 token 变成向量」这张表就这么大。

练习 §2

  1. $V=32000$,$d=4096$ 时嵌入矩阵有多少参数?如果每个参数用 2 字节(fp16)存储,占多少显存?

    看答案

    $32000\times4096=131{,}072{,}000$ 个参数。乘 2 字节得 $262{,}144{,}000$ 字节,除以 $1024^2$ 正好是 250 MiB。

  2. 证明任意两个不同的 one-hot 向量之间的距离都是 $\sqrt2$。

    看答案
    $$\lVert e_i-e_j\rVert^2=\sum_k(\delta_{ik}-\delta_{jk})^2.$$

    $i\ne j$ 时,只有 $k=i$(值为 $1-0$)和 $k=j$(值为 $0-1$)两项非零,各贡献 1,总和为 2,开方得 $\sqrt2$。

  3. 如果两个不同 token 的嵌入向量完全相同,模型还能区分它们吗?

    看答案

    不能。模型之后的所有计算都只看到这个向量,两个 token 会得到完全相同的输出。在本课的 bigram 模型里,它们后面跟什么字的预测分布会一模一样。嵌入的作用正是给每个 token 一个能区分、又能体现相似度的坐标。

ℝd × ℝd → ℝ

§3点积与相似度

两个同维向量的点积(内积)是对应分量相乘再相加:

$$a\cdot b=\sum_{k}a_kb_k.$$

它的几何意义是 $a\cdot b=\lVert a\rVert\,\lVert b\rVert\cos\theta$,其中 $\lVert a\rVert=\sqrt{a\cdot a}$ 是长度,$\theta$ 是两向量的夹角。下面分三步推出来。

第一步:用代数展开 $\lVert a-b\rVert^2$。

$$\begin{aligned}\lVert a-b\rVert^2&=\sum_k(a_k-b_k)^2=\sum_k\left(a_k^2-2a_kb_k+b_k^2\right)\\&=\sum_ka_k^2-2\sum_ka_kb_k+\sum_kb_k^2=\lVert a\rVert^2+\lVert b\rVert^2-2\,a\cdot b.\end{aligned}$$

第二步:用几何。把 $a$、$b$ 画在同一个起点 $O$,两个终点之间的线段正好是 $a-b$,三者围成一个三角形,$O$ 处的角是 $\theta$。由余弦定理:

$$\lVert a-b\rVert^2=\lVert a\rVert^2+\lVert b\rVert^2-2\lVert a\rVert\,\lVert b\rVert\cos\theta.$$

第三步:对比两式。左边相同,右边前两项相同,所以

$$\boxed{a\cdot b=\lVert a\rVert\,\lVert b\rVert\cos\theta}\qquad\Longrightarrow\qquad \cos\theta=\frac{a\cdot b}{\lVert a\rVert\,\lVert b\rVert}.$$

这对任意维数都成立:两个向量总能张成一个平面,三角形就在这个平面里。($a$ 或 $b$ 为零向量时两边都是 0,夹角没有定义。)右边的 $\cos\theta$ 叫余弦相似度,取值在 $[-1,1]$:方向相同为 1,垂直为 0,相反为 −1。所以点积 = 方向一致的程度 × 两个长度。

拖动向量,看点积交互 · 拖动箭头端点,或聚焦后用方向键

虚线是第二步里三角形的第三条边 $a-b$。读数最后一行验证第一步的恒等式。

为什么要在意点积

注意力机制的核心一步,就是让每个 token 拿自己的一个向量(query)去和其它每个 token 的向量(key)做点积,得到一组「相关性分数」:方向越一致,分数越高。随后用 §5 的 softmax 把分数变成权重。第 2 课会细讲。另外,点积也受长度影响:方向不变时向量越长,点积越大。第 2 课里除以 $\sqrt{d_k}$,就是为了控制这一点。

练习 §3

  1. $a=(3,4)$,$b=(4,-3)$。求 $a\cdot b$ 和夹角。

    看答案

    $a\cdot b=3\cdot4+4\cdot(-3)=0$,所以 $\cos\theta=0$,两向量垂直,$\theta=90^\circ$。

  2. $a=(1,2,2)$,$b=(2,0,1)$。求 $\cos\theta$ 和 $\theta$。

    看答案

    $a\cdot b=2+0+2=4$,$\lVert a\rVert=\sqrt{1+4+4}=3$,$\lVert b\rVert=\sqrt{4+0+1}=\sqrt5\approx2.236$。

    $\cos\theta=\dfrac{4}{3\sqrt5}\approx0.596$,$\theta\approx53.4^\circ$。

  3. 把 $a$ 换成 $2a$,$a\cdot b$ 和 $\cos\theta$ 各怎么变?

    看答案

    $(2a)\cdot b=\sum_k2a_kb_k=2\,(a\cdot b)$,点积翻倍。$\lVert 2a\rVert=2\lVert a\rVert$,分子分母同乘 2 抵消,$\cos\theta$ 不变:方向没变,只是长度变了。

(n×d)(d×m) → n×m

§4矩阵乘法 = 一批点积 + 形状记账

设 $X\in\mathbb{R}^{n\times d}$,$W\in\mathbb{R}^{d\times m}$。乘积 $XW$ 的每个元素是

$$(XW)_{ij}=\sum_{k}X_{ik}W_{kj}=X_{i,:}\cdot W_{:,j},$$

也就是 $X$ 的第 $i$ 行与 $W$ 的第 $j$ 列的点积。结果有 $n\times m$ 个元素,每个都是一次 $d$ 维点积($d$ 次乘法和加法,合称 $d$ 次「乘加」),所以一次矩阵乘法共 $n\cdot d\cdot m$ 次乘加,约 $2ndm$ 次浮点运算。

形状规则:$(n\times d)(d\times m)\to n\times m$。中间的两个 $d$ 必须相等(点积要求两个向量一样长),它们在结果里消失;外侧的 $n$、$m$ 留下来。

每个输出元素 = 一行 · 一列交互 · 点右边结果的任一格

一次处理所有 token

把 $T$ 个 token 的向量按行叠成 $X\in\mathbb{R}^{T\times d}$,一次 $XW$ 就把它们全部变换完:结果的第 $t$ 行是 $X_{t,:}W$,和让第 $t$ 个 token 单独乘 $W$ 完全一样。$T\times m$ 个输出元素彼此独立,可以同时计算。GPU 有成千上万个计算单元,还有专门做矩阵乘法的硬件单元,最擅长的正是这种大批量、互相独立、结构规整的乘加。这也是 Transformer 训练时能并行处理整段文字的原因之一。

本课模型的形状账如下。以后每一课都会把这张表往中间补。

步骤运算形状
token id分词T 个整数
嵌入X = E[ids]T × d
输出层Z = X W_U(T×d)(d×V) → T × V
softmax每一行单独做T × V,每行和为 1

练习 §4

  1. $X\in\mathbb{R}^{8\times768}$,$W_U\in\mathbb{R}^{768\times50257}$。$XW_U$ 的形状是什么?需要多少次乘加?

    看答案

    形状 $8\times50257$。乘加次数 $8\times768\times50257=308{,}779{,}008$。

  2. $W_UX$ 能算吗?

    看答案

    不能。$(768\times50257)(8\times768)$ 的中间维数是 50257 和 8,不相等。矩阵乘法一般不能交换顺序。

  3. $A\in\mathbb{R}^{2\times3}$,$B\in\mathbb{R}^{3\times4}$,$C\in\mathbb{R}^{4\times5}$。$ABC$ 是什么形状?按 $(AB)C$ 和 $A(BC)$ 两种顺序计算,各需多少次乘加?

    看答案

    形状 $2\times5$。

    $(AB)C$:$AB$ 需 $2\cdot3\cdot4=24$ 次,得到 $2\times4$;再乘 $C$ 需 $2\cdot4\cdot5=40$ 次;共 64 次。

    $A(BC)$:$BC$ 需 $3\cdot4\cdot5=60$ 次,得到 $3\times5$;再被 $A$ 乘需 $2\cdot3\cdot5=30$ 次;共 90 次。

    结果相同(矩阵乘法满足结合律),计算量不同。第 6 课算推理成本时,这种「先乘哪个」的账会反复出现。

ℝV → 概率分布

§5Softmax:把分数变成概率

输出层给出的 $z\in\mathbb{R}^V$ 叫 logits,每个分量可以是任意实数。softmax 把它变成一个概率分布:

$$p_i=\mathrm{softmax}(z)_i=\frac{e^{z_i}}{\sum_j e^{z_j}}.$$

每个 $p_i\gt0$,且 $\sum_ip_i=\dfrac{\sum_ie^{z_i}}{\sum_je^{z_j}}=1$。为什么偏偏用指数函数?

  • 永远为正。$e^x\gt0$,概率不会是负数,也不会是 0。
  • 单调递增。logit 越大概率越大,排序不变。
  • 处处可导,且导数是它自己($(e^x)'=e^x$),后面求梯度会非常干净。
  • 放大差距。$p_i/p_j=e^{z_i}/e^{z_j}=e^{z_i-z_j}$:logit 差 1,概率差 $e\approx2.718$ 倍;差 5,概率差约 148 倍。真正起作用的只是 logit 之间的差。

算一个例子:$z=(2,1,0)$。

$$e^2\approx7.389056,\quad e^1\approx2.718282,\quad e^0=1,\quad \textstyle\sum\approx11.107338,$$
$$p\approx\left(\frac{7.389056}{11.107338},\ \frac{2.718282}{11.107338},\ \frac{1}{11.107338}\right)\approx(0.665,\ 0.245,\ 0.090).$$

平移不变性与数值稳定

给所有 logit 加上同一个常数 $c$,结果不变:

$$\mathrm{softmax}(z+c\mathbf 1)_i=\frac{e^{z_i+c}}{\sum_je^{z_j+c}}\overset{(1)}{=}\frac{e^{c}\,e^{z_i}}{e^{c}\sum_je^{z_j}}\overset{(2)}{=}\frac{e^{z_i}}{\sum_je^{z_j}}=\mathrm{softmax}(z)_i.$$
  1. (1)$e^{a+b}=e^ae^b$,再把每一项共有的因子 $e^c$ 提到求和号外面。
  2. (2)$e^c\gt0$,分子分母可以约掉。

这条性质直接解决了一个实际问题。计算机里的双精度浮点数最大约 $1.8\times10^{308}$,所以 $x\gt709.78$ 时 $e^x$ 就溢出成无穷大;GPU 上常用的单精度更早,$x\gt88.72$ 就溢出。若 $z=(1000,999,998)$,朴素地算 $e^{1000}$ 得到 $\infty$,接着 $\infty/\infty$ 得到 NaN(不是一个数)。

办法是取 $c=-m$,$m=\max_jz_j$:

$$p_i=\frac{e^{z_i-m}}{\sum_je^{z_j-m}}.$$

现在所有指数都 $\le0$,每一项都在 $(0,1]$ 之间,不会溢出;最大的那一项正好是 $e^0=1$,所以分母 $\ge1$,也不会除以 0。上例平移后是 $(0,-1,-2)$,由平移不变性,结果和 $\mathrm{softmax}(2,1,0)$ 一样,是 $(0.665,0.245,0.090)$。所有深度学习框架里的 softmax 都这样实现。

温度

生成文字时常把 logits 先除以一个温度 $T\gt0$:

$$p_i(T)=\frac{e^{z_i/T}}{\sum_je^{z_j/T}}.$$

$T=1$ 就是普通 softmax。为了看清极限,把分子分母同除以 $e^{z_i/T}$:

$$p_i(T)=\frac{1}{\sum_je^{(z_j-z_i)/T}}.$$

设最大的 logit 只有一个,下标为 $k$。

  • $T\to0^+$。对 $i=k$:每个 $j\ne k$ 都有 $z_j-z_k\lt0$,于是 $(z_j-z_k)/T\to-\infty$,$e^{(z_j-z_k)/T}\to0$;只剩 $j=k$ 那一项 $e^0=1$,所以 $p_k\to1$。对 $i\ne k$:分母里 $j=k$ 那一项的指数 $(z_k-z_i)/T\to+\infty$,分母趋于无穷,$p_i\to0$。分布变成落在最大 logit 上的 one-hot,相当于每次都选最可能的 token(贪心解码)。
  • $T\to\infty$。每个 $(z_j-z_i)/T\to0$,每一项 $\to1$,分母 $\to V$,所以 $p_i\to1/V$:均匀分布,完全随机。

温度越低分布越尖,越高越平。仍以 $z=(2,1,0)$ 为例:

$$\begin{aligned}T=0.5:&\quad\mathrm{softmax}(4,\,2,\,0)\approx(0.8668,\ 0.1173,\ 0.0159)\\T=2:&\quad\mathrm{softmax}(1,\,0.5,\,0)\approx(0.5065,\ 0.3072,\ 0.1863)\end{aligned}$$

(如果最大值有好几个并列,$T\to0$ 时概率会平分给它们。)

logits → 概率交互 · 「我吃了一个___」的候选字
1.00
所有 logit 加 c

雅可比矩阵:每个概率对每个 logit 的导数

训练时要知道「改动 $z_j$,$p_i$ 怎么变」。记 $S=\sum_ke^{z_k}$,则 $p_i=e^{z_i}/S$。先准备两块积木:

$$\frac{\partial e^{z_i}}{\partial z_j}=\delta_{ij}\,e^{z_i},\qquad \frac{\partial S}{\partial z_j}=\sum_k\frac{\partial e^{z_k}}{\partial z_j}=\sum_k\delta_{kj}e^{z_k}=e^{z_j}.$$

第一块:$e^{z_i}$ 只含 $z_i$,对别的 $z_j$ 求导为 0。再用商的求导法则 $(u/v)'=(u'v-uv')/v^2$:

$$\frac{\partial p_i}{\partial z_j}=\frac{\delta_{ij}e^{z_i}\cdot S-e^{z_i}\cdot e^{z_j}}{S^2}=\delta_{ij}\frac{e^{z_i}}{S}-\frac{e^{z_i}}{S}\cdot\frac{e^{z_j}}{S}=\delta_{ij}\,p_i-p_ip_j,$$
$$\boxed{\frac{\partial p_i}{\partial z_j}=p_i\,(\delta_{ij}-p_j)}$$

分两种情况读:$i=j$ 时为 $p_i(1-p_i)\gt0$,提高自己的 logit,自己的概率上升;$i\ne j$ 时为 $-p_ip_j\lt0$,别人的 logit 升高,自己的概率下降。

写成矩阵(把 $p$ 看作列向量,$J_{ij}=\partial p_i/\partial z_j$):

$$J=\mathrm{diag}(p)-pp^\top,$$

因为 $\mathrm{diag}(p)_{ij}=\delta_{ij}p_i$,$(pp^\top)_{ij}=p_ip_j$。$J$ 是对称矩阵,而且每一行的和为 0:$\sum_jp_i(\delta_{ij}-p_j)=p_i-p_i\sum_jp_j=0$。这正是平移不变性的导数版本:所有 logit 一起增加同样的量,概率不变。对上面的 $p\approx(0.665,0.245,0.090)$:

$$J\approx\begin{pmatrix}0.2227&-0.1628&-0.0599\\-0.1628&0.1848&-0.0220\\-0.0599&-0.0220&0.0819\end{pmatrix}.$$

练习 §5

  1. 计算 $\mathrm{softmax}(2,1,0)$,保留 3 位小数。

    看答案

    分母 $e^2+e^1+e^0\approx7.389+2.718+1=11.107$,得 $(0.665,\ 0.245,\ 0.090)$。

  2. 证明 $\mathrm{softmax}(z+c\mathbf1)=\mathrm{softmax}(z)$ 对任意实数 $c$ 成立。

    看答案

    对每个分量 $i$:

    $$\frac{e^{z_i+c}}{\sum_je^{z_j+c}}=\frac{e^c\,e^{z_i}}{e^c\sum_je^{z_j}}=\frac{e^{z_i}}{\sum_je^{z_j}}.$$

    第一步用 $e^{a+b}=e^ae^b$ 并提出公因子;第二步因 $e^c\gt0$ 而约去。所有分量都相等,所以两个向量相等。

  3. $\mathrm{softmax}(1000,1000)$ 等于多少?朴素算法会得到什么?

    看答案

    $(0.5,0.5)$。朴素算法里 $e^{1000}$ 溢出为 $\infty$,得到 $\infty/\infty=$ NaN。减去最大值 1000 后是 $\mathrm{softmax}(0,0)=(0.5,0.5)$。

  4. $z=(2,1,0)$,温度 $T=0.5$ 时概率是多少?

    看答案

    $z/T=(4,2,0)$。$e^4\approx54.598$,$e^2\approx7.389$,$e^0=1$,总和 $\approx62.987$,得 $(0.8668,\ 0.1173,\ 0.0159)$。比 $T=1$ 时更集中在第一个上。

概率 → 一个标量损失

§6交叉熵与最大似然

模型有了,怎么衡量它好不好?设训练语料是 token 序列 $x_1,\dots,x_N$,模型的全部参数记作 $\theta$。在每个位置,模型给真实出现的下一个 token 分配一个概率。整个语料的似然就是这些概率的乘积(§0 的链式法则):

$$\mathcal{L}(\theta)=\prod_{t=1}^{N-1}p_\theta(x_{t+1}\mid x_1,\dots,x_t).$$

最大似然:选让 $\mathcal L(\theta)$ 最大的参数,也就是让真实语料「最不意外」的模型。直接乘几千个小于 1 的数会下溢:$0.1^{400}=10^{-400}$,比双精度能表示的最小正数(约 $4.9\times10^{-324}$)还小,算出来就是 0。所以取对数。$\log$ 严格递增,不改变最大值点的位置,还把乘积变成求和:

$$\log\mathcal{L}(\theta)=\sum_{t=1}^{N-1}\log p_\theta(x_{t+1}\mid x_1,\dots,x_t).$$

最大化它,等价于最小化它的相反数;再除以样本数 $N-1$ 也不改变最小值点的位置。于是得到训练目标,平均负对数似然:

$$\bar L(\theta)=-\frac{1}{N-1}\sum_{t=1}^{N-1}\log p_\theta(x_{t+1}\mid x_1,\dots,x_t).$$

单个位置的损失

看一个位置。设正确的下一个 token 编号为 $y$,把它写成 one-hot 向量 $\mathbf y=e_y$($\mathbf y_i=\delta_{iy}$)。这个位置的损失是

$$L=-\log p_y=-\sum_i\mathbf y_i\log p_i.$$

右边的形式叫交叉熵:目标分布 $\mathbf y$(全部概率压在正确答案上)与模型分布 $p$ 之间的交叉熵。求和里只有 $i=y$ 那一项非零,所以两种写法相等。$p_y\to1$ 时 $L\to0$;$p_y\to0$ 时 $L\to\infty$。

梯度:$\partial L/\partial z=p-\mathbf y$

$L$ 通过所有 $p_i$ 依赖 $z_j$。用链式法则和 §5 的雅可比矩阵:

$$\begin{aligned}\frac{\partial L}{\partial z_j}&\overset{(1)}{=}-\sum_i\mathbf y_i\,\frac{1}{p_i}\,\frac{\partial p_i}{\partial z_j}\\&\overset{(2)}{=}-\sum_i\mathbf y_i\,\class{mjx-hot}{\frac{1}{p_i}}\,\class{mjx-hot}{p_i}\,(\delta_{ij}-p_j)\\&\overset{(3)}{=}-\sum_i\mathbf y_i\,(\delta_{ij}-p_j)\\&\overset{(4)}{=}-\mathbf y_j+p_j\sum_i\mathbf y_i\\&\overset{(5)}{=}p_j-\mathbf y_j.\end{aligned}$$
  1. (1)链式法则,以及 $\dfrac{d}{dp}\log p=\dfrac1p$。
  2. (2)代入 §5 的结果 $\partial p_i/\partial z_j=p_i(\delta_{ij}-p_j)$。
  3. (3)对数求导带来的 $1/p_i$ 与 softmax 导数里的 $p_i$ 恰好抵消。
  4. (4)拆开求和:$\sum_i\mathbf y_i\delta_{ij}=\mathbf y_j$;$p_j$ 不依赖 $i$,可以提出来。
  5. (5)$\mathbf y$ 是 one-hot,$\sum_i\mathbf y_i=1$。
$$\boxed{\frac{\partial L}{\partial z}=p-\mathbf y}$$

非常干净:梯度就是「预测分布减去正确答案」。梯度下降把 $z$ 往 $-(p-\mathbf y)$ 方向移动:

  • 正确的 logit $z_y$:梯度分量是 $p_y-1\le0$,所以 $z_y$ 被推高 $1-p_y$(乘学习率)。越没把握,推得越狠。
  • 其它 logit $z_j$:梯度分量是 $p_j\ge0$,被压低 $p_j$。抢走的概率越多,被压得越狠。
  • 所有分量之和 $\sum_j(p_j-\mathbf y_j)=1-1=0$。

例:$z=(2,1,0)$,正确答案 $y=0$。$p\approx(0.665,0.245,0.090)$,$L=-\ln0.665241\approx0.4076$,梯度 $\approx(-0.335,\ 0.245,\ 0.090)$。若正确答案是 $y=2$,则 $L=-\ln0.090031\approx2.4076$,梯度 $\approx(0.665,\ 0.245,\ -0.910)$:错得越离谱,损失和推力都越大。

两个极端:$p_y\to1$ 时其它 $p_j$ 都 $\to0$,梯度 $p-\mathbf y\to\mathbf0$,这个样本基本不再推动参数,学习自动集中到还没学会的样本上。$p_y\to0$ 时损失趋于无穷,但梯度的每个分量仍在 $[-1,1]$ 之内,不会爆炸。

困惑度

困惑度(perplexity)是平均损失的指数:

$$\mathrm{PPL}=\exp(\bar L)=\left(\prod_tp_t\right)^{-1/(N-1)},$$

即正确 token 概率的几何平均数的倒数,其中 $p_t$ 是第 $t$ 个位置上正确 token 的概率。直观意思是:模型的不确定程度,相当于每一步在多少个候选里均匀地猜。

一个重要的基准:如果模型对每个 token 都给 $1/V$(均匀乱猜),每个位置的损失都是 $-\ln(1/V)=\ln V$,困惑度正好是 $V$。GPT-2 的词表下 $\ln50257\approx10.825$。刚初始化的模型几乎就在均匀乱猜,所以训练的第一步损失应该在 $\ln V$ 附近;明显偏高,说明初始化让 logits 太大、太「自信」了。§7 的训练器会验证这一点。

练习 §6

  1. 不看上文,从 $L=-\sum_i\mathbf y_i\log p_i$ 和 $\partial p_i/\partial z_j=p_i(\delta_{ij}-p_j)$ 出发,推导 $\partial L/\partial z=p-\mathbf y$。

    看答案
    $$\frac{\partial L}{\partial z_j}=-\sum_i\frac{\mathbf y_i}{p_i}\,p_i(\delta_{ij}-p_j)=-\sum_i\mathbf y_i\delta_{ij}+p_j\sum_i\mathbf y_i=-\mathbf y_j+p_j.$$

    关键在 $1/p_i$ 与 $p_i$ 抵消,以及 $\sum_i\mathbf y_i=1$。

  2. 当 $p_y\to1$ 时,损失和梯度 $\partial L/\partial z$ 分别趋于什么?这对训练意味着什么?

    看答案

    $L=-\log p_y\to0$。因为 $\sum_jp_j=1$,$p_y\to1$ 迫使其它 $p_j\to0$,所以 $p\to\mathbf y$,梯度 $p-\mathbf y\to\mathbf 0$;其中正确分量 $p_y-1$ 与差距 $1-p_y$ 成正比地趋于 0。

    意味着已经预测得又准又有把握的样本几乎不再改变参数,训练的力气自动花在还预测不好的样本上。

  3. 平均损失为 2.0 时困惑度是多少?

    看答案

    $e^{2}\approx7.39$。相当于每一步在大约 7.4 个候选里均匀地猜。

  4. 一个词表 $V=32000$ 的模型刚初始化,训练第一步的损失大约是多少?

    看答案

    约 $\ln32000\approx10.37$。

id → ℝd → ℝV → 概率 → 损失 → 梯度

§7组装最小的语言模型:bigram

把前面的零件串起来。参数有两个矩阵:嵌入矩阵 $E\in\mathbb{R}^{V\times d}$,以及输出矩阵 $W_U\in\mathbb{R}^{d\times V}$(U 表示 unembedding,把向量变回词表上的分数)。对一个训练对(当前 token 的编号 $i$,下一个 token 的编号 $y$),前向计算是:

$$x=E_{i,:}\in\mathbb{R}^{1\times d},\qquad z=xW_U\in\mathbb{R}^{1\times V},\qquad p=\mathrm{softmax}(z),\qquad L=-\log p_y.$$

预测只依赖前一个 token,所以叫 bigram(二元)模型。它就是图 0-1 去掉中间方框的样子。

反向:三个梯度

记 $g=\partial L/\partial z=p-\mathbf y$($1\times V$,§6 的结果)。

(a) 对 $W_U$。$z_j=\sum_kx_k(W_U)_{kj}$,只有 $z_l$ 含有 $(W_U)_{kl}$,所以 $\dfrac{\partial z_j}{\partial(W_U)_{kl}}=x_k\,\delta_{jl}$。链式法则:

$$\frac{\partial L}{\partial(W_U)_{kl}}=\sum_jg_j\,x_k\,\delta_{jl}=x_k\,g_l\qquad\Longrightarrow\qquad\boxed{\frac{\partial L}{\partial W_U}=x^\top g}$$

形状检查:$(d\times1)(1\times V)=d\times V$,和 $W_U$ 一样。梯度的形状永远和参数相同,这是检查推导的好办法。

(b) 对 $x$。$\dfrac{\partial z_j}{\partial x_k}=(W_U)_{kj}$,所以

$$\frac{\partial L}{\partial x_k}=\sum_jg_j\,(W_U)_{kj}=\sum_jg_j\,(W_U^\top)_{jk}=(gW_U^\top)_k\qquad\Longrightarrow\qquad\boxed{\frac{\partial L}{\partial x}=g\,W_U^\top}$$

形状:$(1\times V)(V\times d)=1\times d$,和 $x$ 一样。

(c) 对 $E$。$x_k=E_{ik}$(§2:$x=e_i^\top E$),所以 $\dfrac{\partial x_k}{\partial E_{mn}}=\delta_{mi}\,\delta_{kn}$:

$$\frac{\partial L}{\partial E_{mn}}=\sum_k\frac{\partial L}{\partial x_k}\,\delta_{mi}\,\delta_{kn}=\delta_{mi}\,\frac{\partial L}{\partial x_n}.$$

只有第 $i$ 行非零,它等于 $gW_U^\top$;其余各行梯度为 0。也就是说,这一步没出现的 token,它的嵌入不会被更新。

一批样本与参数更新

实际训练对一批 $B$ 个训练对 $(i_b,y_b)$ 取平均损失 $\bar L=\frac1B\sum_bL_b$。和的导数等于导数的和,所以把单样本公式叠起来即可:

$$X=\begin{pmatrix}E_{i_1,:}\\\vdots\\E_{i_B,:}\end{pmatrix}\in\mathbb{R}^{B\times d},\quad Z=XW_U,\quad P=\mathrm{softmax}(Z),\quad G=\frac{P-Y}{B},$$
$$\frac{\partial\bar L}{\partial W_U}=X^\top G,\qquad\frac{\partial\bar L}{\partial X}=G\,W_U^\top.$$

其中 softmax 对每一行单独做,$Y$ 的第 $b$ 行是 $e_{y_b}$。$X^\top G$ 确实是各样本 $x^\top g$ 之和:$(X^\top G)_{kl}=\sum_bX_{bk}G_{bl}$,矩阵乘法里的求和正好遍历了所有样本。$E$ 的梯度:把 $GW_U^\top$ 的第 $b$ 行加到第 $i_b$ 行上;同一个 token 在批里出现几次,就累加几次。

最后沿梯度的反方向走一小步,$\eta$ 是学习率:

$$W_U\leftarrow W_U-\eta\,\frac{\partial\bar L}{\partial W_U},\qquad E\leftarrow E-\eta\,\frac{\partial\bar L}{\partial E}.$$

为什么是减:一阶泰勒展开给出 $\bar L(\theta-\eta\nabla)\approx\bar L(\theta)-\eta\lVert\nabla\rVert^2$,其中 $\nabla=\partial\bar L/\partial\theta$。只要 $\eta$ 足够小,损失就会下降。每次随机取一小批样本这样更新,叫随机梯度下降(SGD)。

训练一个字级 bigram交互 · 下面的 JS 逐条实现上面的公式

语料(37 个字,V = 15,36 个训练对)

1
嵌入维数 d
已训练步数
0
每步 = 全部 36 对取平均
平均损失
–
初始应接近 ln 15 ≈ 2.708
困惑度
–
exp(平均损失)
计数法下界
–
困惑度 –
均匀乱猜 ln 15计数法下界

当前字 → 下一个字的概率分布

模型 p(下一个字 | 吃)语料里的实际频率

试试这几件事:

  • 先点几次「训练 100 步」。损失从 $\ln15\approx2.708$ 附近出发,最终逼近点线。点线是计数法下界:把语料里「每个字后面各字出现的频率」直接当概率,能达到的最低平均损失,约 0.653。这是 bigram 能做到的极限:在这份语料上,最大似然的解就是条件频率。
  • 选「吃」:语料里「吃」后面是「苹」2 次、「米」1 次,训练后模型的概率会接近 $2/3$ 和 $1/3$。
  • 把 $d$ 改成 1:所有字被压到一条数轴上,logit 矩阵 $EW_U$($V\times V$)的秩最多为 1,学习率为 1 时训练 3000 步后损失仍在 1.4 以上,到不了下界。嵌入维数决定了模型能表达多少种不同的「下一个字」偏好。
  • 把学习率调到 20 或 30 再训练:步子太大,越过谷底后越弹越远,损失冲高甚至变成 NaN。

同样的算法用 NumPy 写,大约 25 行。每一行都对应上面的一个公式:

import numpy as np

text = "我吃苹果。你吃米饭。我喝水。他吃苹果。苹果很甜。米饭很香。我爱你。他喝水。"
vocab = sorted(set(text), key=text.index)        # 按首次出现的顺序编号
stoi = {c: i for i, c in enumerate(vocab)}
ids = np.array([stoi[c] for c in text])
cur, nxt = ids[:-1], ids[1:]                      # 训练对:(当前字, 下一个字)
V, d, B = len(vocab), 8, len(cur)                 # 15, 8, 36

rng = np.random.default_rng(0)
E  = rng.normal(0.0, 1.0, (V, d))                 # 嵌入矩阵 E:   V×d
WU = rng.normal(0.0, 0.1, (d, V))                 # 输出矩阵 W_U: d×V
lr = 1.0

for step in range(501):
    X = E[cur]                                    # B×d   查表 x = E[i]
    Z = X @ WU                                    # B×V   z = x W_U
    Z = Z - Z.max(axis=1, keepdims=True)          # 减最大值,数值稳定
    P = np.exp(Z)
    P /= P.sum(axis=1, keepdims=True)             # B×V   逐行 softmax
    loss = -np.log(P[np.arange(B), nxt]).mean()   # 平均交叉熵

    G = P.copy()
    G[np.arange(B), nxt] -= 1                     # p − y
    G /= B                                        # 对 B 个样本取平均
    dWU = X.T @ G                                 # d×V   x^T g 的批量版
    dX  = G @ WU.T                                # B×d   g W_U^T
    dE  = np.zeros_like(E)
    np.add.at(dE, cur, dX)                        # 同一个字出现多次,梯度累加

    WU -= lr * dWU
    E  -= lr * dE
    if step % 100 == 0:
        print(step, round(float(loss), 4))        # 0 2.732 … 500 0.6557

注意算 dE 用的是 np.add.at:写成 dE[cur] += dX 时,重复出现的下标只会被加一次,梯度就错了。

成本账

输出层对每个 token 都要算一次 $z=xW_U$,也就是 $d\times V$ 次乘加。这和这个 token 好不好猜毫无关系:哪怕下一个字几乎必然是「。」,词表里每一个候选的分数也照样全部算一遍。

GPT-2 small:$768\times50257=38{,}597{,}376$,约 3860 万次乘加(约 7720 万次浮点运算),而这只是最后一层。GPT-2 还让 $W_U=E^\top$(权重共享),同一批 3860 万个参数在输入端查表、在输出端做乘法。上面的玩具模型是 $8\times15=120$ 次。

你听说的「每个 token 都全量计算」,在这里第一次具体出现了。第 6 课会把整个模型的账算完,第 7 课讲 MoE、跳层等方法怎样打破「每个 token 都走遍全部参数」。

练习 §7

  1. 手算一步。$V=2$,$d=1$,$E=\begin{pmatrix}1\\-1\end{pmatrix}$,$W_U=(0.5,\ -0.5)$。输入 id 0,正确的下一个 id 是 1,学习率 $\eta=1$。求损失、各梯度、更新后的参数,以及更新后的新损失。

    看答案

    前向:$x=E_{0,:}=1$,$z=xW_U=(0.5,-0.5)$,$p\approx(0.7311,\ 0.2689)$,$L=-\ln0.2689\approx1.3133$。

    反向:

    $$\begin{aligned}g&=p-\mathbf y\approx(0.7311,\ -0.7311)\\\frac{\partial L}{\partial W_U}&=x^\top g\approx(0.7311,\ -0.7311)\\\frac{\partial L}{\partial x}&=gW_U^\top\approx0.7311\times0.5+(-0.7311)\times(-0.5)\\&=0.7311\end{aligned}$$

    更新:

    $$\begin{aligned}W_U&\approx(0.5-0.7311,\ -0.5+0.7311)\\&=(-0.2311,\ 0.2311)\\E_{0,:}&\approx1-0.7311=0.2689\end{aligned}$$

    $E_{1,:}=-1$ 不变(id 1 没有作为输入出现)。

    新的前向:$z\approx0.2689\times(-0.2311,\ 0.2311)$,约为 $(-0.0621,\ 0.0621)$;$p_1\approx0.5310$,$L\approx0.6329$。一步之内损失从 1.3133 降到 0.6329。

  2. 一批样本里「我」作为输入出现了 3 次。$E$ 中「我」那一行的梯度怎么得到?

    看答案

    把这 3 个样本各自的 $G_{b,:}W_U^\top$ 相加($G$ 里已经含有 $1/B$)。其余没出现的字,对应行的梯度是 0。

  3. 验证 $\partial\bar L/\partial W_U=X^\top G$ 和 $\partial\bar L/\partial X=GW_U^\top$ 的形状分别与 $W_U$、$X$ 一致。

    看答案

    $X^\top G$:$(d\times B)(B\times V)=d\times V$,与 $W_U$ 相同。$GW_U^\top$:$(B\times V)(V\times d)=B\times d$,与 $X$ 相同。

→ 第 2 课

§8局限:没有上下文,就猜不对

bigram 的预测只看当前这一个 token:$p(x_{t+1}\mid x_t)$。训练得再好,它最多学到「每个字后面各字的频率」,也就是上面那条点线。看两句话:

  • 我吃了一个苹果,味道很__ (应该是「甜」)
  • 苹果发布了新手机,价格很__ (应该是「贵」)

两句的最后一个字都是「很」。bigram 只看到「很」,给出的分布 $p(\cdot\mid\text{很})$ 在两句里完全相同,不可能两句都猜对。正确答案取决于「苹果」指水果还是公司,而这要靠好几个位置之前的「吃」「味道」或者「发布」「手机」「价格」来判断。

能不能把计数表做长,看前面更多的 token?看前 $n-1$ 个 token 的表有 $V^{n-1}$ 种上下文,每种上下文要存 $V$ 个概率。在 GPT-2 的词表下,只看前 2 个 token 就有 $V^2\approx25$ 亿种上下文、$V^3\approx1.27\times10^{14}$ 个数,而且绝大多数上下文在语料里一次都没出现过,根本估不出来。我们需要一种能处理任意长的上下文、参数量又不随上下文长度增长的机制。

思路:让每个 token 去「看」前面的 token

让「很」这个位置回头看前面每个 token,按相关程度决定从每个 token 那里取多少信息:

  1. 打分:用点积衡量相关性,$s_j=q\cdot k_j$(§3)。
  2. 归一:把分数变成权重,$a=\mathrm{softmax}(s)$(§5)。
  3. 汇总:按权重对各位置的向量加权求和,$\sum_ja_jv_j$。这本身是一次矩阵乘法(§4)。

这样「很」位置得到的向量就混入了「吃」「味道」或「手机」「价格」的信息,输出层便有机会在两句里给出不同的预测。

「很」在看谁示意 · 分数为手设,权重由页面实时计算

柱高是权重 $a=\mathrm{softmax}(s)$,每一行的权重加起来等于 1。这里的分数 $s$ 是为了演示手写的;真实模型里,它们由训练得到的矩阵算出,这正是下一课的内容。

把这三步写成矩阵形式,就是 Transformer 的核心公式:

$$\mathrm{Attention}(Q,K,V)=\mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V$$

注意:这里的 $V$ 是「值」(value)矩阵,不是词表大小。下一课会统一记号。本课的每个工具在这个公式里都有位置:

本课工具在注意力里的角色
点积(§3)$QK^\top$ 一次算出所有 query 与所有 key 的相关性分数
矩阵乘法(§4)$Q$、$K$、$V$ 都由输入 $X$ 乘矩阵得到;公式本身是两次矩阵乘法夹一次 softmax
softmax(§5)把每一行分数变成和为 1 的权重
交叉熵与梯度(§6–7)训练目标和方法完全不变,只是梯度要多穿过几层(第 5 课)

下一课逐个符号推导:$Q$、$K$、$V$ 从哪里来,为什么要除以 $\sqrt{d_k}$(一个方差计算),以及训练时为什么必须遮住后面的 token(因果掩码)。

本课小结

  • 分词把文字变成 $V$ 种编号之一;BPE 反复合并最常见的相邻对。
  • 嵌入查表 $x=E_{i,:}$ 等于 $e_i^\top E$,参数量 $V\times d$。
  • 点积 $a\cdot b=\lVert a\rVert\lVert b\rVert\cos\theta$ 衡量方向是否一致。
  • 矩阵乘法是一批点积,$(n\times d)(d\times m)\to n\times m$,共 $ndm$ 次乘加。
  • softmax 把 logits 变成概率;减最大值保证数值稳定;雅可比为 $\mathrm{diag}(p)-pp^\top$。
  • 最大似然 ⇔ 最小化交叉熵,$\partial L/\partial z=p-\mathbf y$;均匀乱猜的损失为 $\ln V$。
  • bigram:$\partial L/\partial W_U=x^\top g$,$\partial L/\partial x=gW_U^\top$,只有出现过的行得到梯度。

练习 §8

  1. 在 $V=50257$ 下,只看前 2 个 token 的计数表(3-gram)要存多少个概率?

    看答案

    $V^2$ 种上下文,每种 $V$ 个概率,共 $V^3=50257^3$,即 $126{,}937{,}424{,}324{,}593$ 个,约 $1.27\times10^{14}$。按每个 2 字节算约 254 TB。

  2. 对上面两句话,训练好的 bigram 在「很」之后给出的两个分布有什么关系?为什么?

    看答案

    完全相同。bigram 的输出 $\mathrm{softmax}(E_{\text{很},:}W_U)$ 只依赖「很」的嵌入行和 $W_U$,跟前面是什么字无关。