在本文中,我们讨论大型语言模型(LLMs)的工作原理,从基础开始——假设您只知道如何加法和乘法。本文旨在完全自给自足。我们首先在纸上构建一个简单的生成式人工智能,然后逐步讲解我们需要了解现代LLMs和Transformer架构的所有内容。本文将剥离机器学习中的所有华丽语言和术语,将一切简单地表示为它们本身:数字。我们仍然会指出事物的名称,以便在您阅读术语内容时将您的思维与之联系起来。
从加法/乘法到今天最先进的人工智能模型,而不假设其他知识或引用其他来源,这意味着我们涵盖了很多内容。这不是一个玩具LLM的解释——一个有决心的人理论上可以从这里的所有信息中重建一个现代的LLM。我已经删去了所有不必要的词语/行,因此这篇文章并不是真正 meant to be browsed。
我们将涵盖哪些内容?让我们深入探讨。
首先需要注意的是,神经网络只能接受数字作为输入,并且只能输出数字。没有例外。关键在于如何将输入以数字的形式提供,如何解读输出的数字以实现你的目标。最后,构建神经网络,使其能够接受你提供的输入并给出你想要的输出(根据你为这些输出选择的解读)。让我们来看看如何从加法和乘法运算过渡到像Llama 3.1 这样的内容。
一个简单的神经网络:让我们来研究一个简单的神经网络,它可以对物体进行分类:
以下是叶子和向日葵的数据可能呈现的样子:
现在让我们构建一个进行分类的神经网络。我们需要决定输入/输出的解释。我们的输入已经是数字,因此我们可以直接将其输入到网络中。我们的输出是两个对象,叶子和花,神经网络无法直接输出。让我们看看可以在这里使用的几种方案:
这两种方案都允许网络输出我们可以解释为叶子或花的数字。我们在这里选择第二种方案,因为它对我们稍后要研究的其他事物具有良好的泛化能力。这里有一个使用该方案进行分类的神经网络。让我们逐步分析它:
蓝色圆圈如下: (32 * 0.10) + (107 * -0.29) + (56 * -0.07) + (11.2 * 0.46) = — 26.6
一些行话:
神经元/节点:圆圈中的数字
权重:线条上的彩色数字
层:一组神经元称为一层。你可以将这个网络视为有 3 层:输入层有 4 个神经元,中间层有 3 个神经元,输出层有 2 个神经元。
要计算该网络的预测/输出(称为“前向传播”),您从左侧开始。我们有输入层中神经元的数据。要“向前”移动到下一层,您将圆圈中的数字与相应神经元配对的权重相乘,然后将它们全部相加。我们在上面演示了蓝色和橙色圆圈的数学运算。运行整个网络时,我们看到输出层中的第一个数字更高,因此我们将其解释为“网络将这些(RGB,Vol)值分类为叶子”。一个训练良好的网络可以接受各种(RGB,Vol)输入并正确分类对象。
该模型并不知道什么是叶子或花朵,也不知道(RGB,Vol)是什么。它的任务是接收恰好 4 个数字并输出恰好 2 个数字。我们解释这 4 个输入数字为(RGB,Vol),同时我们决定观察输出数字并推断如果第一个数字更大则它是叶子,依此类推。最后,选择合适的权重也是由我们决定,以便模型能够接收我们的输入数字并给出正确的两个数字,使得当我们对其进行解释时,能够得到我们想要的解释。
一个有趣的副作用是,你可以使用相同的网络,而不是输入 RGB,改为输入其他四个数字,比如云量、湿度等,并将这两个数字解释为“一个小时内晴天”或“一个小时内下雨”。如果你有良好校准的权重,你可以让同一个网络同时完成两项任务——分类叶子/花朵和预测一个小时内的降雨!网络只给你两个数字,无论你将其解释为分类、预测还是其他,完全取决于你。
省略的内容以简化(可以忽略而不影响理解):
在上述示例中,我们神奇地得到了允许我们将数据输入模型并获得良好输出的权重。但是这些权重是如何确定的呢?设置这些权重(或“参数”)的过程称为“训练模型”,我们需要一些训练数据来训练模型。
假设我们有一些数据,其中包含输入,并且我们已经知道每个输入对应的是叶子还是花,这就是我们的“训练数据”,由于我们为每组(R、G、B、Vol)数字提供了叶子/花的标签,这就是“标记数据”。
这是它的工作原理:
几条说明:
在实践中,训练深度网络是一个困难且复杂的过程,因为梯度在训练过程中很容易失控,可能会趋近于零或无穷大(称为“消失梯度”和“爆炸梯度”问题)。我们在这里讨论的损失的简单定义是完全有效的,但很少使用,因为有更好的功能形式适用于特定目的。随着现代模型包含数十亿个参数,训练一个模型需要大量的计算资源,这本身也存在问题(内存限制、并行化等)。
这些如何有助于生成语言?请记住,神经网络接收一些数字,根据训练参数进行一些数学运算,然后输出其他数字。一切都与解释和训练参数(即将其设置为某些数字)有关。如果我们可以将这两个数字解释为“叶子/花”或“一小时内的雨或阳光”,我们也可以将它们解释为“句子中的下一个字符”。
但是英语中有超过两个字母,因此我们必须将输出层中的神经元数量扩展到,例如,英语中的 26 个字母(我们还可以加入一些符号,如空格、句号等)。每个神经元可以对应一个字符,我们查看输出层中的(大约 26 个)神经元,并说输出层中编号最高的神经元对应的字符就是输出字符。现在我们有了一个可以接收一些输入并输出一个字符的网络。
如果我们用这些字符“Humpty Dumpt”替换网络中的输入,并要求它输出一个字符,并将其解释为“网络对我们刚刚输入的序列中下一个字符的建议”。我们可能可以将权重设置得足够好,以便它输出“y”——从而完成“Humpty Dumpty”。但有一个问题,我们如何将这些字符列表输入到网络中?我们的网络只接受数字!!
一个简单的解决方案是给每个字符分配一个数字。假设 a=1,b=2,以此类推。现在我们可以输入“humpty dumpt”,并训练它给我们“y”。我们的网络看起来像这样:
好的,现在我们可以通过提供一个字符列表来预测下一个字符。我们可以利用这一点来构建整个句子。例如,一旦我们预测出“y”,就可以将这个“y”附加到我们已有的字符列表中,并将其输入网络,要求它预测下一个字符。如果训练得当,它应该会给我们一个空格,依此类推。最终,我们应该能够递归生成“Humpty Dumpty sat on a wall”。我们拥有生成性人工智能。此外,我们现在有一个能够生成语言的网络!现在,没有人会实际输入随机分配的数字,我们将在后续看到更合理的方案。如果你等不及,可以随时查看附录中的独热编码部分。
机智的读者会注意到,我们实际上无法将“Humpty Dumpty”输入到网络中,因为图示的方式是,输入层只有 12 个神经元,每个神经元对应“humpty dumpt”中的一个字符(包括空格)。那么我们如何在下一次传递中输入“y”呢?在这里放置第 13 个神经元将需要我们修改整个网络,这并不可行。解决方案很简单,让我们把“h”去掉,发送最近的 12 个字符。因此,我们将发送“umpty dumpty”,网络将预测一个空格。然后我们输入“mpty dumpty”,它将产生一个“s”,依此类推。它看起来像这样:
我们在最后一行中丢弃了很多信息,因为只给模型输入“ sat on the wal”。那么,今天最新最强的网络是怎么做的呢?或多或少就是这样。我们可以输入到网络中的输入长度是固定的(由输入层的大小决定)。这被称为“上下文长度”——提供给网络以进行未来预测的上下文。现代网络可以具有非常大的上下文长度(几千个单词),这有助于提高性能。有一些输入无限长度序列的方法,但这些方法的性能虽然令人印象深刻,但已经被其他具有大(但固定)上下文长度的模型所超越。
另一个细心的读者会注意到,我们对同一字母的输入和输出有不同的解释!例如,当输入“h”时,我们仅用数字 8 来表示,但在输出层,我们并不是要求模型输出一个单一的数字(“h”对应 8,“i”对应 9,依此类推……),而是要求模型输出 26 个数字,然后我们查看哪个数字最大,如果第 8 个数字最大,我们就将输出解释为“h”。为什么我们不在两端使用相同、一致的解释呢?我们可以这样做,只是对于语言而言,允许自己在不同的解释之间选择,可以更好地构建更好的模型。而恰好目前已知的对输入和输出最有效的解释是不同的。实际上,我们在这个模型中输入数字的方式并不是最佳方式,我们将很快看看更好的方法。
是什么使大型语言模型如此有效?逐字生成“哈姆蒂·邓蒂坐在墙上”与现代LLMs的能力相去甚远。从我们上面讨论的简单生成性人工智能到类人机器人之间,有许多差异和创新。让我们逐一了解这些内容:
嵌入记住我们说过,我们输入字符到模型中的方式并不是最佳的。我们只是随意为每个字符选择了一个数字。如果我们能够分配更好的数字,使我们能够训练出更好的网络,那会怎样?我们如何找到这些更好的数字?这里有一个巧妙的技巧:
当我们训练上述模型时,我们的做法是调整权重,观察这是否能最终带来更小的损失。然后逐渐并递归地改变权重。在每一步中,我们会:
在这个过程中,输入是固定的。当输入是(RGB,Vol)时,这是有道理的。但我们现在输入的数字 a、b、c 等是我们任意选择的。如果在每次迭代中,除了稍微调整权重外,我们还调整输入,看看是否可以通过使用不同的数字来表示“a”等,从而获得更低的损失呢?我们肯定是在减少损失并使模型更好(这是我们设计上对 a 的输入所做的方向)。基本上,不仅对权重应用梯度下降,还对输入的数字表示进行应用,因为它们反正是任意选择的数字。这被称为“嵌入”。它是输入到数字的映射,正如你刚才看到的,它需要被训练。训练嵌入的过程与训练参数的过程非常相似。不过,这样的一个大优势是,一旦你训练了一个嵌入,你可以在另一个模型中使用它。如果你愿意,请记住,你将始终使用相同的嵌入来表示单个token/字符/单词。
我们讨论了每个字符只有一个数字的嵌入。然而,实际上嵌入有多个数字。这是因为用一个数字来捕捉概念的丰富性是困难的。如果我们看一下叶子和花的例子,每个对象都有四个数字(输入层的大小)。这四个数字各自传达了一个属性,模型能够利用它们有效地猜测对象。如果我们只有一个数字,比如颜色的红色通道,模型可能会更难。我们在这里试图捕捉人类语言——我们需要的不止一个数字。
所以,与其用一个数字来表示每个字符,不如用多个数字来捕捉其丰富性?我们可以为每个字符分配一组数字。我们将有序的数字集合称为“向量”(有序是指每个数字都有一个位置,如果我们交换两个数字的位置,就会得到一个不同的向量。以我们的叶子/花朵数据为例,如果我们交换叶子的 R 和 G 数字,我们将得到不同的颜色,它将不再是相同的向量)。向量的长度就是它包含的数字数量。我们将为每个字符分配一个向量。由此产生两个问题:
所有的嵌入向量当然必须具有相同的长度,否则我们将无法将所有字符组合输入到网络中。例如,“humpty dumpt”和在下一次迭代中的“umpty dumpty”——在这两种情况下,我们都在网络中输入 12 个字符,如果这 12 个字符中的每一个没有由长度为 10 的向量表示,我们将无法可靠地将它们全部输入到一个长度为 120 的输入层中。让我们可视化这些嵌入向量:
我们将同大小向量的有序集合称为矩阵。上面的这个矩阵称为嵌入矩阵。您告诉它一个与您的字母对应的列号,查看矩阵中的该列将为您提供用于表示该字母的向量。这可以更一般地应用于嵌入任何任意集合的事物——您只需在这个矩阵中拥有与您拥有的事物数量相等的列。
子词分词器到目前为止,我们一直将字符作为语言的基本构建块。这有其局限性。神经网络的权重需要承担大量的重任,它们必须理解某些字符(即单词)相互靠近时的特定序列,然后再与其他单词相邻。如果我们直接为单词分配嵌入,并让网络预测下一个单词呢?反正网络只理解数字,因此我们可以为每个单词“humpty”、“dumpty”、“sat”、“on”等分配一个长度为 10 的向量,然后我们只需输入两个单词,它就能给我们下一个单词。“Token”是我们嵌入并输入模型的单个单位的术语。到目前为止,我们的模型使用字符作为tokens,现在我们提议使用整个单词作为token(当然,如果你愿意,也可以使用整个句子或短语作为tokens)。
使用词汇标记化对我们的模型产生了深远的影响。英语中有超过 18 万个单词。根据我们的输出解释方案,每个可能的输出需要一个神经元,因此我们在输出层需要数十万个神经元,而不是大约 26 个。考虑到实现现代网络有意义的结果所需的隐藏层规模,这个问题变得不那么紧迫。然而,值得注意的是,由于我们将每个单词单独处理,并且我们为每个单词开始时使用随机的嵌入——非常相似的单词(例如“cat”和“cats”)将没有任何关系。你会期望这两个单词的嵌入应该彼此接近——毫无疑问,模型会学习到这一点。但是,我们是否可以以某种方式利用这种明显的相似性来获得一个起步并简化问题?
是的,我们可以。如今语言模型中最常见的嵌入方案是将单词分解为子词,然后进行嵌入。在“猫”的例子中,我们将“cats”分解为两个部分“cat”和“s”。现在模型更容易理解“s”后面跟着其他熟悉的单词等概念。这也减少了我们需要的数量(sentencpiece 是一个常见的分词器,其词汇量选项在数万到数十万的英语单词之间)。分词器是将输入文本(例如“Humpty Dumpt”)拆分成单元,并给出相应的数字,以便在嵌入矩阵中查找该单元的嵌入向量。例如,在“humpty dumpty”的情况下,如果我们使用字符级分词器,并且我们按照上图排列我们的嵌入矩阵,那么分词器将首先将“humpty dumpt”拆分为字符[‘h’,’u’,…’t’],然后返回数字[8,21,…20],因为你需要查找嵌入矩阵的第 8 列以获取‘h’的嵌入向量(嵌入向量是你将输入到模型中的内容,而不是数字 8,这与之前不同)。 矩阵中列的排列完全无关紧要,我们可以将任何列分配给‘h’,只要每次输入‘h’时查找相同的向量就可以了。分词器只是给我们一个任意(但固定)的数字,以便于查找。我们真正需要它们的主要任务是将句子拆分为tokens。
通过嵌入和子词标记化,模型可能看起来像这样:
接下来的几个部分将讨论语言建模的最新进展,以及使LLMs如此强大的那些进展。然而,要理解这些内容,您需要了解一些基本的数学概念。以下是这些概念:
我在附录中添加了这些概念的摘要。
自注意力到目前为止,我们只见过一种简单的神经网络结构(称为前馈网络),它包含多个层,每一层与下一层完全连接(即,连续层之间的任意两个神经元之间都有一条连接线),并且仅与下一层连接(例如,层 1 和层 3 之间没有连接线等)。然而,正如你所想象的,没有什么可以阻止我们去移除或建立其他连接,甚至构建更复杂的结构。让我们探讨一种特别重要的结构:自注意力。
如果你观察人类语言的结构,我们想要预测的下一个词将依赖于之前的所有词。然而,它们可能对某些之前的词依赖程度更大。例如,如果我们试图预测“达米安有一个秘密孩子,一个女孩,他在遗嘱中写道,所有他的财物,包括魔法球,将属于____”中的下一个词。这里的词可以是“她”或“他”,具体取决于句子中一个更早的词:女孩/男孩。
好消息是,我们的简单前馈模型与上下文中的所有单词相连接,因此它可以学习重要单词的适当权重。但问题在于,通过前馈层连接我们模型中特定位置的权重是固定的(对于每个位置)。如果重要单词总是在同一位置,它将适当地学习权重,我们就没问题了。然而,与下一个预测相关的单词可以在系统中的任何位置。我们可以对上面的句子进行改述,当猜测“她与他”时,无论该单词出现在句子的哪个位置,“男孩/女孩”都是这个预测中一个非常重要的单词。因此,我们需要不仅依赖于位置,还依赖于该位置内容的权重。我们如何实现这一点?
自注意力的作用类似于对每个单词的嵌入向量进行加总,但它并不是直接相加,而是对每个向量应用一些权重。因此,如果“humpty”、“dumpty”、“sat”的嵌入向量分别为 x1、x2、x3,那么它会在相加之前将每个向量乘以一个权重(一个数字)。类似于输出 = 0.5 x1 + 0.25 x2 + 0.25 x3,其中输出是自注意力的输出。如果我们将权重写为 u1、u2、u3,使得输出 = u1x1 + u2x2 + u3x3,那么我们如何找到这些权重 u1、u2、u3 呢?
理想情况下,我们希望这些权重依赖于我们正在添加的向量——正如我们所看到的,有些可能比其他的更重要。但对谁重要呢?对我们即将预测的单词。因此,我们还希望权重依赖于我们即将预测的单词。现在这是一个问题,当然在我们预测之前我们并不知道即将预测的单词。因此,自注意力机制使用的是紧接在我们即将预测的单词之前的单词,即句子中可用的最后一个单词(我并不真的知道为什么是这样而不是其他什么,但深度学习中的很多事情都是试验和错误,我怀疑这样做效果很好)。
很好,所以我们想要这些向量的权重,并且我们希望每个权重依赖于我们正在聚合的单词和我们即将预测的单词之前的单词。基本上,我们想要一个函数 u1 = F(x1, x3),其中 x1 是我们将加权的单词,x3 是我们序列中的最后一个单词(假设我们只有三个单词)。现在,实现这一点的一个简单方法是为 x1 创建一个向量(我们称之为 k1),为 x3 创建一个单独的向量(我们称之为 q3),然后简单地取它们的点积。这将给我们一个数字,并且它将依赖于 x1 和 x3。我们如何获得这些向量 k1 和 q3?我们构建一个小型单层神经网络,从 x1 到 k1(或从 x2 到 k2,从 x3 到 k3,依此类推)。我们再构建另一个网络,从 x3 到 q3 等等……使用我们的矩阵表示法,我们基本上得出权重矩阵 Wk 和 Wq,使得 k1 = Wkx1 和 q1 = Wqx1,依此类推。现在我们可以取 k1 和 q3 的点积来得到一个标量,因此 u1 = F(x1, x3) = Wkx1 · Wqx3。
在自注意力中发生的另一件事是,我们并不直接对嵌入向量本身进行加权求和。相反,我们对该嵌入向量的某些“值”进行加权求和,这些“值”是通过另一个小型单层网络获得的。这意味着,类似于 k1 和 q1,我们现在也有了单词 x1 的 v1,并且通过矩阵 Wv 获得它,使得 v1=Wvx1。然后对这个 v1 进行聚合。因此,如果我们只有 3 个单词并试图预测第四个单词,整个过程看起来大致是这样的:
加号表示向量的简单相加,这意味着它们必须具有相同的长度。这里未显示的最后一个修改是标量 u1、u2、u3 等不一定加起来等于 1。如果我们需要它们作为权重,我们应该使它们相加。因此,我们将在这里应用一个熟悉的技巧,使用 softmax 函数。
这就是自注意力。还有交叉注意力,其中 q3 可以来自最后一个词,但 k 和 v 可以来自另一句话。这在翻译任务中是非常有价值的。现在我们知道什么是注意力。
这个整体现在可以放在一个框中,称为“自注意力块”。基本上,这个自注意力块接收嵌入向量,并输出一个用户选择长度的单一输出向量。这个块有三个参数,Wk、Wq、Wv——不需要更复杂。机器学习文献中有许多这样的块,它们通常在图表中用框表示,并标有名称。像这样:
你会注意到自注意力的一个特点是,目前事物的位置似乎并不相关。我们在各个方面使用相同的 W,因此交换亨普提和邓普提在这里并不会产生实质性的差异——所有的数字最终都会相同。这意味着,尽管注意力可以确定关注的内容,但这并不依赖于单词的位置。然而,我们确实知道单词的位置在英语中是重要的,我们可能通过让模型对单词的位置有一定的感知来提高性能。
因此,当使用注意力机制时,我们通常不会将嵌入向量直接输入自注意力模块。我们稍后将看到“位置编码”是如何在输入注意力模块之前添加到嵌入向量中的。
预先了解者注意:对于那些不是第一次阅读自注意力的人来说,我们并没有提到任何 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,并计算这与网络提供的结果之间的差异来计算损失。这是可行的,但我们能做得更好吗?
回到我们的“哈姆蒂·达姆蒂”例子,假设我们试图逐字符生成达姆蒂角色,而我们的模型在预测“m”时出错。它没有给我们最后一层中“m”作为最高值,而是给了“u”作为最高值,但“m”是一个接近的第二高值。
现在我们可以继续“duu”,并尝试预测下一个字符,依此类推,但模型的信心会很低,因为从“humpty duu..”开始并没有太多好的延续。另一方面,“m”是一个接近的选择,所以我们也可以尝试“m”,预测接下来的几个字符,看看会发生什么?也许它会给我们一个更好的整体单词?
我们在这里讨论的并不仅仅是盲目选择最大值,而是尝试几个选项。有什么好的方法呢?我们需要为每个选项分配一个概率——比如我们以 50%的概率选择第一个,以 25%的概率选择第二个,依此类推。这是一个不错的方法。但也许我们希望这个概率依赖于基础模型的预测。如果模型预测的 m 和 u 的值在这里非常接近(与其他值相比)——那么探索这两个值的 50-50 的机会可能是个好主意?
因此,我们需要一个好的规则,将所有这些数字转换为概率。这就是 softmax 的作用。它是上述逻辑函数的推广,但具有额外的特性。如果你给它 10 个任意数字,它将给你 10 个输出,每个输出介于 0 和 1 之间,并且重要的是,这 10 个输出的总和为 1,以便我们可以将其解释为概率。你会发现 softmax 几乎在每个语言模型的最后一层。
残差连接随着各部分的进展,我们逐渐改变了对网络的可视化。我们现在使用方框/块来表示某些概念。这种符号在表示残差连接这一特别有用的概念时非常有效。让我们来看一下残差连接与自注意力块的结合:
请注意,我们将“输入”和“输出”放置为框以简化事物,但这些基本上仍然只是与上面所示相同的一组神经元/数字。
那么这里发生了什么呢?我们基本上是在将自注意力块的输出传递给下一个块之前,将其与原始输入相加。首先需要注意的是,这要求自注意力块输出的维度必须与输入的维度相同。这不是问题,因为正如我们所指出的,自注意力的输出是由用户决定的。但为什么要这样做呢?我们在这里不深入探讨所有细节,但关键是随着网络的加深(输入和输出之间的层数增多),训练它们变得越来越困难。残差连接已被证明有助于解决这些训练挑战。
层归一化层归一化是一个相对简单的层,它接收进入该层的数据,通过减去均值并除以标准差来进行归一化(可能还有更多,如下所示)。例如,如果我们在输入后立即应用层归一化,它将处理输入层中的所有神经元,然后计算两个统计量:它们的均值和标准差。假设均值为 M,标准差为 S,那么层归一化所做的就是将每个神经元替换为(x-M)/S,其中 x 表示任何给定神经元的原始值。
现在这有什么帮助呢?它基本上稳定了输入向量,并有助于训练深度网络。一个担忧是,通过对输入进行归一化,我们是否去除了其中一些可能对学习我们目标有价值的有用信息?为了解决这个问题,层归一化层有一个缩放和一个偏置参数。基本上,对于每个神经元,你只需将其乘以一个标量,然后加上一个偏置。这些标量和偏置值是可以训练的参数。这使得网络能够学习一些可能对预测有价值的变化。而且由于这些是唯一的参数,层归一化块没有很多参数需要训练。整个过程看起来大致是这样的:
缩放和偏差是可训练的参数。可以看到,层归一化是一个相对简单的模块,其中每个数字仅在逐点上进行操作(在初始均值和标准差计算之后)。这让我们想起了激活层(例如 RELU),主要区别在于这里有一些可训练的参数(尽管由于简单的逐点操作,数量远少于其他层)。
标准差是一个统计量,用于衡量数值的分散程度。例如,如果所有数值都相同,则可以说标准差为零。如果一般来说,每个数值都远离这些数值的均值,那么标准差就会很高。计算一组数字(a1, a2, a3……,假设有 N 个数字)的标准差的公式大致如下:从每个数字中减去均值(这些数字的均值),然后对每个 N 个数字的结果进行平方。将所有这些数字相加,然后除以 N。最后对结果取平方根。
预先启动者注意:经验丰富的机器学习专业人士会注意到这里没有讨论批量归一化。实际上,我们在本文中甚至没有引入批量的概念。在大多数情况下,我认为批量是另一种与核心概念理解无关的训练加速器(也许批量归一化除外,但我们在这里不需要它)。
辍学丢弃法是一种简单但有效的避免模型过拟合的方法。过拟合是指当你在训练数据上训练模型时,模型在该数据集上表现良好,但对模型未见过的示例泛化能力差。帮助我们避免过拟合的技术被称为“正则化技术”,而丢弃法就是其中之一。
如果你训练一个模型,它可能会在数据上出错和/或以特定方式过拟合。如果你训练另一个模型,它可能也会这样,但方式不同。如果你训练多个这样的模型并对输出进行平均呢?这些通常被称为“集成模型”,因为它们通过结合多个模型的输出来预测结果,而集成模型通常比任何单个模型的表现更好。
在神经网络中,你可以做同样的事情。你可以构建多个(略有不同的)模型,然后将它们的输出结合起来以获得更好的模型。然而,这可能在计算上是昂贵的。Dropout 是一种技术,它并不完全构建集成模型,但确实捕捉了这一概念的一些本质。
这个概念很简单,通过在训练过程中插入一个 dropout 层,你所做的是随机删除 dropout 插入层之间的某个百分比的直接神经元连接。考虑到我们的初始网络,在输入层和中间层之间插入一个 50% 的 dropout 率的 Dropout 层可以看起来像这样:
现在,这迫使网络以大量冗余进行训练。本质上,您同时训练多个不同的模型——但它们共享权重。
现在为了进行推断,我们可以采用与集成模型相同的方法。我们可以使用随机失活进行多次预测,然后将它们结合起来。然而,由于这在计算上是密集型的——而且我们的模型共享相同的权重——为什么我们不直接使用所有权重进行预测(也就是说,不是一次使用 50%的权重,而是同时使用所有权重)。这应该能给我们一些集成模型所提供的近似值。
一个问题是:使用 50%权重训练的模型在中间神经元中的数值将与使用所有权重的模型非常不同。我们想要的是更多的集成风格平均。那么我们该如何做到这一点呢?一种简单的方法是将所有权重乘以 0.5,因为我们现在使用了两倍的权重。这就是 Dropout 在推理过程中所做的。它将使用完整的网络和所有权重,并简单地将权重乘以(1 - p),其中 p 是删除概率。这已被证明作为一种正则化技术效果相当好。
多头注意力这是transformer架构中的关键模块。我们已经看到了注意力模块是什么。请记住,注意力模块的输出是由用户决定的,它是 v 的长度。多头注意力的基本概念是你并行运行多个注意力头(它们都使用相同的输入)。然后我们将它们的所有输出简单地连接在一起。它看起来像这样:
请注意,从 v1 -> v1h1 的箭头是线性层——每个箭头上都有一个矩阵进行变换。我只是没有显示它们以避免混乱。
这里发生的事情是我们为每个头生成相同的键、查询和值。但随后我们基本上在此基础上应用线性变换(分别对每个 k、q、v 以及每个头分别进行)在使用这些 k、q、v 值之前。这个额外的层在自注意力中是不存在的。
顺便提一下,对我来说,这是一种稍微令人惊讶的多头注意力创建方式。例如,为什么不为每个头创建单独的 Wk、Wq、Wv 矩阵,而是添加一个新层并共享这些权重。如果你知道,请告诉我——我真的不知道。
位置编码和嵌入我们简要讨论了在自注意力部分使用位置编码的动机。这些是什么?虽然图片展示了位置编码,但使用位置嵌入比使用编码更为常见。因此,我们在这里讨论一个常见的位置嵌入,但附录中也涵盖了原始论文中使用的位置编码。位置嵌入与其他嵌入没有区别,只是我们嵌入的不是单词词汇,而是数字 1、2、3 等。因此,这个嵌入是一个与单词嵌入长度相同的矩阵,每一列对应一个数字。这就是全部内容。
GPT 架构让我们来谈谈 GPT 架构。这是大多数 GPT 模型中使用的架构(存在一些变体)。如果你到目前为止一直在关注这篇文章,这应该是相当简单易懂的。使用框表示法,这就是架构在高层次上的样子:
在这一点上,除了“GPT Transformer 块”之外,其他所有块都已详细讨论。这里的 + 符号仅表示两个向量相加(这意味着两个嵌入必须具有相同的大小)。让我们来看一下这个 GPT Transformer 块:
这就是全部内容。它在这里被称为“transformer”,因为它源自并且是一种transformer——这是一种我们将在下一节中讨论的架构。这并不影响理解,因为我们已经涵盖了这里展示的所有构建块。让我们回顾一下到目前为止我们所讨论的内容,以便理解这个 GPT 架构:
随着公司发展到强大的现代LLMs,对此进行了多次修改,但基本内容保持不变。
现在,这个 GPT transformer 实际上是在最初介绍 transformer 架构的论文中所称的“解码器”transformer。让我们来看看这个。
transformer建筑这是最近推动语言模型能力快速提升的关键创新之一。Transformers不仅提高了预测准确性,而且比以前的模型更容易/更高效(进行训练),允许更大的模型规模。这正是上述 GPT 架构的基础。
如果你查看 GPT 架构,你会发现它非常适合生成序列中的下一个词。它基本上遵循我们在第一部分讨论的相同逻辑。从几个词开始,然后继续逐个生成。但是,如果你想进行翻译呢?如果你有一句德语句子(例如“Wo wohnst du?”=“你住在哪里?”),你想将其翻译成英语。我们将如何训练模型来做到这一点?
好的,首先我们需要做的是找出一种输入德语单词的方法。这意味着我们必须扩展我们的嵌入,以包括德语和英语。现在,我想这里有一个简单的方法来输入信息。我们为什么不把德语句子连接到目前生成的英语句子的开头,然后将其输入上下文呢?为了使模型更容易处理,我们可以添加一个分隔符。每一步看起来会像这样:
这将有效,但还有改进的空间:
Transformer最初是为这个任务创建的,包含一个“编码器”和一个“解码器”——这基本上是两个独立的模块。一个模块简单地接收德语句子并输出一个中间表示(基本上是一堆数字)——这被称为编码器。
第二个模块生成单词(到目前为止我们已经看到了很多)。唯一的区别是,除了将迄今为止生成的单词输入外,我们还将编码后的德语(来自编码器模块)句子输入。因此,在生成语言时,它的上下文基本上是迄今为止生成的所有单词,加上德语。这个模块被称为解码器。
这些编码器和解码器由几个模块组成,特别是夹在其他层之间的注意力模块。让我们看看论文“注意力即所有你需要的”中的一个transformer的插图,并试图理解它:
左侧的垂直块称为“编码器”,右侧的块称为“解码器”。让我们回顾一下并理解我们之前未涉及的内容:
关于如何阅读该图的回顾:这里的每个框都是一个模块,它接收一些以神经元形式输入的信号,并输出一组神经元作为输出,这些输出可以被下一个模块处理或由我们解释。箭头显示了一个模块的输出去向。如您所见,我们通常会将一个模块的输出作为输入馈送到多个模块中。让我们逐一了解这里的每个内容:
前馈:前馈网络是指不包含循环的网络。我们在第一节中的原始网络是一个前馈网络。实际上,这个模块使用了非常相似的结构。它包含两个线性层,每个线性层后面跟着一个 RELU(请参见第一节中的 RELU 说明)和一个 dropout 层。请记住,这个前馈网络是独立应用于每个位置的。这意味着位置 0 的信息有一个前馈网络,位置 1 也有一个,依此类推。但是位置 x 的神经元与位置 y 的前馈网络之间没有连接。这一点很重要,因为如果我们不这样做,网络在训练时就会通过向前查看而作弊。
交叉注意力:你会注意到解码器有一个多头注意力,箭头来自编码器。这是怎么回事?还记得自注意力和多头注意力中的值、键、查询吗?它们都是来自同一个序列。实际上,查询只是来自序列的最后一个词。那么,如果我们保留查询,但从一个完全不同的序列中获取值和键,会发生什么呢?这就是这里发生的事情。值和键来自编码器的输出。除了键和值的输入来源发生了变化,数学上没有任何变化。
Nx:这里的 Nx 仅表示该块被链式重复 N 次。因此,基本上你是在背靠背地堆叠这个块,并将前一个块的输入传递给下一个块。这是一种使神经网络更深的方法。现在,看看图表,关于编码器输出如何传递给解码器可能会引起混淆。假设 N=5。我们是否将每个编码器层的输出传递给相应的解码器层?不。基本上你只需运行编码器一次,并且仅一次。然后你只需将该表示传递给每一个 5 个解码器层。
加法与归一化块:这基本上与下面的内容相同(猜测作者只是想节省空间)
其他所有内容已经讨论过了。现在你对transformer架构有了完整的解释,从简单的加法和乘法操作开始,并且完全自给自足!你知道每一行、每一个求和、每一个框和每一个词在如何从头构建它们方面的含义。从理论上讲,这些笔记包含了你从头编写transformer所需的内容。实际上,如果你感兴趣,这个仓库为上述 GPT 架构做了这些。
附录我们在嵌入的上下文中介绍了向量和矩阵。矩阵有两个维度(行数和列数)。向量也可以被视为一个矩阵,其中一个维度等于一。两个矩阵的乘积定义为:
点表示乘法。现在让我们再看一遍第一张图片中蓝色和有机神经元的计算。如果我们将权重写成矩阵,将输入写成向量,我们可以以以下方式表示整个操作:
如果权重矩阵称为“W”,输入称为“x”,那么 Wx 就是结果(在这种情况下是中间层)。我们也可以将两者转置,写成 xW——这只是一个偏好问题。
我们在层归一化部分使用标准差的概念。标准差是一个统计量,用于衡量一组数值的分散程度,例如,如果所有数值都相同,则可以说标准差为零。如果一般来说,每个数值都远离这些数值的均值,那么标准差就会很高。计算一组数值(a1, a2, a3…,假设有 N 个数)的标准差的公式大致如下:从每个数值中减去均值,然后对每个 N 个数的结果进行平方。将所有这些数值相加,然后除以 N。最后对结果取平方根。
我们在上面讨论了位置嵌入。位置编码只是一个与词嵌入向量长度相同的向量,但它不是一种嵌入,因为它不是经过训练的。我们简单地为每个位置分配一个唯一的向量,例如,位置 1 有一个不同的向量,位置 2 有另一个不同的向量,依此类推。实现这一点的一个简单方法是使该位置的向量完全由位置编号构成。因此,位置 1 的向量将是[1,1,1…1],位置 2 的向量将是[2,2,2…2],依此类推(请记住,每个向量的长度必须与嵌入长度匹配,以便进行加法运算)。这会带来问题,因为我们可能会在向量中得到较大的数字,这在训练过程中会造成挑战。当然,我们可以通过将每个数字除以位置的最大值来规范化这些向量,因此如果总共有 3 个词,那么位置 1 是[.33,.33,..,.33],位置 2 是[.67, .67, ..,.67],依此类推。现在的问题是,我们不断地改变位置 1 的编码(当我们输入 4 个词的句子时,这些数字会不同),这给网络学习带来了挑战。 因此,在这里,我们希望有一个方案为每个位置分配一个唯一的向量,并且数字不会爆炸。基本上,如果上下文长度为 d(即,我们可以输入到网络中以预测下一个单词的最大 tokens/单词数,参见“它是如何生成语言的?”部分的讨论),并且嵌入向量的长度为 10(假设),那么我们需要一个具有 10 行和 d 列的矩阵,其中所有列都是唯一的,所有数字都在 0 和 1 之间。鉴于在零和一之间有无限多个数字,而矩阵的大小是有限的,这可以通过多种方式实现。
在“注意力即全部”论文中使用的方法大致如下:
为什么选择这种方法?通过在 10k 上改变功率,您正在改变在 p 轴上查看时正弦函数的幅度。如果您有 10 个不同幅度的正弦函数,那么在改变 p 值时,您将很长时间才会得到重复(即所有 10 个值相同)。这有助于我们获得唯一的值。现在,实际论文使用了正弦和余弦函数,编码形式为:si(p) = sin(p/10000(i/d)),如果 i 是偶数;si(p) = cos(p/10000(i/d)),如果 i 是奇数。
好文章,需要你的鼓励
现代大语言模型就像一栋拥有数百个房间的豪华大厦,每个房间(或称为"层")都执行特定的功能,共同协作完成复杂的语言理解和生成任务。然而,这些模型的规模已经变得如此庞大,以至于只有拥有高端计算资源的机构才能负担得起它们的运行成本。这就像一辆耗油量极大的豪华跑车,普通人负担不起它的燃料费用。
想象一下,当你在解答一道复杂的数学题时,你不仅需要给出答案,还需要详细解释每一步推导的理由,不能有任何逻辑跳跃或假设——这就是形式化数学推理的严格要求。
想象一下日常生活中我们使用的语音助手,比如Siri或ChatGPT。它们通常是这样工作的:你问一个问题,它回答,然后安静地等待你的下一个指令。这就像是一个只会被动回应的服务员,永远等待你的呼唤。而Voila团队认为,真正高级的AI助手应该更像一个时刻准备着的好朋友或队友,能够自然地融入你的生活节奏中。
想象一下,你正在参加一场料理比赛,有两位评委。第一位评委只给你一个分数,而第二位评委会详细解释你的菜品在口感、创意和技巧上的表现如何,然后才给出最终评价。显然,第二位评委的反馈对于你改进厨艺更有帮助。伊利诺伊大学研究团队正是秉持这一理念,开发了能像第二位评委那样工作的AI评估系统。