add third chapter
|
After Width: | Height: | Size: 894 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 775 KiB |
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 992 KiB |
|
|
@ -3,8 +3,11 @@
|
|||
本章涵盖以下内容:
|
||||
|
||||
+ **探讨在神经网络中使用注意力机制的原因**
|
||||
|
||||
+ **介绍一个基本的自注意力框架,并逐步深入到改进的自注意力机制**
|
||||
|
||||
+ **实现一个因果注意力模块,使 LLM 能够一次生成一个token**
|
||||
|
||||
+ **使用 dropout 随机掩盖部分注意力权重,以减少过拟合**
|
||||
|
||||
|
||||
|
|
@ -211,7 +214,7 @@ tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])
|
|||
> ```python
|
||||
> res = 0.
|
||||
> for idx, element in enumerate(inputs[0]):
|
||||
> res += inputs[0][idx] * query[idx]
|
||||
> res += inputs[0][idx] * query[idx]
|
||||
> print(res)
|
||||
> print(torch.dot(inputs[0], query))
|
||||
> ```
|
||||
|
|
@ -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 类,以便高效应用这两种技术。
|
||||
|
||||
|
|
|
|||