llms-from-scratch-cn/Translated_Book/ch04/4.1.ipynb
2024-04-23 16:29:15 +08:00

465 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "bae559a1",
"metadata": {},
"source": [
"# 第四章 从头开始实现 GPT 模型以生成文本"
]
},
{
"cell_type": "markdown",
"id": "e3b02c54",
"metadata": {},
"source": [
"**本章介绍**"
]
},
{
"cell_type": "markdown",
"id": "65964c57",
"metadata": {},
"source": [
"- 编写类似 GPT 的大型语言模型 LLM 编码,该模型可以训练生成类似人类的文本 \n",
"- 规范化层激活以稳定神经网络训练 \n",
"- 在深度神经网络中添加快捷方式连接以更有效地训练模型 \n",
"- 实现 transformer 模块以创建各种大小的 GPT 模型 \n",
"- 计算 GPT 模型的参数数量和存储需求"
]
},
{
"cell_type": "markdown",
"id": "73c209fb",
"metadata": {},
"source": [
"\t\t在上一章中你学习并编写了多头注意力机制这是 LLM 的核心组件之一。在本章中,我们现在将对 LLM 的其他构建块进行编码,并将它们组装成一个类似 GPT 的模型,我们将在下一章中训练该模型以生成类似人类的文本,如图 4.1 所示。"
]
},
{
"cell_type": "markdown",
"id": "3c5efc3f",
"metadata": {},
"source": [
"图 4.1 对 LLM 进行编码的三个主要阶段的心智模型,在通用文本数据集上预训练 LLM并在标记数据集上对其进行微调。本章重点介绍如何实现 LLM 架构,我们将在下一章中对其进行培训。"
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "5f4fdd61",
"metadata": {},
"source": [
"![image-20240422133749839](../img/fig-4-1.png)"
]
},
{
"cell_type": "markdown",
"id": "783519e0",
"metadata": {},
"source": [
"\t\t图 4.1 中引用的 LLM 架构由几个构建块组成,我们将在本章中实现这些构建块。在下一节中,我们将从模型架构的自上而下的视图开始,然后再更详细地介绍各个组件。"
]
},
{
"cell_type": "markdown",
"id": "708fa8b4",
"metadata": {},
"source": [
"## 4.1 编写 LLM 架构"
]
},
{
"cell_type": "markdown",
"id": "1beee80f",
"metadata": {},
"source": [
"\t\tLLM例如 GPT代表 Generative Pretrained Transformer是大型深度神经网络架构旨在一次生成一个单词或标记的新文本。然而尽管它们的规模很大但模型架构并没有你想象的那么复杂因为它的许多组件都是重复的我们将在后面看到。图 4.2 提供了类似 GPT 的 LLM 的自上而下的视图,其中突出显示了其主要组件。"
]
},
{
"cell_type": "markdown",
"id": "f58ad472",
"metadata": {},
"source": [
"图 4.2 GPT 模型的心智模型。在嵌入层旁边,它由一个或多个变压器模块组成,其中包含我们在上一章中实现的掩蔽多头注意力模块。"
]
},
{
"cell_type": "markdown",
"id": "ee7b74da",
"metadata": {},
"source": [
"![image-20240422133908887](../img/fig-4-2.png)"
]
},
{
"cell_type": "markdown",
"id": "f7237970",
"metadata": {},
"source": [
"\t\t如图 4.2 所示,我们已经介绍了几个方面,例如输入标记化和嵌入,以及屏蔽的多头注意力模块。本章的重点将放在实现 GPT 模型的核心结构上,包括它的 transformer 模块,然后我们将在下一章中训练它以生成类似人类的文本。"
]
},
{
"cell_type": "markdown",
"id": "542d3ae9",
"metadata": {},
"source": [
"\t\t在前几章中为了简单起见我们使用了较小的嵌入维度确保概念和示例可以舒适地放在一个页面上。现在在本章中我们将扩展到一个小型 GPT-2 模型的大小,特别是具有 1.24 亿个参数的最小版本,正如 Radford 等人的论文“语言模型是无监督的多任务学习者”中所描述的那样。请注意,虽然原始报告提到了 1.17 亿个参数,但后来已更正。"
]
},
{
"cell_type": "markdown",
"id": "401bb3ab",
"metadata": {},
"source": [
"\t\t第 6 章将重点介绍如何将预训练的权重加载到我们的实现中,并将其调整为具有 345、762 和 15.42 亿个参数的大型 GPT-2 模型。在深度学习和 GPT 等 LLM 的上下文中,术语“参数”是指模型的可训练权重。这些权重本质上是模型的内部变量,在训练过程中进行调整和优化,以最小化特定的损失函数。这种优化允许模型从训练数据中学习。"
]
},
{
"cell_type": "markdown",
"id": "b075ff94",
"metadata": {},
"source": [
"\t\t例如在由 2,048x2,048 维权重矩阵(或张量)表示的神经网络层中,该矩阵的每个元素都是一个参数。由于有 2,048 行和 2,048 列,因此该图层中的参数总数为 2,048 乘以 2,048等于 4,194,304 个参数。"
]
},
{
"cell_type": "markdown",
"id": "7a9a4bae",
"metadata": {},
"source": [
"**GPT-2 与 GPT-3**"
]
},
{
"cell_type": "markdown",
"id": "3f0ffc50",
"metadata": {},
"source": [
"\t\t请注意我们之所以关注 GPT-2是因为 OpenAI 已经公开了预训练模型的权重,我们将在第 6 章将其加载到我们的实现中。GPT-3 在模型架构方面基本相同,只是它从 GPT-2 的 15 亿个参数扩展到 GPT-3 的 1750 亿个参数并且它使用更多的数据进行训练。在撰写本文时GPT-3 的权重尚未公开。GPT-2 也是学习如何实现 LLM 的更好选择,因为它可以在一台笔记本电脑上运行,而 GPT-3 需要 GPU 集群进行训练和推理。根据 Lambda Labs 的数据,在单个 V100 数据中心 GPU 上训练 GPT-3 需要 355 年,在消费级 RTX 8000 GPU 上训练 GPT-3 需要 665 年。"
]
},
{
"cell_type": "markdown",
"id": "47fa26bc",
"metadata": {},
"source": [
"\t\t我们通过以下 Python 字典指定小型 GPT-2 模型的配置,我们将在后面的代码示例中使用该字典:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb54f9ff",
"metadata": {},
"outputs": [],
"source": [
"GPT_CONFIG_124M = {\n",
"\t\"vocab_size\": 50257, # Vocabulary size\n",
" \"context_length\": 1024, # Context length\n",
" \"emb_dim\": 768, # Embedding dimension\n",
" \"n_heads\": 12, # Number of attention heads\n",
" \"n_layers\": 12, # Number of layers\n",
" \"drop_rate\": 0.1, # Dropout rate\n",
" \"qkv_bias\": False # Query-Key-Value bias\n",
"}"
]
},
{
"cell_type": "markdown",
"id": "9c5bdac9",
"metadata": {},
"source": [
"\t\t在GPT_CONFIG_124M词典中为了清楚起见我们使用简洁的变量名称并防止长代码行"
]
},
{
"cell_type": "markdown",
"id": "5578f3e3",
"metadata": {},
"source": [
"- “vocab_size”是指 50,257 个单词的词汇表,由第 2 章中的 BPE 分词器使用。\n",
"- “context_length”表示模型通过第 2 章中讨论的位置嵌入可以处理的最大输入标记数。\n",
"- “emb_dim”表示嵌入大小将每个标记转换为 768 维向量。\n",
"- “n_heads”表示第3章中实现的多头注意力机制中的注意力头计数。\n",
"- “n_layers”指定模型中变压器块的数量这将在后面的章节中详细阐述。\n",
"- “drop_rate”表示压差机制的强度0.1 表示隐藏单位下降 10%),以防止过拟合,如第 3 章所述。\n",
"- “qkv_bias”确定是否在多头注意力的线性层中包含偏向量以进行查询、键和值计算。按照现代 LLM 的规范,我们最初将禁用它,但当我们将 OpenAI 的预训练 GPT-2 权重加载到我们的模型中时,我们将在第 6 章中重新审视它。"
]
},
{
"cell_type": "markdown",
"id": "fc5fd3bf",
"metadata": {},
"source": [
"\t\t使用上面的配置我们将通过实现本节中的 GPT 占位符架构 DummyGPTModel 来开始本章,如图 4.3 所示。这将为我们提供一个全局视图,了解所有内容如何组合在一起,以及我们需要在即将到来的部分中编写哪些其他组件来组装完整的 GPT 模型架构。"
]
},
{
"cell_type": "markdown",
"id": "67566077",
"metadata": {},
"source": [
"图 4.3 一个心智模型,概述了我们对 GPT 架构进行编码的顺序。在本章中,我们将从 GPT 主干网(占位符架构)开始,然后再讨论各个核心部分,并最终将它们组装到最终 GPT 架构的 transformer 模块中。"
]
},
{
"cell_type": "markdown",
"id": "64a4b7c6",
"metadata": {},
"source": [
"![image-20240422134328260](../img/fig-4-3.png)"
]
},
{
"cell_type": "markdown",
"id": "dc685535",
"metadata": {},
"source": [
"\t\t图 4.3 中所示的编号框说明了我们处理编码最终 GPT 架构所需的各个概念的顺序。我们将从第 1 步开始,一个占位符 GPT 主干,我们称之为 DummyGPTModel"
]
},
{
"cell_type": "markdown",
"id": "f1f01dad",
"metadata": {},
"source": [
"**Listing 4.1 占位符 GPT 模型架构类**"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1406a604",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torch.nn as nn\n",
"class DummyGPTModel(nn.Module):\n",
" def __init__(self, cfg):\n",
" super().__init__()\n",
" self.tok_emb = nn.Embedding(cfg[\"vocab_size\"], cfg[\"emb_dim\"])\n",
" self.pos_emb = nn.Embedding(cfg[\"context_length\"], cfg[\"emb_dim\"])\n",
" self.drop_emb = nn.Dropout(cfg[\"drop_rate\"])\n",
" self.trf_blocks = nn.Sequential(\n",
" *[DummyTransformerBlock(cfg) for _ in range(cfg[\"n_layers\"])]) #A\n",
" self.final_norm = DummyLayerNorm(cfg[\"emb_dim\"]) #B\n",
" self.out_head = nn.Linear(\n",
" cfg[\"emb_dim\"], cfg[\"vocab_size\"], bias=False\n",
" )\n",
" def forward(self, in_idx):\n",
" batch_size, seq_len = in_idx.shape\n",
" tok_embeds = self.tok_emb(in_idx)\n",
" pos_embeds = self.pos_emb(torch.arange(seq_len, device=in_idx.device))\n",
" x = tok_embeds + pos_embeds\n",
" x = self.drop_emb(x)\n",
" x = self.trf_blocks(x)\n",
" x = self.final_norm(x)\n",
" logits = self.out_head(x)\n",
" return logits\n",
"class DummyTransformerBlock(nn.Module): #C\n",
" def __init__(self, cfg):\n",
" \tsuper().__init__()\n",
" def forward(self, x): #D\n",
" \treturn x\n",
"class DummyLayerNorm(nn.Module): #E\n",
" def __init__(self, normalized_shape, eps=1e-5): #F\n",
" \tsuper().__init__()\n",
" def forward(self, x):\n",
" \treturn x"
]
},
{
"cell_type": "markdown",
"id": "69969d27",
"metadata": {},
"source": [
"\t\t此代码中的 DummyGPTModel 类使用 PyTorch 的神经网络模块 nn.模块。DummyGPTModel 类中的模型架构由标记和位置嵌入、dropout、一系列转换器块 DummyTransformerBlock、最终层归一化 DummyLayerNorm 和线性输出层 out_head 组成。配置是通过 Python 字典传入的,例如,我们之前创建的 GPT_CONFIG_124M 字典。"
]
},
{
"cell_type": "markdown",
"id": "fcc70ece",
"metadata": {},
"source": [
"\t\tforward 方法描述了通过模型的数据流:它计算输入索引的标记和位置嵌入,应用 dropout通过 transformer 模块处理数据,应用归一化,最后使用线性输出层生成 logits。"
]
},
{
"cell_type": "markdown",
"id": "1a13ef4b",
"metadata": {},
"source": [
"\t\t上面的代码已经起作用了我们将在本节后面准备输入数据后看到。但是现在请注意在上面的代码中我们已经使用了占位符DummyLayerNorm 和 DummyTransformerBlock来实现转换器块和层规范化我们将在后面的章节中对其进行开发。"
]
},
{
"cell_type": "markdown",
"id": "98fee0c5",
"metadata": {},
"source": [
"\t\t接下来我们将准备输入数据并初始化一个新的 GPT 模型来说明它的用法。图 4.4 基于我们在第 2 章中看到的数字(我们对分词器进行编码)的基础上,提供了数据如何流入和流出 GPT 模型的高级概述。"
]
},
{
"cell_type": "markdown",
"id": "099417b4",
"metadata": {},
"source": [
"图 4.4 显示如何标记、嵌入和馈送到 GPT 模型的输入数据的大图概述。请注意,在我们之前编码的 DummyGPTClass 中,令牌嵌入是在 GPT 模型中处理的。在 LLM 中,嵌入的输入令牌维度通常与输出维度匹配。此处的输出嵌入表示我们在第 3 章中讨论的上下文向量。"
]
},
{
"cell_type": "markdown",
"id": "32ed7f95",
"metadata": {},
"source": [
"![image-20240422134652565](../img/fig-4-4.png)"
]
},
{
"cell_type": "markdown",
"id": "c79dcc6a",
"metadata": {},
"source": [
"\t\t为了实现图 4.4 中所示的步骤,我们使用第 2 章中介绍的 tiktoken 分词器对 GPT 模型的两个文本输入组成的批次进行分词化:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7ea35069",
"metadata": {},
"outputs": [],
"source": [
"import tiktoken\n",
"tokenizer = tiktoken.get_encoding(\"gpt2\")\n",
"batch = []\n",
"txt1 = \"Every effort moves you\"\n",
"txt2 = \"Every day holds a\"\n",
"\n",
"batch.append(torch.tensor(tokenizer.encode(txt1)))\n",
"batch.append(torch.tensor(tokenizer.encode(txt2)))\n",
"batch = torch.stack(batch, dim=0)\n",
"print(batch)"
]
},
{
"cell_type": "markdown",
"id": "7b93c468",
"metadata": {},
"source": [
"\t\t两个文本的结果令牌 ID 如下所示:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2633edec",
"metadata": {},
"outputs": [],
"source": [
"tensor([[ 6109, 3626, 6100, 345], #A\n",
" [ 6109, 1110, 6622, 257]])"
]
},
{
"cell_type": "markdown",
"id": "272f3aaf",
"metadata": {},
"source": [
"\t\t接下来我们初始化一个新的 1.24 亿参数 DummyGPTModel 实例,并向其提供标记化的批处理:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a33ee9db",
"metadata": {},
"outputs": [],
"source": [
"torch.manual_seed(123)\n",
"model = DummyGPTModel(GPT_CONFIG_124M)\n",
"logits = model(batch)\n",
"print(\"Output shape:\", logits.shape)\n",
"print(logits)"
]
},
{
"cell_type": "markdown",
"id": "416827b6",
"metadata": {},
"source": [
"\t\t模型输出通常称为 logit如下"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "94a253a1",
"metadata": {},
"outputs": [],
"source": [
"Output shape: torch.Size([2, 4, 50257])\n",
"tensor([[[-1.2034, 0.3201, -0.7130, ..., -1.5548, -0.2390, -0.4667],\n",
" [-0.1192, 0.4539, -0.4432, ..., 0.2392, 1.3469, 1.2430],\n",
" [ 0.5307, 1.6720, -0.4695, ..., 1.1966, 0.0111, 0.5835],\n",
" [ 0.0139, 1.6755, -0.3388, ..., 1.1586, -0.0435, -1.0400]],\n",
" [[-1.0908, 0.1798, -0.9484, ..., -1.6047, 0.2439, -0.4530],\n",
" [-0.7860, 0.5581, -0.0610, ..., 0.4835, -0.0077, 1.6621],\n",
" [ 0.3567, 1.2698, -0.6398, ..., -0.0162, -0.1296, 0.3717],\n",
" [-0.2407, -0.7349, -0.5102, ..., 2.0057, -0.3694, 0.1814]]],\n",
" grad_fn=<UnsafeViewBackward0>)"
]
},
{
"cell_type": "markdown",
"id": "0038eead",
"metadata": {},
"source": [
"\t\t输出张量有两行对应于两个文本样本。每个文本样本由 4 个标记组成;每个标记都是一个 50,257 维的向量,与标记器词汇表的大小相匹配。"
]
},
{
"cell_type": "markdown",
"id": "c96a3546",
"metadata": {},
"source": [
"\t\t嵌入有 50,257 个维度,因为每个维度都引用词汇表中的唯一标记。在本章的最后,当我们实现后处理代码时,我们将把这些 50,257 维的向量转换回标记 ID然后我们可以将其解码为单词。"
]
},
{
"cell_type": "markdown",
"id": "1ff3c403",
"metadata": {},
"source": [
"\t\t现在我们已经自上而下地了解了 GPT 架构及其 inand 输出,我们将在接下来的部分中对各个占位符进行编码,从实际层规范化类开始,该类将替换上一段代码中的 DummyLayerNorm。"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}