第 1 课 / 共 7 课
从文字到概率:最小的语言模型
学完这一课,你能从零推导并亲手实现一个完整的 bigram 语言模型:嵌入 → logits → softmax → 交叉熵 → 梯度 → 参数更新;也会看清它为什么离不开上下文,从而需要注意力。
需要的数学:高中数学,加上大一的微积分(偏导数、链式法则)和线性代数(向量、矩阵乘法)。忘了也没关系,§0 末尾有速查。每节末尾有练习,答案默认收起。
文本 → 概率分布
§0全景:语言模型到底在算什么
你可能听过这样的说法:大模型对每个 token 都要「全量计算」,所以特别吃显存、吃电。这个系列会把这句话拆到每一次乘法上。第一步,先弄清楚模型到底在算什么。
一个语言模型只做一件事:读入前面的 token $x_1,\dots,x_t$,输出下一个 token 的概率分布
「分布」的意思是:词表里的每个 token 都分到一个概率,全部加起来等于 1。生成文字时,从这个分布里抽一个 token,把它接到输入末尾,再预测下一个,如此循环。
整段文字的概率也由它决定。根据概率的乘法公式(链式法则),
其中 $x_{\lt t}$ 是 $x_1,\dots,x_{t-1}$ 的简写($t=1$ 时为空)。所以「会预测下一个 token」就等于「会给任意一段文字打分」。
数学速查
- 向量
- 本系列把一个 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
词表大小为 $V$ 时,模型每一步输出多少个数?这些数满足什么约束?
看答案
$V$ 个,每个 token 一个概率。每个数都大于 0(§5 会看到 softmax 的输出严格为正),且全部加起来等于 1。
用链式法则写出 $P(\text{我},\text{吃},\text{饭})$ 的分解。
看答案
$$P(\text{我},\text{吃},\text{饭})=P(\text{我})\,P(\text{吃}\mid\text{我})\,P(\text{饭}\mid\text{我},\text{吃})$$语言模型的每一步正好提供其中一个因子。
设 $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,字节对编码)这样构造词表:
- 从最小单位开始(这里是单个汉字),把每个词拆成字的序列。
- 统计语料中所有相邻单位对出现的次数(每个词出现几次,就按几次算)。
- 把出现最多的那一对合并成一个新单位,加入词表。
- 重复第 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。下面的面板可以一步步看。
语料当前的切分(× 出现次数)
相邻对计数
词表(上标 = 第几轮合并得到)
对新文本分词时,先拆成字,再按学到的先后顺序依次套用这些合并规则。
真实系统有两点不同。第一,GPT-2 的分词器在字节上做 BPE:起点是 256 个字节值,学了 50000 次合并,再加 1 个表示「文本结束」的特殊 token,所以 $V=256+50000+1=50257$。一个常用汉字在 UTF-8 编码里占 3 个字节,字节级 BPE 得先把字节合并回汉字,因此在以英文语料为主训练出的词表里,不少汉字要用 2 到 3 个 token 表示。第二,训练前会先按空格和标点粗切一遍,合并不会跨过这些边界。
练习 §1
用上面学到的 6 条合并规则,按顺序对新词「学习机器」分词。结果是几个 token?
看答案
起点:学 习 机 器。规则 1(学+习)→ 学习 机 器;规则 2(机+器)→ 学习 机器;规则 3 要求相邻对按「机器, 学习」的顺序出现,这里是「学习, 机器」,不匹配;规则 4–6 也用不上。
结果是 2 个 token:学习 | 机器。合并规则是有方向的。
词表从 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 向量:
例如 $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$),把它们按行叠起来,就是嵌入矩阵:
它的第 $i$ 行 $E_{i,:}$ 就是 token $i$ 的嵌入向量。
训练开始时 $E$ 里是随机数;训练过程会调整它,让用法相近的 token 得到方向相近的向量。
查表就是矩阵乘法
把 $e_i$ 看成 $1\times V$ 的行向量(记作 $e_i^\top$),它乘 $V\times d$ 的 $E$,结果是 $1\times d$。逐个分量看,第 $k$ 个分量是:
- (1)矩阵乘法的定义:行向量和 $E$ 的第 $k$ 列做点积。
- (2)one-hot 的定义,$(e_i)_m=\delta_{im}$。
- (3)求和里只有 $m=i$ 那一项不为 0。
对每个 $k$ 都成立,所以 $e_i^\top E=E_{i,:}$:用 one-hot 向量乘嵌入矩阵,恰好取出第 $i$ 行。代码里不会真的去乘一个几万维的稀疏向量,而是直接按下标取行(E[i]);但数学上它就是一次矩阵乘法,所以求梯度时可以把它当作普通的线性层,§7 会用到这一点。
这里的 $E$ 是示意数值(真实的嵌入是训练出来的)。可以先记住:「猫」「狗」两行方向相近,「的」那一行方向很不一样,§3 会用点积量化这一点。
参数量
$E$ 一共有 $V\times d$ 个参数。GPT-2 small 的 $V=50257$,$d=768$:
约 3860 万个参数,占 GPT-2 small 全部约 1.24 亿参数的三成左右。光是「把 token 变成向量」这张表就这么大。
练习 §2
$V=32000$,$d=4096$ 时嵌入矩阵有多少参数?如果每个参数用 2 字节(fp16)存储,占多少显存?
看答案
$32000\times4096=131{,}072{,}000$ 个参数。乘 2 字节得 $262{,}144{,}000$ 字节,除以 $1024^2$ 正好是 250 MiB。
证明任意两个不同的 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$。
如果两个不同 token 的嵌入向量完全相同,模型还能区分它们吗?
看答案
不能。模型之后的所有计算都只看到这个向量,两个 token 会得到完全相同的输出。在本课的 bigram 模型里,它们后面跟什么字的预测分布会一模一样。嵌入的作用正是给每个 token 一个能区分、又能体现相似度的坐标。
ℝd × ℝd → ℝ
§3点积与相似度
两个同维向量的点积(内积)是对应分量相乘再相加:
它的几何意义是 $a\cdot b=\lVert a\rVert\,\lVert b\rVert\cos\theta$,其中 $\lVert a\rVert=\sqrt{a\cdot a}$ 是长度,$\theta$ 是两向量的夹角。下面分三步推出来。
第一步:用代数展开 $\lVert a-b\rVert^2$。
第二步:用几何。把 $a$、$b$ 画在同一个起点 $O$,两个终点之间的线段正好是 $a-b$,三者围成一个三角形,$O$ 处的角是 $\theta$。由余弦定理:
第三步:对比两式。左边相同,右边前两项相同,所以
这对任意维数都成立:两个向量总能张成一个平面,三角形就在这个平面里。($a$ 或 $b$ 为零向量时两边都是 0,夹角没有定义。)右边的 $\cos\theta$ 叫余弦相似度,取值在 $[-1,1]$:方向相同为 1,垂直为 0,相反为 −1。所以点积 = 方向一致的程度 × 两个长度。
虚线是第二步里三角形的第三条边 $a-b$。读数最后一行验证第一步的恒等式。
为什么要在意点积
注意力机制的核心一步,就是让每个 token 拿自己的一个向量(query)去和其它每个 token 的向量(key)做点积,得到一组「相关性分数」:方向越一致,分数越高。随后用 §5 的 softmax 把分数变成权重。第 2 课会细讲。另外,点积也受长度影响:方向不变时向量越长,点积越大。第 2 课里除以 $\sqrt{d_k}$,就是为了控制这一点。
练习 §3
$a=(3,4)$,$b=(4,-3)$。求 $a\cdot b$ 和夹角。
看答案
$a\cdot b=3\cdot4+4\cdot(-3)=0$,所以 $\cos\theta=0$,两向量垂直,$\theta=90^\circ$。
$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$。
把 $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$ 的每个元素是
也就是 $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
$X\in\mathbb{R}^{8\times768}$,$W_U\in\mathbb{R}^{768\times50257}$。$XW_U$ 的形状是什么?需要多少次乘加?
看答案
形状 $8\times50257$。乘加次数 $8\times768\times50257=308{,}779{,}008$。
$W_UX$ 能算吗?
看答案
不能。$(768\times50257)(8\times768)$ 的中间维数是 50257 和 8,不相等。矩阵乘法一般不能交换顺序。
$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\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)$。
平移不变性与数值稳定
给所有 logit 加上同一个常数 $c$,结果不变:
- (1)$e^{a+b}=e^ae^b$,再把每一项共有的因子 $e^c$ 提到求和号外面。
- (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$:
现在所有指数都 $\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$:
$T=1$ 就是普通 softmax。为了看清极限,把分子分母同除以 $e^{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)$ 为例:
(如果最大值有好几个并列,$T\to0$ 时概率会平分给它们。)
雅可比矩阵:每个概率对每个 logit 的导数
训练时要知道「改动 $z_j$,$p_i$ 怎么变」。记 $S=\sum_ke^{z_k}$,则 $p_i=e^{z_i}/S$。先准备两块积木:
第一块:$e^{z_i}$ 只含 $z_i$,对别的 $z_j$ 求导为 0。再用商的求导法则 $(u/v)'=(u'v-uv')/v^2$:
分两种情况读:$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$):
因为 $\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)$:
练习 §5
计算 $\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)$。
证明 $\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$ 而约去。所有分量都相等,所以两个向量相等。
$\mathrm{softmax}(1000,1000)$ 等于多少?朴素算法会得到什么?
看答案
$(0.5,0.5)$。朴素算法里 $e^{1000}$ 溢出为 $\infty$,得到 $\infty/\infty=$ NaN。减去最大值 1000 后是 $\mathrm{softmax}(0,0)=(0.5,0.5)$。
$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)$ 最大的参数,也就是让真实语料「最不意外」的模型。直接乘几千个小于 1 的数会下溢:$0.1^{400}=10^{-400}$,比双精度能表示的最小正数(约 $4.9\times10^{-324}$)还小,算出来就是 0。所以取对数。$\log$ 严格递增,不改变最大值点的位置,还把乘积变成求和:
最大化它,等价于最小化它的相反数;再除以样本数 $N-1$ 也不改变最小值点的位置。于是得到训练目标,平均负对数似然:
单个位置的损失
看一个位置。设正确的下一个 token 编号为 $y$,把它写成 one-hot 向量 $\mathbf y=e_y$($\mathbf y_i=\delta_{iy}$)。这个位置的损失是
右边的形式叫交叉熵:目标分布 $\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 的雅可比矩阵:
- (1)链式法则,以及 $\dfrac{d}{dp}\log p=\dfrac1p$。
- (2)代入 §5 的结果 $\partial p_i/\partial z_j=p_i(\delta_{ij}-p_j)$。
- (3)对数求导带来的 $1/p_i$ 与 softmax 导数里的 $p_i$ 恰好抵消。
- (4)拆开求和:$\sum_i\mathbf y_i\delta_{ij}=\mathbf y_j$;$p_j$ 不依赖 $i$,可以提出来。
- (5)$\mathbf y$ 是 one-hot,$\sum_i\mathbf y_i=1$。
非常干净:梯度就是「预测分布减去正确答案」。梯度下降把 $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)是平均损失的指数:
即正确 token 概率的几何平均数的倒数,其中 $p_t$ 是第 $t$ 个位置上正确 token 的概率。直观意思是:模型的不确定程度,相当于每一步在多少个候选里均匀地猜。
一个重要的基准:如果模型对每个 token 都给 $1/V$(均匀乱猜),每个位置的损失都是 $-\ln(1/V)=\ln V$,困惑度正好是 $V$。GPT-2 的词表下 $\ln50257\approx10.825$。刚初始化的模型几乎就在均匀乱猜,所以训练的第一步损失应该在 $\ln V$ 附近;明显偏高,说明初始化让 logits 太大、太「自信」了。§7 的训练器会验证这一点。
练习 §6
不看上文,从 $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$。
当 $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。
意味着已经预测得又准又有把握的样本几乎不再改变参数,训练的力气自动花在还预测不好的样本上。
平均损失为 2.0 时困惑度是多少?
看答案
$e^{2}\approx7.39$。相当于每一步在大约 7.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$),前向计算是:
预测只依赖前一个 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}$。链式法则:
形状检查:$(d\times1)(1\times V)=d\times V$,和 $W_U$ 一样。梯度的形状永远和参数相同,这是检查推导的好办法。
(b) 对 $x$。$\dfrac{\partial z_j}{\partial x_k}=(W_U)_{kj}$,所以
形状:$(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}$:
只有第 $i$ 行非零,它等于 $gW_U^\top$;其余各行梯度为 0。也就是说,这一步没出现的 token,它的嵌入不会被更新。
一批样本与参数更新
实际训练对一批 $B$ 个训练对 $(i_b,y_b)$ 取平均损失 $\bar L=\frac1B\sum_bL_b$。和的导数等于导数的和,所以把单样本公式叠起来即可:
其中 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$ 是学习率:
为什么是减:一阶泰勒展开给出 $\bar L(\theta-\eta\nabla)\approx\bar L(\theta)-\eta\lVert\nabla\rVert^2$,其中 $\nabla=\partial\bar L/\partial\theta$。只要 $\eta$ 足够小,损失就会下降。每次随机取一小批样本这样更新,叫随机梯度下降(SGD)。
语料(37 个字,V = 15,36 个训练对)
当前字 → 下一个字的概率分布
试试这几件事:
- 先点几次「训练 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
手算一步。$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。
一批样本里「我」作为输入出现了 3 次。$E$ 中「我」那一行的梯度怎么得到?
看答案
把这 3 个样本各自的 $G_{b,:}W_U^\top$ 相加($G$ 里已经含有 $1/B$)。其余没出现的字,对应行的梯度是 0。
验证 $\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 那里取多少信息:
- 打分:用点积衡量相关性,$s_j=q\cdot k_j$(§3)。
- 归一:把分数变成权重,$a=\mathrm{softmax}(s)$(§5)。
- 汇总:按权重对各位置的向量加权求和,$\sum_ja_jv_j$。这本身是一次矩阵乘法(§4)。
这样「很」位置得到的向量就混入了「吃」「味道」或「手机」「价格」的信息,输出层便有机会在两句里给出不同的预测。
柱高是权重 $a=\mathrm{softmax}(s)$,每一行的权重加起来等于 1。这里的分数 $s$ 是为了演示手写的;真实模型里,它们由训练得到的矩阵算出,这正是下一课的内容。
把这三步写成矩阵形式,就是 Transformer 的核心公式:
注意:这里的 $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
在 $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。
对上面两句话,训练好的 bigram 在「很」之后给出的两个分布有什么关系?为什么?
看答案
完全相同。bigram 的输出 $\mathrm{softmax}(E_{\text{很},:}W_U)$ 只依赖「很」的嵌入行和 $W_U$,跟前面是什么字无关。