老万故事会

【老万】从0开始学chatGPT(七):位置的艺术

Image

本文是我的 chatGPT 学习心得《从0开始学chatGPT》系列第七篇。欢迎依次阅读:

~~~~

人工智能究竟是科学还是艺术?有这样一个段子:世界上有两种科学家,一种是科学家,另一种是计算机科学家。这当然是世人用来调侃计算机专家的,但也并非全无道理。很多时候,在机器学习和神经网络领域,我们从实验中发现某种方法有效,却不清楚其背后的原因。比如某种网络架构效果好,但为什么效果好,有没有更好的架构?这些问题往往缺乏理论指导。

从这个角度来说,人工智能的发展仍处于一个非常早期的阶段。如果有人能发掘出一些普适的有现实指导意义的理论,将人工智能从手工业提升到科学范畴,他/她的名字必将载入史册,与日月同辉。

也许我该说它的名字 - 能完成这一使命的,说不定只有人工智能本身。

为什么 chatGPT 如此厉害?不仅仅是因为它拥有巨量的神经网络参数,训练时用到了天量的数据和海量的人工反馈,更是因为它架构合理,充分利用了电脑的计算能力。否则,从黄教主那里买再多的 GPU 也没戏。

在这一点上,我们真的应该感谢谷歌的早期研究,是他们提出了 Transformer架构。这是一种用于自然语言处理的深度神经网络模型。相对于其他传统的深度神经网络模型,如循环神经网络(RNN)和卷积神经网络(CNN),Transformer 更并行化,在大规模自然语言处理任务中表现出色,迄今为止还没有更好的架构来取代它。从这一期开始,我们就来一起学习这个Transformer 架构吧。

之前讨论过,每个单词或记号(token)在 GPT 内部都会通过一层简单的神经网络映射层对应到一个嵌入向量(embedding vector)。比如在 GPT-2 中,这是一个768维的向量。这个向量代表了记号的语言特征。相同的输入记号,它们的嵌入向量也相同。这往往会给后续的理解造成困难,因为同一个符号在不同位置上时,其功能和意义可能完全不同。

例如,“一把把把把住”中,第一个“把”是量词,第二个是介词,第三个是名词,第四个是动词。如果无法理解它们之间的差别,就会觉得这是满纸荒唐言,只能一把辛酸泪。

因此,一个很自然的想法是,我们能否将位置信息告诉模型,让它在处理时带上这些信息,以区分不同位置上的相同记号?

我们可以给输入的每一个记号赋予一个位置顺序号,从零开始。比如,如果模型最多允许一次处理 2047 个记号,那么输入记号的顺序编号就是从 0 到 2046。

我们曾提到过,神经网络不适合处理离散数据,比如非连续函数或条件逻辑。因此,谷歌的做法是先将离散的顺序号转换成一个与嵌入向量长度相同的向量,称为位置编码(positional encoding)。然后,将位置编码与嵌入向量相加(即对应的元素相加),结果作为后续处理的输入。

为什么是加法而不是其它运算,比如拼接?

这就涉及到人工智能设计的艺术了。它其实没有什么高深的道理,只不过因为加法计算简单,不增加向量的维度,而且实践中发现效果不错。也许本文的读者中会有人在某天发现一种更好的方法将位置信息融合进嵌入向量,提升大语言模型的功能和效率。你准备好接受挑战了吗?

~~~~

位置编码是如何实现的呢?我一开始的想法是在 768 维(假定是 GPT-2)空间中尽量把位置编码向量均匀分开,以增加它们的区分度。但这种想法过于简单了,因为区分度并不是唯一需要考虑的因素,我们还要考虑位置编码向量间的相对关系。例如,当输入数据平移一个位置时,不应引起位置编码太大的变化。

一种思路是让机器来学习位置编码:通过不断给模型反馈,让它自行调整位置编码。初始时,位置编码是随机数,通过反馈逐渐调整以优化输出结果。谷歌确实这么试过了。他们发现,这样做效果不错,但和他们选择的固定位置编码算法相比,并无明显优势,白白增加了训练成本,徒蘼弹药。为了提高效率,他们选择了一个固定的算法:

对 0,1,2,3,... 每个位置序号 p 我们指定一个相应的位置编码向量 P(p)。这个向量的值只跟位置序号 p 有关,跟在这个位置上是哪个记号无关。因为我们需要把这个向量和记号的嵌入向量相加,它的维数必须和嵌入向量相同。这个维数我们用 d 表示。比如在 GPT-2 里面 d = 768。

把 P(p) 的 d 个元素展开,可以写成:

Image

把 P(0),P(1),P(2),... 从上到下一字排开,也就是说每行对应于一个输入记号,就成了这样一个矩阵:

Image

在谷歌的 transformer 论文里,这个矩阵的元素用如下公式计算:

Image

所以,

  • 矩阵的第 0 列 q_p,0 是周期为 2π 的正弦数列,

  • 矩阵的第 1 列 q_p,1 是周期为 2π 的余弦数列,

  • 矩阵的第 2 列 q_p,2 是周期为 2π*10000^(2/d) 的正弦数列,

  • 矩阵的第 3 列 q_p,3 是周期为 2π*10000^(2/d) 的余弦数列,

  • 矩阵的第 4 列 q_p,4 是周期为 2π*10000^(4/d) 的正弦数列,

  • 矩阵的第 5 列 q_p,5 是周期为 2π*10000^(4/d) 的余弦数列,

  • ...

  • 矩阵的最后一列 q_p,d-1 是周期为 2π*10000^(1 - 2/d) 的余弦数列。

~~~~

我们来看看这样安排的玄机,试试能不能参透 transformer 设计者的意图。

直观地说,如果位置编码矩阵的某一列是周期为 k 的数列,那么对应的位置编码元素就可以用来捕捉距离大致为 k 的记号间的相关性。这里 k 不必正好是整数。比如,如果两个记号的位置相差 6,它们的前两个位置编码向量元素的数值就会很接近(因为这些元素的周期是 2π,大约 6.28),神经网络就会把它们当成相关记号来处理。

从左到右,位置编码向量元素的周期是逐渐加大的。向量的最后两个元素可以帮助捕捉距离大致 2π*10000 ≈ 62832 个记号间的相关。

为什么选择 10000 这个值?又要谈到艺术了。

这个选择在某种程度上是任意的,因为它代表老万在实践中发现它效果很好,就保留下来了。我们也可以用其它常数,主要要求是该常数远大于1,以便在位置编码中捕捉到距离很远的记号间的相关。

至于为什么计算公式中没有用到 π,那是因为位置编码的主要目标是为序列中的每个位置创建独特且可区分的编码。虽然 π 通常用于表示周期性的三角函数,但在这里,正弦和余弦函数根据给定的公式已经提供了一个模型可以识别和利用的重复模式,强行引入 π 并无必要。

我们注意到,这些周期之间是等比数列关系。这种安排保证了不管两个符号距离如何,总有一些周期跟它们的距离相差不远,可以捕捉它们之间的相关。比如,d 是 768 的时候,相邻的两个周期相差 2.4%(10000^(2/768) ≈ 1.024),所以两个符号间的距离和最近的周期不会相差超过 1.2%。

我们以前学过,chatGPT 是用自回归方法生成回答的:它的历史窗口记得最近对话的 N 个记号。每次都从这 N 个记号推测出下一个记号,再把这个记号加入历史,把最早的历史记号忘掉,腾出空间计算新的记号。

比如历史窗口的宽度是 2047,在某一时刻里面记载了记号 T100,T101,...,T2145,T2146。产生下一个记号 T2147后,T100 会被忘掉,窗口更新为 T101,T102,...,T2146,T2147。

所以,在每次生成新记号的时候,每个输入记号的位置都会变化(减一),但它们的相对位置不变。所以,我们选取位置编码方案的时候,最好能保证相对位置固定的两个记号的位置编码间也有一个固定关系,不要因为移位就破坏了这种关系。

谷歌的方案符合这个要求吗?我们来考察一下 P(p+k) 和 P(p) 之间是什么关系(假定 k 是一个常数)。为简化记号,在确定 p 和 k 之后,我们规定

Image

于是,按照位置编码函数的定义

Image

现在,请跟我一起大声诵读高中数学老师教的三角函数公式:

赛可可赛,符号不改。
可可赛赛,符号要改。

(不要说你已经把它还给老师还加了点利息。)

这首四言古风生动描述了曾轶可参加快乐女声比赛时评委纠结要不要过的情节下面这个定理:

Image

果断代入,可得:

Image

也就是说 P(p+k) 可以由一个固定矩阵乘以 P(p) 得到:

Image

这说明什么?说明两个记号的相对位置差别固定时,它们的位置编码具有一个固定的线性关系。谷歌的研究者们猜想这可以让系统学到记号的相对位置和它们的相关性之间的联系。

为了直观展现位置编码向量的样子,我用 Wolfram 网站制作了一段动画。这里,我把维度 d 选为 64,横坐标是向量的下标(从 0 到 63),纵坐标是对应的向量元素值。随着时间的推移,我们将依次看到位置为 0,1,2,3,... 1023 的位置编码向量图像。

大家可以看到,越靠左边的元素变化周期越短,震荡激烈,成落霞与孤雁齐飞之势;越往右边,变化周期越长,波澜不惊,为秋水共长天一色之态。

想要验算的同学,可以使用以下 Wolfram 代码:

Animate[  Plot[Switch[Mod[Floor[x],2],              0,Sin[p/Power[10000,Floor[x]/64]],              1, Cos[p/Power[10000,(Floor[x]-1)/64]]],       {x, 0, 64}],  {p, 0, 1024},  AnimationRunning -> False]

位置编码的设计不过是 transformer 架构的小小一部分,就已经包含了这么多的奇思妙想,值得我们后来者细品。看来,要设计靠谱的人工智能,设计者自己先得有足够的智能。

我们也要指出,这些设计思想并不是不可商榷的金科玉律。先行者们基于自己的经验和数学功底想出来一些点子,在实验验证有效之后就变成了约定俗成的惯例。鲁迅先生说:世界上本没有正弦函数做的位置编码,大抵是谷歌试了还行,大家就都这么干了。但这些方案的有效性是经验性的,没有谁完全理解它们为什么有效。作为后人,我们不能墨守成规束缚了自己的手脚。有机会还是要寻求理论突破。

~~~~

前面说过,一个记号的嵌入向量加上它的位置编码向量就得到了这个记号的输入向量。接下来,这个输入向量会被送到多层 transformer 进行后续处理。每层 transformer 的参数不同,但结构相似。它们都会让输入先经过一个线性投影层。

Image

GPT-2 的输入部分架构

线性投影层(linear projection layer)是一种简单的神经网络层,主要用于将输入向量投影到另一个维度空间。它本质上就是一个矩阵乘法,这个矩阵在训练过程中是可学习的。和一般的神经网络不同,线性投影层没有激活函数,只执行线性变换,也因而得名。

投影(projection)类似于《三体》中的降维打击,是线性代数中的一个基本操作,可以用来将高维向量简化为低维向量。其原理是剔除不重要的维度,只保留我们关心的维度。

Image

示例:二维空间向一维空间的投影

在实际投影过程中,不一定是在已有的坐标系下进行。例如,将一个 1024 维的向量简化为一个 64 维的向量时,我们并不一定是找出 960 个不重要的维度将它们消除,只保留剩下的 64 个维度。这是因为在高维向量中,很难恰好找到现成的维度是我们不关心的。通常在投影的时候,我们需要进行坐标变换,先将高维向量转换到一个维度不变但坐标轴不同的空间,然后再进行投影。Image

示例:二维向量 v1 和 v2 在不正交的坐标系中对 x' 轴的投影

投影在生活中有很现实的应用。例如,在找对象时我们就迫切需要使用这一技能。我们知道,除了小说中的纸片人,现实生活中的每个人都有不同的特质,这些特质来自很多不同的维度,如身高、颜值、健康、学识、谈吐、人品、财富、性格等。一个所有维度都完美的人是不存在的。即便存在,那也是你想多了,因为配不上。

Image

所以,我们需要根据自己的需求,将代表一个人特质的高维向量投影到一个符合自己价值观的坐标系,删除掉一些不必考虑的维度,化繁为简,才能做出合理的选择。建立一个稳定的坐标系,即牢固的三观,非常重要。如果没有稳定的坐标系,我们就会今日爱秋香,明日念昭君,无所适从。

徐志摩写过:“我是天空里的一片云,偶尔投影在你的波心。”这就是在描述一个从三维空间到二维空间的线性变换。在这里,徐志摩忽略了两人地位上的差别,不去管一个在天,一个在静安区。他把高度这个维度完全抹去,只求两个人心心相印。也就是说,把两个人的嵌入向量投影到他关注的低维空间,如果投影后的向量有很大的相似度,他就要去追求。

看到这里,大家一定已经累了,我们先歇歇。欲知 GPT 如何运用自注意力机制发掘上下文之间的相关性,读懂“一把把把把住”,I'll be back。

~~~~~~~~~~

猜你会喜欢:

~~~~~~~~~~

关注老万故事会公众号:

码字不易,呕心沥血只是希望更多人看到。如果喜欢这篇文章,请不吝订阅、转发、评论。谢谢!🙏