[book] initial commit for ch3.3-3.7

This commit is contained in:
SamanthaTso 2024-05-16 21:10:18 +08:00
parent 33303ef91b
commit ae26ef8d1e
5 changed files with 1649 additions and 0 deletions

View File

@ -0,0 +1,666 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 3.3 关注自注意力机制输入的不同部分\n",
"\n",
"我们接下来将深入了解自注意力self-attention机制的工作原理并学习如何从零开始编写它的代码。自注意力机制是所有基于 Transformer 架构的大语言模型的核心组成部分。需要指出的是,理解这一概念需要高度集中精神和注意力,但一旦你掌握了其基本原理,就相当于攻克了本书中最为艰难的部分并在某种意义上实现了大语言模型。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 自注意力机制中的“自”\n",
"\n",
"在自注意力机制中“自”self是指该机制能够通过分析单一输入序列内不同位置的联系来计算注意力权重。它能够评估和学习输入本身各部分之间的关系与依赖比如一句话中的词语或一个图像中的像素。这与传统的注意力机制形成对比后者主要关注两个不同序列之间的元素关系例如在序列对序列模型中注意力可能位于一个输入序列与一个输出序列之间如图 3.5 所示。\n",
"\n",
"自注意力机制可能看起来比较复杂,尤其是如果你是第一次接触它的话。因此,我们将在下一小节首先介绍一个简化版本的自注意力机制。之后,在第 3.4 节中,我们将实现带有可训练权重的自注意力机制,这种机制被广泛应用于大语言模型中。\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3.3.1 无需可训练权重的简单自注意力机制\n",
"\n",
"在本节中,我们将实现一个简化版本的自注意力机制,不涉及任何可训练的权重,该版本在图 3.7 中有所概述。本节的目的是在下一节 3.4 添加可训练权重之前,先阐释自注意力中的几个关键概念。\n",
"\n",
"**图 3.7 自注意力的目标是为每个输入元素计算一个上下文向量,该向量结合了来自所有其他输入元素的信息。在该图中所示的示例中,我们计算了上下文向量 z(2)。计算 z(2) 的每个输入元素的重要性或贡献由注意力权重 α21 到 α2T 决定。在计算 z(2) 时,注意力权重是针对输入元素 x(2) 及所有其他输入计算的。这些注意力权重的具体计算方法将在本节后面讨论。**\n",
"\n",
"\n",
"![3.7](../img/fig-3-7.jpg)\n",
"\n",
"图 3.7 显示了一个输入序列, 标记为 x, 包含 T 个元素, 从 x(1) 到 x(T)。通常, 这样的序列代表了文本, 如句子, 它已经被转换为 Token 嵌入, 正如第 2 章所解释的。\n",
"\n",
"以一个输入文本 \"Your journey starts with one step.\" 为例。在这种情况下, 每个序列元素, 如 x(1), 对应于一个代表特定 Token \"Your\" 的 d 维嵌入向量。在图 3.7 中, 这些输入向量显示为三维嵌入。\n",
"\n",
"在自注意力机制中, 我们的目标是为输入序列中每个元素 x(i) 计算上下文向量 z(i)。可以将上下文向量理解为一个信息更丰富的嵌入向量。\n",
"\n",
"以 x(2) 的嵌入向量为例, 它对应于 Token \"journey\", 以及其对应的上下文向量 z(2), 如图 3.7 底部所示。这个增强的上下文向量 z(2) 包含了关于 x(2) 以及序列中所有其他元素 x(1) 到 x(T) 的信息。\n",
"\n",
"自注意力机制在这里扮演着关键角色。它的作用是通过整合序列中所有其他元素的信息, 为输入序列的每一个元素(如句子中的每一个词)创造出更丰富的表征。这对于大语言模型来说至关重要, 因为它们需要理解句子中词与词之间的联系和重要性。接下来我们将引入可训练的权重使大语言模型能够学习如何构建这些上下文向量从而有效地帮助模型生成下一个Token。\n",
"\n",
"在本节中, 我们逐步实现了一个简化的自注意力机制, 以计算这些权重和相应的上下文向量。\n",
"\n",
"请参考以下输入句子, 它已经按照第 2 章的讨论被嵌入为三维向量。为了示范的需要, 我们选择了一个较小的嵌入尺寸, 确保它在页面上显示时不会被截断。\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"trusted": true
},
"outputs": [
{
"ename": "<class 'ModuleNotFoundError'>",
"evalue": "No module named 'torch'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[8], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtorch\u001b[39;00m\n\u001b[1;32m 2\u001b[0m inputs \u001b[38;5;241m=\u001b[39m torch\u001b[38;5;241m.\u001b[39mtensor(\n\u001b[1;32m 3\u001b[0m [[\u001b[38;5;241m0.43\u001b[39m, \u001b[38;5;241m0.15\u001b[39m, \u001b[38;5;241m0.89\u001b[39m], \u001b[38;5;66;03m# Your (x^1)\u001b[39;00m\n\u001b[1;32m 4\u001b[0m [\u001b[38;5;241m0.55\u001b[39m, \u001b[38;5;241m0.87\u001b[39m, \u001b[38;5;241m0.66\u001b[39m], \u001b[38;5;66;03m# journey (x^2)\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 8\u001b[0m [\u001b[38;5;241m0.05\u001b[39m, \u001b[38;5;241m0.80\u001b[39m, \u001b[38;5;241m0.55\u001b[39m]] \u001b[38;5;66;03m# step \u001b[39;00m\n\u001b[1;32m 9\u001b[0m )\n",
"\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'torch'"
]
}
],
"source": [
"import torch\n",
"inputs = torch.tensor(\n",
" [[0.43, 0.15, 0.89], # Your (x^1)\n",
" [0.55, 0.87, 0.66], # journey (x^2)\n",
" [0.57, 0.85, 0.64], # starts (x^3)\n",
" [0.22, 0.58, 0.33], # with (x^4)\n",
" [0.77, 0.25, 0.10], # one (x^5)\n",
" [0.05, 0.80, 0.55]] # step (x^6) \n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"实现自注意力机制的第一步是计算中间变量 ω,这些变量被称为注意力得分,如图 3.8 所示。\n",
"\n",
"**图 3.8 本节的总体目标是通过使用第二个输入序列 x(2) 作为查询来演示上下文向量 z(2) 的计算过程。此图展示了第一个中间步骤,即通过点积计算查询 x(2) 与所有其他输入元素之间的注意力得分 ω。(请注意,图中的数字为了减少视觉杂乱,小数点后数字被截断到一位。)**\n",
"\n",
"![3.8](../img/fig-3-8.jpg)\n",
"\n",
"图 3.8 展示了我们如何计算查询 Token 与每个输入 Token 之间的中间注意力得分。我们通过计算查询 x(2) 与其他每个输入 Token 的点积来确定这些得分:\n",
" "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"query = inputs[1] #A \n",
"attn_scores_2 = torch.empty(inputs.shape[0])\n",
"for i, x_i in enumerate(inputs):\n",
" attn_scores_2[i] = torch.dot(x_i, query)\n",
"print(attn_scores_2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"计算出的注意力得分如下:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 理解点积\n",
"\n",
"点积是一个简单直接的操作,它通过对两个向量的对应元素进行相乘然后求和来完成,示例如下:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"res = 0.\n",
"\n",
"for idx, element in enumerate(inputs[0]):\n",
" res += inputs[0][idx] * query[idx]\n",
"print(res)\n",
"print(torch.dot(inputs[0], query))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"从输出结果可以看出,逐元素相乘后的和与点积的计算结果一致:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
" tensor(0.9544)\n",
" tensor(0.9544)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"点积不仅仅是一个数学工具,它还能衡量两个向量的相似度。点积越高,表示两个向量的对齐程度或相似度越高。在自注意力机制中,点积用于衡量序列中各元素之间的关注程度:点积值越高,两个元素之间的相似性和注意力得分就越高。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"接下来的步骤,如图 3.9 所示,我们将对之前计算的每个注意力得分进行标准化处理。\n",
"\n",
"**图 3.9 在根据输入查询 x(2) 计算出注意力分数 ω21 到 ω2T 后,下一步是将这些分数归一化,以得到注意力权重 α21 到 α2T。**\n",
"\n",
"![3.9](../img/fig-3-9.jpg)\n",
"\n",
"如图 3.9 所示,进行归一化的主要目的是获取总和为 1 的注意力权重。这种归一化操作是常规做法,它不仅便于我们理解数据,还有助于保持大语言模型训练的稳定性。以下是实现这一归一化步骤的简单方法:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"attn_weights_2_tmp = attn_scores_2 / attn_scores_2.sum()\n",
"print(\"Attention weights:\", attn_weights_2_tmp)\n",
"print(\"Sum:\", attn_weights_2_tmp.sum())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如输出所示,现在的注意力权重加起来等于 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"Attention weights: tensor([0.1455, 0.2278, 0.2249, 0.1285, 0.1077, 0.1656])\n",
"Sum: tensor(1.0000)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在实际应用中,通常推荐使用 softmax 函数来进行归一化。这种方法在处理极端值时表现更佳,且在训练过程中提供了更优的梯度特性。下面是一个基本的 softmax 函数实现,用于归一化注意力得分:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"def softmax_naive(x):\n",
" return torch.exp(x) / torch.exp(x).sum(dim=0) \n",
" \n",
"attn_weights_2_naive = softmax_naive(attn_scores_2)\n",
"print(\"Attention weights:\", attn_weights_2_naive)\n",
"print(\"Sum:\", attn_weights_2_naive.sum())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如输出所示softmax 函数能够实现使得注意力权重的总和达到 1 的目标:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"Attention weights: tensor([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])\n",
"Sum: tensor(1.)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"此外softmax 函数确保注意力权重始终为正值。这意味着输出可以被解释为概率或相对重要性,高权重代表更大的重要性。\n",
"\n",
"值得注意的是,这种简单的 softmax 函数实现softmax_naive在处理大或小输入值时可能面临数值不稳定的问题例如溢出和下溢。因此在实际应用中推荐使用 PyTorch 的 softmax 函数实现,这种实现方法已经针对性能进行了深入优化:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"attn_weights_2 = torch.softmax(attn_scores_2, dim=0)\n",
"print(\"Attention weights:\", attn_weights_2)\n",
"print(\"Sum:\", attn_weights_2.sum())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"从结果来看,这与我们之前使用的简单 softmax_native 函数得到的结果是一致的:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"Attention weights: tensor([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])\n",
"Sum: tensor(1.)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"现在我们已经计算了归一化的注意力权重,接下来就是图 3.10 所示的最后一步:通过将嵌入的输入 Token x(i) 与相应的注意力权重相乘,然后将结果向量求和,计算出上下文向量 z(2)。\n",
"\n",
"**图 3.10 在计算并归一化注意力分数以获取查询 x(2) 的注意力权重后,下一步是计算上下文向量 z(2)。这个上下文向量是所有输入向量 x(1) 到 x(T) 通过注意力权重加权的组合。**\n",
"\n",
"![3.10](../img/fig-3-10.jpg)\n",
"\n",
"图 3.10 中展示的上下文向量 z(2) 是通过所有输入向量的加权求和计算得到的。具体操作是将每个输入向量乘以其相应的注意力权重:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"query = inputs[1] # 2nd input token is the query\n",
"context_vec_2 = torch.zeros(query.shape)\n",
"for i,x_i in enumerate(inputs):\n",
" context_vec_2 += attn_weights_2[i]*x_i\n",
"print(context_vec_2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"计算的结果如下:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([0.4419, 0.6515, 0.5683])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"下一节中,我们将扩展这一过程,同时计算所有上下文向量。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3.3.2 计算所有输入 Token 的注意力权重\n",
"\n",
"前一节,我们为输入 2 计算了注意力权重和上下文向量,如图 3.11 中高亮的行所示。现在我们将这种计算扩展到所有输入的注意力权重和上下文向量。\n",
"\n",
"**图 3.11 的高亮行显示了我们之前为第二个输入元素作为查询所计算的注意力权重。本节将扩展这一计算过程以获得所有其他注意力权重。**\n",
"\n",
"![3.11](../img/fig-3-11.jpg)\n",
"\n",
"我们将遵循之前相同的三个步骤,如图 3.12 所总结,但在代码中进行了修改,计算所有上下文向量而非仅仅是第二个 z(2)。\n",
"\n",
"![3.12](../img/fig-3-12.jpg)\n",
"\n",
"首先,在图 3.12 的第一步中,我们添加了一个额外的循环来计算所有输入对的点积。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"attn_scores = torch.empty(6, 6)\n",
"for i, x_i in enumerate(inputs):\n",
" for j, x_j in enumerate(inputs):\n",
" attn_scores[i, j] = torch.dot(x_i, x_j)\n",
"print(attn_scores)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"得到的注意力分数如下:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],\n",
" [0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],\n",
" [0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],\n",
" [0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],\n",
" [0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],\n",
" [0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如图 3.11 所示,前述张量中的每个元素代表每对输入之间的注意力分数。需要注意的是,图 3.11 中的值已经归一化,这也是为什么它们与前述张量中的未归一化注意力分数不同。我们将稍后处理归一化问题。\n",
"\n",
"在计算前面的注意力分数张量时,我们使用了 Python 中的 for 循环。然而for 循环通常较慢,我们可以通过矩阵乘法达到同样的效果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"attn_scores = inputs @ inputs.T\n",
"print(attn_scores)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"我们可以看到结果与之前相同:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],\n",
" [0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],\n",
" [0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],\n",
" [0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],\n",
" [0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],\n",
" [0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在第二步中,我们将每行的值归一化,使其总和为 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"attn_weights = torch.softmax(attn_scores, dim=1)\n",
"print(attn_weights)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"这返回了与图 3.10 所显示的值匹配的注意力权重张量:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([[0.2098, 0.2006, 0.1981, 0.1242, 0.1220, 0.1452],\n",
" [0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581],\n",
" [0.1390, 0.2369, 0.2326, 0.1242, 0.1108, 0.1565],\n",
" [0.1435, 0.2074, 0.2046, 0.1462, 0.1263, 0.1720],\n",
" [0.1526, 0.1958, 0.1975, 0.1367, 0.1879, 0.1295],\n",
" [0.1385, 0.2184, 0.2128, 0.1420, 0.0988, 0.1896]])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在我们进行到图 3.12 所示的第三步、也是最后一步之前,让我们简单确认一下这些行的确都加起来等于 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"row_2_sum = sum([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])\n",
"print(\"Row 2 sum:\", row_2_sum)\n",
"print(\"All row sums:\", attn_weights.sum(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"返回的结果如下:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"Row 2 sum: 1.0\n",
"All row sums: tensor([1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在第三步也是最后一步中,我们将利用这些注意力权重通过矩阵乘法来生成所有的上下文向量:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"all_context_vecs = attn_weights @ inputs\n",
"print(all_context_vecs)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"在得到的输出张量中,每一行都包含一个三维的上下文向量:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"tensor([[0.4421, 0.5931, 0.5790],\n",
" [0.4419, 0.6515, 0.5683],\n",
" [0.4431, 0.6496, 0.5671],\n",
" [0.4304, 0.6298, 0.5510],\n",
" [0.4671, 0.5910, 0.5266],\n",
" [0.4177, 0.6503, 0.5645]])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"我们可以通过将第二行与我们在 3.3.1 节中之前计算的上下文向量 z(2) 进行对比,来核实代码是否正确:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"print(\"Previous 2nd context vector:\", context_vec_2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"根据结果,我们可以看到之前计算的 context_vec_2 与前一个张量中的第二行完全匹配:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": [
"Previous 2nd context vector: tensor([0.4419, 0.6515, 0.5683])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"至此,我们完成了一个简单自注意力机制的代码演示。在下一节中,我们将添加可训练的权重,使大语言模型能够从数据中学习,并提高其在特定任务上的性能。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"trusted": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (Pyodide)",
"language": "python",
"name": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@ -0,0 +1,355 @@
{
"metadata": {
"kernelspec": {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat_minor": 4,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": "# 3.4 实现带有可训练权重的自注意力\n\n在本节中我们将实现自注意力机制这种机制被广泛应用于原始的 Transformer 架构、GPT 模型以及大多数其他流行的大语言模型中。这种自注意力机制也被称作缩放点积注意力scaled dot\nproduct attention。图 3.13 描述了这种自注意力机制如何融入到构建大语言模型的更广泛的背景中。\n\n**图 3.13 展示了我们在本节编码的自注意力机制是如何与本书及本章的整体背景相融合的。在前一节中,我们实现了一个简化的注意力机制,以便理解注意力机制的基本原理。本节中,我们将在此基础上增加可训练的权重。在后续章节中,我们还将通过加入因果掩码和多头注意力来进一步扩展这种自注意力机制。**\n\n![3.13](../img/fig-3-13.jpg)\n\n如图 3.13 所示,具有可训练权重的自注意力机制是在之前概念的基础上建立的:我们希望根据特定输入元素,将输入向量的加权和计算为上下文向量。如你所见,它与我们在 3.3 节之前编写的基本自注意力机制相比,只有一些细微的差别。\n\n最显著的区别是引入了在模型训练期间会更新的权重矩阵。这些可训练的权重矩阵至关重要因为它们使得模型特别是模型内部的注意力模块能够学习产生“良好”的上下文向量。请注意我们将在第 5 章训练大语言模型。)\n\n我们将在两个小节中讨论这种自注意力机制。首先我们将像以前一样逐步编写代码。其次我们将代码组织成一个紧凑的 Python 类,该类可以导入到我们在第 4 章的大语言模型架构中。\n\n## 3.4.1 逐步计算注意力权重\n\n我们将通过引入三个可训练的权重矩阵 Wq、Wk 和 Wv 逐步实现自注意力机制。这三个矩阵用于将嵌入的输入 Token x(i) 投影为查询向量、键向量和值向量,如图 3.14 所示。\n\n**图 3.14 展示了实现具备可训练权重矩阵的自注意力机制的第一步。在这一步中,我们针对每个输入元素 x计算其对应的查询q、键k和值v向量。如同之前章节所做的那样我们将第二个输入 x(2) 作为查询输入来处理。查询向量 q(2) 是通过将输入 x(2) 与查询权重矩阵 Wq 进行矩阵乘法得到的。类似地,我们也通过对应的矩阵乘法操作,使用权重矩阵 Wk 和 Wv 来分别计算键向量和值向量。**\n \n![3.14](../img/fig-3-14.jpg)\n\n在第 3.3.1 节中,我们定义第二个输入元素 x(2) 作为查询向量,以计算简化的注意力权重并得出上下文向量 z(2)。在第 3.3.2 节中我们将这一计算推广到了“Your journey starts with one step.” 这个六词输入句子的所有上下文向量 z(1) 到 z(T)。\n\n同样出于示例说明的目的我们将首先只计算一个上下文向量 z(2)。在下一节中,我们将修改这段代码以计算所有上下文向量。\n\n让我们开始定义一些变量",
"metadata": {}
},
{
"cell_type": "code",
"source": "x_2 = inputs[1] #A\nd_in = inputs.shape[1] #B\nd_out = 2 #C",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "请注意,在类似 GPT 的模型中输入和输出维度通常相同但为了更好地展示计算过程这里我们选择了不同的输入d_in=3和输出d_out=2维度。\n\n接下来我们初始化图 3.14 中显示的三个权重矩阵 Wq、Wk 和 Wv\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\nW_query = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)\nW_key = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)\nW_value = torch.nn.Parameter(torch.rand(d_in, d_out), requires_grad=False)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "请注意,我们将 requires_grad 设置为 False 是为了示范时输出结果更清晰,但如果我们要将这些权重矩阵用于模型训练,我们会将 requires_grad 设置为 True以便在模型训练期间更新这些矩阵。\n\n接下来如图 3.14 所示,我们计算查询、键和值向量:",
"metadata": {}
},
{
"cell_type": "code",
"source": "query_2 = x_2 @ W_query \nkey_2 = x_2 @ W_key \nvalue_2 = x_2 @ W_value\nprint(query_2)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "从查询的输出可以看到,这产生了一个二维向量,因为我们将相应权重矩阵的列数通过 d_out 设置为了 2",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([0.4306, 1.4551])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "### 权重参数与注意力权重\n\n请注意在权重矩阵 W 中,“权重”一词是“权重参数”的简称,这是在训练过程中被优化的神经网络的值。它不应该与注意力权重混淆。正如我们在前一节中已经看到的,注意力权重决定了上下文向量在多大程度上依赖输入的不同部分,即网络在多大程度上关注输入的不同部分。\n\n也就是说权重参数是定义网络连接的基本学习系数而注意力权重则是动态的、特定于上下文的值。\n\n尽管我们的临时目标只是计算一个上下文向量 z(2),我们仍然需要所有输入元素的键和值向量,因为它们参与了根据查询向量 q(2) 计算注意力权重,如图 3.14 所示。\n\n我们可以通过矩阵乘法获得所有键和值向量\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "keys = inputs @ W_key \nvalues = inputs @ W_value\nprint(\"keys.shape:\", keys.shape)\nprint(\"values.shape:\", values.shape)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "从输出中我们可以看出,我们成功地将 6 个输入 Token 从 3D 空间投影到了 2D 嵌入空间:",
"metadata": {}
},
{
"cell_type": "code",
"source": "keys.shape: torch.Size([6, 2])\nvalues.shape: torch.Size([6, 2])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "第二步是计算注意力得分,如图 3.15 所示。\n\n**图 3.15 注意力得分计算是一个点积计算,类似于我们在第 3.3 节中使用的简化自注意力机制。新的方面在于,我们不是直接计算输入元素之间的点积,而是使用通过各自的权重矩阵转换输入获得的查询向量和键向量。**\n\n![3.15](../img/fig-3-15.jpg)\n\n首先让我们来计算注意力得分ω22\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "keys_2 = keys[1] #A\nattn_score_22 = query_2.dot(keys_2)\nprint(attn_score_22)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到未归一化的注意力得分结果:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor(1.8524)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "再次,我们可以通过矩阵乘法将此计算拓展到所有注意力得分上:",
"metadata": {}
},
{
"cell_type": "code",
"source": "attn_scores_2 = query_2 @ keys.T # All attention scores for given query\nprint(attn_scores_2)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "从输出中我们很快可以发现,输出的第二个元素与我们之前计算的 attn_score_22 相匹配:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([1.2705, 1.8524, 1.8111, 1.0795, 0.5577, 1.5440])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "第三步是从注意力得分转到注意力权重,如图 3.16 所示。\n\n**图 3.16 在计算注意力得分 ω 之后,下一步是使用 softmax 函数归一化这些得分以获得注意力权重 α。**\n\n![3.16](../img/fig-3-16.jpg)\n\n接下来如图 3.16 所示,我们通过缩放注意力得分并使用我们之前使用的 softmax 函数来计算注意力权重。与之前不同的是,我们现在通过除以键的嵌入维度的平方根来缩放注意力得分,(注意,取平方根在数学上与指数化为 0.5 相同)",
"metadata": {}
},
{
"cell_type": "code",
"source": "d_k = keys.shape[-1]\nattn_weights_2 = torch.softmax(attn_scores_2 / d_k**0.5, dim=-1)\nprint(attn_weights_2)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到的注意力权重如下:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([0.1500, 0.2264, 0.2199, 0.1311, 0.0906, 0.1820])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "### 缩放点积注意力背后的逻辑\n\n通过嵌入维度大小进行归一化的原因是为了通过避免小梯度来提高训练性能。例如在扩大嵌入维度时对于类似 GPT 的大语言模型,其维度通常超过千,较大的点积可能会因为应用了 softmax 函数而在反向传播过程中产生非常小的梯度。随着点积的增加softmax 函数的表现更像是一个阶跃函数,导致梯度接近零。这些小梯度可以极大地减缓学习速度或导致训练停滞。\n\n这种自注意力机制也被称为缩放点积注意力的原因是它通过嵌入维度的平方根进行缩放。\n\n最后一步是计算上下文向量如图 3.17 所示。 \n\n**图 3.17 在自注意力计算的最后一步中,我们通过注意力权重组合所有值向量来计算上下文向量。**\n\n![3.17](../img/fig-3-17.jpg)\n\n类似于第 3.3 节,我们通过输入向量的加权求和计算上下文向量,现在我们通过值向量的加权求和来计算上下文向量。在这里,注意力权重充当了衡量每个值向量相应重要性的权重因子。类似于第 3.3 节,我们可以使用矩阵乘法一步获得输出:",
"metadata": {}
},
{
"cell_type": "code",
"source": "context_vec_2 = attn_weights_2 @ values\nprint(context_vec_2)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "生成的张量内容如下:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([0.3061, 0.8210])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "到目前为止我们只计算了一个上下文向量z(2)。在下一节中我们将拓展代码以计算输入序列中的所有上下文向量z(1) 到 z(T)中。",
"metadata": {}
},
{
"cell_type": "markdown",
"source": "### 为什么是查询、键和值?\n\n在注意力机制的上下文中“键”、“查询”和“值”这些术语是从信息检索和数据库领域借鉴来的在这些领域中类似的概念被用于存储、搜索和检索信息。\n\n“查询”query类似于数据库中的搜索查询。它代表模型当前关注或试图理解的项目例如句子中的一个词或 Token。查询用于探查输入序列的其他部分以确定应该给予它们多少注意力。\n\n“键”key类似于数据库中用于索引和搜索的键。在注意力机制中输入序列中的每个项目例如句子中的每个词都有一个关联的键。这些键用于与查询匹配。\n\n“值”value在这个上下文中类似于数据库中键值对的值。它代表输入项目的实际内容或表示。一旦模型确定哪些键哪些输入部分与查询当前关注项目最相关它就检索相应的值。\n",
"metadata": {}
},
{
"cell_type": "markdown",
"source": "## 3.4.2 实现一个紧凑的自注意力 Python 类\n\n在之前的章节里我们详细介绍了计算自注意力输出的各个步骤这样做主要是为了便于逐步讲解和展示。但在实际应用中特别是考虑到下一章将要介绍的大语言模型的实现把这些代码整合到一个 Python 类中会更加高效。如下所示:\n\n### 清单 3.1 紧凑型 self-attention 类",
"metadata": {}
},
{
"cell_type": "code",
"source": "import torch.nn as nn\nclass SelfAttention_v1(nn.Module):\n def __init__(self, d_in, d_out):\n super().__init__()\n self.d_out = d_out\n self.W_query = nn.Parameter(torch.rand(d_in, d_out))\n self.W_key = nn.Parameter(torch.rand(d_in, d_out))\n self.W_value = nn.Parameter(torch.rand(d_in, d_out))\n \n def forward(self, x):\n keys = x @ self.W_key\n queries = x @ self.W_query\n values = x @ self.W_value\n attn_scores = queries @ keys.T # omega\n attn_weights = torch.softmax(\n attn_scores / keys.shape[-1]**0.5, dim=-1)\n context_vec = attn_weights @ values\n return context_vec",
"metadata": {
"trusted": true
},
"outputs": [
{
"ename": "<class 'ModuleNotFoundError'>",
"evalue": "No module named 'torch'",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[3], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtorch\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mnn\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnn\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m \u001b[38;5;21;01mSelfAttention_v1\u001b[39;00m(nn\u001b[38;5;241m.\u001b[39mModule):\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\u001b[38;5;28mself\u001b[39m, d_in, d_out):\n",
"\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'torch'"
],
"output_type": "error"
}
],
"execution_count": 3
},
{
"cell_type": "markdown",
"source": "在这段 PyTorch 代码中SelfAttention_v1 是继承自 nn.Module 的一个类,而 nn.Module 是构成 PyTorch 模型的基本单元,它提供了创建和管理模型层所需的功能。\n\n__init__方法负责初始化可训练的权重矩阵W_query、W_key 和 W_value这些矩阵分别用于查询、键和值每个矩阵都将输入维度 d_in 转换为输出维度 d_out。\n\n在前向传播过程中通过 forward 方法我们通过查询和键的乘积计算注意力得分attn_scores并使用 softmax 函数对这些得分进行归一化处理。最后,我们通过这些归一化的注意力得分对值进行加权,以此创建一个上下文向量。\n\n我们可以按照以下方式使用这个类\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\nsa_v1 = SelfAttention_v1(d_in, d_out)\nprint(sa_v1(inputs))",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "由于输入包含六个嵌入向量,因此我们得到一个包含六个上下文向量的矩阵:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[0.2996, 0.8053],\n [0.3061, 0.8210],\n [0.3058, 0.8203],\n [0.2948, 0.7939],\n [0.2927, 0.7891],\n [0.2990, 0.8040]], grad_fn=<MmBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "快速核对一下,第二行 ([0.3061, 0.8210]) 实际上与上一节中的 context_vec_2 的内容相对应。\n\n图 3.18 概述了我们刚刚实现的自注意力机制。\n\n**图3.18 在自注意力机制中,我们通过三个权重矩阵 Wq、Wk 和 Wv 来转换输入矩阵 X 中的输入向量。然后,我们基于生成的查询 (Q) 和键 (K) 计算注意力权重矩阵。利用这些注意力权重和值 (V),我们计算出上下文向量 (Z)。为了视觉上的简洁,本图仅展示了一个包含 n 个 Token 的单一输入文本,而不是多个输入的批次。这种简化为 2D 矩阵的表示,使得过程的可视化和理解变得更为直观。**\n\n![3.18](../img/fig-3-18.jpg)\n\n如图 3.18 所示,自注意力涉及三个可训练的权重矩阵 Wq、Wk 和 Wv。这些矩阵将输入数据转化为查询、键和值它们是注意力机制的核心组成部分。随着模型在训练过程中接触更多数据这些可训练的权重会进行相应的调整我们将在后续章节中详细讨论。\n\n通过使用 PyTorch 的 nn.Linear 层,我们可以进一步改进 SelfAttention_v1 的实现。这些层在不使用偏置单元时可以有效地执行矩阵乘法。此外与手动实现nn.Parameter(torch.rand(...)) 相比,使用 nn.Linear 的一个显著优势是其具有优化的权重初始化方案,有助于实现更稳定和有效的模型训练。\n\n### 清单 3.2 使用 PyTorch 的 Linear 层的 self-attention 类",
"metadata": {}
},
{
"cell_type": "code",
"source": "class SelfAttention_v2(nn.Module):\n def __init__(self, d_in, d_out, qkv_bias=False):\n super().__init__()\n self.d_out = d_out\n self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)\n \n def forward(self, x):\n keys = self.W_key(x)\n queries = self.W_query(x)\n values = self.W_value(x)\n attn_scores = queries @ keys.T\n attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=\n context_vec = attn_weights @ values\n return context_vec",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "你可以像使用 SelfAttention_v1 那样使用 SelfAttention_v2",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(789)\nsa_v2 = SelfAttention_v2(d_in, d_out)\nprint(sa_v2(inputs))",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "输出结果是:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[-0.0739, 0.0713],\n [-0.0748, 0.0703],\n [-0.0749, 0.0702],\n [-0.0760, 0.0685],\n [-0.0763, 0.0679],\n [-0.0754, 0.0693]], grad_fn=<MmBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "注意SelfAttention_v1 和 SelfAttention_v2 的输出不同因为它们采用了不同的权重矩阵初始方案。nn.Linear 采用了比 nn.Parameter(torch.rand(d_in, d_out)) 更为复杂的权重初始化方案。",
"metadata": {}
},
{
"cell_type": "markdown",
"source": "### 练习 3.1 比较 SelfAttention_v1 和 SelfAttention_v2\n\n请注意SelfAttention_v2 中的 nn.Linear 使用了与 SelfAttention_v1 中的 nn.Parameter(torch.rand(d_in, d_out)) 不同的权重初始化方案,这导致两种机制产生不同的结果。为了检查 SelfAttention_v1 和 SelfAttention_v2 的实现在其他方面是否相似我们可以将SelfAttention_v2对象的权重矩阵转移到SelfAttention_v1对象上以便两个对象生成相同的结果。\n\n你的任务是正确地将SelfAttention_v2实例的权重分配给SelfAttention_v1实例。为此你需要理解两种版本中权重之间的关系。提示nn.Linear以转置形式存储权重矩阵。完成权重分配后你应该会发现两个实例生成相同的输出。\n\n在下一节中我们将对自注意力机制进行增强特别是我们将结合因果关系和多头注意力元素。因果关系方面的改进涉及修改注意力机制以防止模型访问序列中的未来信息这对于语言建模等任务至关重要在这些任务中每个词的预测只能依赖于之前的词。\n\n多头注意力的组件涉及将注意力机制分成多个“头”。每个头学习数据的不同方面使模型能够同时关注来自不同表示子空间的不同位置的信息。这提高了模型在复杂任务中的表现。\n\n\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
}
]
}

View File

@ -0,0 +1,369 @@
{
"metadata": {
"kernelspec": {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat_minor": 4,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": "# 3.5 使用因果注意力机制隐藏后续词\n\n在本节中我们将修改标准的自注意力机制创建一个因果注意力Causal Attention机制这对于后续章节中大语言模型的开发至关重要。\n\n因果注意力也称为遮蔽注意力masked attention是自注意力的一种特殊形式。它限制模型在处理任何给定 Token 时,只考虑序列中之前和当前的输入。这与标准的自注意力机制形成对比,后者允许一次访问整个输入序列。\n\n因此在计算注意力得分时因果注意力机制确保模型只考虑序列中当前 Token 或之前出现的 Token。\n\n在类似 GPT 的大语言模型中,为了实现这一点,我们对每个处理的 Token 遮蔽掉输入文本中当前 Token 之后的后续 Token如图 3.19 所示。\n\n**图 3.19 在因果注意力中我们遮蔽掉对角线以上的注意力权重以便在计算上下文向量时大语言模型无法访问后续的Token。例如在第二行中对于单词“journey”我们只保留“Your”之前的单词和“journey”当前位置的注意力权重。**\n\n![3.19](../img/fig-3-19.jpg)\n\n正如图 3.19 所示,我们遮蔽了对角线以上的注意力权重,并标准化未遮蔽的注意力权重,使得每一行的注意力权重之和为 1。在下一节中我们将在代码中实现这种遮蔽和标准化的过程。\n\n## 3.5.1 应用因果注意力遮蔽\n\n在本节中我们将在代码中实现因果注意力遮蔽causal attention mask。我们从图 3.20 中总结的程序开始。\n\n**图 3.20 在因果注意力机制中获取遮蔽的注意力权重矩阵的一种方式是对注意力得分应用 softmax 函数,将对角线以上的元素归零并标准化结果矩阵。**\n\n![3.20](../img/fig-3-20.jpg)\n\n为了实现图3.20所示的因果注意力遮蔽步骤并获得遮蔽的注意力权重,让我们使用前一节中的注意力得分和权重来编码因果注意力机制。\n\n第一步如图 3.20 所示,我们使用 softmax 函数计算注意力权重,如前几节所做的那样:\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "queries = sa_v2.W_query(inputs) #A\nkeys = sa_v2.W_key(inputs) \nattn_scores = queries @ keys.T\nattn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=1)\nprint(attn_weights)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到以下的注意力权重:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[0.1921, 0.1646, 0.1652, 0.1550, 0.1721, 0.1510],\n [0.2041, 0.1659, 0.1662, 0.1496, 0.1665, 0.1477],\n [0.2036, 0.1659, 0.1662, 0.1498, 0.1664, 0.1480],\n [0.1869, 0.1667, 0.1668, 0.1571, 0.1661, 0.1564],\n [0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.1585],\n [0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],\n grad_fn=<SoftmaxBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "我们可以使用 PyTorch 的 tril 函数实现图 3.20 中的第二步,创建一个遮蔽,使得对角线以上的值为零:",
"metadata": {}
},
{
"cell_type": "code",
"source": "context_length = attn_scores.shape[0]\nmask_simple = torch.tril(torch.ones(context_length, context_length))\nprint(mask_simple)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到的遮蔽如下:",
"metadata": {}
},
{
"cell_type": "code",
"source": " tensor([[1., 0., 0., 0., 0., 0.],\n [1., 1., 0., 0., 0., 0.],\n [1., 1., 1., 0., 0., 0.],\n [1., 1., 1., 1., 0., 0.],\n [1., 1., 1., 1., 1., 0.],\n [1., 1., 1., 1., 1., 1.]])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "现在,我们可以将这个遮蔽与注意力权重相乘,将对角线以上的值归零:",
"metadata": {}
},
{
"cell_type": "code",
"source": "masked_simple = attn_weights*mask_simple\nprint(masked_simple)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "我们可以看到,对角线以上的元素已成功归零:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[0.1921, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.2041, 0.1659, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.2036, 0.1659, 0.1662, 0.0000, 0.0000, 0.0000],\n [0.1869, 0.1667, 0.1668, 0.1571, 0.0000, 0.0000],\n [0.1830, 0.1669, 0.1670, 0.1588, 0.1658, 0.0000],\n [0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],\n grad_fn=<MulBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "图 3.20 中的第三步是重新标准化注意力权重,使每一行的和再次为 1。我们可以通过将每一行中的每个元素除以该行的总和来实现这一点",
"metadata": {}
},
{
"cell_type": "code",
"source": "row_sums = masked_simple.sum(dim=1, keepdim=True)\nmasked_simple_norm = masked_simple / row_sums\nprint(masked_simple_norm)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到了一个注意力权重矩阵,其中对角线以上的注意力权重被归零,并且每行的和为 1",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],\n [0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],\n [0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],\n [0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],\n grad_fn=<DivBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "### 信息泄露\n\n当我们应用遮蔽然后重新标准化注意力权重时可能会出现后续的 Token我们打算遮蔽的的信息仍影响当前的 Token 的情况,这是因为它们的值是 softmax 函数计算的一部分。然而,关键点在于,当我们在遮蔽后重新标准化注意力权重时,我们实际上是在一个更小的子集上重新计算 softmax 函数(因为遮蔽位置不会对 softmax 值有任何贡献)。\n\nsoftmax的数学优雅之处在于尽管在最初的计算中分母包含了所有位置但在遮蔽和重新归一化之后被遮蔽的位置的影响被消除了————它们不会以任何有意义的方式对 softmax 得分产生影响。\n\n简而言之经过遮蔽和重新标准化后注意力权重的分布就好像一开始只在未遮蔽位置上计算一样。这确保了后续或其他遮蔽Token 的信息不会像我们想象的那样泄露。\n\n虽然此时技术上可以完成因果注意力的实现但我们可以利用 softmax 函数的一个数学特性,并以更少的步骤更有效地实现遮蔽注意力权重的计算,如图 3.21 所示。\n\n**图3.21 一种在因果注意力中获得掩蔽注意力权重矩阵的更高效方法,是在应用 softmax 函数之前,用负无穷大值遮蔽注意力得分。**\n\n![3.21](../img/fig-3-21.jpg)\n\nsoftmax 函数将其输入转换为概率分布。当一行中存在负无穷大(-∞值时softmax 函数将其概率视为零。(数学上,这是因为 e^-∞ 趋近于 0。\n\n我们可以通过创建一个对角线以上是 1 的遮蔽,然后将这些 1 替换为负无穷大(-inf值来实现这种更高效的遮蔽技巧\n\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "mask = torch.triu(torch.ones(context_length, context_length), diagonal=1)\nmasked = attn_scores.masked_fill(mask.bool(), -torch.inf)\nprint(masked)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到以下遮蔽:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[0.2899, -inf, -inf, -inf, -inf, -inf],\n [0.4656, 0.1723, -inf, -inf, -inf, -inf], [0.4594, 0.1703, 0.1731, -inf, -inf, -inf],\n [0.2642, 0.1024, 0.1036, 0.0186, -inf, -inf],\n [0.2183, 0.0874, 0.0882, 0.0177, 0.0786, -inf],\n [0.3408, 0.1270, 0.1290, 0.0198, 0.1290, 0.0078]],\n grad_fn=<MaskedFillBackward0>)\n",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "现在,我们只需对这些遮蔽结果应用 softmax 函数即可完成:",
"metadata": {}
},
{
"cell_type": "code",
"source": "attn_weights = torch.softmax(masked / keys.shape[-1]**0.5, dim=1)\nprint(attn_weights)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "可以看出,根据输出,每行的值之和为 1无需进一步标准化",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.5517, 0.4483, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.3800, 0.3097, 0.3103, 0.0000, 0.0000, 0.0000],\n [0.2758, 0.2460, 0.2462, 0.2319, 0.0000, 0.0000],\n [0.2175, 0.1983, 0.1984, 0.1888, 0.1971, 0.0000],\n [0.1935, 0.1663, 0.1666, 0.1542, 0.1666, 0.1529]],\n grad_fn=<SoftmaxBackward0>)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "我们现在可以使用修改后的注意力权重通过 context_vec = attn_weights @ values 来计算上下文向量,正如 3.4 节中所做的那样。\n\n在下一节中我们首先将介绍对因果注意力机制的另一个小调整这对于在训练大语言模型时减少过拟合非常有用。\n\n## 3.5.2 通过 Dropout 遮蔽额外的注意力权重\n\n在深度学习中Dropout 是一种技术即在训练过程中随机忽略选定的隐藏层单元有效地将它们“丢弃”。这种方法有助于防止过拟合确保模型不会过度依赖任何特定的隐藏层单元组。需要强调的是Dropout 仅在训练期间使用,在之后不可以使用。\n\n在包括 GPT 在内的 Transformer 架构中,注意力机制中的 Dropout 通常应用于两个特定区域:计算注意力得分之后,或将注意力权重应用于值向量之后。\n\n这里我们将在计算注意力权重后应用 Dropout 遮蔽,如图 3.22 所示,这是实践中更常见的变体。\n\n**图 3.22 利用因果注意力遮蔽(左上角),我们应用额外的 Dropout 遮蔽(右上角)来归零额外的注意力权重,以减少训练期间的过拟合。**\n\n![3.22](../img/fig-3-22.jpg)\n\n在以下代码示例中我们使用了 50% 的 Dropout 率,这意味着遮蔽掉一半的注意力权重。(在后面的章节中训练 GPT 模型时,我们将使用较低的 Dropout 率,例如 0.1 或 0.2。)\n\n在下面的代码中我们首先将 PyTorch 的 Dropout 实现应用于一个由 1 组成的 6x6 张量,用于示例说明:\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\ndropout = torch.nn.Dropout(0.5) #A\nexample = torch.ones(6, 6) #B\nprint(dropout(example))",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "可以看到,大约一半的值被归零了:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[2., 2., 0., 2., 2., 0.],\n [0., 0., 0., 2., 0., 2.],\n [2., 2., 2., 2., 0., 2.],\n [0., 2., 2., 0., 0., 2.],\n [0., 2., 0., 2., 0., 2.],\n [0., 2., 2., 2., 2., 0.]])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "当对注意力权重矩阵应用 50% 的 Dropout 率时,矩阵中一半的元素被随机设为零。为了补偿活跃元素的减少,矩阵中剩余元素的值被放大了 1/0.5 = 2 倍。这种放大对于保持注意力权重的整体平衡至关重要,它能确保在训练和推理阶段,注意力机制的平均影响保持一致。\n\n现在让我们将 Dropout 应用于注意力权重矩阵本身:",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\nprint(dropout(attn_weights))",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "处理后的注意力权重矩阵中有更多的元素被归零,剩余的元素被重新缩放:",
"metadata": {}
},
{
"cell_type": "code",
"source": " tensor([[2.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n [0.7599, 0.6194, 0.6206, 0.0000, 0.0000, 0.0000],\n [0.0000, 0.4921, 0.4925, 0.0000, 0.0000, 0.0000],\n [0.0000, 0.3966, 0.0000, 0.3775, 0.0000, 0.0000],\n [0.0000, 0.3327, 0.3331, 0.3084, 0.3331, 0.0000]],\n grad_fn=<MulBackward0>",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "请注意由于操作系统的不同Dropout 的输出结果可能会有所不同;关于这种不一致性的更多信息,您可以在 PyTorch 问题跟踪器上(https://github.com/pytorch/pytorch/issues/121595) 阅读。\n\n在了解了因果注意力和 Dropout 遮蔽后,我们将在下一节中开发一个简洁的 Python 类。这个类旨在便于高效地应用这两种技术。\n\n## 3.5.3 实现一个紧凑的 causal attention 类\n\n在这一节中我们将因果注意力和 Dropout 技术集成到我们在第 3.4 节开发的 SelfAttention Python 类中。这个类随后将作为在即将到来的章节中开发多头注意力( multi-head\n attention 的模板这是本章中我们将实现的最后的attention类。\n\n但在开始之前还有一件事要确保那就是代码能够处理由多个输入组成的批次以便 CausalAttention 类支持我们在第二章中实现的数据加载器生成的批次输出。\n\n为简化模拟这种批次输入我们复制输入的文本示例",
"metadata": {}
},
{
"cell_type": "code",
"source": "batch = torch.stack((inputs, inputs), dim=0)\nprint(batch.shape) #A ",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "这将生成一个三维张量,包含 2 个输入文本,每个文本有 6 个 Token每个标记是一个三维嵌入向量",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.Size([2, 6, 3])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "下面的 CausalAttention 类与我们之前实现的 SelfAttention 类相似,我们只是额外添加了下面代码中突出显示的 Dropout 和因果遮蔽部分:\n\n### 清单 3.3 一个紧凑的因果注意力类",
"metadata": {}
},
{
"cell_type": "code",
"source": "class CausalAttention(nn.Module):\n def __init__(self, d_in, d_out, context_length, dropout, qkv_bias=False)\n super().__init__()\n self.d_out = d_out\n self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.dropout = nn.Dropout(dropout) #A\n self.register_buffer(\n 'mask',\n torch.triu(torch.ones(context_length, context_length),\n diagonal=1)\n ) #B\n \n def forward(self, x):\n b, num_tokens, d_in = x.shape #C \nNew batch dimension b\n keys = self.W_key(x)\n queries = self.W_query(x)\n values = self.W_value(x)\n \n attn_scores = queries @ keys.transpose(1, 2) #C\n attn_scores.masked_fill_( #D\n self.mask.bool()[:num_tokens, :num_tokens], -torch.inf) \n attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=\n attn_weights = self.dropout(attn_weights)\n \n context_vec = attn_weights @ values\n return context_vec",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "虽然所有新增代码行和之前小节中的代码相似,但我们现在在 __init__ 方法中添加了一个 self.register_buffer() 调用。在 PyTorch 中使用 register_buffer 并不是所有情况下都必须的,但在这里有几个优点。例如,当我们在大型语言模型中使用 CausalAttention 类时缓冲区会随着模型自动移动到适当的设备CPU或GPU这在后续章节中训练大语言模型时会很有用。这意味着我们不需要手动确保这些张量与模型参数在同一设备上从而避免设备不匹配错误。\n\n我们可以像之前使用 SelfAttention 类 那样使用 CausalAttention 类:",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\ncontext_length = batch.shape[1]\nca = CausalAttention(d_in, d_out, context_length, 0.0)\ncontext_vecs = ca(batch)\nprint(\"context_vecs.shape:\", context_vecs.shape)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到的上下文向量是一个三维张量,其中每个 Token 现在由一个二维嵌入表示:",
"metadata": {}
},
{
"cell_type": "code",
"source": "context_vecs.shape: torch.Size([2, 6, 2])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "图 3.23 提供了一个能够总结我们到目前为止所完成内容的心智模型。\n\n图 3.23 一个概括了本章我们编写的四种不同注意力模块的心智模型。我们从一个简化的注意力机制开始,添加了可训练的权重,然后增加了因果注意力遮蔽。在本章的剩余部分,我们将扩展因果注意力机制,并编写多头注意力机制,这是我们在下一章的大语言模型实现中将使用的最终模块。\n\n![3.23](../img/fig-3-23.jpg)\n\n如图 3.23 所示,本节中,我们专注于神经网络中因果注意力的概念梳理和实现。在下一节中,我们将进一步扩展这一概念,实现一个多头注意力模块,该模块并行实现多个这样的因果注意力机制。\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
}
]
}

View File

@ -0,0 +1,220 @@
{
"metadata": {
"kernelspec": {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat_minor": 4,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": "# 3.6 将单头注意力扩展到多头注意力\n\n在本章的最后一节中我们将在多个头的基础上扩展之前实现的 CausalAttention 类。这被称为多头注意力机制Multi-head Attention。\n\n“多头”这一术语指的是将注意力机制分为多个“头”每个头独立运作。在这种情况下单个因果注意力模块可以被视为单头注意力其中只有一组注意力权重顺序处理输入。\n\n在以下小节中我们将从因果注意力扩展到多头注意力。第一小节将直观地通过堆叠多个 CausalAttention 模块来构建 Multi-head Attention 模块,用于示例说明。第二小节将以更复杂但计算上更高效的方式实现相同的多头注意力模块。\n\n## 3.6.1 堆叠多个 Single-head Attention 层\n\n在实际操作中实现多头注意力机制需要创建多个自注意力机制的实例如第 3.4.1 节图 3.18 所示),其中每个实例都有自己的权重,然后合并这些示例的输出。尽管使用多个自注意力机制实例计算量很大,但这对于像 Transformer 基础的大语言模型所需的复杂模式识别至关重要。\n\n图 3.24 展示了 Multi-head Attention 模块的结构,它由(如图 3.18 所示的)多个 Single-head Attention 模块堆叠而成。\n\n**图 3.24 这张图中的多头注意力模块由两个单头注意力模块堆叠在一起。因此,在一个具有两个头的多头注意力模块中,我们不再使用单个矩阵 Wv 来计算值矩阵而是使用两个值权重矩阵Wv1 和 Wv2 。同样地Wq 和 Wk 也各自有两组权重矩阵。我们得到两组上下文向量 Z1 和 Z2 ,然后将它们组合成一个上下文向量矩阵 Z 。**\n\n![3.24](../img/fig-3-24.jpg)\n\n如前所述多头注意力的主要思想是通过不同的、学习到的线性投影多次并行地运行注意力机制————即将输入数据如注意力机制中的查询、键和值向量与权重矩阵相乘。\n\n在代码中我们可以通过实现一个简单的 MultiHeadAttentionWrapper 类来实现这一点,该类堆叠了我们之前实现的多个 CausalAttention 模块实例:\n\n### 清单 3.4 实现 MultiHeadAttentionWrapper 类\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "class MultiHeadAttentionWrapper(nn.Module):\n def __init__(self, d_in, d_out, context_length,\n dropout, num_heads, qkv_bias=False):\n super().__init__()\n self.heads = nn.ModuleList(\n [CausalAttention(d_in, d_out, context_length, dropout, qkv_bias)\n for _ in range(num_heads)]\n )\n \n def forward(self, x):\n return torch.cat([head(x) for head in self.heads], dim=-1",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "例如,如果我们使用这个 MultiHeadAttentionWrapper 类并通过 num_heads=2 设置两个注意力头,且将 CausalAttention 的输出维度设置为2d_out=2这将导致一个四维的上下文向量 (d_out*num_heads=4),如图 3.25 所示。\n\n**图 3.25 使用 MultiHeadAttentionWrapper 我们指定了注意力头的数量num_heads。如果我们设置 num_heads=2如图所示我们将得到一个包含两组上下文向量矩阵的张量。在每个上下文向量矩阵中行表示对应于 Token 的上下文向量,列对应于通过 d_out=4 指定的嵌入维度。我们沿列维度连接这些上下文向量矩阵。由于我们有 2 个注意力头和嵌入维度为 2最终的嵌入维度为 2 × 2 = 4。**\n\n![3.25](../img/fig-3-25.jpg)\n\n为了进一步说明图 3.25,我们可以像之前使用 CausalAttention 类那样使用 MultiHeadAttentionWrapper 类:",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\ncontext_length = batch.shape[1] # This is the number of tokens\nd_in, d_out = 3, 2\nmha = MultiHeadAttentionWrapper(d_in, d_out, context_length, 0.0, num_heads=\ncontext_vecs = mha(batch)\n \nprint(context_vecs)\nprint(\"context_vecs.shape:\", context_vecs.shape)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "得到以下张量表示上下文向量:",
"metadata": {}
},
{
"cell_type": "code",
"source": " tensor([[[-0.4519, 0.2216, 0.4772, 0.1063],\n [-0.5874, 0.0058, 0.5891, 0.3257],\n [-0.6300, -0.0632, 0.6202, 0.3860],\n [-0.5675, -0.0843, 0.5478, 0.3589],\n [-0.5526, -0.0981, 0.5321, 0.3428],\n [-0.5299, -0.1081, 0.5077, 0.3493]],\n \n [[-0.4519, 0.2216, 0.4772, 0.1063],\n [-0.5874, 0.0058, 0.5891, 0.3257],\n [-0.6300, -0.0632, 0.6202, 0.3860],\n [-0.5675, -0.0843, 0.5478, 0.3589],\n [-0.5526, -0.0981, 0.5321, 0.3428],\n [-0.5299, -0.1081, 0.5077, 0.3493]]], grad_fn=<CatBackward0>)\n context_vecs.shape: torch.Size([2, 6, 4])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "第一个维度的上下文向量张量为 2因为我们有两个输入文本输入文本是复制的这就是为什么这些上下文向量对于它们来说完全相同。第二维度指的是每个输入中的 6 个 Token。第三维度指的是每个 Token 的四维嵌入。\n\n### 练习 3.2 返回二维嵌入向量\n\n更改 MultiHeadAttentionWrapper (..., num_heads=2) 调用的输入参数,使输出上下文向量为二维而不是四维,同时保持 num_heads=2 的设置。提示:你不需要修改类的实现、你只需要更改其他一个输入参数。\n\n在本节中我们实现了 MultiHeadAttentionWrapper ,它结合了多个 Single-head Attention 模块。请注意,这些要在 forward 方法中 [head(x) for head in self.heads] 顺序处理。我们可以通过并行处理头来改进这个实现。一种实现这点的方法是通过矩阵乘法同时计算所有注意力头的输出,我们将在下一节中探讨这一点。\n\n\n## 3.6.2 通过权重分割实现多头注意力\n\n在前一节中我们创建了一个 MultiHeadAttentionWrapper 来通过堆叠多个 Single-head Attention 模块实现多头注意力。这是通过实例化并组合几个 CausalAttention 对象完成的。\n\n我们可以将 MultiHeadAttentionWrapper 和 CausalAttention 这两个概念合并成一个单一的 MultiHeadAttentionWrapper 类,而不是同时保有两个单独的类。此外,除了将 MultiHeadAttentionWrapper 与 CausalAttention 的代码合并之外,我们还将进行一些其他修改以更有效地实现多头注意力机制。\n\n在 MultiHeadAttentionWrapper 中,通过创建一系列 CausalAttention 对象self.heads来实现多个头每个头代表一个单独的注意力头。 CausalAttention 类独立执行注意力机制,每个头的结果被连接起来。相比之下,下面的 MultiHeadAttention 类将多头功能集成在一个类中。它通过重塑投影的查询、键和值张量将输入分割成多个头,然后在计算注意力后组合这些头的结果。\n\n让我们在进一步讨论之前先看一下 MultiHeadAttention 类:\n\n### 清单 3.5 一个高效的 MultiHeadAttention 类\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "class MultiHeadAttention(nn.Module):\n def __init__(self, d_in, d_out, \n context_length, dropout, num_heads, qkv_bias=False):\n super().__init__()\n assert d_out % num_heads == 0, \"d_out must be divisible by num_heads\n self.d_out = d_out\n self.num_heads = num_heads\n self.head_dim = d_out // num_heads #A\n self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)\n self.out_proj = nn.Linear(d_out, d_out) #B\n self.dropout = nn.Dropout(dropout)\n self.register_buffer(\n 'mask',\n torch.triu(torch.ones(context_length, context_length), diagonal\n )\n \n def forward(self, x):\n b, num_tokens, d_in = x.shape\n keys = self.W_key(x) #C\n queries = self.W_query(x) #C\n values = self.W_value(x) #C\n \n keys = keys.view(b, num_tokens, self.num_heads, self.head_dim) #D\n values = values.view(b, num_tokens, self.num_heads, self.head_dim) #\n queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)\n \n keys = keys.transpose(1, 2) #E\n queries = queries.transpose(1, 2) #E\n values = values.transpose(1, 2) #E\n \n attn_scores = queries @ keys.transpose(2, 3) #F \n mask_bool = self.mask.bool()[:num_tokens, :num_tokens] #G\n \n attn_scores.masked_fill_(mask_bool, -torch.inf) #H\n \n attn_weights = torch.softmax(\n attn_scores / keys.shape[-1]**0.5, dim=-1)\n attn_weights = self.dropout(attn_weights)\n \n context_vec = (attn_weights @ values).transpose(1, 2) #I\n #J\n context_vec = context_vec.contiguous().view(b, num_tokens, self.d_ou\n context_vec = self.out_proj(context_vec) #K\n return context_vec",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "尽管 MultiHeadAttention 类中的张量重塑 (.view) 和转置 (.transpose) 看起来非常复杂但从数学上讲MultiHeadAttention 类实现的概念与之前的 MultiHeadAttentionWrapper 相同。\n\n从宏观层面上看在之前的 MultiHeadAttentionWrapper 中,我们堆叠了多个 Single-head Attention 层,然后将它们组合成一个 MultiHeadAttention 层。MultiHeadAttention 类采取了一种集成的方法。它从一个 Multi-head Attention 层开始,然后在内部将这个层分割成单独的注意力头,如图 3.26 所示。\n\n**图 3.26 在带有两个注意力头的 MultiHeadAttentionWrapper 类中,我们初始化了两个权重矩阵 Wq1 和 Wq2并计算了两个查询矩阵 Q1 和 Q2如图顶部所示。在 MultiHeadAttention 类中,我们初始化一个更大的权重矩阵 Wq只执行一次与输入的矩阵乘法以获得查询矩阵 Q然后将查询矩阵分割成 Q1 和 Q2如图底部所示。我们对键和值做同样的处理为了减少视觉混乱这部分处理没有显示出来。**\n\n![3.26](../img/fig-3-26.jpg)\n\n如图 3.26 所示,查询、键和值张量的分割是通过使用 PyTorch 的 .view 和 .transpose 方法进行张量重塑和转置操作来实现的。输入首先通过线性层转换(针对查询、键和值),然后被重塑来表示多个头。\n\n关键操作是将 d_out 维度分割为 num_heads 和 head_dim其中 head_dim = d_out / num_heads。这种分割随后通过 .view 方法实现:将维度为 (b, num_tokens, d_out) 的张量重塑为维度 (b, num_tokens, num_heads, head_dim)。\n\n随后张量被转置使得多头维度num_heads排在序列长度维度num_tokens之前形成 (b, num_heads, num_tokens, head_dim) 的结构。这种转置对于正确匹配不同头的查询、键和值,以及高效进行批量矩阵乘法至关重要。\n\n为了说明这种批量矩阵乘法假设我们有以下示例张量\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "a = torch.tensor([[[[0.2745, 0.6584, 0.2775, 0.8573], #A\n [0.8993, 0.0390, 0.9268, 0.7388],\n [0.7179, 0.7058, 0.9156, 0.4340]],\n \n [[0.0772, 0.3565, 0.1479, 0.5331],\n [0.4066, 0.2318, 0.4545, 0.9737],\n [0.4606, 0.5159, 0.4220, 0.5786]]]])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "现在,我们执行一个批量矩阵乘法,将张量本身和一个转置了最后两个维度的张量视图之间进行矩阵乘法:",
"metadata": {}
},
{
"cell_type": "code",
"source": "print(a @ a.transpose(2, 3))",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "结果如下:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[[[1.3208, 1.1631, 1.2879],\n [1.1631, 2.2150, 1.8424],\n [1.2879, 1.8424, 2.0402]],\n \n [[0.4391, 0.7003, 0.5903],\n [0.7003, 1.3737, 1.0620],\n [0.5903, 1.0620, 0.9912]]]])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "在该例中PyTorch 中的矩阵乘法实现可以处理四维输入张量使得矩阵乘法在最后两个维度num_tokens, head_dim之间进行然后针对各个头重复执行。\n\n例如上述方法可以更简洁地分别计算每个头的矩阵乘法",
"metadata": {}
},
{
"cell_type": "code",
"source": "first_head = a[0, 0, :, :]\nfirst_res = first_head @ first_head.T\nprint(\"First head:\\n\", first_res)\n\nsecond_head = a[0, 1, :, :]\nsecond_res = second_head @ second_head.T\nprint(\"\\nSecond head:\\n\", second_res)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "结果与我们之前使用批量矩阵乘法 print(a @ a.transpose(2, 3)) 获得的结果完全相同:",
"metadata": {}
},
{
"cell_type": "code",
"source": "First head:\n tensor([[1.3208, 1.1631, 1.2879],\n [1.1631, 2.2150, 1.8424],\n [1.2879, 1.8424, 2.0402]])\n\nSecond head:\n tensor([[0.4391, 0.7003, 0.5903],\n [0.7003, 1.3737, 1.0620],\n [0.5903, 1.0620, 0.9912]])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "继续进行 MultiHeadAttention 的操作,计算完注意力权重和上下文向量后,所有头部的上下文向量被重新转置为 (b, num_tokens, num_heads, head_dim) 的形状。然后,这些向量被重塑(扁平化)成 (b, num_tokens, d_out) 的形状,有效地合并了所有头部的输出。\n\n此外在将头部合并后我们在 MultiHeadAttention 中添加了一个所谓的输出投影层self.out_proj这在因果注意力类中并不存在。这个输出投影层虽然不是绝对必要的更多细节请参见附录 B 的参考文献部分),但它在许多大语言模型架构中常见,这也是为什么我们在这里添加它以保持完整性。\n\n尽管由于额外的重塑和张量转置 MultiHeadAttention 类看起来比 MultiHeadAttentionWrapper 更复杂,但它更高效。原因是我们只需要一次矩阵乘法就可以计算出键,例如 keys = self.W_key(x)(对查询和值同样适用)。在 MultiHeadAttentionWrapper 中,我们需要重复这一矩阵乘法,这在计算上是最昂贵的步骤之一,需要为每一个注意力头重复。\n\n MultiHeadAttention 类可以像我们之前实现的 SelfAttention 和 CausalAttention 类一样使用:\n",
"metadata": {}
},
{
"cell_type": "code",
"source": "torch.manual_seed(123)\nbatch_size, context_length, d_in = batch.shape\nd_out = 2\nmha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)\ncontext_vecs = mha(batch)\nprint(context_vecs)\nprint(\"context_vecs.shape:\", context_vecs.shape)",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "从结果来看,输出维度直接由 d_out 参数控制:",
"metadata": {}
},
{
"cell_type": "code",
"source": "tensor([[[0.3190, 0.4858],\n [0.2943, 0.3897],\n [0.2856, 0.3593],\n [0.2693, 0.3873],\n [0.2639, 0.3928],\n [0.2575, 0.4028]],\n \n [[0.3190, 0.4858],\n [0.2943, 0.3897],\n [0.2856, 0.3593],\n [0.2693, 0.3873],\n [0.2639, 0.3928],\n [0.2575, 0.4028]]], grad_fn=<ViewBackward0>)\ncontext_vecs.shape: torch.Size([2, 6, 2])",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": "在本节中,我们实现了在即将到来的章节中用于实现和训练大语言模型的 MultiHeadAttention 类。请注意,虽然代码完全可用,但我们使用了相对较小的嵌入规模和注意力头数量,以保持输出的可读性。\n\n作为比较最小的 GPT-2 模型1.17亿参数)有 12 个注意力头和 768 的上下文向量嵌入规模。最大的 GPT 2 模型15 亿参数)有 25 个注意力头和 1600 的上下文向量嵌入规模。注意,在 GPT 模型中Token 输入和上下文嵌入的嵌入规模是相同的d_in = d_out。\n\n## 练习 3.3 初始化具有 GPT-2 规模的注意力模块\n使用 MultiHeadAttention 类,初始化一个具有与最小 GPT-2 模型相同数量的注意力头12 个注意力头)的 MultiHeadAttention 模块。同时确保你使用与 GPT-2 相似的输入和输出嵌入规模768 维)。请注意,最小的 GPT-2 模型支持 1024 个 Token 的上下文长度。",
"metadata": {}
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
}
]
}

View File

@ -0,0 +1,39 @@
{
"metadata": {
"kernelspec": {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python"
},
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
}
},
"nbformat_minor": 4,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": "# 3.7 总结\n\n- 注意力Attention机制将输入元素转换为增强的上下文向量表示这些表示融合了所有输入的信息。\n\n- 自注意力Self Attention机制通过对输入的加权求和来计算上下文向量表示。\n\n- 在简化的注意力机制中,注意力权重通过点积计算得出。\n\n- 点积是将两个向量的相应元素相乘然后求和的简洁方式。\n\n- 虽然不是绝对必要,但矩阵乘法通过替代嵌套的 for 循环,帮助我们更高效、紧凑地实施计算。\n\n- 用于大语言模型的自注意力机制,也称为缩放点积注意力,其中包含了可训练的权重矩阵来计算输入的中间转换向量:查询、值和键。\n\n- 在处理从左到右阅读和生成文本的大语言模型时我们添加因果注意力遮蔽CausalAttention Mask以防止大语言模型访问后续的 Token 。\n\n- 除了使用因果注意力遮蔽将注意力权重归零外,我们还可以添加 Dropout 遮蔽来减少大语言模型中的过拟合问题。\n\n- 基于 Transformer 的大语言模型中的注意力模块涉及多个因果注意力CausalAttention实例这称为多头注意力MultiHeadAttention。\n\n- 我们可以通过堆叠多个 CausalAttention 模块来创建一个 MultiHeadAttention 模块。\n\n- 创建 MultiHeadAttention 模块的更有效方式涉及到批量矩阵乘法。",
"metadata": {}
},
{
"cell_type": "code",
"source": "",
"metadata": {
"trusted": true
},
"outputs": [],
"execution_count": null
}
]
}