跳至主要内容

人工智能:从图灵之问到大语言模型

返回信息学目录

1950 年,数学家艾伦·图灵在一篇论文的开头提出了一个很短的问题:**“机器能思考吗?”**七十多年后,我们可以向一个程序输入问题,得到一段通顺、细致的回答。这样的程序里面到底发生了什么?数一数哪些词经常相邻,就能做出一个猜下一个词的小程序,但这种办法能记住的信息很少。把词表示成**一串数**,就能用计算比较它们,并组合上下文的信息。这会引出**“注意力”机制**(让每个词参考句中其他词),它是 **Transformer** 的重要组成部分。这种结构被广泛用于语言模型。我们也会了解它的发展和训练方法。图片生成则给出了另一个问题:计算机怎样把一团噪声变成图像?**“扩散”方法**会让我们看到这个过程。

读这一页不需要会编程。我们会用到加法、乘法和少量乘方;新的符号会在使用它的地方说明含义。交互模型里的每一个数都是真实算出来的,你可以自己核对每一步。

01

机器能思考吗?

图灵的论文《计算机器与智能》1950 年发表在《心灵》(Mind)期刊上。他马上注意到自己这个问题的困难:要回答“机器能思考吗”,我们得先对“机器”和“思考”的意思达成一致,而人们在这一点上从来没有完全一致过。于是,他把这个问题换成了一个真正可以检验的问题。

他把它叫作“模仿游戏”。一位提问者坐在一个房间里,通过打字向另一个房间里看不见的两位回答者提问:一位是人,一位是机器。提问者只能读到打出来的回答,他的任务是判断哪一位是人、哪一位是机器。图灵认为,如果机器被当成人的次数,和一个真人被当成人的次数差不多,我们就很难再坚持说它不会思考。今天,人们通常把这个游戏叫作“图灵测试”。

只有打出来的文字能传过去提问者A: …B: …哪一位是人?A人 ?机器 ?B人 ?机器 ?
模仿游戏。提问者只能看到 A 和 B 打出来的回答,不知道其中哪一位是机器。

论文里还有两个想法和这一页关系很大。第一,图灵做了一个预测。他写道,大约五十年后,只要机器有足够的存储,它就能把这个游戏玩得很好。那时普通提问者在提问五分钟后,正确认出它的机会不超过 70%。第二,他建议与其把一个成年人头脑里的规则一条条写出来,不如造一台会学习的机器。这台机器像孩子一样被教着学。请记住第二个想法,因为今天的人工智能正是这样做的。没有人给大语言模型逐条写规则,它的那些数是从例子里学出来的。

赢了这个游戏,是不是就证明机器在思考呢?人们至今还在争论,这一页也不打算给出结论。我们能做的,是一件更朴素、也更有用的事:一步一步看清楚,今天的机器在回答我们的时候,到底计算了什么。这样,每个人都可以对着这套机制,而不是对着一个谜,来判断图灵的问题。

02

七十年简史

“人工智能”这个名字,是图灵论文发表几年后才出现的:1956 年,达特茅斯学院为一次夏季研讨会起了这个名字。从那以后,这个领域一直在两种大思路之间来回。第一种是由人来写规则:如果程序要诊断疾病,就请专家列出症状和连接它们的规则。第二种是图灵说的会学习的机器:给程序看大量例子,让它自己调整内部的数,直到它的回答和例子一致。点选时间轴上的事件,读读发生了什么,以及它属于哪一种思路。

1950 · 问题与命名图灵问:“机器能思考吗?”

图灵提出模仿游戏,并建议制造会学习的机器。

问题与命名由人来写规则从例子中学习人工智能的冬天

按规则工作的程序遵循人提供的步骤;学习型模型则用例子调整内部的数值设置。两种办法各有用途,一个系统也可以把它们结合起来。更多数据和更快的计算,让学习方法能够处理更大规模的图像和语言任务。2017 年的《Attention Is All You Need》为机器翻译提出了 Transformer。下面的小例子会把注意力的计算展开,让每个数的来源都能被核对。理解这些数怎样组合信息,就能继续讨论文字生成时怎样选出下一个词元。这是一类重要的人工智能机制,并不代表所有人工智能。

03

猜下一个词

我们先从语言模型最简单的样子开始。读一读这个没写完的句子:“那只猫坐在……上”。你大概想到了“垫子”或“地板”,肯定不会想到“想法”。你是在猜下一个词,而且是凭经验在猜,因为你以前见过很多类似的句子。语言模型就是专门做这件事的程序:给它一段文字,它为每一个可能出现的下一个词给出一个概率。

这里的概率是一个 00 到 11 之间的数,表示在很多次尝试中,我们预计某件事发生的比例。如果某个词每 4 次里有 1 次接在后面,它的概率就是 1÷4=0.251 \div 4 = 0.25。所有可能的下一个词的概率加起来等于 11,因为下一个位置上必定恰好出现其中一个词。

得到这些数最直接的办法就是数数。下面是一小组句子,我们把它叫作语料,也就是我们用来学习的文本。选一个词:语料中它出现的每个位置都会被标出,表格列出在这些位置后面出现过的每个词、出现了几次,以及这个次数除以总次数的结果。

前一个词: 在
下一个词次数概率
地毯113≈0.33\tfrac{1}{3}\approx 0.33
垫子113≈0.33\tfrac{1}{3}\approx 0.33
椅子113≈0.33\tfrac{1}{3}\approx 0.33
合计333=1\tfrac{3}{3}=1

例如,在语料里,“在”后面一共出现了 3 个词:“垫子”“地毯”“椅子”各 1 次。所以“在”后面出现“垫子”的概率是 1÷3≈0.331 \div 3 \approx 0.33,三个概率加起来是 3÷3=13 \div 3 = 1。

现在按“写一句话”。程序从你选的词出发,随机抽出下一个词,每个词被抽中的机会等于它在表格中的概率。然后它把这个新词当作“前一个词”,重复同样的步骤,直到写出句号。“预测、选择、接上”是一种自回归生成过程:下一次选择使用已经写出的内容。这一课研究这种方法。利用两边文字填一个被遮住的词等语言模型任务,并不一定按这种顺序生成。

数数的办法有两个严重的问题。第一,它对没见过的组合一无所知。语料里“小猫”后面只出现过“坐”,所以计数模型会认为“小猫吃了鱼”不可能出现。可这是一句完全正常的话。第二,它只往前看一个词。在“她在巴黎住了十年,所以她能说一口流利的……”里,最合适的下一个词“法语”取决于前面的“巴黎”。只往前看一个词,我们只能看到“的”,它完全没说是哪种语言。我们可以改成数更长的词串,可那样一来,几乎每个词串都是新的,绝大多数计数都是零。

所以我们需要两样东西。第一,能表示“词和词相似”的办法,这样“骨头”就能借用我们对“鱼”的了解,因为两者都可以吃。第二,让每个词都能使用前文任何位置的信息的办法。接下来的几章就来搭这两样东西:用向量表示相似,用注意力往回看。

在语料里,“在”后面一共出现了 3 个词,其中“椅子”出现 1 次。计数模型给“在”后面出现“椅子”的概率是多少?

04

把文字切成小段

在模型能计算任何东西之前,文字必须先变成数。第一步是把文字切成一个个小块,叫作 token(词元),再给每一块一个编号,编号来自一张固定的列表,叫作词表。为什么不直接用整个词?因为词永远列不完:人名、错别字、新词、数字、其他语言……为什么不用单个字母?因为那样一句话会变成很长的序列,而单个字母本身几乎不带意思。真正的分词器走中间路线:常见的词各占一个 token,少见的词则由几个常见的小块拼起来。

机器335学习336让362小猫339图片342变得343更344清楚345。371

9 个 token · 灰色芯片是单个字节(编号 0–255)。

在输入框里打字,看看切出来的小块。比如“机器学习”,我们的小词表里正好有“机器”和“学习”两块,分词器在每个位置都取它认识的最长一块,于是得到两个 token。每块芯片上写着这一块文字和它的编号,也就是这个 token 在词表中的号码。

再看看词表里根本没有的字会怎样,比如“猫咪”里的“咪”。它会被拆成计算机存储它时用的字节。一个字节是 8 位二进制数字,所以它有 28=2562^8 = 256 种不同的取值。大多数汉字用三个字节存储,这里用十六进制写成 E5、92、AA。十六进制的数字是 0 到 9 再加上 A 到 F,这样每个字节正好用两个符号写下。编号 0 到 255 专门留给这 256 种字节,所以任何文字都能变成 token,不会出现“不认识”的情况。

词表又是从哪里来的?它也是从文本中学出来的。一种常用的方法叫字节对编码:从单个字符开始,反复把出现次数最多的那一对相邻小块合并成一块。下面在一个只有四个词的小语料上一步步演示,每个词出现的次数不同。

词出现次数当前的小块
low5low
lower2lower
newest6newest
widest3widest
已做的合并:—
合并步数: 0 / 12

第一步,e 后面跟 s 这一对,在 newest 里出现 6 次,在 widest 里出现 3 次,一共 9 次。s 后面跟 t 这一对也出现 9 次,没有哪一对出现得更多。两对次数相同时,分词器取在语料中先遇到的那一对,所以“es”成为一个新的小块。接着,“es”后面跟“t”也出现 9 次,于是得到“est”。合并几次以后,常见的结尾“est”和常见的词“low”都各自变成了一整块,而少见的字母组合仍然是分开的。真正的分词器会一直合并下去,直到词表里有几万到几十万个小块。粗略地说,一个英文 token 大约是四分之三个单词。

05

把小段变成一串数

现在每个 token 都有了编号。可是编号只是个名字:257 号(“猫”)和 258 号(“狗”)挨在一起并没有什么意义,300 号也不比 200 号“多”什么。我们想要带意思的数,让相似的词得到相似的数。办法是:不给每个 token 一个数,而是给它一串数。

按固定顺序排好的一串数叫作向量。如果只有两个数,比如 (3,2)(3, 2),我们可以把它画成一支箭头:从坐标原点出发,向右 3 格、向上 2 格。三个数可以画成空间里的一支箭头。模型把每个 token 的向量存在一张大表里,叫作嵌入表:第 257 行存的就是“猫”的向量。把 token 变成向量,就是去查它那一行。

下面有 19 个词,每个词有 8 个数。为了让你读得懂这些数,我手工设计了它们,并给每个位置一个意思。第 1 个数表示这个词有多像动物,第 2 个数表示有多像交通工具,依此类推。选一个词,就能看到它的 8 个数。我们一次只能画出三个方向,所以图里只显示八个位置中的三个;你可以选显示哪三个,也可以拖动画面把它转过来。

动物交通工具食物国王男孩女人女王男人自行车公交车汽车小猫狮子狗猫马蛋糕面包苹果
拖动可旋转 ·
显示的坐标轴
这个词的 8 个数: 猫
动物1.01
交通工具0.04
食物−0.04
人−0.05
年幼0.01
女性0.05
王室0.00
体型大0.14

当三条坐标轴是动物、交通工具和食物时,词聚成三团,各靠近一条轴。再看表示人的词:男人、女人、国王、女王、男孩、女孩都挤在中间,叠在一起。它们并不是在所有方面都相像,只是这个视角恰好没有显示它们不同的那些方向。把一条轴换成“人”,另一条换成“女性”,它们就散开了。

这是理解向量时最重要的习惯:一张图一次只能显示三个方向,可向量拥有全部方向。两个词在某个视角里看起来很近,在完整的一串数里却可能离得很远。

真实的模型也是这样工作的,只是数要多得多:最初的 Transformer 给每个 token 512 个数,今天的大模型用几千个,GPT-3 用了 12288 个。和我的表相比,有一点必须说明:真实模型里没有人决定每个位置的意思。这些数一开始是随机的,再由训练(第 10 章)慢慢调整。用法相似的词最后会得到相似的向量,但单独一个位置很少有“动物”这样清楚的意思。我们给位置贴上标签,只是为了让这些数读得懂。

06

两个向量有多像?

我们说过,相似的词应该得到相似的向量。那么,怎样把“相似”变成一个计算?先在二维里看,那里一切都画得出来。取两个向量 a=(a1,a2)\mathbf a = (a_1, a_2) 和 b=(b1,b2)\mathbf b = (b_1, b_2),右下角的小数字 1、2 只是标出第一个数和第二个数。它们的点积,就是把对应位置的数相乘,再把结果加起来:

a⋅b=a1b1+a2b2\mathbf a\cdot\mathbf b=a_1b_1+a_2b_2

数更多时也一样:逐个位置相乘,再全部加起来。例如 (3,1)⋅(2,2)=3×2+1×2=8(3, 1)\cdot(2, 2) = 3\times2 + 1\times2 = 8;(3,1)⋅(−1,3)=3×(−1)+1×3=0(3, 1)\cdot(-1, 3) = 3\times(-1) + 1\times3 = 0;(3,1)⋅(−2,−1)=(−6)+(−1)=−7(3, 1)\cdot(-2, -1) = (-6) + (-1) = -7。

为什么它能衡量相似?拖动下面两支箭头的尖端,看读数怎样变化。两支箭头大致指向同一边时,对应的数往往同号,乘积为正,和就大。两支箭头互相垂直时,正的乘积和负的乘积相互抵消,点积为 00。一支箭头是另一支的负数倍时,每个非零的对应乘积都为负,因此相加也为负。不过有一个问题:把一支箭头拉长到两倍,它的每个数都变成两倍,点积也变成两倍,可它的方向并没有变。

ab
拖动 a\mathbf a 和 b\mathbf b 的箭头尖
a\mathbf a
(3.0, 1.0)(3.0,\ 1.0)
b\mathbf b
(2.0, 2.0)(2.0,\ 2.0)
点积
3.0×2.0+1.0×2.0=8.003.0\times2.0 + 1.0\times2.0 = 8.00
长度
∣a∣=3.16, ∣b∣=2.83|\mathbf a| = 3.16,\ |\mathbf b| = 2.83
余弦相似度
8.00÷(3.16×2.83)=0.8948.00 \div (3.16\times2.83) = 0.894
夹角
θ=26.6∘, cos⁡θ=0.894\theta = 26.6^\circ,\ \cos\theta = 0.894

为了比较方向而不把长度的影响算进去,我们需要知道每支箭头的长度。箭头 (a1,a2)(a_1, a_2) 是一个直角三角形的斜边,两条直角边的长度分别是 ∣a1∣|a_1| 和 ∣a2∣|a_2|。由勾股定理,它的长度是 ∣a∣=a12+a22|\mathbf a| = \sqrt{a_1^2 + a_2^2}。这也等于 a⋅a\sqrt{\mathbf a\cdot\mathbf a},因为 a⋅a=a1a1+a2a2\mathbf a\cdot\mathbf a = a_1a_1 + a_2a_2。数更多时,就在根号里照样多加几个平方。对于非零向量,把点积除以两支箭头的长度,就得到余弦相似度。零向量没有方向,也不能这样除。下面会把长度公式和比较方向的理由展开说明:

长度公式从哪里来

一支箭头向右走 a、向上走 b。我们需要从起点直达终点的长度 c,才能把“方向相同、只是整体更长”分开比较。横向和纵向相互垂直,它们与箭头组成一个直角三角形。先只取正的 a、b,把四个一样的三角形放进下面的大正方形。

绿色是四个三角形,金色是中间的正方形。大正方形的每条边由 a 和 b 接成。中间每条边都是一个三角形的斜边 c;相邻两边的夹角是直角,因为旁边两个锐角的和是九十度。一个三角形的面积是 ab/2ab/2:把它与一份翻转的副本拼起来,正好得到边长 a、b 的长方形。

现在用“大块减四个小块”求金色面积。同一个面积又等于边长 c 的平方,因此两种算法必须相等。

c2=(a+b)2−4⋅ab2=a2+2ab+b2−2ab=a2+b2\begin{aligned}c^2&=(a+b)^2-4\cdot\frac{ab}{2}\\ &=a^2+2ab+b^2-2ab\\ &=a^2+b^2\end{aligned}
c=32+42=25≈5.000c=\sqrt{3^2+4^2}=\sqrt{25}\approx5.000

长度不取负数,所以取非负平方根。若坐标为负,用坐标的绝对值作为边长,平方后仍得到同一公式。某一条边为零时,箭头就是另一条边;两条都为零时,长度为零。零向量没有方向,接下来不能拿它的长度作除数。

余弦相似度=a⋅b∣a∣ ∣b∣\text{余弦相似度}=\frac{\mathbf a\cdot\mathbf b}{|\mathbf a|\,|\mathbf b|}

把向量除以它的长度,相当于把它缩短或拉长到长度 11,而方向不变,所以这个数只取决于方向。不论两支箭头多长,方向相同时它等于 11,垂直时等于 00,方向相反时等于 −1-1。读数还显示它等于两支箭头夹角的余弦;下面接着推导为什么会这样,始终要求两个向量非零。

推导:为什么余弦相似度等于夹角的余弦

先看一个关于角的事实。在以原点为圆心、半径为 11 的圆上取两点,它们与水平轴的夹角分别是 α\alpha 和 β\beta。它们的坐标是 (cos⁡α,sin⁡α)(\cos\alpha, \sin\alpha) 和 (cos⁡β,sin⁡β)(\cos\beta, \sin\beta),这正是余弦和正弦对单位圆上的点的意思。由勾股定理,两点距离的平方是 (cos⁡α−cos⁡β)2+(sin⁡α−sin⁡β)2(\cos\alpha - \cos\beta)^2 + (\sin\alpha - \sin\beta)^2。展开后,对每个点用 cos⁡2+sin⁡2=1\cos^2 + \sin^2 = 1,就得到 2−2(cos⁡αcos⁡β+sin⁡αsin⁡β)2 - 2(\cos\alpha\cos\beta + \sin\alpha\sin\beta)。

现在把整个圆往回转 α\alpha。两点之间的距离不变,但现在一个点在角度 00 处,即点 (1,0)(1, 0),另一个点在角度 β−α\beta - \alpha 处。同样的计算给出距离平方 2−2cos⁡(β−α)2 - 2\cos(\beta - \alpha)。两个结果描述的是同一段距离,所以 cos⁡(β−α)=cos⁡αcos⁡β+sin⁡αsin⁡β\cos(\beta - \alpha) = \cos\alpha\cos\beta + \sin\alpha\sin\beta。

最后,把 a\mathbf a 写成 ∣a∣(cos⁡α,sin⁡α)|\mathbf a|(\cos\alpha, \sin\alpha),把 b\mathbf b 写成 ∣b∣(cos⁡β,sin⁡β)|\mathbf b|(\cos\beta, \sin\beta)。于是 a⋅b=∣a∣∣b∣(cos⁡αcos⁡β+sin⁡αsin⁡β)=∣a∣∣b∣cos⁡(β−α)\mathbf a\cdot\mathbf b = |\mathbf a||\mathbf b|(\cos\alpha\cos\beta + \sin\alpha\sin\beta) = |\mathbf a||\mathbf b|\cos(\beta - \alpha),而 β−α\beta - \alpha 正是两支箭头的夹角。再除以 ∣a∣∣b∣|\mathbf a||\mathbf b|,就得到这个夹角的余弦。在三维中,两支箭头位于过原点的一个平面内,在这个平面里同样的论证成立。超过三维时,我们根本画不出这个角,于是就用这个公式来定义它;在能画出来的情形里,它和画出来的角一致。

现在把它用在我们每个词 8 个数的向量上。在下面的模型里选一个词:列表按余弦相似度给其他所有词排序,计算时用的是全部 8 个数,而不只是图里画出的三个。“猫”和“狗”几乎指向同一个方向,“猫”和“卡车”则不是。

动物交通工具食物国王男孩女人女王男人自行车公交车汽车狮子狗猫马蛋糕面包苹果
拖动可旋转 ·
显示的坐标轴
这个词的 8 个数: 猫
动物1.01
交通工具0.04
食物−0.04
人−0.05
年幼0.01
女性0.05
王室0.00
体型大0.14
与它最相似的词: 猫
  1. 狗0.963
  2. 狮子0.810
  3. 马0.792
  4. 小狗0.742
  5. 小猫0.711

向量还可以做算术。取“国王”,减去“男人”,再加上“女人”:国王−男人+女人\text{国王} - \text{男人} + \text{女人}。减去“男人”,就去掉了国王和男人共有的部分(是人、体型),主要剩下“王室”;加上“女人”,又把“人”和体型加了回来,同时加上“女性”。结果最接近“女王”。学出来的词向量也有同样的现象,只不过是近似的,所以我们找的是最近的词,而不是完全相等的词。

哪一对向量的点积等于 0?

07

注意力:让小段互相看

现在每个 token 都有了向量。可是从表里查出来的向量总是同一个,不管这个 token 出现在哪里。看这个句子:“那只猫坐下了,因为它很累。”嵌入表在每个句子里都给“它”同一个向量,可在这里,“它”指的是猫。要读懂这个句子,“它”的向量必须吸收来自“猫”的信息。而在“那条路封了,因为它被水淹了”里,同一个“它”需要的却是“路”的信息。所以,每个 token 都需要一种办法去看其他 token,并判断哪些对自己重要。注意力做的就是这件事。

注意力的工作方式很像一次搜索。每个 token 用自己的数算出三个新的向量:

  • **查询(query)**:描述这个 token 在找什么;
  • **键(key)**:描述这个 token 能向寻找者提供什么;
  • **值(value)**:当别的 token 注意到它时,它传过去的信息。

对“它”来说,查询大致在说“我在找一个我能指代的事物”,而“猫”的键在说“我是一个事物”。查询和键匹配得好,点积就大,这正是上一章衡量相似的办法。所以计划是:把“它”的查询和它能看到的每个 token 的键比较,匹配得越好,传过来的信息就越多。

那么,token 怎样算出这些向量?在我们的例子里,每个 token 有 4 个数,顺序是 [事物,指代词,动作,描述]。查询和键各有 2 个数,这样就能画成箭头。为了把 4 个数变成 2 个数,模型保存了一张数表,叫作矩阵,算查询用的记作 WQW_Q。它对 4 个特征各有一行,每行有 2 个数,表示这个特征给查询加上什么。算一个 token 的查询时,把每一行乘以这个 token 在该特征上的数,再把四个结果加起来。“它”的 4 个数是 (0.2,1,0,0)(0.2, 1, 0, 0),所以四行分别乘以 0.20.2、11、00 和 00:

q=0.2(0,0.8)+1(2.2,0)+0(1.2,0)+0(1.2,0.4)=(0,0.16)+(2.2,0)=(2.2,0.16)\begin{aligned}\mathbf q&=0.2(0,0.8)+1(2.2,0)\\ &\quad+0(1.2,0)+0(1.2,0.4)\\ &=(0,0.16)+(2.2,0)\\ &=(2.2,0.16)\end{aligned}

用同样的办法,第二个矩阵 WKW_K 算出键,第三个矩阵 WVW_V 算出值。“猫”的 4 个数是 (1,0,0,0.1)(1, 0, 0, 0.1),它的键是 1×(2,0)+0.1×(0,0.4)=(2,0.04)1\times(2, 0) + 0.1\times(0, 0.4) = (2, 0.04),主要在说“我是一个事物”。每个输出的数,都是 token 的 4 个数和矩阵某一列的点积,这就是为什么这种运算叫作“向量乘以矩阵”。

“它”和“猫”之间的分数,是“它”的查询和“猫”的键的点积,再除以 d\sqrt d,这里 d=2d = 2 是每个查询和键里数的个数。为什么要除,我们稍后解释。

分数(它,猫)=q它⋅k猫d=2.2×2+0.16×0.042≈4.40641.4142≈3.12\begin{aligned}\text{分数}(\text{它},\text{猫})&=\frac{\mathbf q_{\text{它}}\cdot\mathbf k_{\text{猫}}}{\sqrt d}\\ &=\frac{2.2\times2+0.16\times0.04}{\sqrt2}\\ &\approx\frac{4.4064}{1.4142}\approx3.12\end{aligned}

分数可以是任何数,可正可负。我们想要的是权重:都是正数,而且加起来等于 11。这样每个权重就表示一个 token 分到了多大份额的注意力。softmax 函数分两步把分数变成权重。第一步:以每个分数为指数求 ee 的乘方。这里 e≈2.718e \approx 2.718 是一个固定的数。指数是整数时,乘方就是连乘,例如 e3=e×e×ee^3 = e\times e\times e。分数不是整数时,乘方在这些整数之间平滑地补齐。用计算器就能算出,例如 e0.32≈1.38e^{0.32} \approx 1.38。重要的有两点:对任何分数 ss,ese^s 都是正数;分数越大,ese^s 越大,所以分数的大小顺序保留了下来。

第二步:把每个结果除以所有结果的总和。每一份都除以同一个总和,各份额加起来就等于 11,这和第 3 章用次数除以总次数是同一个道理。设能看到的 nn 个 token 的分数是 s1,…,sns_1, \ldots, s_n,那么第 jj 个 token 的权重是

wj=esjes1+es2+⋯+esnw_j=\frac{e^{s_j}}{e^{s_1}+e^{s_2}+\cdots+e^{s_n}}

对“它”来说,它能看到的 token 是 那只、猫、坐下了、因为 和它自己,分数分别是 0.320.32、3.123.12、0.230.23、0.450.45 和 1.561.56。第一步约得 1.381.38、22.5522.55、1.251.25、1.581.58 和 4.744.74,总和约为 31.5031.50。第二步:“猫”分到“它”的注意力的 22.55÷31.50≈0.7222.55 \div 31.50 \approx 0.72,“它”自己约分到 4.74÷31.50≈0.154.74 \div 31.50 \approx 0.15。

有一个有用的性质:给每个分数都加上同一个数 cc,权重都不变。每个乘方都变成 es+c=ec⋅ese^{s+c} = e^c\cdot e^s,同一个因子 ece^c 出现在每一项和总和里,做除法时就约掉了。

最后,每个 token 的输出是值向量的加权平均:把每个能看到的 token 的值乘以它的权重,再加起来。对“它”来说,结果的 0.720.72 来自“猫”的值,所以“它”的新向量主要带着“猫”的信息。在模型里,各个值箭头按权重缩短后首尾相接,最后那个箭头尖就是输出。

输出它=0.04 v那只+0.72 v猫+0.04 v坐下了+0.05 v因为+0.15 v它\begin{aligned}\text{输出}_{\text{它}}&=0.04\,\mathbf v_{\text{那只}}+0.72\,\mathbf v_{\text{猫}}\\ &\quad+0.04\,\mathbf v_{\text{坐下了}}+0.05\,\mathbf v_{\text{因为}}\\ &\quad+0.15\,\mathbf v_{\text{它}}\end{aligned}

还有一条规则。模型写文字时,一个 token 只能看它自己和它前面的 token,因为后面的还没有写出来。后面那些的分数被直接去掉,可以理解为分数是 −∞-\infty,而它的 ee 乘方是 00。这叫作因果遮罩。在模型里把它关掉,就能看到编码器——一次读入整段已写好文字的结构——会怎样做。

选一个“去看”的 token。左图显示它的查询(金色)和它能看到的那些 token 的键(灰色)。条形显示分数和 softmax 权重。右图把按权重缩短的值首尾相接,得到输出(金色)。下面的方格同时列出所有 token 的权重:行是“去看”的 token,列是“被看”的 token。

“去看”的 token
查询 · 键
那只0.320.04
猫3.120.72
坐下了0.230.04
因为0.450.05
它1.560.15
很被遮罩挡住
累被遮罩挡住
分数权重

分数(它,猫)=2.20×2.00+0.16×0.041.41=3.12\text{分数}(\text{它},\text{猫}) = \dfrac{2.20\times2.00 + 0.16\times0.04}{1.41} = 3.12

值 × 权重 → 输出 (0.80, 0.21)
全部权重(行看列)
那只猫坐下了因为它很累
1.00
0.470.53
0.160.710.13
0.140.520.180.16
0.040.720.040.050.15
0.100.440.090.100.190.09
0.070.430.100.080.150.090.07

为什么要除以 d\sqrt d?点积要把 dd 个乘积加起来。在真实模型里,每个查询和键有 64 或 128 个数,把很多乘积加起来,往往得到很大的分数。分数之间差得很大时,softmax 会把几乎全部权重都给一个 token。学习就会停滞:一个权重接近 00 的 token 几乎不影响结果,也就几乎得不到修正(第 10 章)。如果这些数像典型大小为 11 的随机数,那么 dd 个乘积之和的典型大小约为 d\sqrt d。所以除以 d\sqrt d,就能把分数拉回到典型大小约为 11。在模型里关掉这次除法:“它”对“猫”的权重从 0.720.72 升到 0.860.86,其他权重都变小了。

推导:为什么点积的典型大小是 d\sqrt d

设点积为 S=x1y1+x2y2+⋯+xdydS = x_1y_1 + x_2y_2 + \cdots + x_dy_d,其中每个数都是独立的随机数,平均值为 00,平方的平均值为 11。为了衡量 SS 的典型大小,我们看 S2S^2 的平均值,再开平方。

把 S2S^2 展开,得到 dd 个平方项 (xiyi)2(x_iy_i)^2,以及许多 i≠ji \ne j 的交叉项 2xiyixjyj2x_iy_ix_jy_j。由于 xix_i 和 yiy_i 独立,每个平方项的平均值是 1×1=11\times1 = 1。每个交叉项里都含有比如 xix_i 这样的因子,它和这一项里的其他数都独立,平均值为 00,所以整个交叉项的平均值是 00。

所以 S2S^2 的平均值是 d×1=dd\times1 = d,SS 的典型大小是 d\sqrt d。除以 d\sqrt d 之后,不论 dd 是多少,分数平方的平均值都是 11。学出来的查询和键并不完全是这样的随机数,所以这是选择缩放比例的依据,而不是精确的定律。

一个注意力头在每个位置算出一份值的加权组合;它不一定只承担一种固定、能用日常语言命名的关系。Transformer 会让好几个头并排工作,每个头有自己的三个矩阵,再把它们的输出拼起来。切换到注意力头 B:换了一组矩阵,同样的 token 现在找的是最近发生的动作,而不是被指代的事物。最初的 Transformer 每层有 8 个头,大模型有几十个。没有人告诉一个头该找什么;和词向量一样,它的矩阵是学出来的,只有一部分头最后有清楚的意义。

如果一行里每个注意力分数都加上 55,softmax 权重会怎样?

08

搭起 Transformer

注意力让 token 之间交换信息。Transformer 的一层在注意力前后加上几个更简单的步骤,整个模型就是许多这样的层叠起来,每层有自己学出来的数。点选图中的每个部分,看看它做什么、为什么需要它。

多头注意力

多个注意力头(第 7 章)让每个 token 从它能看到的 token 那里收集信息。

跟着两个数算一次归一化

相加后的数值可能整体偏大,也可能分散得很开。减去共同的偏移,可以让它们围绕零分布;调整分散程度,则让它们不至于相差太大。下面只用两个数,每次推进都会保留前面的结果,方便确认新数从哪里来。这里暂时把最后可学习的缩放设为 1、偏移设为 0。

0 / 3
x=(−1, 3)x=(-1,\,3)

真实层还可以把这些结果逐项乘上学出来的系数,再加学出来的偏移。那以后平均值、方差就未必是 0、1。我们拆开算的是这一步的算术,并不是声称归一化能保证整个模型学得好。

为什么需要位置?注意力通过点积和加权平均把 token 组合起来,这两样运算都不管 token 站在哪里。把句子打乱顺序,输出也只是跟着打乱而已。在下面的演示中,两个句子都由“狗”“咬”“人”三个词组成,我们在三个 token 上运行第 7 章的注意力头 A,不加遮罩。比较两个句子里“狗”的输出。

狗 咬 人
狗咬人
“狗”的输出 = (0.477, 0.577)
人 咬 狗
人咬狗
“狗”的输出 = (0.477, 0.577)
“狗”的输出: 完全相同

没有位置时,两个输出完全相等,模型就分不出是谁咬了谁。给每个 token 加上位置向量(句子中每个位置有一个固定的模式)以后,两个输出就不一样了,后面的层可以利用这种差别。

原论文用不同波长的正弦和余弦波来构造位置向量。今天许多模型改成按 token 的位置,把每个查询和键旋转一个角度(旋转位置编码),这样点积就取决于两个 token 相隔多远。

在 2017 年的论文里,Transformer 是为翻译设计的,比如把英语翻译成德语。它分成两半。编码器一次读入整个英语句子,不加遮罩。解码器一个 token 一个 token 地写出德语句子,加上因果遮罩。它在每一层还要注意编码器的输出,以便看到英语原句。这叫作交叉注意力。基础版模型每一半有 6 层,每个 token 512 个数,8 个注意力头,约 6500 万个学出来的数,用 8 块 GPU 训练了大约 12 小时。它的大号版本在英德和英法翻译上创造了新纪录。GPT 系列语言模型只保留了解码器那一半,也不需要交叉注意力:它读入已有的文字,写出下一个 token。

选读:论文里的那一行公式

论文把第 7 章写成了一行:Attention⁡(Q,K,V)=softmax⁡ ⁣(QKT/d)V\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\!\left(QK^{\mathsf T}/\sqrt d\right)V。这里 QQ 是一张表,它的每一行是一个 token 的查询;KK 和 VV 分别是键和值的表。KTK^{\mathsf T} 读作“KK 的转置”,就是把 KK 的行变成列。

表 QQ 乘以表 KTK^{\mathsf T},在第 ii 行第 jj 列得到查询 ii 和键 jj 的点积,也就是一次得到全部分数。对每一行做 softmax,就得到我们模型中那张权重方格。再用这张方格乘以 VV,每一行都得到值的加权和,也就是一次得到全部输出。这一行公式里没有新东西,它只是把我们对每个 token 的计算合在一起做,而这正是 GPU 做得最快的事。

09

选出下一小段

回到“那只猫坐在……上”。经过最后一层后,模型为最后一个 token 留下一个向量 h\mathbf h。为了给每个候选的下一个 token 打分,它计算 h\mathbf h 和这个 token 的向量的点积:分数大,说明 h\mathbf h 和这个候选词指向相近的方向。这些原始分数叫作 logits。在我们的小例子里,向量有 4 个数,表示 [可以坐的东西,室内,柔软,食物],h=(2.2,1,0.8,−1)\mathbf h = (2.2, 1, 0.8, -1),“垫子”的向量是 (1.1,0.8,0.9,0)(1.1, 0.8, 0.9, 0)。所以“垫子”的 logit 是 2.2×1.1+1×0.8+0.8×0.9+(−1)×0=2.42+0.8+0.72+0=3.942.2\times1.1 + 1\times0.8 + 0.8\times0.9 + (-1)\times0 = 2.42 + 0.8 + 0.72 + 0 = 3.94。

softmax,也就是注意力里用过的那个函数,把 logits 变成概率。然后模型必须做出选择。总是选概率最大的 token 听起来很合理,可那样写出的文字重复、乏味,同一个问题每次的回答也完全一样。所以模型通常采用抽样:随机抽一个 token,每个 token 被抽中的机会等于它的概率。按几次“抽一个”,再按“抽 100 个”,比较每个词被抽中的次数和它的概率。

那只猫坐在____上

下一个词logit概率被抽中次数
垫子3.9434.0%0
沙发3.5823.7%0
椅子3.2216.6%0
地板3.0413.8%0
屋顶1.763.8%0
桌子2.266.3%0
汽车0.621.2%0
月亮−1.440.2%0
香蕉−1.580.1%0
想法−1.780.1%0

p(垫子)=e3.94/1.0∑jezj/1.0=51.4151.1=0.340p(\text{垫子}) = \dfrac{e^{3.94/1.0}}{\sum_j e^{z_j/1.0}} = \dfrac{51.4}{151.1} = 0.340

top-kk
top-pp

0 次. 绿色条:概率;细金色条:实际被抽中的比例。

温度 TT 在 softmax 之前去除每一个 logit。T=1T = 1 时什么都不变。为了看清 TT 的作用,比较 logits 为 z1z_1 和 z2z_2 的两个候选词。它们概率的比是 ez1/T÷ez2/Te^{z_1/T} \div e^{z_2/T},因为 softmax 里的总和对两者相同,会约掉;这又等于 e(z1−z2)/Te^{(z_1 - z_2)/T}。“垫子”和“沙发”的 logits 是 3.943.94 和 3.583.58,T=1T = 1 时比值是 e0.36≈1.43e^{0.36} \approx 1.43,T=0.5T = 0.5 时是 e0.72≈2.05e^{0.72} \approx 2.05。所以温度低于 11 会拉大差别,让可能的 token 更占优势;TT 接近 00 时,模型几乎总是选第一名。温度高于 11 会缩小差别,不常见的词就出现得更多。

pj=ezj/Tez1/T+⋯+ezn/Tp1p2=ez1/Tez2/T=e(z1−z2)/T\begin{gathered}p_j=\frac{e^{z_j/T}}{e^{z_1/T}+\cdots+e^{z_n/T}}\\ \frac{p_1}{p_2}=\frac{e^{z_1/T}}{e^{z_2/T}}=e^{(z_1-z_2)/T}\end{gathered}

还有两个控制项,在抽样之前把一长串不太可能的 token 截掉。top-kk 只保留最可能的 kk 个 token。top-pp 从最可能的开始依次保留,直到保留下来的概率加起来至少为 pp;所以模型有把握时它保留得少,没把握时保留得多。两种情况下,保留下来的概率都要除以它们新的总和,让它们重新加起来等于 11。

抽出一个 token 后,模型把它接到文字后面,再运行一遍,选出下一个。这和第 3 章计数模型的循环完全一样,只是概率好得多。一段长回答要经过成千上万次这样的步骤,每一步都要完整地经过每一层。

温度非常接近 00 时,抽样会怎样?

10

这些数从哪里来?

我们遇到过的所有数——嵌入表、每个注意力头的矩阵、前馈网络的矩阵——都叫作参数。GPT-3 有 1750 亿个参数。它们的值从哪里来?一开始它们是很小的随机数,训练用一个非常简单的任务来调整它们。拿一段真实的文字,遮住下一个 token,让模型预测。然后轻轻推动每一个参数,让正确的 token 本来能更可能一点。

为了朝正确的方向推,我们需要一个数来说明预测错得有多厉害。如果模型给正确 token 的概率是 pp,我们就用损失 −ln⁡p-\ln p。这里 ln⁡\ln 是自然对数,它是“求 ee 的乘方”的逆运算:ln⁡p\ln p 就是 ee 要乘方多少次才得到 pp 的那个指数。p=1p = 1 时损失是 −ln⁡1=0-\ln 1 = 0;p=0.5p = 0.5 时约为 0.690.69;p=0.01p = 0.01 时约为 4.64.6。模型有把握而且答对时,损失很小;正确 token 的概率越接近 00,损失就越大,没有上限。

为什么用对数?模型给整段文字的概率,是其中各个 token 概率依次相乘的积。乘积的对数等于对数之和。设 a=exa = e^x、b=eyb = e^y,那么 ab=ex⋅ey=ex+yab = e^x\cdot e^y = e^{x+y},所以 ln⁡(ab)=x+y=ln⁡a+ln⁡b\ln(ab) = x + y = \ln a + \ln b。因此,整段文字的损失就是各个 token 的损失之和,每个 token 都可以当作一个单独的小例子来处理。

接下来是“推”。对每个参数,微积分给出它的斜率:参数增加一点点时,损失变化得有多快,也就是它的导数。对于可微、梯度不为零的损失,沿反方向走足够小的一步,可以让附近的损失下降;随意取一个大步并没有这个保证。这叫梯度下降,步子的大小由学习率 η\eta(读作 eta)决定。为了看清算术,下面的小实验直接调整三个输出 logits,不是在训练整个网络。对这些 logits,斜率的形式简单得惊人:对正确的 token 是 p−1p - 1,对其他每个 token 就是它自己的概率 pp。正确 token 记 y=1y = 1,其他记 y=0y = 0,那么一步就是

zj  ←  zj−η (pj−yj)z_j\;\leftarrow\;z_j-\eta\,(p_j-y_j)

所以每一步把正确 token 的 logit 提高 η(1−p)\eta(1 - p),把每个错误 token 的 logit 降低 ηp\eta p。在下面一步步训练;正确答案是“垫子”。一开始三个 logits 是 0.50.5、0.80.8 和 0.20.2,所以 p(垫子)≈0.32p(\text{垫子}) \approx 0.32,损失约为 1.131.13。用 η=1\eta = 1 走一步后,“垫子”的 logit 提高 1×(1−0.32)=0.681\times(1 - 0.32) = 0.68,变成 1.181.18;“沙发”和“月亮”分别降低它们的概率 0.440.44 和 0.240.24。每走一步,正确答案都更可能一些,而且 pp 越接近 11,增加得越少。学习率越大走得越快,但在有几十亿参数的真实模型里,步子太大会冲过头,损失会上下乱跳,所以学习率要仔细选择。

下一个词logit zzppp−yp-y改变量 −eta(p−y)-\\eta(p-y)
垫子 · 正确答案0.500.32−0.68+0.68
沙发0.800.440.44−0.44
月亮0.200.240.24−0.24
学习率 η\eta
步
0 / 12
损失 −ln⁡p(垫子)-\ln p(\text{垫子})
−ln⁡0.324=1.128-\ln 0.324 = 1.128

推导:斜率 p−yp - y 是怎么来的

把 logits 记作 z1,…,znz_1, \ldots, z_n,把它们的 ee 乘方之和记作 S=ez1+⋯+eznS = e^{z_1} + \cdots + e^{z_n}。如果 cc 号 token 是正确的,pc=ezc/Sp_c = e^{z_c}/S,所以损失是 −ln⁡pc=−(ln⁡ezc−ln⁡S)=−zc+ln⁡S-\ln p_c = -\left(\ln e^{z_c} - \ln S\right) = -z_c + \ln S。这里用了 ln⁡(a/b)=ln⁡a−ln⁡b\ln(a/b) = \ln a - \ln b 和 ln⁡ez=z\ln e^z = z。

现在只改变一个 logit zjz_j。如果 j=cj = c,−zc-z_c 这一项以 −1-1 的速率变化,否则不变,所以这一项的变化率是 −yj-y_j。对 ln⁡S\ln S 用链式法则:ln⁡u\ln u 的导数是 1/u1/u,而 zjz_j 变化时 SS 以 ezje^{z_j} 的速率变化,因为 eze^z 的导数是 eze^z。所以 ln⁡S\ln S 的变化率是 ezj/S=pje^{z_j}/S = p_j。两部分相加,损失对 zjz_j 的斜率就是 pj−yjp_j - y_j。

前面各层的参数通过中间计算影响最后的损失。反向传播利用链式法则,把这些局部变化率有次序地合起来,高效地求出每个参数该怎样调整;它不是另外提供正确答案的方法。下面会把两个局部变化率相乘,看看前面的参数为什么收到这样的调整。预测下一个 token 只是一种训练目标,之后还可以加入指令例子、偏好反馈等目标,因此不能推断一个助手的所有行为都只来自这一项训练。

前面的参数怎样收到调整

先用一条能算到底的链:输入是 2,参数是 w,预测是 2w2w,希望得到 3。误差记为 a=2w−3a=2w-3。为了不让正负误差抵消,把误差平方作为损失 L(w)=(2w−3)2L(w)=(2w-3)^2。现在只改变 w,追踪它怎样一路影响损失。

若 w 增加 h,误差就增加 2h。把变化后的误差代回平方,与原来的平方相减,就得到损失的变化。h 不为零时,把这个变化除以 h;让 h 逐渐接近零,所得比值就趋近此处所需的斜率。

L(w+h)−L(w)=(a+2h)2−a2=4ah+4h2L(w+h)−L(w)h=4a+4h(h≠0)L′(w)=4a=4(2w−3)\begin{gathered}L(w+h)-L(w)\\ =(a+2h)^2-a^2\\ =4ah+4h^2\\ \frac{L(w+h)-L(w)}h=4a+4h\\ (h\ne0)\\ L^{\prime}(w)=4a=4(2w-3)\end{gathered}

也能把它拆成两段:w 每增加一点,误差按两倍变化;平方对误差的局部变化率为 2a,因为 ((a+t)2−a2)/t=2a+t((a+t)^2-a^2)/t=2a+t。两段相乘得到 2×2a=4a2\times2a=4a。反向传播就是把这类局部关系有次序地合起来;这里的简单链让乘法的来源看得见。

η 是你选择的步长。用 0.1 调整后,还原参数并试试 0.3,就能比较步长的影响:方向仍是逆着斜率,大步却会越过合适位置,反而把损失推高。所以“小一点”需要结合这个函数判断,不能保证任何步长都有效。最小损失在 w=1.5w=1.5,因为它恰好使误差为零。下面的读数保留三位小数,所以用约等号 ≈;每次调整仍使用未舍入的数,不能把显示出来的近似数当作精确数计算。

w≈0.000L≈9.000L′≈−12.000\begin{aligned}w&\approx0.000\\ L&\approx9.000\\ L^{\prime}&\approx-12.000\end{aligned}
wnext=w−ηL′(w)≈0.000−0.1×(−12.000)≈1.200Lnext≈0.360\begin{aligned}w_{\rm next}&=w-\eta L^{\prime}(w)\\ &\approx0.000-0.1\times(-12.000)\\ &\approx1.200\\ L_{\rm next}&\approx0.360\end{aligned}
11

Transformer 从哪里来

2017 年以前,处理语言的标准工具是循环神经网络(RNN)。它一次读一个 token,就像一个人用手指指着字一行行往下读,同时带着一个记忆向量往前走。每一步,它把记忆和新 token 的向量组合起来,得到新的记忆。读完最后一个 token,这个记忆就应该装下整个句子。

这种设计有两个弱点。前面 token 的信息要经过后面的每一步,才能在最后被用上,一路上会逐渐变淡,就像一句话沿着一排人传下去。LSTM(1997)加上了“门”——学出来的开关,决定保留什么、忘掉什么、加入什么——让记忆保持得更久,但并没有消除这个问题。第二个弱点是速度:第 50 步需要第 49 步的记忆,所以这些步骤不能同时计算,而 GPU 恰恰是在同时做成千上万次计算时最快。

2014 年,一种叫“序列到序列”的翻译系统用一个 RNN 把英语句子读成一个记忆向量,再用第二个 RNN 根据它写出法语句子。把整个句子挤进一个向量是一个瓶颈,长句子翻译得很差。同一年,巴赫达瑙、赵和本吉奥让负责写的 RNN 在每一步都能回头看负责读的 RNN 的全部记忆,并按每个记忆与当前需要的匹配程度算出权重。这就是注意力,当时它只是 RNN 的一个帮手。

Transformer 的作者们问:如果让注意力承担全部工作,把逐步传递的记忆完全去掉,会怎样?这就是标题《Attention Is All You Need》的意思。每个 token 一步就能到达前面任何一个 token,在整个训练序列已提供时,许多位置能够并行计算,同时用遮罩挡住后面的答案。但通常的自回归生成仍要先选出这一个 token,才能决定接下来那个,新词元的选择仍然有先后依赖。比较下面两种设计:选定文字的长度,看看第一个 token 的信息要走多少步才能到达最后一个。

从第一个到最后一个 token 的步数
8−1=78 - 1 = 7
必须一个接一个完成的步骤
88
每个头的比较次数
—

注意力也有代价。加上因果遮罩时,第 ii 个 token 要和 ii 个 token 比较,所以 nn 个 token 的文字在每一层的每个头里需要 1+2+⋯+n1 + 2 + \cdots + n 次比较。

1+2+⋯+n=n(n+1)21+2+\cdots+n=\frac{n(n+1)}{2}

要把它加起来,可以把这个和写两遍,一遍正着写,一遍倒着写,再把两行逐项相加:1+n1 + n、2+(n−1)2 + (n - 1)……每一对都等于 n+1n + 1。一共有 nn 对,所以和的两倍是 n(n+1)n(n + 1),和就是 n(n+1)/2n(n + 1)/2。1000 个 token 就需要 1000×1001÷2=500 5001000\times1001\div2 = 500\,500 次比较;长度翻倍,工作量大约变成四倍。这就是长文字计算昂贵的原因,也是后来许多研究致力于让注意力更便宜的原因。时间复杂度一课专门讲这种增长。

12

从 Transformer 到今天的大模型

不到一年,就出现了两种使用 Transformer 的方式。GPT(OpenAI,2018)保留了解码器,用一大批书籍训练它预测下一个 token,这叫预训练。得到这个模型后,研究者用针对具体任务的小型标注数据继续训练,这叫微调。BERT(Google,2018)保留了编码器,训练它利用两侧的文字填出被遮住的词,这让它适合搜索、分类这样的任务。两者都说明,大量原始文本上的训练能学到可供多种任务使用的语言规律。

接下来是规模的增长。GPT-2(2019)有 15 亿个参数;GPT-3(2020)有 1750 亿个,用约 3000 亿个 token 训练。GPT-3 展示了一件新事:不再做任何训练,只要在提示里写几个例子,它就能完成任务。比如写几对英法句子,再给一个新的英语句子。2020 年,研究者测量发现,当参数、数据和计算量一起增长时,损失会平稳地、可预测地下降。这叫作规模定律。2022 年的 Chinchilla 研究发现,许多模型相对于它们读过的文字量来说太大了。比较好的比例大约是每个参数配 20 个训练 token。Chinchilla 有 700 亿个参数,用了 700 亿×20=14000 亿700\text{ 亿}\times20 = 14000\text{ 亿},也就是 1.4 万亿个训练 token。

这张图显示公开发表的模型大小。纵轴上每一条网格线都是下面一条的 10 倍。之所以要这样的刻度,是因为大小从 6500 万一直到 6710 亿,大约跨了四个“十倍”;如果用普通刻度,2020 年以前的所有模型都会被压扁在底部。点选一个点,看它的详细信息。有几个最强的模型从未公布大小,所以图中没有画出它们,而不是去猜测。

GPT-3 · 2020 · 参数: 1750 亿

一个只学会接着写文字的模型,还算不上助手:你问它一个问题,它可能接着写出更多问题。又经过两个阶段的训练,它才变成了助手。在指令微调中,模型用人写的“请求加好回答”的例子来训练。在基于人类反馈的强化学习(RLHF)中,人们比较模型给出的两个回答。第二个模型学会预测人们更喜欢哪一个。然后调整语言模型,让它给出这个第二个模型打分高的回答。InstructGPT(2022)表明,人们更喜欢一个这样训练、只有 13 亿参数的模型的回答,而不是原来 1750 亿参数的 GPT-3 的回答。2022 年 11 月发布的 ChatGPT 把这一成果带给了大众。训练 Claude 的公司 Anthropic 提出了一种相关的方法,叫“宪法式 AI”(Constitutional AI)。模型还会按照一份写好的原则清单,评判并修改自己的回答。

从那以后,基本的机器一直是 Transformer,只是它的许多部分得到了改进。

混合专家

把前馈网络拆成许多较小的网络,叫作“专家”,再由一个小的路由器把每个 token 只送给其中少数几个。这样模型可以拥有非常多的参数,而每个 token 只用到一部分:DeepSeek-V3(2024)有 6710 亿个参数,但每个 token 只用 370 亿个。

更长的上下文

模型一次能注意到的文字量,从 GPT-3 的 2048 个 token 增长到几十万个,有些模型达到一百万个甚至更多。这靠的是更省的注意力算法和更好的位置编码。

不止文字

图片、声音和视频被切成小块,变成向量,于是同样的注意力可以把它们和文字混在一起处理。

推理

从 2024 年起,一些模型通过在答案可以检验的题目上做强化学习,学会在回答之前先写出一长串中间步骤。这大大提高了它们在数学和编程上的表现。

工具与智能体

模型被训练去调用搜索引擎、运行代码、操作软件,从而完成需要很多步的任务。

今天最有名的模型系列包括 GPT(OpenAI)、Claude(Anthropic)、Gemini(Google)、Llama(Meta)、DeepSeek 和通义千问 Qwen(阿里巴巴)。有些公开参数,任何人都可以下载;有些连大小都不公布。

有些东西并没有改变,值得说清楚。模型仍然是根据从训练文本中学到的概率,一次产生一个 token。它可能流利地说出错误的内容,这叫“幻觉”,因为一个可能的续写不一定是真的。除非给它搜索之类的工具,它的知识停在训练数据为止。至于它的计算在多大程度上算是“思考”,这正是图灵的问题,至今仍没有定论。

13

扩散模型:去掉噪声来画画

刚才研究的文字生成器一次选择一个 token。接下来研究另一种生成图像的方法,叫扩散。我们先要知道图片在计算机里是什么:它是一格一格的像素,每个像素存一个表示亮度的数(彩色图片则存红、绿、蓝三个数)。一张 28×2828\times28 的灰色小图,就是 28×28=78428\times28 = 784 个数。

现在加上噪声:给每个像素加一个随机数,这些随机数服从钟形的正态分布,大多数接近 00,少数较大。分很多步慢慢地加,同时每一步把原图的亮度数乘以较小的系数;缩小的是数值贡献,不是图片的宽度或高度,图片就逐渐变成纯粹的噪声。第 tt 步之后,像素是

xt=αˉt x0+1−αˉt εx_t=\sqrt{\bar\alpha_t}\,x_0+\sqrt{1-\bar\alpha_t}\,\varepsilon

这里 x0x_0 是原来的图片,ε\varepsilon(读作 epsilon)是噪声,αˉt\bar\alpha_t(读作“alpha bar t”)是原图保留下来的份额:开始时是 11,最后几乎降到 00。所以图片是两种成分的混合:原图乘以权重 αˉt\sqrt{\bar\alpha_t},噪声乘以权重 1−αˉt\sqrt{1 - \bar\alpha_t}。拖动滑块加噪声,读数会带着一个像素走一遍这个公式。为什么取平方根?因为它们的平方加起来正好是 11:(αˉt)2+(1−αˉt)2=αˉt+1−αˉt=1\left(\sqrt{\bar\alpha_t}\right)^2 + \left(\sqrt{1 - \bar\alpha_t}\right)^2 = \bar\alpha_t + 1 - \bar\alpha_t = 1。只有在明确的模型条件下,这样才保持方差为 1:干净数据这个随机量与独立噪声的方差都为 1。它并不表示任意一张固定照片的可见对比度保持不变。对一个固定的原始像素,只有噪声是随机的,其方差是 1 减去 alpha-bar。下面的推导会区分这两种情况,并说明为什么出现平方。

保留份额 αˉt\bar\alpha_t
1.0001.000
αˉt, 1−αˉt\sqrt{\bar\alpha_t},\ \sqrt{1-\bar\alpha_t}
1.000, 0.0001.000,\ 0.000
一个像素: x0, εx_0,\ \varepsilon
0.85, −0.860.85,\ -0.86
xtx_t
1.000×0.85+0.000×(−0.86)=0.8501.000\times0.85 + 0.000\times(-0.86) = 0.850

加噪声很容易,也不需要学习。巧妙的是把它倒过来。我们在几百万张图片上训练一个神经网络,任务很简单:给它一张带噪声的图片 xtx_t 和步数 tt,让它猜加进去的噪声 ε\varepsilon。这个任务很容易准备,因为带噪声的图片是我们自己做出来的,知道正确答案。网络一旦能猜出噪声,把上面的公式变形,也就能猜出干净的图片:

x^0=xt−1−αˉt ε^αˉt\hat x_0=\frac{x_t-\sqrt{1-\bar\alpha_t}\,\hat\varepsilon}{\sqrt{\bar\alpha_t}}

字母上的小帽子表示“估计值”。推导方法是:在加噪公式两边同时减去 1−αˉt ε\sqrt{1 - \bar\alpha_t}\,\varepsilon,右边只剩 αˉt x0\sqrt{\bar\alpha_t}\,x_0;再把两边同时除以 αˉt\sqrt{\bar\alpha_t}。

要画一张新图,就从纯噪声出发,把步骤倒着走一遍。每一步都先猜出干净的图片,算出这个猜测所对应的噪声。然后在稍低一点的更低噪声水平上把两者重新合起来。这里用加噪公式,把 αˉt\bar\alpha_t 换成 αˉt−1\bar\alpha_{t-1}。开始时猜出来的是模糊的平均;噪声越来越少,猜测就越来越清楚;到最后一步 αˉ0=1\bar\alpha_0 = 1,结果就是最后的猜测本身。这种走法叫作 DDIM;最初的方法 DDPM 每一步还会再加一点新的噪声。

xt−1=αˉt−1 x^0+1−αˉt−1 ε^x_{t-1}=\sqrt{\bar\alpha_{t-1}}\,\hat x_0+\sqrt{1-\bar\alpha_{t-1}}\,\hat\varepsilon

一整张图片的数太多,没法画成一个点,所以下面的模型用最小的“图片”:只有 2 个数的单个点。训练数据是沿着一颗心形排列的 240 个点。在左边的标签页里加噪声,看心形化成一团圆圆的云。在右边的标签页里生成:500 个纯噪声的点一步步走回心形。选中一个点,可以跟踪它的路径,以及每一步做出的猜测。

训练点xtx_t (训练点)
保留份额 αˉt\bar\alpha_t
1.0001.000

我们的小模型不需要训练网络,因为它完全知道那 240 个训练点。这时,对干净点的猜测是训练点的加权平均;和注意力一样,权重由 softmax 从分数算出。训练点 dd 的分数,表示 dd 能多好地解释这个带噪声的点 xtx_t。怎样得到分数呢?加噪过程会把 dd 缩小成 αˉt d\sqrt{\bar\alpha_t}\,d,所以这是干净点 dd 在加入噪声之前所在的位置。量出 xtx_t 到这个位置的平方距离,就得到了它们相差多少的一个量:距离越短,解释得越好。把这个平方距离除以噪声份额的两倍 2(1−αˉt)2(1 - \bar\alpha_t),并加上负号,距离较短的点就会得到较高的分数:

sd=−∣xt−αˉt d∣22(1−αˉt)s_d=-\frac{\left|x_t-\sqrt{\bar\alpha_t}\,d\right|^2}{2\left(1-\bar\alpha_t\right)}

为什么要除以噪声份额?生成刚开始时,噪声份额 1−αˉt1 - \bar\alpha_t 很大,即使离得较远的训练点,分数也和最好的那个相差不多;许多点分享权重,猜测就是模糊的平均。接近结束时,噪声份额非常小,最近的那个点几乎拿走全部权重,猜测就变得清晰。这和注意力里的 softmax 加权平均完全一样:带噪声的点是查询,训练点既是键也是值。选读部分推导这个分数。

推导:为什么起作用的是平方,以及小模型的分数从哪里来

随机数的分散程度用方差来衡量:它离自己平均值的距离的平方的平均。取 x=aX+bYx = aX + bY,其中 XX、YY 独立,平均值都是 00。x2x^2 的平均值等于 a2×(X2 的平均)+2ab×(XY 的平均)+b2×(Y2 的平均)a^2\times(X^2\text{ 的平均}) + 2ab\times(XY\text{ 的平均}) + b^2\times(Y^2\text{ 的平均})。独立且平均为 00,使 XYXY 的平均为 00,所以 xx 的方差是 a2var⁡X+b2var⁡Ya^2\operatorname{var}X + b^2\operatorname{var}Y。把像素值缩放到方差为 11,噪声的方差也是 11,取 a=αˉta = \sqrt{\bar\alpha_t}、b=1−αˉtb = \sqrt{1 - \bar\alpha_t},每一步的方差都是 αˉt+(1−αˉt)=1\bar\alpha_t + (1 - \bar\alpha_t) = 1。

再看分数。如果干净的点是 dd,那么 xt−αˉt d=1−αˉt εx_t - \sqrt{\bar\alpha_t}\,d = \sqrt{1 - \bar\alpha_t}\,\varepsilon,所以噪声一定是 ε=(xt−αˉt d)/1−αˉt\varepsilon = \left(x_t - \sqrt{\bar\alpha_t}\,d\right)/\sqrt{1 - \bar\alpha_t}。这里假设各坐标的噪声独立,且服从标准正态分布。其概率密度正比于 e−∣ε∣2/2e^{-|\varepsilon|^2/2},其中长度平方就是各噪声坐标的平方和。密度比较的是落在同样小的邻域内的可能性,并不是某一个精确实数有正的概率。代入上面求出的噪声,“来自 dd”这个解释的似然正比于 e−∣xt−αˉt d∣2/(2(1−αˉt))e^{-|x_t - \sqrt{\bar\alpha_t}\,d|^2/(2(1 - \bar\alpha_t))},也就是 ee 的“分数”次方。

在看到 xtx_t 之前,每个训练点的可能性都一样。由贝叶斯公式,xtx_t 来自 dd 的概率,就是它的可能性除以所有可能性的总和,这正是对分数做 softmax。平均平方误差最小的猜测,是各个可能的干净点按这些概率加权的平均,这就是小模型使用的公式。

为什么把可能的干净点加权平均

上面的点模型先假设每个存储的干净点同样可能。固定当前带噪声的位置后,某个干净点能产生这份观测的密度越大,它得到的权重越高。共同的先验概率和高斯密度常数会在分子、分母中约掉,剩下各个指数分数除以它们的总和。

pi=esi∑jesj,∑ipi=1p_i=\frac{e^{s_i}}{\sum_j e^{s_j}},\qquad\sum_i p_i=1

这些权重仍没有告诉我们该输出哪一个数。我们还要说明目标:如果用平方误差评价猜测,怎样让平均损失最小?先只看一个坐标。候选干净数记为 did_i,权重为 pip_i,加权平均记为 μ=∑ipidi\mu=\sum_i p_i d_i,我们的猜测记为 u。

把“猜测到候选”的差拆成“猜测到平均”与“平均到候选”。展开平方以后,中间项消失:平均值的定义保证加权偏差的总和为零。

∑ipi(μ−di)=μ∑ipi−∑ipidi=μ−μ=0\begin{aligned}\sum_i p_i(\mu-d_i)&=\mu\sum_i p_i-\sum_i p_id_i\\ &=\mu-\mu=0\end{aligned}
∑ipi(u−di)2=(u−μ)2+∑ipi(μ−di)2\begin{gathered}\sum_i p_i(u-d_i)^2\\ =(u-\mu)^2+\sum_i p_i(\mu-d_i)^2\end{gathered}

右边第二项不随猜测 u 改变,第一项是非负的平方,在 u=μu=\mu 时为零。因此平方误差下最好的猜测是这个加权平均。二维时把两个坐标的平方误差相加,各自做同样的推理即可。它一般不同于“挑概率最大的一个点”;我们明确选择了平方误差这个评价方法。

这个小模型明确保存了 240 个干净点,每次估计都从这些点计算。噪声水平很小时,权重集中到附近的存储点,输出因而趋向这个有限集合。真实扩散模型改为从例子中学习一个函数;这个函数可以组合出新结果,也可能记忆或复现训练材料。网络大小有限,并不意味着它不会记忆。这个点模型演示的是它选定的经验分布,不能用来保证生成结果一定原创,也不是所有图像模型的完整机制。

像“垫子上的一只猫,油画风格”这样的提示,是怎样控制画面的呢?文字先由一个 Transformer 变成向量。去噪网络在每一步都通过交叉注意力(和翻译用的 Transformer 一样)去注意这些向量。于是它的猜测被拉向与文字相符的图片。为了省计算,Stable Diffusion(2022)在图片的压缩版本上完成整个过程,而不是逐个像素处理。这叫潜空间扩散。许多更新的图像和视频模型,干脆直接用 Transformer 作为去噪网络。于是这一页的两半在这里会合:注意力和扩散如今一起工作。

在加噪过程中,最开始 t=0t = 0 时,αˉt\bar\alpha_t 是多少?

14

回到图灵的问题

让我们回到图灵。1950 年,他提出了一个只看对话的测试,也提出了一种会学习、而不是一条条规则编出来的机器。今天的语言模型正是这样的学习机器。它有一张向量表,有让 token 互相参考的注意力,还有每个 token 内部的小网络。几十亿个参数被调整过,用来预测海量文字中的下一个 token。

这算不算思考,人们还在讨论。在我们研究的简化生成器中,每次选择下一个 token,都依赖学出来的数与已经提供的上下文。完整的助手还可以结合写好的指令、检索和其他程序工具。看清这个学习得到的计算过程,有助于我们核对事实,分清流利表达与可靠证据,并提出更有用的问题。