mirror of
https://github.com/skindhu/Build-A-Large-Language-Model-CN.git
synced 2026-07-29 15:20:24 +08:00
update with img path
This commit is contained in:
@@ -37,13 +37,17 @@
|
||||
|
||||
在本章中,我们将关注 LLM 架构中的重要组成部分,即注意力机制,如图 3.1 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.1.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.1.png" width="75%" />
|
||||
</div>
|
||||
|
||||
注意力机制是一个复杂的话题,因此我们将专门用一整章来讨论它。我们将注意力机制作为独立模块来研究,重点关注其内部的工作原理。在下一章中,我们将编写与自注意力机制相关的 LLM 的其他部分,以观察其实际运作并创建一个生成文本的模型。
|
||||
|
||||
本章中,我们将实现四种不同的注意力机制变体,如图 3.2 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.2.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.2.png" width="75%" />
|
||||
</div>
|
||||
|
||||
图 3.2 中展示的这些不同的注意力变体是逐步构建的,其目标是在本章末尾实现一个简单且高效的多头注意力机制,以便在下一章中可以将其整合到我们将编写的 LLM 架构中。
|
||||
|
||||
@@ -53,7 +57,9 @@
|
||||
|
||||
在深入了解自注意力机制之前(这是大语言模型的核心),让我们先探讨一下缺乏注意力机制的架构存在哪些问题(这些架构在大语言模型之前已经存在)。假设我们想要开发一个将一种语言翻译成另一种语言的翻译模型。如图 3.3 所示,我们无法简单地逐词翻译文本,因为源语言和目标语言的语法结构往往存在差异。
|
||||
|
||||
<img src="../Image/chapter3/figure3.3.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.3.png" width="75%" />
|
||||
</div>
|
||||
|
||||
为了解决逐词翻译的局限性,通常使用包含两个子模块的深度神经网络,即所谓的编码器(encoder)和解码器(decoder)。编码器的任务是先读取并处理整个文本,然后解码器生成翻译后的文本。
|
||||
|
||||
@@ -63,7 +69,9 @@
|
||||
|
||||
在编码器-解码器架构的 RNN 网络中,输入文本被输入到编码器中,编码器按顺序处理文本内容。在每个步骤中,编码器会更新其隐状态(即隐藏层的内部值),试图在最终的隐状态中捕捉整个输入句子的含义,如图 3.4 所示。随后,解码器使用该最终隐状态来开始逐词生成翻译句子。解码器在每一步也会更新其隐状态,用于携带生成下一个词所需的上下文信息。
|
||||
|
||||
<img src="../Image/chapter3/figure3.4.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.4.png" width="75%" />
|
||||
</div>
|
||||
|
||||
尽管我们不需要深入了解这些编码器-解码器架构的 RNN 的内部工作原理,但这里的关键思想在于,编码器部分将整个输入文本处理为一个隐藏状态(记忆单元)。解码器随后使用该隐藏状态生成输出。您可以将这个隐藏状态视为一个嵌入向量,这是我们在第 2 章中已讨论过的概念。
|
||||
|
||||
@@ -114,7 +122,9 @@
|
||||
|
||||
因此,研究人员在 2014 年为 RNN 开发了所谓的 Bahdanau 注意力机制(该机制以论文的第一作者命名)。该机制对编码器-解码器架构的 RNN 进行了改进,使得解码器在每个解码步骤可以选择性地访问输入序列的不同部分,如图 3.5 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.5.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.5.png" width="75%" />
|
||||
</div>
|
||||
|
||||
有趣的是,仅仅三年后,研究人员发现构建用于自然语言处理的深度神经网络并不需要 RNN 结构,随后提出了基于自注意力机制的原始 Transformer 架构(在第 1 章中讨论),其灵感来自 Bahdanau 提出的注意力机制。
|
||||
|
||||
@@ -122,7 +132,9 @@
|
||||
|
||||
本章将重点讲解并实现 GPT 类模型中使用的自注意力机制,如图 3.6 所示。在下一章中,我们将继续编码 LLM 的其它部分。
|
||||
|
||||
<img src="../Image/chapter3/figure3.6.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.6.png" width="75%" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -144,7 +156,9 @@
|
||||
|
||||
在本节中,我们实现了一个简化的自注意力机制版本,没有包含任何可训练的权重,如图 3.7 所示。本节的目标是先介绍自注意力机制中的一些关键概念,然后在 3.4 节引入可训练的权重。
|
||||
|
||||
<img src="../Image/chapter3/figure3.7.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.7.png" width="75%" />
|
||||
</div>
|
||||
|
||||
图 3.7 显示了一个输入序列,记作 x,由 T 个元素组成,表示为 x<sup>(1)</sup> 到 x<sup>(T)</sup>。该序列通常代表文本,例如一个句子,并且该文本已被转换为 token 嵌入(不记得嵌入概念的请回顾第 2 章)。
|
||||
|
||||
@@ -174,7 +188,9 @@ inputs = torch.tensor(
|
||||
|
||||
实现自注意力机制的第一步是计算中间值 **ω**,即注意力得分,如图 3.8 所示。(请注意,图 3.8 中展示的输入张量值是截断版的,例如,由于空间限制,0.87 被截断为 0.8。在此截断版中,单词 "journey" 和 "starts" 的嵌入向量可能会由于随机因素而看起来相似)。
|
||||
|
||||
<img src="../Image/chapter3/figure3.8.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.8.png" width="75%" />
|
||||
</div>
|
||||
|
||||
图 3.8 展示了如何计算查询 token 与每个输入 token 之间的中间注意力得分。我们通过计算查询 x<sup>(2)</sup> 与每个其他输入 token 的点积来确定这些得分:
|
||||
|
||||
@@ -251,7 +267,9 @@ tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])
|
||||
|
||||
如图 3.9 所示,接下来,我们对先前计算的每个注意力分数进行归一化。
|
||||
|
||||
<img src="../Image/chapter3/figure3.9.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.9.png" width="75%" />
|
||||
</div>
|
||||
|
||||
图3.9中所示的归一化的主要目的是使注意力权重之和为 1。这种归一化是一种有助于解释和保持LLM训练稳定性的惯例。以下是一种实现此归一化步骤的简单方法:
|
||||
|
||||
@@ -330,7 +348,9 @@ Sum: tensor(1.)
|
||||
|
||||
现在我们已经计算出了归一化的注意力权重,接下来可以执行图 3.10 所示的最后一步:通过将嵌入后的输入 token x<sup>(i)</sup> 与相应的注意力权重相乘,再将所得向量求和来计算上下文向量 z<sup>(2)</sup>。
|
||||
|
||||
<img src="../Image/chapter3/figure3.10.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.10.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.10 所示,上下文向量 z<sup>(2)</sup> 是所有输入向量的加权和。其计算方法为将每个输入向量与对应的注意力权重相乘后相加。
|
||||
|
||||
@@ -356,11 +376,15 @@ tensor([0.4419, 0.6515, 0.5683])
|
||||
|
||||
在前一节中,我们计算了第二个输入元素的注意力权重和上下文向量(如图 3.11 中的高亮行所示)。现在,我们将扩展该计算,以对所有输入计算注意力权重和上下文向量。
|
||||
|
||||
<img src="../Image/chapter3/figure3.11.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.11.png" width="75%" />
|
||||
</div>
|
||||
|
||||
我们沿用之前的三个步骤(如图 3.12 所示),只是对代码做了一些修改,用于计算所有的上下文向量,而不仅仅是第二个上下文向量 z<sup>(2)</sup>。
|
||||
|
||||
<img src="../Image/chapter3/figure3.12.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.12.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.12 所示,在第 1 步中,我们添加了一个额外的 for 循环,用于计算所有输入对之间的点积。
|
||||
|
||||
@@ -476,7 +500,9 @@ tensor([[0.4421, 0.5931, 0.5790],
|
||||
|
||||
在本节中,我们将实现一种在原始 Transformer 架构、GPT 模型以及大多数其他流行的大语言模型中使用的自注意力机制。这种自注意力机制也被称为缩放点积注意力。图 3.13 提供了一个概念框架,展示了这种自注意力机制如何应用在在大语言模型的架构设计中。
|
||||
|
||||
<img src="../Image/chapter3/figure3.13.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.13.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.13 所示,带有可训练权重的自注意力机制是基于之前简化自注意力机制的改进:我们希望计算某个特定输入元素的嵌入向量的加权和来作为上下文向量。您将看到,与我们在 3.3 节中编码的简化自注意力机制相比,只有细微的差别。
|
||||
|
||||
@@ -490,7 +516,9 @@ tensor([[0.4421, 0.5931, 0.5790],
|
||||
|
||||
我们通过引入三个可训练的权重矩阵:W<sub>q</sub>、W<sub>k</sub> 和 W<sub>v</sub> 来逐步实现自注意力机制。这三个矩阵用于将嵌入后的输入 token x<sup>(i)</sup> 映射为查询向量、键向量和值向量(如图 3.14 所示)。
|
||||
|
||||
<img src="../Image/chapter3/figure3.14.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.14.png" width="75%" />
|
||||
</div>
|
||||
|
||||
在 3.3.1 节中,我们将第二个输入元素 x<sup>(2)</sup> 定义为查询(query),通过计算简化的注意力权重来得到上下文向量 z<sup>(2)</sup>。随后,在第 3.3.2 节中,我们将这一过程推广到整个输入句子 "Your journey starts with one step",为这六个词的输入句子计算所有的上下文向量 z<sup>(1)</sup> 到 z<sup>(T)</sup>。
|
||||
同样地,为了便于说明,我们将先计算一个上下文向量 z<sup>(2)</sup>。接下来,我们将修改代码以计算所有的上下文向量。让我们从定义一些变量开始:
|
||||
@@ -562,7 +590,9 @@ values.shape: torch.Size([6, 2])
|
||||
|
||||
接下来的第二步是计算注意力得分(如图 3.15 所示)。
|
||||
|
||||
<img src="../Image/chapter3/figure3.15.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.15.png" width="75%" />
|
||||
</div>
|
||||
|
||||
首先,我们计算注意力得分ω<sub>22</sub> :
|
||||
|
||||
@@ -616,7 +646,9 @@ tensor([1.2705, 1.8524, 1.8111, 1.0795, 0.5577, 1.5440])
|
||||
|
||||
第三步是将注意力得分转换为注意力权重,如图 3.16 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.16.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.16.png" width="75%" />
|
||||
</div>
|
||||
|
||||
接下来,如图 3.16 所示,我们通过缩放注意力得分并使用前面提到的 softmax 函数来计算注意力权重。与之前的不同之处在于,现在我们通过将注意力得分除以`keys`嵌入维度的平方根来进行缩放(注意,取平方根在数学上等同于指数为 0.5 的运算)。
|
||||
|
||||
@@ -651,7 +683,9 @@ tensor([0.1500, 0.2264, 0.2199, 0.1311, 0.0906, 0.1820])
|
||||
|
||||
好了,我们只剩最后一步,也就是计算上下文向量,如图3.17所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.17.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.17.png" width="75%" />
|
||||
</div>
|
||||
|
||||
与第 3.3 节中我们通过输入向量的加权和来计算上下文向量相似,现在我们通过值向量的加权和来计算上下文向量。这里,注意力权重作为加权因子,用于衡量每个值向量的重要性。与第 3.3 节类似,我们可以通过矩阵乘法一步得到输出结果:
|
||||
|
||||
@@ -737,7 +771,9 @@ tensor([[0.2996, 0.8053],
|
||||
|
||||
图 3.18 概述了我们刚刚实现的自注意力机制。
|
||||
|
||||
<img src="../Image/chapter3/figure3.18.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.18.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图3.18所示,自注意力机制涉及可训练的权重矩阵 W<sub>q</sub>、W<sub>k</sub> 和 W<sub>v</sub>。这些矩阵将输入数据转换为查询、键和值,它们是自注意力机制的重要组成部分。随着训练过程中数据量的增加,模型会不断调整这些可训练的权重,在后续章节中我们会学习相关细节。
|
||||
|
||||
@@ -808,7 +844,9 @@ tensor([[-0.0739, 0.0713],
|
||||
|
||||
在 GPT 类大语言模型中,要实现这一点,我们需要对每个处理的 token 屏蔽其后续 token,即在输入文本中当前词之后的所有词,如图 3.19 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.19.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.19.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.19 所示,我们对注意力权重的对角线上方部分进行了掩码操作,并对未掩码的注意力权重进行归一化,使得每一行的注意力权重之和为 1。在下一节中,我们将用代码实现这个掩码和归一化过程。
|
||||
|
||||
@@ -818,7 +856,9 @@ tensor([[-0.0739, 0.0713],
|
||||
|
||||
在本节中,我们将编码实现因果注意力掩码。我们首先按照图 3.20 中总结的步骤开始。
|
||||
|
||||
<img src="../Image/chapter3/figure3.20.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.20.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图3.20总结,我们可以利用上一节的注意力得分和权重来实现因果注意力机制,以获得掩码后的注意力权重。
|
||||
|
||||
@@ -916,7 +956,9 @@ tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
|
||||
|
||||
尽管通过上文的方式我们已经完成了因果注意力的实现,但我们还可以利用 softmax 函数的数学特性,更高效地计算掩码后的注意力权重,减少计算步骤,具体实现如图 3.21 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.21.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.21.png" width="75%" />
|
||||
</div>
|
||||
|
||||
Softmax 函数将输入值转换为概率分布。当一行中存在负无穷值(-∞)时,Softmax 函数会将这些值视为零概率。(从数学上讲,这是因为 e<sup>−∞</sup> 接近于 0。)
|
||||
|
||||
@@ -971,7 +1013,9 @@ Dropout 在深度学习中是一种技术,即在训练过程中随机忽略一
|
||||
|
||||
在这里,我们会在计算完注意力权重之后应用 dropout 掩码(如图 3.22 所示),因为在实际应用中这是更为常见的做法。
|
||||
|
||||
<img src="../Image/chapter3/figure3.22.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.22.png" width="75%" />
|
||||
</div>
|
||||
|
||||
在以下代码示例中,我们使用了50%的 dropout 率,这意味着屏蔽掉一半的注意力权重。(在后续章节中训练 GPT 模型时,我们将使用更低的 dropout 率,比如 0.1 或 0.2)
|
||||
|
||||
@@ -1139,7 +1183,9 @@ context_vecs.shape: torch.Size([2, 6, 2])
|
||||
|
||||
图 3.23 提供了一个概念框架,总结了我们迄今为止完成的内容。
|
||||
|
||||
<img src="../Image/chapter3/figure3.23.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.23.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.23 所示,本节我们重点介绍了神经网络中的因果注意力的概念和实现。在下一节中,我们将进一步扩展这一概念,实现一个多头注意力模块,该模块可以并行实现多个因果注意力机制。
|
||||
|
||||
@@ -1161,7 +1207,9 @@ context_vecs.shape: torch.Size([2, 6, 2])
|
||||
|
||||
图3.24展示了多头注意力模块的结构,该模块由多个单头注意力模块组成,如图3.18所示,彼此堆叠在一起。
|
||||
|
||||
<img src="../Image/chapter3/figure3.24.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.24.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如前所述,多头注意力机制的核心思想是在并行运行多个注意力机制的过程中,对输入数据(如注意力机制中的 query、key 和 value 向量)使用不同的、可学习的线性投影。具体来说,就是将这些输入数据与权重矩阵相乘,得到不同的投影结果。
|
||||
|
||||
@@ -1183,7 +1231,9 @@ class MultiHeadAttentionWrapper(nn.Module):
|
||||
|
||||
例如,如果我们使用这个 MultiHeadAttentionWrapper 类,并通过设置 num_heads=2 使用两个注意力头,同时将 CausalAttention 的输出维度 d_out 设置为 2,那么生成的上下文向量将是 4 维的(d_out*num_heads=4),如图 3.25 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.25.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.25.png" width="75%" />
|
||||
</div>
|
||||
|
||||
为了通过一个具体的例子进一步说明图 3.25,我们可以按如下方式使用 MultiHeadAttentionWrapper 类(使用方式类似于之前的 CausalAttention 类):
|
||||
|
||||
@@ -1306,7 +1356,9 @@ class MultiHeadAttention(nn.Module):
|
||||
|
||||
从宏观层面上看,在之前的 MultiHeadAttentionWrapper 中,我们通过堆叠多个单头注意力层的方式来组合成一个多头注意力层。而 MultiHeadAttention 类采用了一种集成的方法:它从一个多头注意力层开始,并在内部将该层分解为各个独立的注意力头,如图 3.26 所示。
|
||||
|
||||
<img src="../Image/chapter3/figure3.26.png" width="75%" />
|
||||
<div style="text-align: center;">
|
||||
<img src="Image/chapter3/figure3.26.png" width="75%" />
|
||||
</div>
|
||||
|
||||
如图 3.26 所示,query、key 和 value 张量的拆分是通过张量的重塑和转置操作实现的,这些操作分别使用了 PyTorch 的 `.view` 和 `.transpose` 方法。首先,通过线性层对输入进行投影(分别生成 query、key 和 value),然后将其重塑为多个注意力头的形式。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user