从零开始用中学数学理解LLM:大型语言模型完全指南
一个完整的、自包含的LLM内部工作原理详解
在本文中,我们将从零开始探讨大型语言模型(LLM)的工作原理——假设你只会加法和乘法。本文旨在完全自包含。我们首先在纸上构建一个简单的生成式AI,然后逐步讲解理解现代LLM和Transformer架构所需的所有内容。文章将剥离所有机器学习中的花哨术语和行话,将一切简单地表示为数字。我们仍会指出这些术语的名称,以便你在阅读术语化内容时能够对应理解。
从加法/乘法到当今最先进的AI模型,不假设其他知识或参考其他资料意味着我们将涵盖大量内容。这不是一个玩具LLM的解释——一个有决心的人理论上可以仅凭本文信息重建一个现代LLM。我已经删除了所有不必要的文字/行,因此本文不适合浏览。
我们将涵盖什么?
- 一个简单的神经网络
- 这些模型是如何训练的?
- 这些模型如何生成语言?
- 什么使LLM如此有效?
- 嵌入
- 子词分词器
- 自注意力
- Softmax
- 残差连接
- 层归一化
- Dropout
- 多头注意力
- 位置嵌入
- GPT架构
- Transformer架构
让我们开始。
首先需要注意到的是,神经网络只能接受数字作为输入,并且只能输出数字。没有例外。关键在于如何将输入转换为数字,如何将输出数字解释为你的目标。最后,构建能够接受你提供的输入并给出所需输出的神经网络(基于你对输出的解释)。让我们逐步讲解如何从加法和乘法过渡到像Llama 3.1这样的模型。
一个简单的神经网络:
让我们构建一个可以分类物体的简单神经网络:
- 可用物体数据:主色(RGB)& 体积(毫升)
- 分类为:叶或花
叶子和向日葵的数据示例如下:
按Enter或点击查看全尺寸图像

作者图片
现在构建一个执行此分类的神经网络。我们需要决定输入/输出的解释。我们的输入已经是数字,可以直接输入网络。输出是两个物体(叶和花),神经网络无法直接输出。我们可以采用以下方案:
- 网络输出一个数字。如果数字为正则认为是叶,为负则认为是花
- 或者,网络输出两个数字。我们将第一个数字解释为叶的数值,第二个为花的数值,并选择数值较大的作为结果
两种方案都允许网络输出可解释为叶或花的数字。我们选择第二种方案,因为它更适用于后续内容。以下是使用此方案进行分类的神经网络。让我们逐步讲解:
按Enter或点击查看全尺寸图像

作者图片
一些术语:
神经元/节点:圆圈中的数字
权重:线上的彩色数字
层:神经元的集合称为层。可以认为此网络有3层:输入层4个神经元,中间层3个神经元,输出层2个神经元。
要计算此网络的预测/输出(称为“前向传播”),从左侧开始。我们有输入层神经元的数据。要“前进”到下一层,将圆圈中的数字与对应神经元的权重相乘并求和。我们演示了上面的蓝色和橙色圆圈的数学运算。运行整个网络后,输出层的第一个数字较大,因此我们解释为“网络将这些(RGB,Vol)值分类为叶”。训练良好的网络可以接受各种(RGB,Vol)输入并正确分类物体。
模型对叶、花或(RGB,Vol)没有任何概念。它的任务是接受恰好4个数字并输出恰好2个数字。我们解释这4个输入数字为(RGB,Vol),并决定输出数字中第一个较大则为叶。最终,我们选择合适的权重,使模型接受输入数字并输出正确的两个数字,从而得到我们想要的解释。
一个有趣的副作用是,你可以使用相同的网络,输入其他4个数字(如云量、湿度等),将两个输出数字解释为“一小时后晴朗”或“一小时后下雨”。如果权重校准良好,同一网络可以同时执行两个任务——分类叶/花和预测降雨!网络只输出两个数字,无论你将其解释为分类、预测还是其他内容,完全由你决定。
为简化省略的内容(可忽略而不影响理解):
- 激活层:此网络缺少一个关键部分——“激活层”。这是指对每个圆圈中的数字应用非线性函数(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 …等等。如果有非线性函数,通常无法简化。这有助于网络处理更复杂的情况。
- 偏置:网络通常还包含与每个节点关联的另一个数字,该数字直接加到乘积上以计算节点值,称为“偏置”。例如,如果顶部蓝色节点的偏置为0.25,则节点值为:(32 * 0.10) + (107 * -0.29) + (56 * -0.07) + (11.2 * 0.46) + 0.25 = — 26.35。术语“参数”通常指模型中除神经元/节点外的所有数字。
- 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(在相加前忽略负号)。我们可以称这个总和为“损失”。理想情况下,我们希望损失接近零,即“最小化损失”。
- 一旦有了损失,我们可以略微调整每个参数,观察增加或减少它是否会增加损失。这被称为该参数的“梯度”。然后我们可以沿着损失下降的方向(与梯度方向相反)将每个参数移动一小步。一旦所有参数都略微移动,损失应该会降低
- 持续重复这个过程,损失会减少,最终得到一组“训练完成”的权重/参数。整个过程被称为“梯度下降”。
几点说明:
- 通常有多个训练样本,因此当你略微调整权重以最小化一个样本的损失时,可能会使另一个样本的损失变差。解决方法是将损失定义为所有样本的平均损失,然后对这个平均损失进行梯度计算。这会减少整个训练数据集的平均损失。每个这样的循环称为一个“epoch”。然后你可以持续重复这些epoch,从而找到减少平均损失的权重。
- 我们实际上不需要“移动权重”来计算每个权重的梯度——我们可以直接从公式中推断(例如,如果最后一个步骤中的权重是0.17,神经元的值为正,并且我们希望输出更大的数值,我们可以看出将这个数值增加到0.18会有帮助)。
在实践中,训练深度网络是一个困难且复杂的过程,因为梯度很容易失控,在训练过程中变为零或无穷大(称为“梯度消失”和“梯度爆炸”问题)。我们在这里讨论的简单损失定义是完全有效的,但很少使用,因为有更好的函数形式适用于特定目的。随着现代模型包含数十亿个参数,训练模型需要巨大的计算资源,这本身也存在问题(内存限制、并行化等)。
所有这些如何帮助生成语言?
请记住,神经网络输入一些数值,根据训练好的参数进行数学运算,然后输出其他数值。一切都与解释和训练参数(即设置它们为某些数值)有关。如果我们能将这两个数值解释为“叶子/花朵”或“一小时后下雨或晴天”,我们也可以将它们解释为“句子中的下一个字符”。
但英语中不止有2个字母,因此我们必须将输出层的神经元数量扩展到,例如,英语中的26个字母(再加上一些符号,如空格、句号等)。每个神经元可以对应一个字符,我们查看输出层中的(约26个)神经元,并说输出层中数值最高的神经元对应的字符就是输出字符。现在我们有了一个可以输入一些内容并输出字符的网络。
如果我们用这些字符替换网络中的输入:“Humpty Dumpt”,并要求它输出一个字符,并将其解释为“网络对我们刚刚输入的序列的下一个字符的建议”,我们可能可以设置权重使其输出“y”——从而完成“Humpty Dumpty”。但有一个问题,如何将这些字符列表输入网络?我们的网络只接受数值!!
一个简单的解决方案是为每个字符分配一个数值。例如,a=1,b=2,依此类推。现在我们可以输入“humpty dumpt”并训练它输出“y”。我们的网络看起来像这样:
点击或按回车查看全尺寸图片

图片由作者提供
好的,现在我们可以通过提供字符列表来预测下一个字符。我们可以利用这一事实来构建整个句子。例如,一旦我们预测了“y”,我们可以将这个“y”添加到我们拥有的字符列表中,并将其输入网络,要求它预测下一个字符。如果训练良好,它应该会给出一个空格,依此类推。最终,我们应该能够递归生成“Humpty Dumpty sat on a wall”。我们拥有了生成式AI。此外,我们现在拥有了能够生成语言的网络! 现在,没有人会实际输入随机分配的数值,我们将在后续看到更合理的方案。如果你迫不及待,可以随时查看附录中的一-hot编码部分。
敏锐的读者会注意到,我们实际上无法将“Humpty Dumpty”输入网络,因为根据图示,输入层只有12个神经元,每个对应“humpty dumpt”中的一个字符(包括空格)。那么如何在下一次传递中输入“y”呢?添加第13个神经元需要修改整个网络,这不可行。解决方案很简单,让我们移除“h”并发送最近的12个字符。因此,我们将发送“umpty dumpty”,网络将预测一个空格。然后我们将输入“mpty dumpty “,它将生成一个s,依此类推。看起来像这样:
点击或按回车查看全尺寸图片

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

图片由作者提供
让我们将相同大小的向量有序集合称为矩阵。上面的矩阵称为嵌入矩阵。你告诉它一个对应于字母的列号,查看矩阵中的该列即可得到表示该字母的向量。这可以更广泛地应用于嵌入任何任意集合——你只需要在这个矩阵中有与事物数量相同的列。
子词标记器
到目前为止,我们一直以字符作为语言的基本构建块。这有其局限性。神经网络权重必须承担大量工作,它们必须理解某些字符序列(即单词)如何出现在彼此旁边,然后出现在其他单词旁边。如果我们直接为单词分配嵌入并让网络预测下一个单词会怎样?网络无论如何都不理解任何东西,除了数字,因此我们可以为每个单词“humpty”、“dumpty”、“sat”、“on”等分配一个10长度的向量,然后我们只需输入两个单词,它就能给出下一个单词。“标记”是我们嵌入并输入模型的单个单元的术语。到目前为止,我们的模型使用字符作为标记,现在我们提议使用整个单词作为标记(当然,你也可以使用整个句子或短语作为标记)。
使用单词标记化对我们的模型有一个深远的影响。英语中有超过18万个单词。使用每个可能输出对应一个神经元的输出解释方案,我们需要在输出层有数十万个神经元,而不是26个左右。随着现代网络需要的隐藏层大小以达到有意义的结果,这个问题变得不那么紧迫。然而值得注意的是,由于我们将每个单词单独处理,并且每个单词的嵌入都是从随机数字开始的——非常相似的单词(例如“cat”和“cats”)将没有关系。你期望这两个单词的嵌入彼此接近——毫无疑问模型会学习到这一点。但是,我们能否以某种方式利用这种明显的相似性来提前启动并简化问题呢?
是的,我们可以。目前语言模型中最常见的嵌入方案是将单词分解为子词,然后进行嵌入。以“cat”为例,我们会将其分解为两个标记“cat”和“s”。这样模型更容易理解“s”后接其他熟悉单词的概念等。这也减少了我们需要的标记数量(sentencpiece 是一个常见的分词器,其词汇量选项为数万到数十万,而英语单词数量为数十万)。分词器是一种将输入文本(例如“Humpty Dumpt”)拆分为标记并提供对应数字的工具,这些数字用于在嵌入矩阵中查找该标记的嵌入向量。例如,如果使用字符级分词器且嵌入矩阵的排列如上图所示,那么分词器会首先将“humpty dumpty”拆分为字符['h','u',...'t'],然后返回数字[8,21,...20],因为需要查找嵌入矩阵的第8列以获取字符'h'的嵌入向量(嵌入向量是输入模型的数据,而不是数字8,与之前不同)。矩阵列的排列完全无关紧要,我们可以将任何列分配给'h',只要每次输入'h'时查找相同的向量即可。分词器只是提供一个任意(但固定)的数字以方便查找。它们的主要任务是将句子拆分为标记。
通过嵌入和子词分词,模型可能如下所示:
按Enter或点击查看全尺寸图片

图片由作者提供
接下来的几个部分将讨论语言建模的最新进展,这些进展使LLM变得如此强大。然而,要理解这些内容,您需要了解一些基本的数学概念。以下是这些概念:
- 矩阵和矩阵乘法
- 数学中函数的一般概念
- 幂数(例如a3 = aaa)
- 样本均值、方差和标准差
我在附录中添加了这些概念的摘要。
自注意力
到目前为止,我们只见过一种简单的神经网络结构(称为前馈网络),其中包含多个层,每层完全连接到下一层(即,任何两个连续层的神经元之间都有连线),并且仅连接到下一层(例如,没有层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,则输出=0.5x1 + 0.25x2 + 0.25x3,其中输出是自注意力的输出。如果我们用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。
在您的收件箱中获取Rohit Patel的故事
免费加入Medium以获取此作者的更新。
自注意力中另一个发生的事情是,我们不直接对嵌入向量本身进行加权求和。相反,我们对嵌入向量的某些“值”进行加权求和,该值通过另一个小型单层网络获得。这意味着类似于k1和q1,我们还为单词x1获得一个v1,并通过矩阵Wv获得v1=Wvx1。然后聚合v1。因此,如果我们只有三个单词并试图预测第四个单词,整体看起来如下:
按Enter或点击查看全尺寸图片

自注意力。图片由作者提供
加号表示向量的简单相加,意味着它们必须长度相同。这里未显示的最后一个修改是标量u1、u2、u3等可能不会加起来等于1。如果需要它们作为权重,我们应该让它们加起来。因此,我们将应用一个熟悉的技巧,使用softmax函数。
这就是自注意力。还有交叉注意力,其中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%的机会可能是个好主意?
所以我们需要一个很好的规则,将所有这些数字转换为机会。这就是softmax的作用。它是上述逻辑函数的推广,但具有额外功能。如果你给它10个任意数字——它会给你10个输出,每个在0到1之间,并且重要的是,所有10个加起来等于1,因此我们可以将它们解释为机会。你几乎会在每个语言模型的最后一层找到softmax。
残差连接
随着各部分的进展,我们逐渐改变了对网络的可视化。我们现在使用框/块来表示某些概念。这种表示法在表示残差连接这一特别有用的概念时很有用。让我们看看残差连接与自注意力块的结合:
按回车或点击查看全尺寸图片

残差连接。图片由作者提供
请注意,我们将“输入”和“输出”放入框中以简化,但它们本质上仍然只是神经元/数字的集合,如上所示。
那么这里发生了什么?我们基本上是将自注意力块的输出取出,并在传递给下一个块之前,将其与原始输入相加。首先要注意的是,这要求自注意力块的输出维度现在必须与输入相同。这不成问题,因为如我们所知,自注意力输出由用户决定。但为什么要这样做?我们不会在这里深入所有细节,但关键点是,随着网络变得更深(输入和输出之间的层数更多),训练它们变得越来越困难。残差连接已被证明有助于解决这些训练挑战。
层归一化
层归一化是一个相对简单的层,它接收进入该层的数据,并通过减去均值并除以标准差(可能还有更多,如下所述)对其进行归一化。例如,如果我们立即在输入后应用层归一化,它会取输入层中的所有神经元,然后计算两个统计量:它们的均值和标准差。假设均值为M,标准差为D,那么层归一化所做的就是取每个神经元并将其替换为(x-M)/D,其中x表示任何给定神经元的原始值。
现在这有什么帮助呢?它基本上稳定了输入向量,并有助于训练深度网络。一个担忧是,通过归一化输入,我们是否会去除一些对学习目标有用的信息?为了解决这个问题,层归一化层有一个缩放参数和一个偏置参数。基本上,对于每个神经元,你只需将其乘以一个标量,然后加上一个偏置。这些标量和偏置值是可训练的参数。这使得网络能够学习一些对预测有价值的变化。而且由于这些是唯一的参数,层归一化模块不需要训练很多参数。整个过程大致如下:
按回车或点击以查看全尺寸图像

层归一化。图片由作者提供
缩放和偏置是可训练参数。你可以看到,层归一化是一个相对简单的模块,每个数字仅在初始均值和标准差计算后进行逐点操作。这让我们联想到激活层(例如RELU),关键区别在于这里有一些可训练参数(尽管由于简单的逐点操作,参数数量比其他层少很多)。
标准差是衡量数值分布离散程度的统计指标,例如,如果所有数值都相同,标准差为零。如果每个数值通常与这些数值的均值相差很远,则标准差会很高。计算一组数值(a1, a2, a3…共N个数)的标准差公式大致如下:从每个数值中减去均值,然后对每个N个数值的结果进行平方。将所有这些数值相加后除以N。现在对结果开平方根。
给预先了解者的注释:有经验的机器学习专业人士会注意到这里没有讨论批量归一化。事实上,本文甚至没有引入批次的概念。总的来说,我认为批次是另一种与核心概念理解无关的训练加速方法(批量归一化除外,但这里不需要)。
丢弃法
丢弃法是一种简单但有效的避免模型过拟合的方法。过拟合是指在训练数据上训练模型时,模型在该数据集上表现良好,但对未见过的示例泛化能力差。帮助我们避免过拟合的技术称为“正则化技术”,而丢弃法就是其中之一。
如果你训练一个模型,它可能会在数据上犯错误和/或以特定方式过拟合。如果你训练另一个模型,它可能会以不同的方式犯同样的错误。如果你训练多个这样的模型并平均它们的输出会怎样?这些通常被称为“集成模型”,因为它们通过结合多个模型的输出进行预测,而集成模型通常比任何单个模型表现更好。
在神经网络中,你可以这样做。你可以构建多个(略有不同)模型,然后结合它们的输出以获得更好的模型。然而,这在计算上可能很昂贵。丢弃法是一种不完全构建集成模型但捕捉其概念本质的技术。
概念很简单,通过在训练中插入丢弃层,你随机删除插入丢弃层的层之间一定百分比的直接神经元连接。考虑我们最初的网络,并在输入层和中间层之间插入一个50%丢弃率的丢弃层,可能如下所示:
按回车或点击以查看全尺寸图像

按回车或点击以查看全尺寸图像

按回车或点击以查看全尺寸图像

图片由作者提供
现在,这迫使网络以大量冗余的方式进行训练。本质上,你同时训练多个不同的模型——但它们共享权重。
在进行推理时,我们可以遵循与集成模型相同的方法。我们可以使用丢弃进行多次预测,然后将它们结合起来。然而,由于这在计算上很密集——而且我们的模型共享公共权重——我们为什么不直接使用所有权重进行预测(而不是每次使用50%的权重)呢?这应该能给我们提供一个集成模型的近似结果。
一个问题:使用50%权重训练的模型在中间神经元中的数值会与使用所有权重的模型有很大不同。我们想要的是更多的集成式平均。如何做到这一点?很简单,只需将所有权重乘以0.5,因为我们现在使用了两倍的权重。这就是丢弃法在推理时的做法。它会使用所有权重的完整网络,并简单地将权重乘以(1-p),其中p是删除概率。这已被证明作为正则化技术效果相当好。
多头注意力机制
这是Transformer架构中的关键模块。我们已经了解了注意力模块是什么。记住,注意力模块的输出由用户决定,其长度等于v的长度。多头注意力机制基本上是并行运行多个注意力头(它们都接收相同的输入)。然后我们将所有输出简单地拼接起来。大致如下所示:
按回车或点击以查看全尺寸图像

多头注意力。图片由作者提供
请注意从v1到v1h1的箭头是线性层——每个箭头都有一个矩阵进行转换。我为了简化没有显示它们。
这里发生的事情是,我们为每个头生成相同的键、查询和值。但随后我们基本上在这些键、查询和值上分别应用线性变换(每个k、q、v和每个头分别处理)。这一额外层在自注意力中不存在。
一个附注是,对我来说,这种创建多头注意力的方式有点令人惊讶。例如,为什么不为每个头创建单独的Wk、Wq、Wv矩阵,而不是添加一个新层并共享这些权重?如果你知道原因,请告诉我——我真的没有头绪。
位置编码和嵌入
我们在自注意力部分简要讨论了使用位置编码的动机。这些是什么?虽然图片显示了位置编码,但使用位置嵌入比使用编码更常见。因此,我们在这里讨论一个常用的位置嵌入,但附录也涵盖了原始论文中使用的位置编码。位置嵌入与其他嵌入没有区别,只是我们嵌入的是数字1、2、3等,而不是单词词汇表。因此,这种嵌入的长度与单词嵌入相同,每一列对应一个数字。这就是全部内容。
GPT架构
让我们谈谈GPT架构。这是大多数GPT模型使用的架构(各模型略有不同)。如果你已经跟随本文至此,这应该很容易理解。使用方框表示法,架构的高层结构如下所示:
按回车或点击以查看全尺寸图像
GPT架构。图片由作者提供
在此时,除了“GPT Transformer Block”之外,其他所有模块都已详细讨论。这里的+号仅仅表示两个向量相加(这意味着两个嵌入必须大小相同)。让我们看一下这个GPT Transformer Block:
按Enter或点击以查看完整大小的图片

基本上就是如此。这里称为“transformer”是因为它源自并属于transformer的一种——这是我们在下一节将探讨的架构。这不会影响理解,因为我们已经涵盖了此处显示的所有构建模块。让我们回顾一下到目前为止我们所涵盖的内容,以构建这个GPT架构:
- 我们看到了神经网络如何将数字输入并输出其他数字,并且具有可训练的权重作为参数
- 我们可以为这些输入/输出数字附加解释,并赋予神经网络现实世界的意义
- 我们可以将神经网络串联起来创建更大的网络,并且可以将每个网络称为“模块”,用方框表示以简化图表。每个模块仍然执行相同的操作,即输入一组数字并输出另一组数字
- 我们学习了许多不同类型的模块,它们服务于不同的目的
- GPT只是这些模块的一种特殊排列,如上图所示,并且我们已经在第一部分讨论了其解释
随着时间的推移,公司们在构建强大的现代LLM时对此进行了修改,但基本结构保持不变。
现在,这个GPT transformer实际上在原始transformer论文中被称为“解码器”,该论文引入了transformer架构。让我们来看看这一点。
Transformer架构
这是最近推动语言模型能力快速提升的关键创新之一。Transformer不仅提高了预测准确性,而且比之前的模型更容易/更高效(训练),从而允许更大的模型规模。这就是上面GPT架构所基于的。
如果你看一下GPT架构,你会发现它非常适合生成序列中的下一个词。它本质上遵循我们在第一部分讨论的相同逻辑。从几个词开始,然后逐个生成。但如果你想要做翻译呢?假设你有一句德语(例如“Wo wohnst du?” = “你住在哪里?”),想要翻译成英语。我们如何训练模型来完成这个任务?
首先,我们需要找到一种方法来输入德语单词。这意味着我们必须扩展嵌入以包含德语和英语。这里,我猜测这是一种简单的输入信息的方法。为什么不将德语句子连接在到目前为止生成的英语句子的开头,并将其输入到上下文中?为了方便模型处理,我们可以添加一个分隔符。在每一步中,这看起来会像这样:
按Enter或点击以查看完整大小的图片

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

Vaswani等人(2017)的图片
左侧的垂直模块集称为“编码器”,右侧的称为“解码器”。让我们回顾并理解我们尚未涵盖的内容:
如何阅读图表的回顾: 这里的每个方框都是一个模块,以神经元的形式输入,输出一组神经元,可以由下一个模块处理或由我们解释。箭头显示了模块的输出去向。如你所见,我们经常将一个模块的输出作为多个模块的输入。让我们逐一分析:
Feed forward:前馈网络是没有循环的网络。我们在第一部分的原始网络是前馈网络。事实上,这个模块的结构非常相似。它包含两个线性层,每个后面跟着一个RELU(参见第一部分的RELU注释)和一个dropout层。请记住,这个前馈网络是独立应用于每个位置的。这意味着位置0的信息有一个前馈网络,位置1有一个,依此类推。但位置x的神经元与位置y的前馈网络没有连接。这很重要,因为如果我们不这样做,网络在训练时会作弊,向前看。
交叉注意力: 你会注意到解码器有一个多头注意力,箭头来自编码器。这里发生了什么?还记得自注意力和多头注意力中的值、键、查询吗?它们都来自同一序列。实际上,查询只是序列中最后一个词。那么如果我们保留查询,但从完全不同的序列中获取值和键呢?这就是这里发生的情况。值和键来自编码器的输出。数学上没有任何变化,只是键和值的输入来源不同了。
Nx: 这里的Nx只是表示这个模块被链式重复N次。基本上,你将模块前后堆叠,并将前一个模块的输出传递给下一个模块。这是使神经网络更深的一种方式。现在,看这个图表可能会对编码器输出如何传递给解码器产生混淆。假设N=5。我们是否将每个编码器层的输出传递给对应的解码器层?不。基本上,你只运行一次编码器,然后取该表示并将其传递给所有5个解码器层。
Add & Norm模块: 这基本上与下面相同(作者可能只是试图节省空间)
作者图片
其他所有内容已经讨论过。现在你有了一个完整的Transformer架构解释,从简单的加法和乘法操作构建而来,并且完全自包含!你知道每条线、每个加法、每个方框和单词在如何从头构建它们方面意味着什么。理论上,这些笔记包含了从头编码Transformer所需的内容。事实上,如果你感兴趣,这个仓库 为上面的GPT架构做了这一点。
构建预训练模型
此时,我们已经拥有设计和训练LLM所需的所有部件。让我们为英语语言模型组合这些部件:
- 首先,我们构建一个分词器,它可以对英语进行编码和解码,正如我们在子词分词器部分所讨论的。假设词汇量为32k
- 接下来,我们构建一个基于transformer架构的LLM。输出向量和嵌入矩阵都必须有32k个元素,等于词汇量的大小
- 现在我们收集英语语料库来训练模型。这是我们的训练数据。通常这个数据集的关键部分是整个互联网的爬取数据,比如Common Crawl
- 我们现在可以开始训练模型,通过要求模型预测下一个token,正如训练部分所讨论的。通过为下一个token定义损失函数(你已经从训练数据中知道)并推动网络预测该token来实现
- 对数十亿或数万亿个token进行训练将得到一组权重,这就是你的预训练模型。这是一个能够预测下一个token的模型,现在可以用来完成句子,甚至整篇文章
附录
矩阵乘法
我们在嵌入的上下文中介绍了向量和矩阵。矩阵有两个维度(行数和列数)。向量也可以看作是一个维度等于一的矩阵。两个矩阵的乘积定义为:
按下或点击查看完整大小的图片
作者图片
点表示乘法。现在让我们再看一下第一张图中蓝色和有机神经元的计算。如果我们把权重写成矩阵,输入写成向量,整个操作可以这样表示:
作者图片
如果权重矩阵称为"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]等。现在的问题是,当输入4个词的句子时,位置1的编码会不断变化,这给网络学习带来挑战。因此,我们需要一种方案,为每个位置分配唯一的向量,且数字不会爆炸。基本上,如果上下文长度是d(即我们可以输入到网络中预测下一个token/词的最大token/词数,参见“它是如何生成语言的?”部分的讨论),如果嵌入向量的长度是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(p),例如,位置1的编码向量的第5个元素是s5(1)=sin (1/10000(5/d))
为什么选择这种方法?通过改变10k的幂次,你会在p轴上改变正弦函数的振幅。如果你有10个不同振幅的正弦函数,那么在p值变化时,要过很长时间才会出现重复(即所有10个值都相同),这有助于我们获得唯一值。现在,论文实际上同时使用了正弦和余弦函数,编码形式为:si(p) = sin (p/10000(i/d)) 如果i为偶数,si(p) = cos(p/10000(i/d)) 如果i为奇数。