前一篇:《全面解释人工智能LLM模型的真实工作原理(二)》
**序言:**前面两节中,我们介绍了大语言模型的设计图和实现了一个能够生成自然语言的神经网络。这正是现代先进人工智能语言模型的雏形。不过,目前市面上的语言模型远比我们设计的这个复杂得多。那么,它们到底复杂在什么地方?本节将为你详细介绍,这些模型是如何通过一些关键技术使得神经网络在特定领域的表现达到甚至超越人类水平的?总结起来就是下面的九个方面。
(关注不迷路,及时收到最新的人工智能资料更新)
到底是什么让大型语言模型如此有效?
最早的模型通过逐字符生成'Humpty Dumpty sat on a wall',这与当前最先进的大型语言模型的功能相去甚远,但它却是这些先进模型的核心原理。通过一系列创新和改进,生成式AI从这种简单的形式演变为能够进行类人对话的机器人、AI客服、虚拟员工等,成为解决现实问题的强大工具。那么,当前的先进模型究竟在哪些方面做了改进?让我们逐一解析。
嵌入
还记得我们提到的输入字符方式并非最佳吗?之前我们随意给每个字符分配了一个数字。若是可以找到更合适的数字,或许可以训练出更好的网络。那么如何找到这些更好的数字呢?这里有个聪明的方法:
在前面的模型训练中,我们通过调整权重,观察最终损失是否减小来训练模型,不断调整权重。在每个步骤中,我们会:
• 输入数据
• 计算输出层
• 与期望输出比较并计算平均损失
• 调整权重,重新开始
在这个过程中,输入是固定的,这在RGB和体积作为输入时是合理的。但现在的输入字符a、b、c等的数字是我们随意选定的。如果在每次迭代中,不仅调整权重,还调整输入表示法,看看用不同数字代表"a"能否降低损失呢?这确实可以减少损失,让模型变得更好(这是我们设计的方向)。基本上,不仅对权重应用梯度下降,对输入的数字表示也应用梯度下降,因为它们本身就是随意选择的数字。这被称为"嵌入"。它是一种输入到数字的映射,需要像参数一样进行训练。嵌入训练完成后,还可以在其他模型中复用。请注意,要始终用相同的嵌入表示同一符号/字符/词。
我们讨论的嵌入只有每个字符一个数字。然而,实际上嵌入通常由多个数字组成,因为用单个数字难以表达一个概念的丰富性。回顾我们的叶子和花朵例子,每个物体有四个数(输入层的大小),这些数分别表达了物体的属性,模型可以有效利用这些数去识别物体。若只有一个数字,例如红色通道,模型可能会更难判断。要捕捉人类语言的复杂性,需要不止一个数字。
因此,我们可以用多个数字表示每个字符以捕捉更多丰富性吗?让我们给每个字符分配一组数字,称之为"向量"(有顺序地排列每个数字,如果交换位置会变成不同的向量。比如在叶子和花朵的数据中,交换红色和绿色的数会改变颜色,得到不同的向量)。向量的长度即包含多少个数字。我们会给每个字符分配一个向量。这里有两个问题:
• 如果给每个字符分配向量而非数字,如何将"humpty dumpt"输入到网络?答案很简单。假设我们为每个字符分配了10个数字的向量,那么输入层中12个神经元就变成120个神经元,因为"humpty dumpt"中的每个字符有10个数字。然后我们将神经元并排放好即可。
• 如何找到这些向量?幸运的是,我们刚刚学习了嵌入训练。训练嵌入向量与训练参数相似,只是现在有120个输入而不是12个,目标还是减少损失。取前10个数就是对应"h"的向量,依此类推。
所有嵌入向量的长度必须相同,否则无法一致输入不同字符组合。比如"humpty dumpt"和"umpty dumpty"------两者都输入12个字符,若每个字符的向量长度不同,就无法输入到120长的输入层。我们来看嵌入向量的可视化:
让我们称这组相同长度的向量为矩阵。上图的矩阵称为嵌入矩阵。你告诉它列号,代表字符,然后在矩阵中找到对应列即可获得表示该字符的向量。这种嵌入适用于嵌入任何事物集合,你只需为每个事物提供足够的列数。
子词分词器
到目前为止,我们使用字符作为语言的基本构件,但这种方法有局限性。神经网络的权重必须做大量工作来理解某些字符序列(即单词)以及它们之间的关系。如果我们直接将嵌入分配给单词,并让网络预测下一个单词呢?反正网络也只理解数字,我们可以给"humpty"、"dumpty"、"sat"、"on"等单词分配一个10维向量,然后输入两个单词让它预测下一个。"Token"指的是嵌入的单元,我们的模型之前使用字符作为token,现在提议用整个单词作为token(当然你也可以用整个句子或短语作为token)。
使用单词分词对模型有深远影响。英语中有超过18万个单词,若每个可能输出用一个神经元表示,则输出层需要几十万个神经元,而不是26个左右。随着现代网络中隐藏层大小的增加,这一问题变得不那么棘手。需要注意的是,由于每个单词都是独立处理的,初始嵌入也用随机数表示,所以相似的单词(如"cat"和"cats")的初始表示毫无关系。可以预期模型会学习到两个单词的相似性,但能否利用这个明显的相似性以简化学习呢?
可以的。今天语言模型中最常用的嵌入方案是将单词分成子词并嵌入。例如,我们将"cats"分成两个token:"cat"和"s"。模型更容易理解其他词后的"s"的含义等。这也减少了token数量(sentencepiece是一种常用的分词器,词汇表大小为数万,而不是英语中的几十万单词)。分词器将输入文本(如"Humpty Dumpt")拆分为token并返回相应的数字,用于查找该token在嵌入矩阵中的向量。例如,"humpty dumpty"在字符级分词器下会拆成字符数组['h','u',...'t'],然后返回对应数字[8,21,...20],因为你需要查找嵌入矩阵的第8列以获得'h'的嵌入向量(嵌入向量是输入模型的,而不是数字8,不同于之前的操作)。矩阵列的排列无关紧要,给'h'分配任何列都可以,只要每次输入'h'时查找相同的向量就行。分词器给我们一个随意(但固定)的数字以便查找,而真正需要分词器的是将句子切分成token。
利用嵌入和子词分词,模型可能如下所示:
接下来的几节涉及语言建模中的最新进展,正是它们让LLM如此强大。然而,理解这些之前需要掌握一些基础数学概念。以下是这些概念的总结:
• 矩阵及矩阵乘法
• 数学中函数的基本概念
• 数字的幂次方(例如a³=aaa)
• 样本均值、方差和标准差
附录中有这些概念的总结。
自注意力机制
到目前为止,我们只讨论了一种简单的神经网络结构(称为前馈网络),这种网络包含若干层,每层都与下一层完全连接(即,任何相邻层的两个神经元之间都有一条线),并且它仅连接到下一层(例如,层1和层3之间没有连接线)。然而,你可以想象,其实没有什么可以阻止我们移除或创建其他连接,甚至构建更复杂的结构。让我们来探讨一种特别重要的结构:自注意力机制。
观察人类语言的结构,我们想要预测的下一个词通常取决于前面的所有词。然而,它可能更依赖某些前面的词。例如,如果我们要预测"Damian有一个秘密孩子,是个女孩,他在遗嘱中写到他的所有财产,连同魔法球,都将归____"。此处填的词可以是"她"或"他",具体取决于句子前面的某个词:女孩/男孩。
好消息是,我们的简单前馈模型可以连接到上下文中的所有词,因此它可以学习重要词的适当权重。但问题在于,通过前馈层连接特定位置的权重是固定的(对每个位置都是如此)。如果重要的词总是在同一个位置,它可以学习到适当的权重,那就没问题了。然而,下一步预测所需的相关词可以出现在系统的任何地方。我们可以将上面的句子进行改写,在猜"她还是他"时,无论出现在句子的哪个地方,男孩/女孩这个词都是非常重要的。所以我们需要让权重不仅依赖于位置,还依赖于该位置的内容。如何实现这一点?
自注意力机制的运作类似于对每个词的嵌入向量进行加权,但不是直接将它们相加,而是为每个词应用一些权重。例如,如果humpty、dumpty、sat的嵌入向量分别是x1、x2、x3,那么它会在相加之前将每个向量乘以一个权重(一个数值)。比如output = 0.5 * x1 + 0.25 * x2 + 0.25 * x3,其中output是自注意力的输出。如果我们将权重写作u1、u2、u3,那么output = u1 * x1 + u2 * x2 + u3 * x3,那么这些权重u1、u2、u3是怎么得到的呢?
理想情况下,我们希望这些权重依赖于我们所加的向量------如前所述,有些词可能比其他词更重要。但对谁更重要呢?对我们即将预测的词更重要。因此,我们还希望这些权重取决于我们即将预测的词。不过,这里有一个问题:在预测之前,我们当然不知道即将预测的词是什么。所以,自注意力机制采用紧接着我们将要预测的词的前一个词,即句子中当前可用的最后一个词(我不确定为什么是这样而不是其他词,不过深度学习中的许多事情都是通过反复试验得出的,我猜这是个有效的选择)。
好了,我们想要这些向量的权重,并且希望每个权重依赖于当前聚合的词和即将预测词的前一个词。基本上,我们想要一个函数u1 = F(x1, x3),其中x1是我们要加权的词,x3是我们已有序列中的最后一个词(假设我们只有3个词)。一种直接的实现方法是为x1定义一个向量(称为k1),为x3定义一个独立的向量(称为q3),然后取它们的点积。这会得到一个数值,且它依赖于x1和x3。那么,这些向量k1和q3是如何得到的?我们可以构建一个简单的单层神经网络,将x1映射为k1(或者将x2映射为k2,x3映射为k3等)。同时构建另一个网络将x3映射为q3,依此类推。用矩阵表示法,我们基本上可以得到权重矩阵Wk和Wq,使得k1 = Wk * x1,q1 = Wq * x1,依此类推。现在我们可以对k1和q3进行点积得到一个标量,所以u1 = F(x1, x3) = Wk * x1 · Wq * x3。
在自注意力机制中,还有一个额外步骤,我们不会直接取嵌入向量的加权和,而是取该嵌入向量的某种"值"的加权和,这个"值"通过另一个小的单层网络获得。这意味着类似于k1和q1,我们还需要一个v1用于词x1,通过矩阵Wv得到,即v1 = Wv * x1。然后聚合这些v1。因此,若我们仅有3个词,并试图预测第四个词,整个过程看起来是这样的:
图中的加号表示向量的简单相加,意味着它们必须长度相同。最后一个未展示的修改是标量u1、u2、u3等不一定加和为1。如果我们需要它们作为权重,应该让它们加和为1。所以这里我们会应用熟悉的技巧,使用softmax函数。
这就是自注意力。还有一种交叉注意力(cross-attention),可以将q3来自最后一个词,但k和v可以来自完全不同的句子。例如,这在翻译任务中很有价值。现在我们已经了解了什么是注意力机制。
我们可以将这个整体封装成一个"自注意力块"。基本上,这个自注意力块接收嵌入向量并输出一个任意用户选择长度的单一向量。这个块有三个参数,Wk、Wq、Wv------它并不需要更复杂。机器学习文献中有许多这样的块,通常在图中用一个标注其名称的方框来表示。类似这样:
在自注意力中你会注意到,词的顺序似乎不那么重要。我们在整个过程中使用相同的W矩阵,所以交换Humpty和Dumpty并不会有实质差别------所有数值的结果都会相同。这意味着,虽然注意力可以识别需要关注的内容,但它不会依赖词的位置。不过我们知道词的位置在英语中很重要,通过给模型一些位置信息可以提高性能。
因此,在使用注意力机制时,我们通常不会直接将嵌入向量输入自注意力块。稍后我们将看到如何在输入注意力块之前,通过"位置编码"将位置信息添加到嵌入向量中。
注意:对于已经了解自注意力的人可能会注意到,我们没有提到任何K和Q矩阵,也没有应用掩码等。这是因为这些是模型常见训练方式的实现细节。数据批量输入,模型同时被训练预测从humpty到dumpty、从humpty dumpty到sat,等等。这是为了提高效率,并不影响理解或模型输出,因此我们选择忽略了训练效率上的优化技巧。
Softmax
我们在最开始简要提到过softmax。这是softmax试图解决的问题:在输出层中,我们有与可能选项数量相同的神经元,并且我们说将选择网络中值最高的神经元作为输出。然后我们会计算损失,方法是求网络提供的值和我们期望的理想值之间的差。但我们理想的值是什么呢?在叶子/花朵的例子中,我们设为0.8。但为什么是0.8?为什么不是5、10或1000万?理论上,越高越好!理想情况下,我们想要无穷大!不过这样会让问题变得不可解------所有的损失都将是无穷大,我们通过调整参数来最小化损失的计划(记得"梯度下降"吗)就失效了。该如何处理呢?
一个简单的方法是限制理想值在某个范围内,比如0到1之间。这样所有损失都会是有限的,但现在又出现了新问题:如果网络输出值超出这个范围怎么办?比如在某个例子中它输出(5,1)表示(叶子, 花朵),而另一个例子输出(0,1)。第一个例子做出了正确的选择,但损失却更高!好吧,我们需要一种方法也将输出层的值转换到(0,1)的范围内,同时保持顺序不变。我们可以使用任何数学上的"函数"来实现这个目标(一种"函数"就是将一个数字映射到另一个数字的规则------输入一个数字,输出另一个数字),一个可行的选择是逻辑函数(如下图所示),它将所有数字映射到(0,1)之间,并保持顺序不变:
现在,输出层中每个神经元都有一个0到1之间的数值,我们可以通过设定正确的神经元为1、其他为0来计算损失,这样就可以比较网络输出与理想值的差异。这样能行,不过能不能更好一点?
回到我们"Humpty Dumpty"的例子,假设我们逐字生成"dumpty",模型在预测"m"时出错了,输出层中最高的不是"m"而是"u",但"m"紧随其后。如果我们继续用"duu"来预测下一个字符,模型的信心会很低,因为"humpty duu..."后续可能性不多。然而,"m"是接近的次高值,我们可以也给"m"一个机会,预测接下来的字符,看看结果如何?也许能给出一个更合理的词。
所以,我们这里谈到的不是盲目选择最大值,而是试试几种可能性。怎么做才好呢?我们得给每个可能性一个概率------比如选最高的概率为50%,次高的为25%,依次类推,这样挺好。不过,或许我们还希望概率与模型的预测结果相关联。如果模型对m和u的预测值相当接近(相对其他值),那么50-50的机会可能会是不错的选择。
我们需要一个漂亮的规则,将这些数值转换为概率。softmax就做了这个工作。它是上述逻辑函数的推广,但增加了一些特性。如果你输入10个任意数字,它会返回10个输出,每个在0到1之间,且总和为1,因此我们可以将它们解释为概率。你会发现,softmax在几乎每个语言模型中作为最后一层出现。
残差连接
随着章节的进展,我们逐渐用方框/模块表示网络中的概念。这种表示法在描述"残差连接"这种有用概念时特别有效。让我们看看与自注意力块结合的残差连接:
注意,我们将"输入"和"输出"用方框表示,以简化内容,但它们仍然基本上只是数字或神经元的集合,和上图所示的类似。
这里发生了什么呢?我们基本上是在自注意力块的输出传递到下一个块之前,将它与原始输入相加。首先要注意的是,这要求自注意力块输出的维度必须与输入相同。这并不是问题,因为自注意力块的输出维度是用户确定的。但为什么要这样做呢?我们不会深入所有细节,这里关键是当网络层次加深(输入和输出之间的层数增加)时,训练难度会显著增加。研究表明,残差连接有助于缓解这些训练难题。
层归一化
层归一化是一个相对简单的层,它对传入的数据进行归一化,方式是减去均值,然后除以标准差(如下文稍微多做一些)。例如,如果我们在输入后立即应用层归一化,它将对输入层的所有神经元计算均值和标准差。假设均值为M,标准差为S,那么层归一化会将每个神经元的值替换为(x-M)/S,其中x表示任意神经元的原始值。
那么,这有什么帮助?它基本上稳定了输入向量,有助于训练深层网络。一个问题是,通过归一化输入,我们是否会丢失一些可能对目标有帮助的有用信息?为了解决这个问题,层归一化层有一个"缩放"和一个"偏置"参数。基本上,对每个神经元,你可以乘以一个缩放值,然后加一个偏置。缩放和偏置值是可以训练的参数,允许网络学习到一些可能对预测有价值的变化。由于这是唯一的参数,层归一化块不需要大量参数进行训练。整个过程看起来大概是这样的:
缩放和偏置是可训练参数。可以看到,层归一化是一个相对简单的块,操作主要是逐点进行(在初始均值和标准差计算之后)。让人联想到激活层(如ReLU),唯一不同的是这里我们有一些可训练参数(虽然比其他层要少很多,因为它是简单的逐点操作)。
标准差是一种统计指标,表示值的分布范围,例如,如果所有值都相同,则标准差为零。如果每个值都与均值相距甚远,标准差就会较高。计算一组数字a1, a2, a3...(假设有N个数字)的标准差的公式如下:将每个数字减去均值,然后将每个N个数字的结果平方。将所有这些数字相加,然后除以N,最后对结果开平方根。
Dropout
Dropout是一种简单而有效的方法来防止模型过拟合。过拟合是指当模型在训练数据上效果很好,但对模型未见过的示例泛化能力不佳。帮助避免过拟合的技术称为"正则化技术",而Dropout就是其中之一。
如果你训练一个模型,它可能会在数据上产生错误,或以某种方式过拟合。如果你训练另一个模型,它可能也会产生错误,但方式不同。如果你训练多个模型并对它们的输出取平均值呢?这通常被称为"集成模型",因为它通过组合多个模型的输出来进行预测,集成模型通常比任何单个模型表现更好。
在神经网络中,你也可以这么做。可以构建多个(略有不同的)模型,然后组合它们的输出以获得更好的模型。然而,这可能计算开销很大。Dropout是一种不会实际构建集成模型的方法,但它捕捉到了一些集成模型的精髓。
概念很简单,通过在训练期间插入一个dropout层,你可以随机删除一定比例的神经元连接。以我们初始网络为例,在输入和中间层之间插入一个50%的Dropout层,可能看起来像这样:
这迫使网络在大量冗余中进行训练。本质上,你同时训练了许多不同的模型------但它们共享权重。
在进行推断时,我们可以采用类似集成模型的方式。我们可以使用dropout进行多次预测,然后组合结果。然而,这计算开销较大------而且由于模型共享权重------为什么不直接用所有权重进行预测呢(而不是每次只用50%的权重)?这应该能近似集成模型的效果。
不过,有一个问题:用50%权重训练的模型与用全部权重的模型在中间神经元的数值上会有很大不同。我们想要的是更像集成模型的平均效果。如何实现呢?一个简单的方法是将所有权重乘以0.5,因为现在使用的权重数量是原来的两倍。这就是Dropout在推断期间所做的:使用全网络所有权重,并将权重乘以(1-p),其中p是删除的概率。研究表明,这作为一种正则化技术效果相当不错。
多头注意力
这是Transformer架构中的关键模块。我们已经了解了什么是注意力模块。还记得吗?一个注意力模块的输出长度是由用户决定的,即v的长度。多头注意力就是在并行中运行多个注意力头(它们都接受相同的输入),然后将所有的输出简单地串联起来,看起来像这样:
请注意,从v1 -> v1h1的箭头表示线性层------每条箭头上都有一个矩阵来进行转换。我这里没展示出来是为了避免图形过于复杂。
这里的过程是为每个头生成相同的key、query和value。但是我们基本上是在它们之上应用了线性变换(分别对每个k、q、v和每个头单独应用),然后才使用这些k、q、v的值。这个额外层在自注意力中并不存在。
说句题外话,我认为这种创建多头注意力的方法有点奇特。比如,为什么不给每个头创建独立的Wk、Wq、Wv矩阵,而是添加新的一层并共享这些权重?如果你知道原因,告诉我------我还真没弄明白。
位置编码与嵌入
在自注意力部分,我们简要讨论了使用位置编码的动机。那这些是什么呢?虽然图中展示了位置编码,但使用位置嵌入比位置编码更为常见。因此,我们在此讨论常见的"位置嵌入",但附录中也包含了原始论文中使用的"位置编码"。位置嵌入和其他嵌入没有区别,唯一不同的是这里不是对词汇表中的单词进行嵌入,而是对数字1、2、3等进行嵌入。因此,这种嵌入是一个与词汇嵌入同长度的矩阵,每一列对应一个数字。就是这么简单。
未完待续。下一节将是本篇的最后部分,我们将简单提及一下目前最先进的语言模型------GPT及其架构,并分享一个由OpenAI前工程师完全开源的人工智能模型代码...