全文约1.7w字,分为上中下篇,本文(下篇)建议阅读8分钟
本文通过通俗易懂的初中数学知识来辅助理解大语言模型的工作机制。
8. 归一化指数函数
我们在第一篇笔记中简要讨论了归一化函数。归一化函数试图解决一个问题:在输出解释中,神经元数量与我们希望网络中的选项一样多。我们说过,将把网络的选择解释为最高值的神经元。计算损失作为网络提供的值与我们想要的理想值之间的差值。但我们想要的理想价值是什么?我们在叶子/花示例中将其设置为 0.8。但为什么是 0.8 呢?为什么没有 5 个、10 个或 1000 万个?对于该训练示例,越高越好。理想情况下,我们希望那里有无限!现在,这将使问题变得棘手 — 所有的损失都是无限的,我们通过移动参数(记住“梯度下降”)来最小化损失的计划失败了。我们该怎么处理呢?
我们可以做的一件简单的事情是限制我们想要的值。假设在0 和 1 之间,这将使所有损失都是有限的,但现在我们遇到了当网络超调时会发生什么的问题。假设它在一种情况下为 (叶,花) 输出 (5,1),在另一种情况下输出 (0,1)。第一个案例做出了正确的选择,但损失更严重。现在我们需要一种方法来转换 (0,1) 范围内最后一层的输出,以便保持顺序。我们可以使用任何函数(数学中的“函数”只是一个数字到另一个数字的映射 ——一个数字进去,另一个数字出来——它是基于给定输入的输出的规则)来完成工作。一个可能的选项是 logistic 函数(见下图),它将所有数字映射到 (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 机会是个好主意。
所以我们需要一个好的规则来获取所有这些数字并将它们转换为机会。这就是归一化的作用。它是上述logistic 函数的泛化,但具有附加功能。如果你给它 10 个任意的数字 — 它会给你 10 个输出,每个输出都在 0 到 1 之间,重要的是,所有 10 加起来都是 1,这样我们就可以将它们解释为机会。会发现归一化几乎是每个语言模型中的最后一层。
9. 残差连接
随着各部分的进展,我们慢慢改变了网络的可视化。我们现在使用盒/块来表示某些概念。此表示法可用于表示一个特别有用的残差连接概念。让我们看看残差连接与自注意力块的组合:
残差连接。图片来自作者
请注意,我们将“输入” 和 “输出” 作为框来简化操作,但它们基本上仍然只是神经元/数字的集合,与上面显示的相同。
那么这是怎么回事呢?我们基本上是获取自注意块的输出,在将其传递给下一个块之前,我们将向其添加原始输入。首先要注意的是,这将要求自注意块输出的维度现在必须与输入的维度相同。这不是问题,因为正如我们所指出的,自注意输出是由用户决定的。但为什么要这样做呢?我们不会在这里详细介绍所有细节,但关键是,随着网络越来越深(输入和输出之间的层数增加),训练它们变得越来越困难。残差连接已被证明有助于应对这些培训挑战。
10. 层归一化
层归一化是一个相当简单的层,它通过减去平均值并将其除以标准差(可能更多一点,如下所示)来获取进入层的数据并对其进行归一化。例如,如果我们在输入后立即应用层归一化,它将获取输入层中的所有神经元,然后计算两个统计数据:它们的平均值和标准差。假设平均值是M,标准差是 S,那么层正态化所做的是取这些神经元中的每一个,并将其替换为 (x-M)/S,其中 x 表示任何给定神经元的原始值。
这有什么帮助呢?它基本上稳定了输入向量,并有助于训练深度网络。一个问题是,通过规范化输入,我们是否从中删除了一些有用的信息,这些信息可能有助于了解我们的目标?为了解决这个问题,层归一化图层具有尺度和偏差参数。基本上,对于每个神经元,只需将其与标量相乘,然后为其添加偏差。这些标量值和偏差值是可以训练的参数。这允许网络学习一些可能对预测有价值的变体。由于这些是唯一的参数,层归一块没有很多需要训练的参数。整个事情看起来是这样的:
层归一化。图片来自作者
尺度和偏差是可训练的参数。你可以看到层归一化 是一个相对简单的块,其中每个数字只逐个点(在初始 mean 和 std 计算之后)进行操作。让我们想起了激活层(例如 RELU),关键区别在于这里我们有一些可训练的参数(尽管由于简单的逐点操作,比其他层少得多)。
标准差是衡量值分布程度的统计量度,例如,如果值都相同,会说标准差为零。一般来说,如果每个值确实与这些相同值的平均值相去甚远,将有一个很高的标准差。计算一组数字a1、a2、a3 的标准差的公式。(比如 N 个数字)是这样的:从每个数字中减去(这些数字的)平均值,然后对 N 个数字中每个数字的答案求平方。将所有这些数字相加,然后除以 N。现在取答案的平方根。
初学者注意事项:经验丰富的机器学习 专业人员会注意到,这里没有讨论批处理规范。事实上,我们甚至没有在本文中介绍批处理的概念。在大多数情况下,我相信 批处理是另一种与理解核心概念无关的训练加速器(可能除了批处理规范,我们在这里不需要)。
11. 随机失活
随机失活是一个简单但有效的方法,可以避免模型过拟合。过拟合是当你用训练集训练模型的时候,它在数据集上表现很好但是并不能很好地泛化到模型没见过的例子当中。帮助我们避免过拟合的技术叫做“正则化技术”,随机失活就是其中之一。
如果你训练一个模型,它可能在数据集上出错,并且/或者以特定方式发生过拟合。如果你训练另一个模型,它可能会以一种不同的方式做同样的事。如果你训练一系列这种模型并将输出平均化呢?这些通常叫做“集成模型”因为他们通过一系列集成模型整合输出来预测最终的输出结果,集成模型通常比单个模型表现更好。
在神经网络中,可以执行相同的操作。可以构建多个(略有不同的)模型,然后整合它们的输出以获得更好的模型。但是,这在计算上可能代价很高。随机失活是一种不能构建集成模型的技术,但确实捕捉到了这个概念的一些精髓。
这个概念很简单,就是在训练过程中插入一个随机失活层,对连接插入随机失活层的直接神经元进行一定比例的随机删除。考虑我们的初始网络,在输入层和中间层之间插入一个随机失活层,随机失活率为50%,如下所示:
图片来自作者
现在,这迫使网络进行大量冗余的训练。从本质上讲,这是正在同时训练许多不同的模型,但它们的权重相同。
现在为了进行推理,我们可以遵循与集成模型相同的方法。我们可以使用随机失活进行多个预测,然后将它们组合在一起。但是,既然这是计算密集型的,而且我们的模型享有共同权重,为什么我们不直接使用所有权重进行预测(这样,我们就同时使用所有权重,而不是一次使用50% 的权重)。这会为我们提供一些集成将提供内容的近似值。
但有一个问题:使用50% 权重训练的模型在中间神经元中的数字与使用所有权重的模型在中间神经元中的数字截然不同。我们想要的是更多的集成风格平均。如何才能做到?一个简单的方法是简单地将所有权重乘以 0.5,因为我们现在使用的权重是原来的两倍。这就是随机失活在推理过程中所做的。它将使用具有所有权重的完整网络,并简单地将权重乘以(1- p),其中 p 是删除概率。这已被证明是一种相当有效的正则化技术。
12. 多头注意力
这是transformer 架构中的关键块。我们已经看到了什么是注意力块。请记住,注意力块的输出是由用户决定的,它是 v 的长度。多注意力头是并行运行注意力头的数量(它们都采用相同的输入)。然后我们获取它们的所有输出并简单地将它们连接起来。它看起来像这样:
多头注意。图片来自作者
请记住,从v1 -> v1h1 的箭头是线性层 — 每个箭头上都有一个转换的矩阵。我只是没有给它们看以避免混乱。
我们为每个头生成相同的 键、查询 和 值。但是,在使用这些 k,q,v 值之前,我们在此基础上应用线性变换(分别应用于每个 k,q,v 和每个头部)。这个额外的层在自注意力中不存在。
附带说明一下,对我来说,这是一种创建多头注意力的非常规方式。例如,为什么不为每个头创建单独的Wk、Wq、Wv 矩阵,而是添加新层并共享这些权重。如果你知道,请告诉我——我真的不知道。
13. 位置编码和嵌入
我们在自注意部分简要讨论了使用位置编码的动机。虽然图片显示了位置编码,但使用位置嵌入比使用编码更常见。因此,我们在这里讨论一种常见的位置嵌入,但附录还涵盖了原始论文中使用的位置编码。位置嵌入与任何其他嵌入没有什么不同,只是我们将嵌入数字1、2、3 等,而不是嵌入单词词汇。所以这个嵌入是一个与单词嵌入长度相同的矩阵,每列对应一个数字。这就是它的全部内容。
14. GPT架构
我们来谈谈GPT 架构。这是大多数 GPT 模型中使用的(有变化)。使用 流程图表示,这是架构在高层次上的样子:
GPT转换器。图片来自作者
在这一点上,除了“GPT transformer块” 之外,所有其他块都已经被非常详细地讨论了。此处的 + 号仅表示两个向量相加(这意味着两个嵌入的大小必须相同)。让我们看看这个 GPT transformer块:
差不多就是这样。它在这里被称为“transformer”,因为它源自 transformer 并且是一种 transformer 类型——这是我们将在下一节中介绍的一种架构。这不会影响理解,因为我们之前已经介绍了此处显示的所有构建块。让我们回顾一下到目前为止我们介绍的构建这个 GPT 架构的所有内容:
随着时间的推移,随着公司已经建立起强大的现代大语言模型,对此进行了修改,但基本保持不变。
现在,这个GPT transformer 实际上是介绍transformer架构的原始 transformer 论文中所谓的“解码器”。让我们来看看。
15. Transformer架构
这是最近推动语言模型功能快速加速的关键创新之一。Transformers不仅提高了预测准确性,而且比以前的模型更容易/更高效(训练),允许更大的模型尺度。这就是上述 GPT 架构的基础。
如果你看一下GPT 架构,你会发现它非常适合生成序列中的下一个单词。它基本上遵循我们在 第 1 部分 中讨论的相同逻辑。从几个单词开始,然后继续一次生成一个单词。但是,如果想进行翻译怎么办。如果你有一个德语句子(例如 “Wo wohnst du?” = “Where do you live?”),并且你想把它翻译成英语,该怎么办。我们将如何训练模型来做到这一点?
好吧,我们需要做的第一件事是想办法输入德语单词。这意味着我们必须扩展我们的嵌入内容,以包括德语和英语。现在,我想这是一种简单的输入信息的方法。我们为什么不把德语句子连在到目前为止生成的英语的开头,然后把它提供给上下文呢。为了简化模型,我们可以添加一个分隔符。每个步骤将如下所示:
图片来自作者
这将奏效,但还有改进的余地:
Transformer最初是为这项任务创建的,由一个 “编码器” 和一个 “解码器” 组成 — 它们基本上是两个独立的块。一个块简单地接受德语句子并给出一个中间表示形式(同样,基本上是一堆数字)——这被称为编码器。
第二个块生成单词(到目前为止我们已经看到了很多这样的情况)。唯一的区别是,除了向它提供到目前为止生成的单词外,我们还向它提供编码的德语(来自编码器块)句子。所以当它生成语言时,它的上下文基本上是到目前为止生成的所有单词,加上德语。此块称为解码器。
这些编码器和解码器中的每一个都由几个块组成,特别是夹在其他层之间的注意力块。让我们看一下论文“Attention is all you need” 中的transformer插图,并尝试理解它:
图片来自Vaswani et al. (2017)
左侧的垂直块集称为“编码器”,右侧的块称为 “解码器”。让我们回顾一下并了解我们之前没有介绍过的任何内容:
回顾如何阅读图表:这里的每个框都是一个块,它以神经元的形式接收一些输入,并输出一组神经元,然后可以由下一个块处理或由我们解释。箭头显示块的输出去向。如你所见,我们通常会获取一个区块的输出,并将其作为输入到多个区块中。让我们在这里逐一了解一下:
Add & Norm块:这基本上与下面的相同(猜作者只是为了节省空间)
图片来自作者
其他一切都已经讨论过了。现在,你已经对transformer 架构有了完整的解释,该架构由简单的求和、乘操作构建而成,并且是完全独立的!你知道每一行、每一总和、每个框和单词在如何从头开始构建它们方面意味着什么。从理论上讲,这些说明包含了从头开始编写 transformer 代码所需的内容。事实上,如果您有兴趣,此库(https://github.com/karpathy/nanoGPT) 会为上面的 GPT 架构执行此操作。
附录
矩阵乘法
我们在嵌入的上下文中介绍了上面的向量和矩阵。矩阵有两个维度(数字或行和列)。向量也可以被认为是一个矩阵,其中1 个维度等于 1。两个矩阵的乘积定义为:
图片来自作者
点表示乘法。现在让我们再看一下第一张图片中蓝色和橘色神经元的计算。如果我们将权重写成矩阵,把输入写成向量,我们可以按以下方式写出整个运算:
图片来自作者
如果权重矩阵称为“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” 论文中使用的方法如下:
为什么选择这种方法?通过更改10000的幂级,可以在 p 轴上查看改变正弦函数的振幅。如果你有 10 个不同的振幅的正弦函数,要花很长时间才能得到 p 值变化的周期(即所有 10 个值都相同)。这有助于赋予我们独特的值。现在,论文同时使用正弦和余弦函数,编码形式为:如果 i 为偶数,则为 si(p) = sin (p/10000(i/d)),如果 i 为奇数,则为 si(p) = cos(p/10000(i/d))。