add third chapter

This commit is contained in:
skindhu 2024-11-02 14:49:21 +08:00
parent 3623d637aa
commit fb39185680
9 changed files with 405 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

BIN
Image/image3.22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 KiB

View File

@ -3,8 +3,11 @@
本章涵盖以下内容:
+ **探讨在神经网络中使用注意力机制的原因**
+ **介绍一个基本的自注意力框架,并逐步深入到改进的自注意力机制**
+ **实现一个因果注意力模块,使 LLM 能够一次生成一个token**
+ **使用 dropout 随机掩盖部分注意力权重,以减少过拟合**
@ -328,7 +331,7 @@ tensor([0.4419, 0.6515, 0.5683])
### 3.2 为所有输入的 token 计算注意力权重
### 3.3.2 为所有输入的 token 计算注意力权重
在前一节中,我们计算了第二个输入元素的注意力权重和上下文向量,如图 3.11 中的高亮行所示。现在,我们将扩展该计算,以对所有输入计算注意力权重和上下文向量。
@ -623,6 +626,404 @@ print(attn_weights_2)
> + **Softmax函数的特性**在计算注意力权重时点积结果会通过Softmax函数转换为概率分布。而Softmax函数对输入值的差异非常敏感当输入值较大时Softmax的输出会趋近于0或1表现得类似于阶跃函数step function
> + **梯度消失问题**当Softmax的输出接近0或1时其梯度会非常小接近于零可以通过3.3.1小节中提到的Softmax公式推断。这意味着在反向传播过程中梯度更新幅度会很小导致模型学习速度减慢甚至训练停滞。
>
> 为了解决上述问题,在计算点积后,将结果除以嵌入维度的平方根(即 $` \sqrt{d\<sub\>k\</sub\>} `$),其中 d<sub>k</sub> 是键向量的维度。这样可以将点积结果缩放到适当的范围避免Softmax函数进入梯度平缓区从而保持梯度的有效性促进模型的正常训练。$`\sqrt{\$4}`$
> 为了解决上述问题,在计算点积后,将结果除以嵌入维度的平方根(即 $` \sqrt{dk} `$),其中 d<sub>k</sub> 是键向量的维度。这样可以将点积结果缩放到适当的范围避免Softmax函数进入梯度平缓区从而保持梯度的有效性促进模型的正常训练。
好了我们只剩最后一步也就是计算上下文向量如图3.17所示。
<img src="../Image/chapter3/figure3.17.png" width="75%" />
与第 3.3 节中我们通过输入向量的加权和来计算上下文向量相似,现在我们通过值向量的加权和来计算上下文向量。这里,注意力权重作为加权因子,用于衡量每个值向量的重要性。与第 3.3 节类似,我们可以通过矩阵乘法一步得到输出结果:
```python
context_vec_2 = attn_weights_2 @ values
print(context_vec_2)
```
结果如下:
```
tensor([0.3061, 0.8210])
```
到目前为止,我们只计算了一个上下文向量 z<sup>(2)</sup>。在下一节中,我们将完善代码,以计算输入序列中的所有上下文向量,从 z<sup>(1)</sup> 到 z<sup>(T)</sup>
> [!NOTE]
>
> **为什么使用`Q`、`K`和`V`向量?**
>
> 在注意力机制的上下文中“键”key、“查询”query和“值”value这些术语来源于信息检索和数据库领域在这些领域中也使用类似的概念来存储、搜索和检索信息
>
> **查询**query类似于数据库中的搜索查询。它代表模型当前关注或试图理解的项如句子中的某个词或 token。通过查询模型可以探查输入序列中的其他部分以确定对它们应关注的程度。
>
> **键**key类似于数据库中用于索引和查找的键。在注意力机制中输入序列的每个元素例如句子中的每个单词都对应一个关联的。这些用于与查询进行匹配。
>
> **值**value类似于数据库中的键值对中的“值”。它表示输入项的实际内容或表示。当模型确定哪些键即输入中的哪些部分与查询当前的关注项最相关时就会检索出对应的值。
### 3.4.2 实现一个简洁的自注意力机制 Python 类
在前面的章节中,我们逐步讲解了计算自注意力输出的多个步骤。这样做主要是为了便于分步骤展示每个环节的细节。在实际应用中,考虑到下一章将介绍的大语言模型的实现,采用如下方式将这段代码组织到一个 Python 类中会更为有利:
```python
# Listing 3.1 A compact self-attention class
import torch.nn as nn
class SelfAttention_v1(nn.Module):
def __init__(self, d_in, d_out):
super().__init__()
self.d_out = d_out
self.W_query = nn.Parameter(torch.rand(d_in, d_out))
self.W_key = nn.Parameter(torch.rand(d_in, d_out))
self.W_value = nn.Parameter(torch.rand(d_in, d_out))
def forward(self, x):
keys = x @ self.W_key
queries = x @ self.W_query
values = x @ self.W_value
attn_scores = queries @ keys.T # omega
attn_weights = torch.softmax(
attn_scores / keys.shape[-1]**0.5, dim=-1)
context_vec = attn_weights @ values
return context_vec
```
在这段 PyTorch 代码中,`SelfAttention_v1` 是一个从 `nn.Module` 派生的类。`nn.Module` 是 PyTorch 模型的基础组件,提供了创建和管理模型层所需的必要功能。
`__init__` 方法初始化了用于计算查询query、键key和值value的可训练权重矩阵`W_query`、`W_key` 和 `W_value`),每个矩阵都将输入维度 `d_in` 转换为输出维度 `d_out`
前向传播过程在 forward 方法中实现我们通过将查询query和键key相乘来计算注意力得分attn_scores并使用 softmax 对这些得分进行归一化。最后我们使用这些归一化的注意力得分对值value加权生成上下文向量。
我们可以按如下方式使用这个类:
```python
torch.manual_seed(123)
sa_v1 = SelfAttention_v1(d_in, d_out)
print(sa_v1(inputs))
```
由于输入包含六个嵌入向量,因此会生成一个用于存储这六个上下文向量的矩阵:
```
tensor([[0.2996, 0.8053],
[0.3061, 0.8210],
[0.3058, 0.8203],
[0.2948, 0.7939],
[0.2927, 0.7891],
[0.2990, 0.8040]], grad_fn=<MmBackward0>)
```
观察以上的输出,注意第二行 ([0.3061, 0.8210]) 的内容与上一节中的 `context_vec_2` 内容一致。
图 3.18 概述了我们刚刚实现的自注意力机制。
<img src="../Image/chapter3/figure3.18.png" width="75%" />
如图3.18所示,自注意力机制涉及可训练的权重矩阵 W<sub>q</sub>、W<sub>k</sub> 和 W<sub>v</sub>。这些矩阵将输入数据转换为查询、键和值,它们是注意力机制的重要组成部分。随着训练过程中数据量的增加,模型会不断调整这些可训练的权重,在后续章节中我们会学习相关细节。
我们可以通过使用 PyTorch 的 `nn.Linear` 层来进一步改进 SelfAttention_v1 的实现。当禁用偏置单元时,`nn.Linear` 层可以有效地执行矩阵乘法。此外,使用 `nn.Linear` 替代手动实现的 `nn.Parameter(torch.rand(...))` 的一个显著优势在于,`nn.Linear` 具有优化的权重初始化方案,从而有助于实现更稳定和更高效的模型训练。
```python
# Listing 3.2 A self-attention class using PyTorch's Linear layers
class SelfAttention_v2(nn.Module):
def __init__(self, d_in, d_out, qkv_bias=False):
super().__init__()
self.d_out = d_out
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
def forward(self, x):
keys = self.W_key(x)
queries = self.W_query(x)
values = self.W_value(x)
attn_scores = queries @ keys.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
context_vec = attn_weights @ values
return context_vec
```
SelfAttention_v2 的使用方法和 SelfAttention_v1 一样:
```python
torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))
```
输出如下:
```
tensor([[-0.0739, 0.0713],
[-0.0748, 0.0703],
[-0.0749, 0.0702],
[-0.0760, 0.0685],
[-0.0763, 0.0679],
[-0.0754, 0.0693]], grad_fn=<MmBackward0>)
```
`SelfAttention_v1` 和` SelfAttention_v2` 的输出不同,因为它们的权重矩阵使用了不同的初始权重,这是由于 `nn.Linear` 层采用了一种更复杂的权重初始化方案。
> [!NOTE]
>
> **练习 3.1:比较`SelfAttention_v1`和 `SelfAttention_v2`**
>
> 请注意,`SelfAttention_v2` 中的 `nn.Linear` 层使用了一种不同的权重初始化方式,而 `SelfAttention_v1` 则使用 `nn.Parameter(torch.rand(d_in, d_out))` 进行初始化。这导致两种机制生成的结果有所不同。为了验证 `SelfAttention_v1``SelfAttention_v2` 的其他部分是否相似,我们可以将 `SelfAttention_v2` 对象中的权重矩阵转移到 `SelfAttention_v1` 中,从而使两者生成相同的结果。
>
> 你的任务是将 `SelfAttention_v2` 实例中的权重正确分配给 `SelfAttention_v1` 实例。为此,你需要理解两个版本中权重之间的关系。(提示:`nn.Linear` 存储的是转置形式的权重矩阵。)分配完成后,你应该能观察到两个实例生成相同的输出。
在下一节中,我们将对自注意力机制进行增强,重点加入因果和多头机制。因果属性涉及对注意力机制的修改,防止模型访问序列中的后续信息。这在语言建模等任务中至关重要,因为在这些任务中,每个词的预测只能依赖之前的词。
多头组件将注意力机制分解为多个‘头’。每个头能够学习数据的不同方面,使模型能够同时关注来自不同表示子空间的不同位置的信息。这提高了模型在复杂任务中的性能。
## 3.5 使用因果注意力机制来屏蔽后续词
在本节中,我们将标准自注意力机制修改为因果注意力机制,这对于后续章节中开发大语言模型至关重要。
因果注意力(也称为掩蔽注意力)是一种特殊的自注意力形式。它限制模型在处理任何给定的 token 时,只能考虑序列中的前一个和当前输入,而不能看到后续的内容。这与标准的自注意力机制形成对比,后者允许模型同时访问整个输入序列。
因此,在计算注意力分数时,因果注意力机制确保模型只考虑当前 token 之前或之前的 token。
在 GPT 类大语言模型中,为了实现这一点,我们会对每个处理的 token 屏蔽其后续 token即在输入文本中当前词之后的所有词如图 3.19 所示。
<img src="../Image/chapter3/figure3.19.png" width="75%" />
如图 3.19 所示,我们对注意力权重的对角线上方部分进行了掩码操作,并对未掩码的注意力权重进行归一化,使得每一行的注意力权重之和为 1。在下一节中我们将用代码实现这个掩码和归一化过程。
### 3.5.1 应用因果注意力掩码
在本节中,我们将编码实现因果注意力掩码。我们首先按照图 3.20 中总结的步骤开始。
<img src="../Image/chapter3/figure3.20.png" width="75%" />
如图3.20总结,我们可以利用上一节的注意力得分和权重来实现因果注意力机制,以获得掩码后的注意力权重。
在图 3.20 所示的第一步中,我们使用 softmax 函数计算注意力权重,如在前几节中所做的那样:
```python
queries = sa_v2.W_query(inputs) #A
keys = sa_v2.W_key(inputs)
attn_scores = queries @ keys.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=1)
print(attn_weights)
#A 为了方便起见,我们复用上一节中 SelfAttention_v2 对象的query和key权重矩阵。
```
这会得到以下注意力权重:
```
tensor([[0.1921, 0.1646, 0.1652, 0.1550, 0.1721, 0.1510],
[0.2041, 0.1659, 0.1662, 0.1496, 0.1665, 0.1477],
[0.2036, 0.1659, 0.1662, 0.1498, 0.1664, 0.1480],
[0.1869, 0.1667, 0.1668, 0.1571, 0.1661, 0.1564],
[0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.1585],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<SoftmaxBackward0>)
```
我们可以使用 PyTorch 的 `tril` 函数来实现图 3.20 中的步骤 2该函数生成一个掩码矩阵使对角线以上的值为零
```python
context_length = attn_scores.shape[0]
mask_simple = torch.tril(torch.ones(context_length, context_length))
print(mask_simple)
```
生成的掩码如下所示:
```
tensor([[1., 0., 0., 0., 0., 0.],
[1., 1., 0., 0., 0., 0.],
[1., 1., 1., 0., 0., 0.],
[1., 1., 1., 1., 0., 0.],
[1., 1., 1., 1., 1., 0.],
[1., 1., 1., 1., 1., 1.]])
```
现在,我们可以将这个掩码矩阵与注意力权重相乘,从而将对角线以上的值置零。
```python
masked_simple = attn_weights*mask_simple
print(masked_simple)
```
可以看到,对角线以上的元素已成功被置零:
```
tensor([[0.1921, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.2041, 0.1659, 0.0000, 0.0000, 0.0000, 0.0000],
[0.2036, 0.1659, 0.1662, 0.0000, 0.0000, 0.0000],
[0.1869, 0.1667, 0.1668, 0.1571, 0.0000, 0.0000],
[0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<MulBackward0>)
```
图 3.20 中的第三步是将注意力权重重新归一化,使得每一行的权重和再次等于 1。我们可以通过将每一行中的每个元素除以该行的总和来实现这一点
```python
row_sums = masked_simple.sum(dim=1, keepdim=True)
masked_simple_norm = masked_simple / row_sums
print(masked_simple_norm)
```
最终得到的注意力权重矩阵具有以下特性:主对角线以上的注意力权重被置零,每一行的权重和为 1
```
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],
[0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],
[0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],
[0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<DivBackward0>)
```
> [!NOTE]
>
> **信息泄露**
>
> 当我们应用掩码并重新归一化注意力权重时,乍一看似乎未来的 token即我们打算掩盖的部分仍可能影响当前 token因为它们的值仍然参与了 softmax 计算。然而,关键在于,当我们在掩码之后重新归一化注意力权重时,本质上是在一个更小的子集上重新计算 softmax因为被掩盖的位置不会贡献到 softmax 的计算值中)。
>
> softmax 算法的优雅之处在于,尽管最初所有位置都包含在分母中,但经过掩码处理和重新归一化后,被掩盖的位置的影响被抵消了——它们在任何实质性意义上都不会影响 softmax 得分。
>
> 简而言之,在应用掩码和重新归一化之后,注意力权重的分布就像一开始只在未被掩码的位置上计算的一样。这确保了不会有来自未来(或其他掩码位置)的信息泄露,从而实现了我们的预期。
尽管通过上文的方式我们已经完成了因果注意力的实现,但我们还可以利用 softmax 函数的数学特性,更高效地计算掩码后的注意力权重,减少计算步骤,具体实现如图 3.21 所示。
<img src="../Image/chapter3/figure3.21.png" width="75%" />
Softmax 函数将输入值转换为概率分布。当一行中存在负无穷值(-∞Softmax 函数会将这些值视为零概率。(从数学上讲,这是因为 e<sup>−∞</sup> 接近于 0。
我们可以通过创建一个对角线以上全为 1 的掩码,然后将这些 1 替换为负无穷大(-inf从而实现这种更高效的掩码技巧
```python
mask = torch.triu(torch.ones(context_length, context_length), diagonal=1)
masked = attn_scores.masked_fill(mask.bool(), -torch.inf)
print(masked)
```
由此生成以下掩码:
```
tensor([[0.2899, -inf, -inf, -inf, -inf, -inf],
[0.4656, 0.1723, -inf, -inf, -inf, -inf],
[0.4594, 0.1703, 0.1731, -inf, -inf, -inf],
[0.2642, 0.1024, 0.1036, 0.0186, -inf, -inf],
[0.2183, 0.0874, 0.0882, 0.0177, 0.0786, -inf],
[0.3408, 0.1270, 0.1290, 0.0198, 0.1290, 0.0078]],
grad_fn=<MaskedFillBackward0>)
```
现在我们只需要对这些掩码后的结果应用 softmax 函数,就可以完成了:
```python
attn_weights = torch.softmax(masked / keys.shape[-1]**0.5, dim=1)
print(attn_weights)
```
如输出所示,每一行的值之和为 1因此不再需要进一步的归一化
```
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],
[0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],
[0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],
[0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],
[0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],
grad_fn=<SoftmaxBackward0>)
```
现在,我们可以使用修改后的注意力权重,通过 `context_vec = attn_weights @ values` 来计算上下文向量,这在第 3.4 节中介绍过。不过,在下一节中,我们将首先介绍一个对因果注意力机制的细微调整,这一调整在训练大语言模型时有助于减少过拟合现象。
### 3.5.2 使用 dropout 遮掩额外的注意力权重
Dropout 在深度学习中是一种技术即在训练过程中随机忽略一些隐藏层单元实际上将它们“丢弃”。这种方法有助于防止过拟合确保模型不会过于依赖任何特定的隐藏层单元组合。需要特别强调的是Dropout 仅在训练过程中使用,训练结束后则会禁用。
在 Transformer 架构中(包括 GPT 等模型),注意力机制中的 Dropout 通常应用于两个特定区域:计算注意力得分之后,或将注意力权重应用于 value 向量之后。
在这里,我们会在计算完注意力权重之后应用 dropout 掩码(如图 3.22 所示),因为在实际应用中这是更为常见的做法。
<img src="../Image/chapter3/figure3.22.png" width="75%" />
在以下代码示例中我们使用了50%的 dropout 率,这意味着屏蔽掉一半的注意力权重。(在后续章节中训练 GPT 模型时,我们将使用更低的 dropout 率,比如 0.1 或 0.2
在以下代码中,我们首先将 PyTorch 的 dropout 实现应用于一个由 1 组成的 6×6 张量以作说明:
```python
torch.manual_seed(123)
dropout = torch.nn.Dropout(0.5) #A
example = torch.ones(6, 6) #B
print(dropout(example))
#A 我们使用的dropout率为0.5
#B 创建一个由1组成的矩阵
```
如我们所见,约一半的数值被置零:
```
tensor([[2., 2., 0., 2., 2., 0.],
[0., 0., 0., 2., 0., 2.],
[2., 2., 2., 2., 0., 2.],
[0., 2., 2., 0., 0., 2.],
[0., 2., 0., 2., 0., 2.],
[0., 2., 2., 2., 2., 0.]])
```
当对注意力权重矩阵应用 50% 的 dropout 时,矩阵中一半的元素会被随机设置为零。为了补偿有效元素的减少,矩阵中剩余元素的值会被放大 1/0.5 = 2 倍。这个缩放操作至关重要,可以在训练和推理阶段保持注意力机制的整体权重平衡,确保注意力机制在这两个阶段的平均影响保持一致。
> [!TIP]
>
> **个人思考:** 读到这一段时我有些不解Dropout相当于丢弃一定比例的注意力权重这表明对输入中的某些token关注度降为0了完全不关注这样的处理方式难道对最终的预测效果没有影响么另外如何理解Dropout之后的缩放操作是为了保持注意力在不同阶段的平衡
>
> 经过查阅额外的资料及深度思考,我觉得可以从以下几个方面理解上述的疑问:
>
> 1. **Dropout 的目的:提高模型的泛化能力**
>
> dropout 的设计初衷是**提高模型的泛化能力**。通过随机丢弃一部分神经元或注意力权重dropout 迫使模型在每次训练时学习略有不同的表示方式,而不是依赖某一特定的注意力模式。这种随机化的训练方式可以帮助模型在**面对新数据时更具鲁棒性**,减少过拟合的风险。
>
> 2. **注意力机制的冗余性**
>
> 在 Transformer 的注意力机制中,模型通常会对多个 token 进行注意力计算,实际上会有一些冗余信息。也就是说,**不同 token 之间的信息通常会有部分重叠**并且模型能够从多个来源获取类似的信息。在这种情况下dropout 随机丢弃一部分注意力权重并不会完全破坏模型的性能,因为模型可以依赖于其他未被丢弃的注意力路径来获取所需信息。
>
> 3. **缩放操作的作用**
>
> 在应用 dropout 时,一部分注意力权重被随机置零(假设 dropout 率为 p。剩余的权重会被放大其放大倍数为 $ \frac{1}{1-p} $。放大后的权重记为 z
>
> $$ z_{i}^{\prime}=\frac{z_{i}}{1-p} \quad \text { (对于未被置零的权重) } $$
>
>
>
>
现在,让我们将 dropout 应用于注意力权重矩阵本身:
```python
torch.manual_seed(123)
print(dropout(attn_weights))
```
由此生成的注意力权重矩阵中,部分元素被置零,剩余的元素重新进行了缩放:
```
tensor([[2.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
[0.7599, 0.6194, 0.6206, 0.0000, 0.0000, 0.0000],
[0.0000, 0.4921, 0.4925, 0.0000, 0.0000, 0.0000],
[0.0000, 0.3966, 0.0000, 0.3775, 0.0000, 0.0000],
[0.0000, 0.3327, 0.3331, 0.3084, 0.3331, 0.0000]],
grad_fn=<MulBackward0>
```
请注意,由于操作系统的不同,生成的 dropout 输出可能看起来有所差异;您可以在 PyTorch 问题跟踪页面上查看更多关于此不一致性的信息,网址为:[https://github.com/pytorch/pytorch/issues/121595](https://github.com/pytorch/pytorch/issues/121595)。
在理解了因果注意力和 dropout 掩码的基础上,接下来的部分中我们将开发一个简洁的 Python 类,以便高效应用这两种技术。