add fourth chapter
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 792 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 745 KiB |
|
After Width: | Height: | Size: 642 KiB |
|
After Width: | Height: | Size: 924 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
|
@ -24,7 +24,7 @@ LLM(如GPT,即生成式预训练 Transformer,Generative Pretrained Transfo
|
|||
|
||||
<img src="../Image/chapter4/figure4.2.png" width="75%" />
|
||||
|
||||
如图 4.2 所示,我们已经在之前的章节中讲解过几个模块,如输入的分词和嵌入,以及掩码多头注意力模块。本章的重点是实现 GPT 模型的核心结构,包括其 Transformer 块。我们将在下一章对该模型进行训练,使其能够生成类人文本。
|
||||
如图 4.2 所示,我们已经在之前的章节中讲解过几个模块,如输入的分词和嵌入,以及掩码多头注意力模块。本章的重点是实现 GPT 模型的核心结构,包括其 Transformer 模块。我们将在下一章对该模型进行训练,使其能够生成类人文本。
|
||||
|
||||
在前几章中,为了简单起见,我们使用了较小的嵌入维度,确保概念和示例能够更方便地展示在一页内。而在本章中,我们将逐步扩展模型规模,达到小型 GPT-2 模型的大小(拥有1.24 亿参数量的最小版本)。该模型在 Radford 等人的论文《Language Models are Unsupervised Multitask Learners.》中有详细描述。请注意,尽管最初的报告中提到参数量为 1.17 亿,但后来更正为 1.24 亿。
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ GPT_CONFIG_124M = {
|
|||
+ `vocab_size`指的是第 2 章中 BPE 分词器使用的 50,257 个词汇的词表大小。
|
||||
+ `context_length`表示模型所能处理的最大输入 token 数(在第 2 章介绍位置嵌入时讨论过)。
|
||||
+ `emb_dim`表示嵌入维度,将每个 token 转换为 768 维的向量。
|
||||
+ `n_layers`指定模型中 Transformer 块的层数,后续章节将对此详解。
|
||||
+ `n_layers`指定模型中 Transformer 模块的层数,后续章节将对此详解。
|
||||
+ `drop_rate`表示 dropout 机制的强度(例如,0.1 表示丢弃 10% 的隐藏单元),用于防止过拟合,具体内容请回顾第 3 章。
|
||||
+ `qkv_bia 参数决定是否在多头注意力的查询、键和值的线性层中加入偏置向量。我们最初会禁用该选项,以遵循现代大语言模型的标准,之后在第 6 章加载 OpenAI 预训练的 GPT-2 权重时再重新考虑该设置。
|
||||
|
||||
|
|
@ -120,11 +120,11 @@ class DummyLayerNorm(nn.Module): #E
|
|||
#F 此处的参数仅用于模拟LayerNorm接口
|
||||
```
|
||||
|
||||
此代码中的 DummyGPTModel 类使用 PyTorch 内置的神经网络模块(nn.Module)定义了一个简化版的类 GPT 模型。该类包括 token 嵌入、位置嵌入、dropout、多个 Transformer 块(DummyTransformerBlock)、最终的层归一化(DummyLayerNorm)以及线性输出层(out_head)。模型配置通过 Python 字典传入,稍后将传入我们之前创建的 GPT_CONFIG_124M 字典。
|
||||
此代码中的 DummyGPTModel 类使用 PyTorch 内置的神经网络模块(nn.Module)定义了一个简化版的类 GPT 模型。该类包括 token 嵌入、位置嵌入、dropout、多个 Transformer 模块(DummyTransformerBlock)、最终的层归一化(DummyLayerNorm)以及线性输出层(out_head)。模型配置通过 Python 字典传入,稍后将传入我们之前创建的 GPT_CONFIG_124M 字典。
|
||||
|
||||
`forward`方法定义了数据在模型中的流动方式:计算输入索引的 token 嵌入和位置嵌入,应用 dropout,通过 transformer block 处理数据,应用归一化,最后通过线性输出层生成 logits。
|
||||
|
||||
上面的代码已经可以正常运行,不过需要先准备输入数据,在本节后面我们会看到运行效果。需要注意的是,目前代码中我们使用了 `DummyLayerNorm` 和 `DummyTransformerBlock` 作为 Transformer 块和层归一化的占位符,实际的实现会在后续部分详细介绍。
|
||||
上面的代码已经可以正常运行,不过需要先准备输入数据,在本节后面我们会看到运行效果。需要注意的是,目前代码中我们使用了 `DummyLayerNorm` 和 `DummyTransformerBlock` 作为 Transformer 模块和层归一化的占位符,实际的实现会在后续部分详细介绍。
|
||||
|
||||
接下来,我们将准备输入数据并初始化一个新的 GPT 模型,以展示它的用法。基于第二章实现的分词器,图 4.4 展示了数据在 GPT 模型中流入和流出的整体流程概览。
|
||||
|
||||
|
|
@ -380,7 +380,7 @@ Variance:
|
|||
|
||||
## 4.3 实现带有 GELU 激活函数的前馈神经网络
|
||||
|
||||
在本节中,我们将实现一个小型神经网络子模块,作为 LLM 架构中的 Transformer 块的一部分。我们首先实现 GELU 激活函数,它将在这个神经网络子模块中起着至关重要的作用。(关于在 PyTorch 中实现神经网络的更多信息,请参考附录 A 的 A.5 节:实现多层神经网络)
|
||||
在本节中,我们将实现一个小型神经网络子模块,作为 LLM 架构中的 Transformer 模块的一部分。我们首先实现 GELU 激活函数,它将在这个神经网络子模块中起着至关重要的作用。(关于在 PyTorch 中实现神经网络的更多信息,请参考附录 A 的 A.5 节:实现多层神经网络)
|
||||
|
||||
过去,ReLU 激活函数因其简单且有效,常用于各种神经网络架构中。但在大语言模型中,除了传统的 ReLU,还使用了其他几种激活函数,其中两个典型的例子是 GELU(高斯误差线性单元)和 SwiGLU(Swish 门控线性单元)。
|
||||
|
||||
|
|
@ -390,7 +390,384 @@ GELU 激活函数可以通过多种方式实现,其确切版本定义为 `GELU
|
|||
|
||||
$$ \text{GELU}(x) \approx 0.5 \cdot x \cdot\left(1+\tanh \left[\sqrt{(2 / \pi)} \cdot\left(x+0.044715 \cdot x^{3}\right]\right)\right. $$
|
||||
|
||||
|
||||
我们可以编码将该函数实现为一个 PyTorch 模块,如下所示:
|
||||
|
||||
```python
|
||||
# Listing 4.3 An implementation of the GELU activation function
|
||||
class GELU(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def forward(self, x):
|
||||
return 0.5 * x * (1 + torch.tanh(
|
||||
torch.sqrt(torch.tensor(2.0 / torch.pi)) *
|
||||
(x + 0.044715 * torch.pow(x, 3))
|
||||
))
|
||||
```
|
||||
|
||||
接下来,为了更直观的观察 GELU 函数的形状,并与 ReLU 函数进行对比,我们将这两个函数并排绘制:
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
gelu, relu = GELU(), nn.ReLU()
|
||||
|
||||
x = torch.linspace(-3, 3, 100) #A
|
||||
y_gelu, y_relu = gelu(x), relu(x)
|
||||
plt.figure(figsize=(8, 3))
|
||||
for i, (y, label) in enumerate(zip([y_gelu, y_relu], ["GELU", "ReLU"]), 1):
|
||||
plt.subplot(1, 2, i)
|
||||
plt.plot(x, y)
|
||||
plt.title(f"{label} activation function")
|
||||
plt.xlabel("x")
|
||||
plt.ylabel(f"{label}(x)")
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
#A 在 -3 到 3 的范围内生成 100 个样本数据点
|
||||
```
|
||||
|
||||
如图 4.8 所示,ReLU 是一个分段线性函数,输入为正时输出输入值本身,否则输出零。而 GELU 是一种平滑的非线性函数,它近似于 ReLU,但在负值上也具有非零梯度。
|
||||
|
||||
<img src="../Image/chapter4/figure4.8.png" width="75%" />
|
||||
|
||||
如图 4.8 所示,GELU 的平滑性使其在训练过程中具有更好的优化特性,能够对模型参数进行更细微的调整。相比之下,ReLU 在零点处有一个拐角,这在网络深度较大或结构复杂时可能会增加优化难度。此外,与 ReLU 不同,ReLU 对所有负输入的输出为零,而 GELU 对负值允许一个小的非零输出。这意味着在训练过程中,接收负输入的神经元也能对学习过程产生一定的贡献,尽管贡献程度不及正输入。
|
||||
|
||||
接下来让我们使用 GELU 激活函数实现一个小型的神经网络模块 FeedForward,该模块稍后会应用在 LLM 的 transformer 模块中:
|
||||
|
||||
```python
|
||||
# Listing 4.4 A feed forward neural network module
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.layers = nn.Sequential(
|
||||
nn.Linear(cfg["emb_dim"], 4 * cfg["emb_dim"]),
|
||||
GELU(),
|
||||
nn.Linear(4 * cfg["emb_dim"], cfg["emb_dim"]),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.layers(x)
|
||||
```
|
||||
|
||||
如代码所示,FeedForward 模块是一个小型神经网络,由两个线性层和一个 GELU 激活函数组成。在 1.24 亿参数的 GPT 模型中,该模块可以接收批量输入,每个输入 token 是一个 768 维的向量表示。这一嵌入维度大小通过 `GPT_CONFIG_124M` 配置字典中的 `GPT_CONFIG_124M["emb_dim"]` 参数指定。
|
||||
|
||||
图 4.9 展示了当我们输入数据后,这个前馈网络内部如何调整嵌入维度。
|
||||
|
||||
<img src="../Image/chapter4/figure4.9.png" width="75%" />
|
||||
|
||||
按照图 4.9 中的示例,我们初始化一个新的 FeedForward 模块,设置 token 嵌入维度为 768,并输入一个包含 2 个样本且每个样本有 3 个 token 的数据集:
|
||||
|
||||
```python
|
||||
ffn = FeedForward(GPT_CONFIG_124M)
|
||||
x = torch.rand(2, 3, 768) #A
|
||||
out = ffn(x)
|
||||
print(out.shape)
|
||||
|
||||
#A 创建一个 batch 大小为 2 的示例输入
|
||||
```
|
||||
|
||||
显然,输出张量的形状与输入张量相同:
|
||||
|
||||
```python
|
||||
torch.Size([2, 3, 768])
|
||||
```
|
||||
|
||||
我们在本节实现的 FeedForward 模块对模型能力的增强(主要体现在从数据中学习并泛化方面)起到了关键作用。尽管该模块的输入和输出维度相同,但在内部,它首先通过第一个线性层将嵌入维度扩展到一个更高维度的空间(如图 4.10 所示)。之后再接入非线性 GELU 激活,最后再通过第二个线性层变换回原始维度。这样的设计能够探索更丰富的表示空间。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> **个人思考:** 这段描述一笔带过了扩展和收缩嵌入维度为模型训练带来的好处,那到底该如何理解这样的设计能够探索更丰富的表示空间呢?
|
||||
>
|
||||
> 可以将扩展和收缩的过程类比为一种**数据解压缩与重新压缩**的机制:
|
||||
>
|
||||
> + **扩展(解压缩)**:假设我们有一段压缩的音乐文件(例如 MP3),里面包含了音频的基本信息。通过解压缩(扩展),我们把这个文件变成了一个更高质量的音频格式,允许我们看到(听到)更多的细节,比如乐器的细微声响和音调变化。
|
||||
> + **特征提取**:接着,我们可以在这个高质量的音频文件中应用各种音频处理算法(相当于非线性激活),分析出更多细节,比如每种乐器的声音特点。
|
||||
> + **收缩(压缩)**:最后,我们将音频再次压缩为一种更适合传输和存储的格式。虽然最终文件变小了,但这个文件已经包含了之前提取出的更多的声音细节。
|
||||
>
|
||||
> 将这种理解再应用到神经网络中,扩展后的高维空间可以让模型“看到”输入数据中更多的隐藏特征,提取出更丰富的信息。然后在收缩回低维度时,这些丰富的特征被整合到了输入的原始维度表示中,使模型最终的输出包含更多的上下文和信息。
|
||||
|
||||
<img src="../Image/chapter4/figure4.10.png" width="75%" />
|
||||
|
||||
|
||||
|
||||
此外,输入输出维度保持一致也有助于简化架构,方便堆叠多层(在后续的章节实现),无需调整各层维度,从而提升了模型的可扩展性。
|
||||
|
||||
如图4.11所示,我们目前已经实现了LLM的大部分组成模块。
|
||||
|
||||
<img src="../Image/chapter4/figure4.11.png" width="75%" />
|
||||
|
||||
下一节,我们将介绍“快捷连接”的概念,即在神经网络的不同层之间插入的连接结构,它对于提升深度神经网络架构的训练性能非常重要。
|
||||
|
||||
|
||||
|
||||
## 4.4 添加快捷连接
|
||||
|
||||
接下来,我们来讨论快捷连接(也称跳跃连接或残差连接)的概念。快捷连接最初是为计算机视觉中的深度网络(尤其是残差网络)提出的,用于缓解梯度消失问题。梯度消失是指在训练中指导权重更新的梯度在反向传播过程中逐渐减小,导致早期层(靠近输入端的网络层)难以有效训练,如图 4.12 所示。
|
||||
|
||||
<img src="../Image/chapter4/figure4.12.png" width="75%" />
|
||||
|
||||
如图 4.12 所示,快捷连接通过跳过一层或多层,为梯度提供一条更短的流动路径,这是通过将某层的输出加到后续层的输出上来实现的。因此,这种连接方式也称为跳跃连接。在反向传播中,快捷连接对保持梯度流动至关重要。
|
||||
|
||||
在以下代码示例中,我们将实现图 4.12 中所示的神经网络,以展示如何在前向传播方法中添加快捷连接:
|
||||
|
||||
```python
|
||||
# Listing 4.5 A neural network to illustrate shortcut connections
|
||||
class ExampleDeepNeuralNetwork(nn.Module):
|
||||
def __init__(self, layer_sizes, use_shortcut):
|
||||
super().__init__()
|
||||
self.use_shortcut = use_shortcut
|
||||
self.layers = nn.ModuleList([
|
||||
# Implement 5 layers
|
||||
nn.Sequential(nn.Linear(layer_sizes[0], layer_sizes[1]), GELU()),
|
||||
nn.Sequential(nn.Linear(layer_sizes[1], layer_sizes[2]), GELU()),
|
||||
nn.Sequential(nn.Linear(layer_sizes[2], layer_sizes[3]), GELU()),
|
||||
nn.Sequential(nn.Linear(layer_sizes[3], layer_sizes[4]), GELU()),
|
||||
nn.Sequential(nn.Linear(layer_sizes[4], layer_sizes[5]), GELU())
|
||||
])
|
||||
|
||||
def forward(self, x):
|
||||
for layer in self.layers:
|
||||
# Compute the output of the current layer
|
||||
layer_output = layer(x)
|
||||
# Check if shortcut can be applied
|
||||
if self.use_shortcut and x.shape == layer_output.shape:
|
||||
x = x + layer_output
|
||||
else:
|
||||
x = layer_output
|
||||
return x
|
||||
```
|
||||
|
||||
以上代码实现了一个 5 层的深度神经网络,每层包括一个线性层和 GELU 激活函数。在前向传播中,我们将输入逐层传递,同时如果 `self.use_shortcut` 属性设置为 True,则会添加图 4.12 所示的快捷连接。
|
||||
|
||||
我们将使用以下代码初始化一个没有快捷连接的神经网络,其中每一层都被初始化为接受 3 个输入值并返回 3 个输出值。最后一层则返回一个单一的输出值:
|
||||
|
||||
```python
|
||||
layer_sizes = [3, 3, 3, 3, 3, 1]
|
||||
sample_input = torch.tensor([[1., 0., -1.]])
|
||||
torch.manual_seed(123) # specify random seed for the initial weights for reproducibility
|
||||
model_without_shortcut = ExampleDeepNeuralNetwork(
|
||||
layer_sizes, use_shortcut=False
|
||||
)
|
||||
```
|
||||
|
||||
接下来,我们实现一个用于在模型反向传播过程中计算梯度的函数:
|
||||
|
||||
```python
|
||||
def print_gradients(model, x):
|
||||
# Forward pass
|
||||
output = model(x)
|
||||
target = torch.tensor([[0.]])
|
||||
|
||||
# Calculate loss based on how close the target
|
||||
# and output are
|
||||
loss = nn.MSELoss()
|
||||
loss = loss(output, target)
|
||||
|
||||
# Backward pass to calculate the gradients
|
||||
loss.backward()
|
||||
|
||||
for name, param in model.named_parameters():
|
||||
if 'weight' in name:
|
||||
# Print the mean absolute gradient of the weights
|
||||
print(f"{name} has gradient mean of {param.grad.abs().mean().item()}")
|
||||
```
|
||||
|
||||
上述代码中,我们定义了一个损失函数,用来计算模型输出与用户指定目标(此处为简单起见,目标值设为 0)之间的差距。接着,当调用 `loss.backward()` 时,PyTorch 会为模型的每一层计算损失的梯度。我们可以通过 `model.named_parameters()` 遍历权重参数。假设某层的权重参数是一个 3×3 的矩阵,那么这一层会有 3×3 的梯度值。然后我们打印出这 3×3 梯度值的绝对均值,以便得到每层的单一梯度值,从而更容易比较各层之间的梯度大小。
|
||||
|
||||
简而言之,`.backward()` 是 PyTorch 中一个便捷的方法,用于自动计算损失梯度,这在模型训练中是必要的。它让我们无需亲自实现梯度计算的数学过程,从而大大简化了深度神经网络的开发过程。如果您对梯度和神经网络训练不熟悉,建议参考附录 A 中的 A.4 节:**轻松实现自动微分** 和 A.7 节:**典型的训练循环**。
|
||||
|
||||
现在让我们使用 `print_gradients` 函数,并将其应用到没有跳跃连接的模型上:
|
||||
|
||||
```python
|
||||
print_gradients(model_without_shortcut, sample_input)
|
||||
```
|
||||
|
||||
输出如下:
|
||||
|
||||
```python
|
||||
layers.0.0.weight has gradient mean of 0.00020173587836325169
|
||||
layers.1.0.weight has gradient mean of 0.0001201116101583466
|
||||
layers.2.0.weight has gradient mean of 0.0007152041653171182
|
||||
layers.3.0.weight has gradient mean of 0.001398873864673078
|
||||
layers.4.0.weight has gradient mean of 0.005049646366387606
|
||||
```
|
||||
|
||||
从 `print_gradients` 函数的输出可以看出,梯度在从最后一层(layers.4)到第一层(layers.0)时逐渐减小,这种现象称为梯度消失问题。
|
||||
|
||||
我们再来创建一个带有跳跃连接的模型,看看它的表现如何:
|
||||
|
||||
```python
|
||||
torch.manual_seed(123)
|
||||
model_with_shortcut = ExampleDeepNeuralNetwork(
|
||||
layer_sizes, use_shortcut=True
|
||||
)
|
||||
print_gradients(model_with_shortcut, sample_input)
|
||||
```
|
||||
|
||||
输出如下:
|
||||
|
||||
```python
|
||||
layers.0.0.weight has gradient mean of 0.22169792652130127
|
||||
layers.1.0.weight has gradient mean of 0.20694105327129364
|
||||
layers.2.0.weight has gradient mean of 0.32896995544433594
|
||||
layers.3.0.weight has gradient mean of 0.2665732502937317
|
||||
layers.4.0.weight has gradient mean of 1.3258541822433472
|
||||
```
|
||||
|
||||
从输出结果可以看到,最后一层(layers.4)的梯度依然比其他层更大。然而,随着接近第一层(layers.0),梯度值逐渐趋于稳定,并未缩小到几乎消失的程度。
|
||||
|
||||
总之,快捷连接在解决深层神经网络中的梯度消失问题方面具有重要作用。作为 LLM 的核心构建单元,快捷连接可以确保各层之间的梯度稳定流动,从而帮助 GPT 模型更有效的训练(下一章实现训练过程)。
|
||||
|
||||
在介绍了快捷连接后,我们将在下一节把之前讲解的所有概念(层归一化、GELU 激活、前馈网络模块和快捷连接)整合进一个 Transformer 模块中,这是构建 GPT 架构所需的最后一个模块。
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> **个人思考:** 看到这里,不知各位读者是否真正理解了快捷连接在深度神经网络中的作用,这里其实涉及到快捷连接的两个重要的作用:
|
||||
>
|
||||
> + **保持信息(或者说是特征)流畅传递**
|
||||
> + **缓解梯度消失问题**
|
||||
>
|
||||
> 让我们逐一解读,LLM 中的每个Transformer 模块通常包含两个重要组件(**可以先阅读完4.5节,再回头看这里的解读**):
|
||||
>
|
||||
> 1. **自注意力层(Self-Attention Layer)**:计算每个 token 与其他 token 的关联,帮助模型理解上下文。
|
||||
> 2. **前馈网络(Feed Forward Network)**:对每个 token 的嵌入(embedding)进行进一步的非线性转换,使模型能够提取更复杂的特征。
|
||||
>
|
||||
> 这两个部分都在**层归一化(Layer Normalization)**和**快捷连接(Shortcut Connections)**的配合下工作。
|
||||
>
|
||||
> 假设我们正在训练一个 LLM ,并希望它理解下面的句子:
|
||||
>
|
||||
> `The cat sat on the mat because it was tired.`
|
||||
>
|
||||
> 模型需要通过多个 Transformer 层来逐层处理该句子,使得每个词(token)在上下文中能被理解。为了达到这一目的,每个 token 的嵌入会在多层中进行注意力计算和前馈网络处理。
|
||||
>
|
||||
> 1. **没有快捷连接时的情况**
|
||||
>
|
||||
> 如果没有快捷连接,那么每个 Transformer 层的输出就直接传递到下一个层。这种情况下,网络中的信息流大致如下:
|
||||
>
|
||||
> + **层间信息传递的局限**:假设当前层的注意力机制计算出了“it”和“cat”之间的关系,如果前馈网络进一步转换了这个信息,那么下一层就只能基于该层的输出,可能丢失一些最初的语义信息。
|
||||
> + **梯度消失**:在训练过程中,梯度从输出层逐层向回传播。如果层数过多,梯度会逐渐变小(即“梯度消失”),从而导致模型难以有效更新前面层的参数。
|
||||
>
|
||||
> 这种情况下,由于信息不能直接流动到更深层次的网络,可能会导致模型难以有效捕捉到前层的一些原始信息。
|
||||
>
|
||||
> 2. **加入快捷连接后的情况**
|
||||
>
|
||||
> 加入快捷连接后,信息可以在层与层之间**直接跳跃**。例如,假设在第 n 层,我们有输入 X<sub>n</sub>,经过注意力和前馈网络得到输出F(X<sub>n</sub>)。加入快捷连接后,这一层的输出可以表示为:
|
||||
>
|
||||
> $$ \text { 输出 }=X_{n}+F\left(X_{n}\right) $$
|
||||
>
|
||||
> 这意味着第 n 层的输出不仅包含了这一层的新信息 F(X<sub>n</sub>,还保留了原始输入 X<sub>n </sub>的信息。下面是这样做的好处:
|
||||
>
|
||||
> - **保留原始信息**
|
||||
>
|
||||
> 快捷连接让输入的原始信息直接传递到后续层,避免了在多层处理过程中丢失重要信息。例如,“it” 和 “cat” 之间的关系在较浅层中被捕捉到后,即使后面的层有进一步的处理,模型依然能够从快捷连接中获得最初的上下文信息。
|
||||
>
|
||||
> - **减轻梯度消失**
|
||||
>
|
||||
> 假设我们有一个简单的三层网络,第三层的输出 O 是整个网络的输出。我们从损失函数 LLL 开始计算梯度:
|
||||
>
|
||||
> - 根据反向传播的原理,**无快捷连接**时,梯度必须逐层传递,如下:
|
||||
>
|
||||
> $$ \frac{\partial L}{\partial X_{1}}=\frac{\partial L}{\partial X_{3}} \cdot \frac{\partial X_{3}}{\partial X_{2}} \cdot \frac{\partial X_{2}}{\partial X_{1}} $$
|
||||
>
|
||||
> 这里,如果某一层的梯度值很小,那么梯度会被逐层缩小,导致梯度消失。
|
||||
>
|
||||
> - **有快捷连接**时,假设我们在每一层之间都添加快捷连接,梯度的传播路径就多了一条直接路径:
|
||||
>
|
||||
> $$ \frac{\partial L}{\partial X_{1}}=\frac{\partial L}{\partial\left(X_{1}+F\left(X_{1}\right)\right)} \cdot\left(1+\frac{\partial F\left(X_{1}\right)}{\partial X_{1}}\right) $$
|
||||
>
|
||||
> 这样,即使 $` \frac{\partial F\left(X_{1}\right)}{\partial X_{1}} `$ 很小,梯度依然可以通过 111 这条路径直接传递到更前面的层。
|
||||
|
||||
|
||||
|
||||
## 4.5 在 Transformer 模块中连接注意力层与线性层
|
||||
|
||||
本节我们将实现 Transformer 模块,它是 GPT 和其他大语言模型架构的基本模块。这个在 124M 参数的 GPT-2 架构中重复了十几次的模块,结合了多头注意力、层归一化、dropout、前馈层和 GELU 激活等多个概念,详见图 4.13。在下一节中,我们将把这个 Transformer 模块连接到 GPT 架构的其余部分。
|
||||
|
||||
<img src="../Image/chapter4/figure4.13.png" width="75%" />
|
||||
|
||||
如图 4.13 所示,Transformer 模块结合了多个组件,包括第 3 章中的带掩码的多头注意力模块以及我们在 4.3 节中实现的前馈网络模块。
|
||||
|
||||
当 Transformer 模块处理输入序列时,序列中的每个元素(如单词或子词 token)都会被表示为固定大小的向量(如图 4.13 中为 768 维)。Transformer 模块中的操作,包括多头注意力和前馈层,旨在以维度不变的方式对这些向量进行转换。
|
||||
|
||||
之所以这样设计,是因为多头注意力模块中的自注意力机制用于识别和分析输入序列中各元素之间的关系,而前馈神经网络则对输入序列中每个位置的数据单独进行修改。这种组合不仅能够更细致地理解和处理输入信息,还增强了模型处理复杂数据模式的整体能力。
|
||||
|
||||
可以通过以下代码实现 Transformer 模块:
|
||||
|
||||
```python
|
||||
# Listing 4.6 The transformer block component of GPT
|
||||
from previous_chapters import MultiHeadAttention
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, cfg):
|
||||
super().__init__()
|
||||
self.att = MultiHeadAttention(
|
||||
d_in=cfg["emb_dim"],
|
||||
d_out=cfg["emb_dim"],
|
||||
context_length=cfg["context_length"],
|
||||
num_heads=cfg["n_heads"],
|
||||
dropout=cfg["drop_rate"],
|
||||
qkv_bias=cfg["qkv_bias"])
|
||||
self.ff = FeedForward(cfg)
|
||||
self.norm1 = LayerNorm(cfg["emb_dim"])
|
||||
self.norm2 = LayerNorm(cfg["emb_dim"])
|
||||
self.drop_shortcut = nn.Dropout(cfg["drop_rate"])
|
||||
|
||||
def forward(self, x):
|
||||
shortcut = x #A
|
||||
x = self.norm1(x)
|
||||
x = self.att(x)
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut # Add the original input back
|
||||
shortcut = x #B
|
||||
x = self.norm2(x)
|
||||
x = self.ff(x)
|
||||
x = self.drop_shortcut(x)
|
||||
x = x + shortcut #C
|
||||
return x
|
||||
|
||||
|
||||
#A 注意力模块中的快捷连接
|
||||
#B 前馈网络模块中的快捷链接
|
||||
#C 将原始输入加回到输出中
|
||||
```
|
||||
|
||||
给定的代码在 PyTorch 中定义了一个 TransformerBlock 类,包含多头注意力机制(MultiHeadAttention)和前馈网络(FeedForward),并根据提供的配置字典(cfg)进行配置,例如前文定义的 GPT_CONFIG_124M。
|
||||
|
||||
层归一化(LayerNorm)在这两个组件(即自注意力和前馈网络)之前应用,而 dropout 则在它们之后应用,用于正则化模型并防止过拟合。这种方式也称为前置层归一化(Pre-LayerNorm)。在早期的架构中(如原始的 Transformer 模型),一般将层归一化应用在自注意力和前馈网络之后,这被称为后置层归一化(Post-LayerNorm),这种方式通常会导致较差的训练效果。
|
||||
|
||||
该类还实现了前向传播(`forward方法`),其中每个组件后面都设有一个快捷连接,将对应组件的输入添加到输出中。这一关键特性有助于在训练过程中促进梯度流动,从而提升 LLM 的学习能力,相关内容详见第 4.4 节。
|
||||
|
||||
现在使用我们之前定义的 `GPT_CONFIG_124M` 配置字典,实例化一个 Transformer 模块,并向其中输入一些示例数据:
|
||||
|
||||
```python
|
||||
torch.manual_seed(123)
|
||||
x = torch.rand(2, 4, 768) #A
|
||||
block = TransformerBlock(GPT_CONFIG_124M)
|
||||
output = block(x)
|
||||
|
||||
print("Input shape:", x.shape)
|
||||
print("Output shape:", output.shape)
|
||||
|
||||
#A 建一个形状为 [batch_size, num_tokens, emb_dim] 的输入张量
|
||||
```
|
||||
|
||||
输出如下:
|
||||
|
||||
```python
|
||||
Input shape: torch.Size([2, 4, 768])
|
||||
Output shape: torch.Size([2, 4, 768])
|
||||
```
|
||||
|
||||
从代码输出可以看出,Transformer 模块的输出维度与输入维度保持一致,这说明 Transformer 架构在整个网络中处理序列数据时不会改变数据的形状。
|
||||
|
||||
Transformer 模块结构中保持数据形状不变并非偶然,而是其设计的一个关键特性。这种设计使 Transformer 擅长处理各种序列到序列任务,因为每个输出向量直接对应一个输入向量,保持一一对应关系。然而,输出向量是包含整个输入序列上下文信息的“上下文向量”。也就是说,尽管序列的物理维度(长度和特征维度)在经过 Transformer 模块时保持不变,但每个输出向量的内容会被重新编码,融合整个输入序列的上下文信息。
|
||||
|
||||
在本节完成了 Transformer 模块的实现后,我们已经具备了实现 GPT 架构所需的全部基础模块(如图 4.14 所示)。
|
||||
|
||||
<img src="../Image/chapter4/figure4.14.png" width="75%" />
|
||||
|
||||
如图 4.14 所示,Transformer 模块由层归一化、带有 GELU 激活函数的前馈网络和快捷连接组成,这些内容在本章前面已经讨论过。正如我们将在接下来的章节中看到的,这个 Transformer 模块将构成我们要实现的 GPT 架构的核心部分。
|
||||
|
||||
|
||||
|
||||
|
|
|
|||