前言
在本文中,我们将从零开始探讨大语言模型(LLMs)的工作原理——我们只假设你懂得如何做加法和乘法。本文旨在做到完全自成体系。我们将从在纸笔上构建一个简单的生成式AI开始,然后逐步讲解理解现代LLM和Transformer架构所需的一切知识。本文将剥离机器学习中所有花哨的语言和术语,将一切都以其本质形式呈现:数字。我们仍然会指出这些概念的专业名称,以便你在阅读其他充满术语的内容时能够将思路联系起来。
从加法/乘法讲到当今最先进的AI模型,且不假设其他知识或引用其他来源,意味着我们将涵盖非常广泛的内容。这不是一个玩具级的LLM解释——一个有决心的人理论上可以根据这里的所有信息复现一个现代LLM。我删减了每一个不必要的词句,因此本文并不适合随意浏览。
首先要注意的是,神经网络只能接受数字作为输入,也只能输出数字。没有例外。其巧妙之处在于,如何将你的输入表示为数字,如何解释输出的数字以实现你的目标。最后,构建一个神经网络,它能接受你提供的输入,并给出你想要的输出(基于你为这些输出选择的解释方式)。让我们来看看我们是如何从加法和乘法一步步走向像Llama 3.1这样的模型的。
一个简单的神经网络
让我们通过一个可以对物体进行分类的简单神经网络来学习:
**可用的物体数据:**主色调(RGB)和体积(毫升)
**分类目标:**叶子 或 花朵
下面是一片叶子和一朵向日葵的数据可能的样子:

现在,让我们构建一个能完成这个分类任务的神经网络。我们需要决定输入/输出的解释方案。我们的输入已经是数字了,所以可以直接输入网络。我们的输出是两个物体,叶子和花朵,神经网络不能直接输出这些。我们来看看可以使用的几种方案:
- 我们可以让网络输出一个单一的数字。如果这个数字是正数,我们说它是叶子;如果是负数,我们说它是花朵。
- 或者,我们可以让网络输出两个数字。我们将第一个数字解释为代表叶子的分数,第二个数字解释为代表花朵的分数,然后我们说,哪个数字更大,分类结果就是哪个。
两种方案都允许网络输出数字,而我们可以将这些数字解释为叶子或花朵。我们这里选择第二种方案,因为它能很好地推广到我们稍后将要讨论的其他问题。下面是一个使用该方案进行分类的神经网络。让我们来分析一下:

一些术语:
- 神经元/节点(Neurons/nodes):圆圈中的数字。
- 权重(Weights):连接线上的彩色数字。
- 层(Layers):一组神经元的集合称为一个层。你可以认为这个网络有3个层:包含4个神经元的输入层,包含3个神经元的中间层,以及包含2个神经元的输出层。
要计算这个网络的预测/输出(称为一次“前向传播”),你从左边开始。我们有输入层神经元的可用数据。要“向前”移动到下一层,你将圆圈中的数字与对应神经元配对的权重相乘,然后将它们全部相加。我们在上图中演示了蓝色和橙色圆圈的计算过程。运行整个网络后,我们看到输出层的第一个数字更大,所以我们将其解释为“网络将这些(RGB, Vol)值分类为叶子”。一个训练良好的网络可以接受各种(RGB, Vol)的输入,并正确地对物体进行分类。
这个模型本身对叶子、花朵或(RGB, Vol)是什么没有任何概念。它的任务就是接收4个数字,并输出2个数字。是我们自己的解释,认为这4个输入数字是(RGB, Vol),也是我们自己决定查看输出数字,并推断如果第一个数字更大,它就是叶子,等等。最后,也是由我们来选择合适的权重,使得模型能够接受我们的输入数字,并给出正确的两个数字,以便我们在解释它们时得到我们想要的结果。
一个有趣的副作用是,你可以用同一个网络,不输入RGB和体积,而是输入其他4个数字,比如云量、湿度等,并将两个输出数字解释为“一小时后天晴”或“一小时后下雨”。然后,如果你有校准得当的权重,你就可以让完全相同的网络同时做两件事——分类叶子/花朵和预测一小时后的天气!网络只是给你两个数字,你把它解释为分类、预测还是别的什么,完全取决于你。
为简化而省略的内容(可放心忽略,不影响理解):
- 激活层(Activation layer):这个网络中缺少一个关键部分——“激活层”。这是一个花哨的词,意思是我们将每个圆圈中的数字应用一个非线性函数(RELU是一个常见的函数,它将数字与零比较,如果数字是负数就设为零,如果是正数则保持不变)。所以在我们上面的例子中,我们会取中间层的两个数字(-26.6和-47.1),在进入下一层之前将它们替换为零。当然,我们必须重新训练权重才能使网络再次变得有用。没有激活层,网络中所有的加法和乘法都可以被压缩成一个单层。在我们的例子中,你可以把绿色圆圈写成RGB的直接加权和,而不需要中间层。它会是这样的:
(0.10 * -0.17 + 0.12 * 0.39 – 0.36 * 0.1) * R + (-0.29 * -0.17 – 0.05 * 0.39 – 0.21 * 0.1) * G …等等。如果我们有一个非线性函数在那里,这通常是不可能的。这有助于网络处理更复杂的情况。 - 偏置(Bias):网络通常还会在每个节点上关联另一个数字,这个数字被简单地加到乘积上以计算节点的值,这个数字被称为“偏置”。所以,如果顶部蓝色节点的偏置是0.25,那么该节点的值将是:
(32 * 0.10) + (107 * -0.29) + (56 * -0.07) + (11.2 * 0.46) + 0.25 = -26.35。“参数”(parameters)这个词通常用来指模型中所有不是神经元/节点的数字。 - Softmax:我们通常不像我们的模型中那样直接解释输出层。我们会将这些数字转换成概率(即让所有数字都为正数且总和为1)。如果输出层的所有数字都已经是正数,一种实现方式是将每个数字除以输出层所有数字的总和。不过,通常使用“softmax”函数,它可以处理正数和负数。

这些模型是如何训练的?
在上面的例子中,我们神奇地拥有了合适的权重,使得我们可以将数据输入模型并得到一个好的输出。但是这些权重是如何确定的呢?设置这些权重(或“参数”)的过程被称为“训练模型”,我们需要一些训练数据来训练模型。
假设我们有一些数据,其中我们有输入,并且已经知道每个输入对应的是叶子还是花朵,这就是我们的“训练数据”。由于我们为每组(R, G, B, Vol)数字都有叶子/花朵的标签,这被称为“标注数据”。
工作原理如下:
- 从随机数开始,即把每个参数/权重设置为一个随机数。
- 现在,我们知道当输入对应叶子的数据(R=32, G=107, B=56, Vol=11.2)时,我们希望输出层中代表叶子的数字更大。假设我们希望代表叶子的数字是0.8,代表花朵的是0.2(如上例所示,但这些是用于演示训练的说明性数字,实际上我们不想要0.8和0.2。在现实中,它们应该是概率,而这里不是,我们希望它们是1和0)。
- 我们知道我们想要的输出层数字,也知道从随机选择的参数中得到的数字(这与我们想要的数字不同)。所以,对于输出层的所有神经元,我们计算出想要的数字和现有数字之间的差异。然后将这些差异相加。例如,如果输出层的两个神经元是0.6和0.4,那么我们得到:(0.8–0.6)=0.2 和 (0.2–0.4)= -0.2,所以总共得到0.4(相加前忽略负号)。我们可以称之为我们的“损失”(loss)。理想情况下,我们希望损失接近于零,即我们想要“最小化损失”。
- 一旦我们有了损失,我们可以稍微改变每个参数,看看增加或减少它是否会增加损失。这被称为该参数的“梯度”(gradient)。然后我们可以将每个参数朝着损失下降的方向(梯度的反方向)移动一小步。一旦我们稍微移动了所有参数,损失应该会更低。
- 不断重复这个过程,你就会减少损失,并最终得到一组“训练好”的权重/参数。这整个过程被称为“梯度下降”(gradient descent)。
几点说明:
- 你通常有多个训练样本,所以当你为了最小化一个样本的损失而稍微改变权重时,可能会使另一个样本的损失变差。处理这个问题的方法是将损失定义为所有样本的平均损失,然后对该平均损失求梯度。这样可以减少整个训练数据集上的平均损失。每个这样的循环被称为一个“轮次”(epoch)。然后你可以不断重复这些轮次,从而找到减少平均损失的权重。
- 我们实际上不需要“移动权重”来计算每个权重的梯度——我们可以直接从公式中推断出来(例如,如果在最后一步中权重是0.17,并且神经元的值是正的,而我们希望输出有更大的数字,我们可以看到将这个数字增加到0.18会有帮助)。
- 在实践中,训练深度网络是一个困难而复杂的过程,因为梯度在训练过程中很容易失控,变为零或无穷大(称为“梯度消失”和“梯度爆炸”问题)。我们这里谈到的简单损失定义是完全有效的,但很少使用,因为有更好的函数形式适用于特定目的。对于包含数十亿参数的现代模型,训练一个模型需要巨大的计算资源,这本身也带来了问题(内存限制、并行化等)。
这一切如何帮助生成语言?
记住,神经网络接收一些数字,根据训练好的参数进行一些数学运算,然后输出另一些数字。一切都关乎解释和训练参数(即将它们设置为某些数字)。如果我们能将两个数字解释为“叶子/花朵”或“一小时后晴或雨”,我们也可以将它们解释为“句子中的下一个字符”。
但是英文字母不止2个,所以我们必须将输出层的神经元数量扩展到,比如说,英语中的26个字母(我们再加入一些符号,如空格、句号等)。每个神经元可以对应一个字符,我们查看输出层中的(大约26个)神经元,并说输出层中数值最高的神经元对应的字符就是输出字符。现在我们有了一个可以接收一些输入并输出一个字符的网络。
如果我们把输入替换成这些字符:“Humpty Dumpt”,并要求它输出一个字符,并将其解释为“网络对我们刚输入的序列的下一个字符的建议”,会怎么样?我们或许可以把权重设置得足够好,让它输出“y”——从而完成“Humpty Dumpty”。但有一个问题,我们如何将这些字符列表输入到网络中?我们的网络只接受数字!
一个简单的解决方案是为每个字符分配一个数字。比方说,a=1, b=2,依此类推。现在我们可以输入“humpty dumpt”并训练它给我们输出“y”。我们的网络看起来像这样:

好了,现在我们可以通过向网络提供一个字符列表来预测下一个字符。我们可以利用这一点来构建一个完整的句子。例如,一旦我们预测出“y”,我们可以将这个“y”附加到我们已有的字符列表中,然后将其输入网络,要求它预测下一个字符。如果训练得好,它应该会给我们一个空格,然后依此类推。到最后,我们应该能够递归地生成“Hum-pty Dump-ty sat on a wall”。我们拥有了生成式AI。而且,我们现在有了一个能够生成语言的网络!
现在,没有人会真的输入随机分配的数字,我们稍后会看到更合理的方案。如果你等不及,可以随时查看附录中的one-hot编码部分。
敏锐的读者会注意到,我们实际上无法将“Humpty Dumpty”输入到网络中,因为图示的方式,输入层只有12个神经元,每个神经元对应“humpty dumpt”(包括空格)中的一个字符。那么我们如何在下一次传递中放入“y”呢?在那里增加第13个神经元将需要我们修改整个网络,这是行不通的。解决方案很简单,让我们把“h”踢出去,发送最近的12个字符。所以我们会发送“umpty dumpty”,网络将预测一个空格。然后我们会输入“mpty dumpty ”,它会产生一个s,依此类推。它看起来像这样:

在最后一行中,我们通过只给模型输入“ sat on the wal”而丢弃了大量信息。那么当今最新、最强大的网络是怎么做的呢?或多或少正是这样。我们可以输入到网络中的输入长度是固定的(由输入层的大小决定)。这被称为“上下文长度”(context length)——即提供给网络以进行未来预测的上下文。现代网络可以有非常大的上下文长度(几千个单词),这很有帮助。有一些方法可以输入无限长度的序列,但这些方法的性能虽然令人印象深刻,但此后已被具有大(但固定)上下文长度的其他模型所超越。
细心的读者会注意到的另一件事是,我们对同一个字母的输入和输出有不同的解释!例如,当输入“h”时,我们只是用数字8来表示它,但在输出层,我们不是要求模型输出一个单一的数字(8代表“h”,9代表“i”等等),而是要求模型输出26个数字,然后我们看哪个最高,如果第8个数字最高,我们就将输出解释为“h”。为什么我们不在两端使用相同、一致的解释呢?我们可以,只是在语言的情况下,让自己在不同解释之间自由选择,会给你更好的机会构建更好的模型。而且恰好,目前已知的最有效的输入和输出解释方式是不同的。事实上,我们在这个模型中输入数字的方式并不是最好的,我们很快就会看到更好的方法。
是什么让大语言模型如此强大?
逐字符生成“Humpty Dumpty sat on a wall”与现代LLM所能做到的相去甚远。从我们上面讨论的简单生成式AI到类似人类的机器人,存在许多差异和创新。让我们来逐一了解它们:
嵌入(Embeddings)
还记得我们说过,将字符输入模型的方式不是最好的。我们只是为每个字符任意选择了一个数字。如果存在更好的数字,能让我们训练出更好的网络,那该怎么办?我们如何找到这些更好的数字?这里有一个聪明的技巧:
当我们训练上面的模型时,我们的方法是调整权重,看是否能最终得到更小的损失。然后缓慢地、递归地改变权重。在每一轮中,我们会:
- 输入数据
- 计算输出层
- 将其与我们理想的输出进行比较,并计算平均损失
- 调整权重,然后重新开始
在这个过程中,输入是固定的。当输入是(RGB, Vol)时,这很有道理。但是我们现在为a, b, c等输入的数字是我们任意挑选的。如果在每次迭代中,除了稍微调整权重之外,我们还稍微调整输入,看看是否可以通过使用不同的数字来表示“a”等来获得更低的损失,会怎么样?我们肯定是在减少损失并使模型变得更好(根据设计,我们就是朝着这个方向移动“a”的输入的)。基本上,不仅对权重应用梯度下降,也对输入的数字表示应用梯度下降,因为它们反正也是任意挑选的数字。这被称为“嵌入”(embedding)。它是输入到数字的映射,正如你刚才看到的,它需要被训练。训练嵌入的过程很像训练参数。不过,这样做的一个巨大优势是,一旦你训练好一个嵌入,你可以在另一个模型中使用它。请记住,你将始终使用相同的嵌入来表示单个词元/字符/单词。
我们谈到的是每个字符只有一个数字的嵌入。然而,在现实中,嵌入有多个数字。这是因为很难用一个单一的数字来捕捉一个概念的丰富性。如果我们看我们的叶子和花朵的例子,我们为每个物体提供了四个数字(输入层的大小)。这四个数字中的每一个都传达了一个属性,模型能够利用所有这些属性来有效地猜测物体。如果我们只有一个数字,比如说颜色的红色通道,模型可能会困难得多。我们在这里试图捕捉人类语言——我们将需要不止一个数字。
所以,与其用一个单一的数字来表示每个字符,也许我们可以用多个数字来表示它,以捕捉其丰富性?让我们为每个字符分配一组数字。我们把一组有序的数字称为“向量”(vector)(有序是指每个数字都有一个位置,如果我们交换两个数字的位置,就会得到一个不同的向量。我们的叶子/花朵数据就是这种情况,如果我们交换叶子的R和G数字,我们会得到一个不同的颜色,它就不再是同一个向量了)。向量的长度就是它包含的数字数量。我们将为每个字符分配一个向量。这就引出了两个问题:
- 如果我们为每个字符分配一个向量而不是一个数字,现在我们如何将“humpty dumpt”输入到网络中?答案很简单。假设我们为每个字符分配了一个包含10个数字的向量。那么输入层就不再是12个神经元,而是120个神经元,因为“humpty dumpt”中的12个字符每个都有10个数字要输入。我们只需将这些神经元并排放置即可。
- 我们如何找到这些向量?幸运的是,我们刚刚学会了如何训练嵌入数字。训练一个嵌入向量并没有什么不同。你现在有120个输入而不是12个,但你所做的仍然是调整它们,看看如何能最小化损失。然后你取前10个,那就是对应“h”的向量,依此类推。
所有的嵌入向量当然必须是相同长度的,否则我们就无法将所有的字符组合输入到网络中。例如,“humpty dumpt”和下一次迭代的“umpty dumpty”——在这两种情况下,我们都向网络输入12个字符,如果这12个字符中的每一个不是由长度为10的向量表示,我们就无法可靠地将它们全部输入到一个长度为120的输入层中。让我们将这些嵌入向量可视化:

我们把一组大小相同的向量的有序集合称为矩阵。上面的这个矩阵被称为嵌入矩阵(embedding matrix)。你告诉它对应于你的字母的列号,查看矩阵中的那一列就会给你用来表示那个字母的向量。这可以更普遍地应用于嵌入任何任意的集合——你只需要在这个矩阵中有与你的事物数量一样多的列。
子词分词器(Subword Tokenizers)
到目前为止,我们一直以字符作为语言的基本构建块。这有其局限性。神经网络的权重必须做大量的繁重工作,它们必须理解某些字符序列(即单词)相邻出现,然后再与其他单词相邻的意义。如果我们直接为单词分配嵌入,并让网络预测下一个单词,会怎么样?反正网络除了数字什么都不懂,所以我们可以为“humpty”、“dumpty”、“sat”、“on”等每个单词分配一个长度为10的向量,然后我们只需输入两个单词,它就可以给我们下一个单词。“词元”(Token)是我们嵌入然后输入到模型的单个单元的术语。我们之前的模型使用字符作为词元,现在我们提议使用整个单词作为词元(当然,如果你喜欢,也可以使用整个句子或短语作为词元)。
使用单词分词对我们的模型有一个深远的影响。英语中有超过18万个单词。使用我们每个可能输出都有一个神经元的输出解释方案,我们需要在输出层设置数十万个神经元,而不是大约26个。随着现代网络为实现有意义的结果所需的隐藏层规模的增大,这个问题变得不那么紧迫。然而,值得注意的是,由于我们分别处理每个单词,并且我们为每个单词从随机数嵌入开始——非常相似的单词(例如“cat”和“cats”)将以没有关系的方式开始。你可能会期望这两个单词的嵌入应该彼此接近——模型无疑会学到这一点。但是,我们能否以某种方式利用这种明显的相似性来获得一个开端并简化问题呢?
是的,我们可以。当今语言模型中最常见的嵌入方案是将单词分解为子词,然后对它们进行嵌入。在cat的例子中,我们会将cats分解为两个词元“cat”和“s”。现在模型更容易理解“s”跟在其他熟悉单词后面的概念,等等。这也减少了我们需要的词元数量(sentencepiece是一个常见的分词器,其词汇量选项在数万级别,而不是英语中的数十万单词)。分词器(tokenizer)的作用是接收你的输入文本(例如“Humpty Dumpt”),将其分割成词元,并给你相应的数字,你需要用这些数字在嵌入矩阵中查找该词元的嵌入向量。例如,在“humpty dumpty”的情况下,如果我们使用字符级分词器,并且我们像上图那样排列我们的嵌入矩阵,那么分词器将首先将humpty dumpt分割成字符 [‘h’,’u’,…’t’],然后给你返回数字 [8,21,…20],因为你需要查找嵌入矩阵的第8列来获取‘h’的嵌入向量(嵌入向量是你将输入到模型的,而不是数字8,与之前不同)。矩阵中列的排列是完全无关紧要的,我们可以将任何列分配给‘h’,只要我们每次输入‘h’时都查找同一个向量,就可以了。分词器只是给我们一个任意(但固定)的数字,以便于查找。我们真正需要它们的主要任务是将句子分割成词元。
有了嵌入和子词分词,一个模型可能看起来像这样:

接下来的几节将讨论语言建模中更近期的进展,也是那些使LLM变得如此强大的进展。然而,要理解这些,你需要了解一些基本的数学概念。这些概念是:
- 矩阵和矩阵乘法
- 数学中函数的一般概念
- 数的乘方(例如 a³ = aaa)
- 样本均值、方差和标准差
我在附录中添加了这些概念的摘要。
自注意力机制(Self Attention)
到目前为止,我们只看到了一种简单的神经网络结构(称为前馈网络),它包含许多层,每一层都与下一层完全连接(即,连续两层中的任意两个神经元之间都有一条线连接),并且它只连接到下一层(例如,第1层和第3层之间没有连接线)。然而,你可以想象,没有什么能阻止我们移除或建立其他连接,甚至构建更复杂的结构。让我们来探索一个特别重要的结构:自注意力机制。
如果你观察人类语言的结构,我们想要预测的下一个词将取决于它之前的所有词。然而,它可能在更大程度上依赖于某些词而不是其他词。例如,如果我们试图预测“Damian had a secret child, a girl, and he had written in his will that all his belongings, along with the magical orb, will belong to ____”中的下一个词。这里的词可能是“her”或“him”,它特别依赖于句子中更早的一个词:girl/boy。
好消息是,我们的简单前馈模型连接到上下文中的所有词,所以它可以学习重要词的适当权重。但问题在于,通过前馈层连接我们模型中特定位置的权重是固定的(对于每个位置)。如果重要的词总是在同一个位置,它会适当地学习权重,我们就没问题了。然而,与下一次预测相关的词可能出现在系统中的任何位置。我们可以改写上面的句子,在猜测“her vs his”时,一个对这个预测非常重要的词将是boy/girl,无论它出现在句子的哪个位置。所以,我们需要权重不仅依赖于位置,还依赖于该位置的内容。我们如何实现这一点?
自注意力机制做的事情有点像将每个词的嵌入向量相加,但它不是直接相加,而是对每个向量应用一些权重。所以,如果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,我们现在也有一个词x1的v1,我们通过一个矩阵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,而我们的模型在预测dumpty中的“m”时犯了一个错误。它没有给我们最后一层中“m”值最高的输出,而是给了我们“u”值最高的输出,但“m”是紧随其后的第二名。
现在我们可以继续用“duu”来预测下一个字符等等,但模型的置信度会很低,因为从“humpty duu…”开始没有那么多好的续写。另一方面,“m”是紧随其后的第二名,所以我们也可以给“m”一个机会,预测接下来的几个字符,看看会发生什么?也许它会给我们一个更好的整体单词?
所以我们这里谈论的不仅仅是盲目地选择最大值,而是尝试几个。有什么好方法呢?嗯,我们必须为每个选项分配一个机会——比如说,我们将以50%的概率选择第一个,25%的概率选择第二个,依此类推。这是一个好方法。但也许我们希望这个机会依赖于底层模型的预测。如果模型预测m和u的值在这里非常接近(与其他值相比)——那么也许给两者一个接近50-50的机会去探索是个好主意?
所以我们需要一个好的规则,它能接收所有这些数字,并将它们转换成机会。这就是softmax所做的。它是上述逻辑函数的推广,但具有额外的特性。如果你给它10个任意的数字——它会给你10个输出,每个都在0和1之间,而且重要的是,这10个输出加起来等于1,这样我们就可以把它们解释为概率。你会在几乎每个语言模型的最后一层找到softmax。
残差连接(Residual connections)
随着章节的推进,我们已经慢慢改变了对网络的视觉化表示。我们现在使用方框/模块来表示某些概念。这种表示法在表示一个特别有用的概念——残差连接时非常有用。让我们看看残差连接与自注意力模块的结合:

注意,我们把“输入”和“输出”画成了方框以简化事物,但它们基本上仍然只是一组神经元/数字,和上面显示的一样。
这里发生了什么?我们基本上是取自注意力模块的输出,在将其传递给下一个模块之前,我们将原始的输入加到它上面。首先要注意的是,这将要求自注意力模块输出的维度必须与输入的维度相同。这不是问题,因为我们注意到自注意力输出是由用户决定的。但为什么要这样做呢?我们在这里不深入所有细节,但关键是,随着网络变得更深(输入和输出之间有更多的层),训练它们变得越来越困难。残差连接已被证明有助于解决这些训练挑战。

层归一化(Layer Normalization)
层归一化是一个相当简单的层,它接收进入该层的数据,并通过减去均值和除以标准差来对其进行归一化(可能还有更多,如下所述)。例如,如果我们在输入之后立即应用层归一化,它会取输入层中的所有神经元,然后计算两个统计量:它们的均值和标准差。假设均值是M,标准差是D,那么层归一化所做的就是取这些神经元中的每一个,并用 (x-M)/D 替换它,其中x表示任何给定神经元的原始值。
这有什么帮助呢?它基本上稳定了输入向量,并有助于训练深度网络。一个担忧是,通过归一化输入,我们是否移除了其中一些可能有助于学习我们目标有价值信息的信息?为了解决这个问题,层归一化层有一个缩放(scale)和一个偏置(bias)参数。基本上,对于每个神经元,你只需将其乘以一个标量,然后加上一个偏置。这些标量和偏置值是可以训练的参数。这允许网络学习一些可能对预测有价值的变化。由于这些是唯一的参数,LayerNorm模块没有很多参数需要训练。整个过程看起来像这样:

缩放和偏置是可训练的参数。你可以看到层归一化是一个相对简单的模块,其中每个数字都只是逐点操作(在初始的均值和标准差计算之后)。这让我们想起了激活层(例如RELU),关键区别在于这里我们有一些可训练的参数(尽管由于简单的逐点操作,比其他层少得多)。
标准差是一个统计度量,衡量值的离散程度。例如,如果值都相同,你会说标准差为零。如果,总的来说,每个值都离这些值的均值很远,那么你将有一个高的标准差。计算一组数a1, a2, a3…(假设有N个数)的标准差的公式大致如下:从每个数中减去均值(这些数的均值),然后对N个数的答案分别求平方。将所有这些数相加,然后除以N。现在取答案的平方根。
给已有基础的读者的说明:有经验的机器学习专业人士会注意到,这里没有讨论批量归一化(batch norm)。事实上,我们甚至没有在本文中引入批次(batch)的概念。在很大程度上,我相信批次是另一种训练加速器,与理解核心概念无关(除了我们在这里不需要的批量归一化)。
Dropout
Dropout是一种简单而有效的避免模型过拟合的方法。过拟合是一个术语,指当你在训练数据上训练模型时,它在该数据集上表现良好,但对模型未见过的新样本泛化能力不佳。帮助我们避免过拟合的技术被称为“正则化技术”,dropout就是其中之一。
如果你训练一个模型,它可能会在数据上犯错和/或以某种特定方式过拟合。如果你训练另一个模型,它可能也会这样做,但方式不同。如果你训练多个这样的模型并对它们的输出进行平均呢?这些通常被称为“集成模型”(ensemble models),因为它们通过组合一组模型的输出来预测结果,而集成模型通常比任何单个模型表现得更好。
在神经网络中,你也可以这样做。你可以构建多个(略有不同的)模型,然后组合它们的输出来获得一个更好的模型。然而,这在计算上可能很昂贵。Dropout是一种技术,它不完全是构建集成模型,但确实抓住了这个概念的一些精髓。
这个概念很简单,通过在训练期间插入一个dropout层,你所做的是随机删除dropout插入的层之间一定比例的直接神经元连接。考虑我们最初的网络,在输入层和中间层之间插入一个带有50% dropout率的Dropout层,可能看起来像这样:



现在,这迫使网络以大量的冗余进行训练。本质上,你是在同时训练多个不同的模型——但它们共享权重。
现在为了进行推理,我们可以采用与集成模型相同的方法。我们可以使用dropout进行多次预测,然后将它们组合起来。然而,由于这在计算上很密集——而且由于我们的模型共享公共权重——为什么我们不直接使用所有权重进行一次预测呢(即一次使用所有权重而不是一次使用50%的权重)。这应该能给我们提供集成模型结果的某种近似。
不过有一个问题:用50%的权重训练的模型在中间神经元中的数字将与使用所有权重的模型大不相同。我们想要的是更像集成式的平均。我们怎么做呢?嗯,一个简单的方法是简单地取所有权重并将它们乘以0.5,因为我们现在使用的权重是原来的两倍。这就是Dropout在推理期间所做的。它将使用带有所有权重的完整网络,并简单地将权重乘以(1- p),其中p是删除概率。这已被证明作为一种正则化技术效果相当好。
多头注意力(Multi-head Attention)
这是Transformer架构中的关键模块。我们已经看到了什么是注意力模块。还记得注意力模块的输出是由用户决定的,并且是v的长度。多头注意力基本上是你并行运行几个注意力头(它们都接收相同的输入)。然后我们取它们所有的输出并简单地将它们拼接起来。它看起来像这样:

请记住,从v1 -> v1h1的箭头是线性层——每个箭头上都有一个进行变换的矩阵。我只是为了避免混乱而没有显示它们。
这里发生的是,我们为每个头生成相同的键、查询和值。但然后我们基本上在此之上应用一个线性变换(分别对每个k,q,v,并且对每个头都分别应用),然后才使用那些k,q,v的值。这个额外的层在自注意力中是不存在的。
一个旁注是,对我来说,这是一种有点令人惊讶的创建多头注意力的方式。例如,为什么不为每个头创建独立的Wk, Wq, Wv矩阵,而不是添加一个新层并共享这些权重。如果你知道的话请告诉我——我真的不知道。
位置编码和嵌入(Positional encoding and embedding)
我们在自注意力部分简要地谈到了使用位置编码的动机。这些是什么?虽然图片显示的是位置编码,但使用位置嵌入比使用编码更常见。因此,我们在这里讨论一个常见的位置嵌入,但附录也涵盖了原始论文中使用的位置编码。位置嵌入与其他任何嵌入没有什么不同,只是我们不是嵌入词汇表,而是嵌入数字1, 2, 3等。所以这个嵌入是一个与词嵌入长度相同的矩阵,每一列对应一个数字。这就是它的全部内容。
GPT架构
让我们来谈谈GPT架构。这是大多数GPT模型中使用的架构(不同版本之间有差异)。如果你一直跟着文章读到这里,这应该相当容易理解。使用方框表示法,这就是架构在宏观层面的样子:

在这一点上,除了“GPT Transformer模块”之外,所有其他模块都已详细讨论过。这里的+号只是意味着两个向量相加(这意味着两个嵌入必须是相同的大小)。让我们看看这个GPT Transformer模块:

基本上就是这样。它在这里被称为“transformer”,因为它是从transformer派生出来的,并且是transformer的一种类型——我们将在下一节中看到这个架构。这不影响理解,因为我们已经涵盖了这里显示的所有构建块。让我们回顾一下我们到目前为止为构建这个GPT架构所涵盖的所有内容:
- 我们看到了神经网络如何接收数字并输出其他数字,并有可以训练的权重作为参数。
- 我们可以为这些输入/输出数字附加解释,并赋予神经网络现实世界的意义。
- 我们可以将神经网络链接起来创建更大的网络,我们可以称每个网络为一个“模块”并用一个方框表示以使图表更容易。每个模块仍然做同样的事情,接收一堆数字并输出另一堆数字。
- 我们学习了许多服务于不同目的的不同类型的模块。
- GPT只是上面显示的这些模块的一种特殊排列,其解释我们在第一部分讨论过。
随着公司构建出强大的现代LLM,这个架构也随时间进行了一些修改,但基本原理保持不变。
现在,这个GPT transformer实际上是原始transformer论文中所谓的“解码器”(decoder)。让我们来看看那个。
Transformer架构
这是推动近期语言模型能力快速提升的关键创新之一。Transformer不仅提高了预测准确性,而且比以前的模型更容易/更有效地训练,从而允许更大的模型尺寸。上面提到的GPT架构就是基于此。
如果你看GPT架构,你可以看到它非常适合生成序列中的下一个词。它基本上遵循我们在第一部分讨论的相同逻辑。从几个词开始,然后一次生成一个词继续下去。但是,如果你想做翻译呢?如果你有一个德语句子(例如“Wo wohnst du?” = “Where do you live?”)并且你想把它翻译成英语。我们将如何训练模型来做这件事?
嗯,首先我们需要想办法输入德语单词。这意味着我们必须扩展我们的嵌入以包括德语和英语。现在,我猜这里有一个简单的输入信息的方法。为什么我们不直接把德语句子连接到目前为止生成的英语的开头,然后把它输入到上下文中呢?为了让模型更容易,我们可以添加一个分隔符。这在每一步看起来会是这样:

这会起作用,但有改进的空间:
- 如果上下文长度是固定的,有时原始句子会丢失。
- 模型在这里需要学习很多东西。同时学习两种语言,还要知道
<SEP>是它需要开始翻译的分隔符。 - 你正在为每个词的生成处理整个德语句子,偏移量不同。这意味着同一事物的内部表示会有所不同,模型应该能够处理所有这些来进行翻译。
Transformer最初就是为这个任务创建的,它由一个“编码器”(encoder)和一个“解码器”(decoder)组成——它们基本上是两个独立的模块。一个模块简单地接收德语句子并给出一个中间表示(同样,一堆数字)——这被称为编码器。
第二个模块生成单词(我们到目前为止已经看到很多这样的东西了)。唯一的区别是,除了给它输入到目前为止生成的单词外,我们还给它输入编码后的德语句子(来自编码器模块)。所以当它生成语言时,它的上下文基本上是到目前为止生成的所有单词,加上德语。这个模块被称为解码器。
这些编码器和解码器中的每一个都由几个模块组成,特别是夹在其他层之间的注意力模块。让我们看看论文“Attention is all you need”中transformer的插图,并试着理解它:

左边的垂直模块集被称为“编码器”,右边的被称为“解码器”。让我们过一遍并理解我们之前没有涵盖的任何内容:
**如何阅读图表的重述:**这里的每个方框都是一个模块,它以神经元的形式接收一些输入,并吐出一组神经元作为输出,然后可以由下一个模块处理或由我们解释。箭头显示了模块的输出去向。如你所见,我们经常会取一个模块的输出并将其作为输入馈送到多个模块中。让我们逐一过一遍:
- 前馈(Feed forward):前馈网络是一个不包含循环的网络。我们在第一节中的原始网络就是一个前馈网络。事实上,这个模块使用的结构非常相似。它包含两个线性层,每个后面都跟着一个RELU(见第一节关于RELU的说明)和一个dropout层。请记住,这个前馈网络独立地应用于每个位置。这意味着位置0上的信息有一个前馈网络,位置1上有一个,依此类推。但是位置x的神经元与位置y的前馈网络没有连接。这很重要,因为如果我们不这样做,它将允许网络在训练时通过向前看来作弊。
- 交叉注意力(Cross-attention):你会注意到解码器有一个多头注意力,其箭头来自编码器。这里发生了什么?还记得自注意力和多头注意力中的值、键、查询吗?它们都来自同一个序列。查询实际上只来自序列的最后一个词。那么,如果我们保留查询,但从一个完全不同的序列中获取值和键呢?这就是这里发生的事情。值和键来自编码器的输出。除了键和值的输入来源现在不同之外,数学上没有任何改变。
- Nx:这里的Nx只是表示这个模块被链式重复N次。所以基本上你是将模块背对背地堆叠起来,并将前一个模块的输入传递给下一个。这是使神经网络更深的一种方式。现在,看图可能会对编码器输出如何馈送到解码器产生困惑。假设N=5。我们是否将每个编码器层的输出馈送到相应的解码器层?不。基本上你只完整地运行一次编码器。然后你只取那个表示,并将同样的东西馈送到5个解码器层中的每一个。
- Add & Norm 模块:这基本上和下图一样(猜作者只是想节省空间)

其他所有内容都已讨论过。现在你已经有了从简单的加法和乘法运算开始,完全自成体系的transformer架构的完整解释!你知道每一条线、每一个和、每一个方框和词在从零开始构建它们方面的含义。理论上,这些笔记包含了你从零开始编写transformer所需的全部内容。事实上,如果你感兴趣,这个仓库为上面的GPT架构做了这件事。
构建一个预训练模型
至此,我们已经拥有了设计和训练一个LLM所需的所有部分。让我们为一个英语语言模型把这些部分组合起来:
- 首先,我们从构建一个可以编码和解码英语的分词器开始,就像我们在子词分词器部分讨论的那样。假设词汇量大小为32k。
- 接下来,我们用transformer架构构建一个LLM。输出向量和嵌入矩阵都必须有32k个元素,等于词汇量的大小。
- 现在我们收集一个英语语料库来训练模型。这是我们的训练数据。通常这个数据集的关键部分是整个互联网的爬取,比如Common Crawl。
- 现在我们可以开始训练模型,通过要求模型预测下一个词元,就像在训练部分讨论的那样。你通过为下一个词元(你已经从你的训练数据中知道了)定义一个损失函数来做到这一点,并推动网络去预测那个词元。
对数百亿或数万亿的词元这样做,会给你一组权重,这就是你的预训练模型。这是一个能够预测下一个词元的模型,现在可以用来完成句子,甚至整篇文章。
附录
矩阵乘法
我们在上面嵌入的上下文中介绍了向量和矩阵。矩阵有两个维度(行数和列数)。向量也可以被认为是一个维度等于一的矩阵。两个矩阵的乘积定义为:

点代表乘法。现在让我们再看一下第一张图中蓝色和橙色神经元的计算。如果我们将权重写成一个矩阵,将输入写成向量,我们可以将整个操作写成以下方式:

如果权重矩阵被称为“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(即,我们可以输入到网络中用于预测下一个词元/词的最大词元/词数,见“这一切如何帮助生成语言?”部分的讨论),并且如果嵌入向量的长度是10(比如说),那么我们需要一个10行d列的矩阵,其中所有列都是唯一的,并且所有数字都在0和1之间。鉴于0和1之间有无限多的数字,而矩阵是有限大小的,这可以通过许多方式来完成。
在“Attention is all you need”论文中使用的方法大致如下:
- 画10条正弦曲线,每条都是
si(p) = sin(p / 10000^(i/d))(那是10k的i/d次方)。 - 用数字填充编码矩阵,使得(i,p)位置的数字是si§,例如,对于位置1,编码向量的第5个元素是
s5(1) = sin(1 / 10000^(5/d))。
为什么选择这种方法?通过改变10k上的幂,你正在改变正弦函数在p轴上观察时的振幅。如果你有10个不同振幅的10个不同正弦函数,那么在p值变化时,需要很长时间才会出现重复(即所有10个值都相同)。这有助于我们获得唯一的值。现在,实际的论文同时使用了正弦和余弦函数,编码的形式是:如果i是偶数,si(p) = sin(p / 10000^(i/d));如果i是奇数,si(p) = cos(p / 10000^(i/d))。
最后
为什么要学AI大模型
当下,⼈⼯智能市场迎来了爆发期,并逐渐进⼊以⼈⼯通⽤智能(AGI)为主导的新时代。企业纷纷官宣“ AI+ ”战略,为新兴技术⼈才创造丰富的就业机会,⼈才缺⼝将达 400 万!
DeepSeek问世以来,生成式AI和大模型技术爆发式增长,让很多岗位重新成了炙手可热的新星,岗位薪资远超很多后端岗位,在程序员中稳居前列。

与此同时AI与各行各业深度融合,飞速发展,成为炙手可热的新风口,企业非常需要了解AI、懂AI、会用AI的员工,纷纷开出高薪招聘AI大模型相关岗位。

最近很多程序员朋友都已经学习或者准备学习 AI 大模型,后台也经常会有小伙伴咨询学习路线和学习资料,我特别拜托北京清华大学学士和美国加州理工学院博士学位的鲁为民老师给大家这里给大家准备了一份涵盖了AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频 全系列的学习资料,这些学习资料不仅深入浅出,而且非常实用,让大家系统而高效地掌握AI大模型的各个知识点。
这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】
AI大模型系统学习路线
在面对AI大模型开发领域的复杂与深入,精准学习显得尤为重要。一份系统的技术路线图,不仅能够帮助开发者清晰地了解从入门到精通所需掌握的知识点,还能提供一条高效、有序的学习路径。

但知道是一回事,做又是另一回事,初学者最常遇到的问题主要是理论知识缺乏、资源和工具的限制、模型理解和调试的复杂性,在这基础上,找到高质量的学习资源,不浪费时间、不走弯路,又是重中之重。
AI大模型入门到实战的视频教程+项目包
看视频学习是一种高效、直观、灵活且富有吸引力的学习方式,可以更直观地展示过程,能有效提升学习兴趣和理解力,是现在获取知识的重要途径

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

海量AI大模型必读的经典书籍(PDF)
阅读AI大模型经典书籍可以帮助读者提高技术水平,开拓视野,掌握核心技术,提高解决问题的能力,同时也可以借鉴他人的经验。对于想要深入学习AI大模型开发的读者来说,阅读经典书籍是非常有必要的。

600+AI大模型报告(实时更新)
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

AI大模型面试真题+答案解析
我们学习AI大模型必然是想找到高薪的工作,下面这些面试题都是总结当前最新、最热、最高频的面试题,并且每道题都有详细的答案,面试前刷完这套面试题资料,小小offer,不在话下


这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】
6485

被折叠的 条评论
为什么被折叠?



