From 759bd920890e798078764632bc5a0b43c5074cc7 Mon Sep 17 00:00:00 2001 From: long_long_ago <149533107@qq.com> Date: Fri, 1 Aug 2025 11:33:51 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"REAEME=20=E6=96=87=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E3=80=8C=E5=9C=A8=E7=BA=BF=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E3=80=8D=E5=9C=B0=E5=9D=80=EF=BC=8C=E5=B9=B6=E4=B8=94?= =?UTF-8?q?=EF=BC=8C=E6=89=80=E6=9C=89=E5=9B=BE=E7=89=87=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=BA=E5=B1=85=E4=B8=AD=E3=80=82"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 ++-- cn-Book/1.理解大语言模型.md | 48 ++----- cn-Book/2.处理文本数据.md | 84 ++++-------- cn-Book/3.实现注意力机制.md | 120 +++++------------- ...从零开始实现一个用于文本生成的 GPT 模型.md | 72 +++-------- cn-Book/5.在无标记数据集上进行预训练.md | 68 +++------- cn-Book/6.用于分类任务的微调.md | 72 +++-------- cn-Book/7.指令遵循微调.md | 88 ++++--------- cn-Book/附录A.PyTorch简介.md | 52 ++------ cn-Book/附录D.给训练循环添加高级技巧.md | 8 +- cn-Book/附录E.使用LoRA的参数高效微调.md | 20 +-- 11 files changed, 169 insertions(+), 489 deletions(-) diff --git a/README.md b/README.md index 36e4736..7413757 100644 --- a/README.md +++ b/README.md @@ -35,20 +35,18 @@ ### 全书章节 -**在线阅读**:[Build a Large Language Model (From Scratch) 中文版](https://skindhu.github.io/Build-A-Large-Language-Model-CN/) - -+ [第一章:理解大语言模型](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/1.理解大语言模型.md) -+ [第二章:处理文本数据](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/2.处理文本数据.md) -+ [第三章:实现注意力机制](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/3.实现注意力机制.md) -+ [第四章:从零开始实现一个用于文本生成的 GPT 模型](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/4.从零开始实现一个用于文本生成的%20GPT%20模型.md) -+ [第五章:在无标记数据集上进行预训练](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/5.在无标记数据集上进行预训练.md) -+ [第六章:用于分类任务的微调](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/6.用于分类任务的微调.md) -+ [第七章:指令遵循微调](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/7.指令遵循微调.md) -+ [附录A:PyTorch简介](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/附录A.PyTorch简介.md) -+ [附录B:参考文献和扩展阅读](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/附录B.参考文献和扩展阅读.md) -+ [附录C:习题解答](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/附录C.习题解答.md) -+ [附录D:给训练循环添加高级技巧](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/附录D.给训练循环添加高级技巧.md) -+ [附录E:使用 LoRA 的参数高效微调](https://skindhu.github.io/Build-A-Large-Language-Model-CN/#/./cn-Book/附录E.使用LoRA的参数高效微调.md) ++ [第一章:理解大语言模型](./cn-Book/1.理解大语言模型.md) ++ [第二章:处理文本数据](./cn-Book/2.处理文本数据.md) ++ [第三章:实现注意力机制](./cn-Book/3.实现注意力机制.md) ++ [第四章:从零开始实现一个用于文本生成的 GPT 模型](./cn-Book/4.从零开始实现一个用于文本生成的%20GPT%20模型.md) ++ [第五章:在无标记数据集上进行预训练](./cn-Book/5.在无标记数据集上进行预训练.md) ++ [第六章:用于分类任务的微调](./cn-Book/6.用于分类任务的微调.md) ++ [第七章:指令遵循微调](./cn-Book/7.指令遵循微调.md) ++ [附录A:PyTorch简介](./cn-Book/附录A.PyTorch简介.md) ++ [附录B:参考文献和扩展阅读](./cn-Book/附录B.参考文献和扩展阅读.md) ++ [附录C:习题解答](./cn-Book/附录C.习题解答.md) ++ [附录D:给训练循环添加高级技巧](./cn-Book/附录D.给训练循环添加高级技巧.md) ++ [附录E:使用 LoRA 的参数高效微调](./cn-Book/附录E.使用LoRA的参数高效微调.md) ## 个人思考 diff --git a/cn-Book/1.理解大语言模型.md b/cn-Book/1.理解大语言模型.md index ce36f2d..2ca4fa5 100644 --- a/cn-Book/1.理解大语言模型.md +++ b/cn-Book/1.理解大语言模型.md @@ -51,9 +51,7 @@ LLM 采用了一种称为 Transformer 的架构(在第 1.4 节中将详细讨 由于 LLM 能够生成文本,因此它们通常被称为一种生成式人工智能 (AI),常缩写为生成 AI 或 GenAI。如图 1.1 所示,人工智能涵盖了创造能执行类似人类智能任务的更广泛领域,包括理解语言、识别模式和做出决策,并包括机器学习和深度学习等子领域。 -
- -
+ 用于实现人工智能的算法是机器学习领域的核心。机器学习往往不需要明确的编程实现,而是涉及可以从数据中学习并基于数据做出预测或决策的算法研究。举例来说,垃圾邮件过滤器就是机器学习的一个实际应用。与其手动编写规则来识别垃圾邮件,不如将标记为垃圾邮件和合法邮件的电子邮件示例输入给机器学习算法。通过最小化训练数据集上的预测误差,模型能够学习识别垃圾邮件的模式和特征,从而将新邮件分类为垃圾邮件或合法邮件。 @@ -71,9 +69,7 @@ LLM 采用了一种称为 Transformer 的架构(在第 1.4 节中将详细讨 由于具备解析和理解非结构化文本数据的高级能力,LLM 在多个领域有着广泛的应用。目前,LLM 被广泛用于机器翻译、新文本生成(见图 1.2)、情感分析、文本摘要等多种任务。最近,LLM 还被用于内容创作,比如撰写小说、文章,甚至计算机代码。 -
- -
+ LLM 还可以支持复杂的聊天机器人和虚拟助手,例如 OpenAI 的 ChatGPT 或谷歌的 Gemini(以前称为 Bard),这些助手能够回答用户的问题,并提升传统搜索引擎的功能,如 Google Search 和 Microsoft Bing。 @@ -102,9 +98,7 @@ LLM 还可以支持复杂的聊天机器人和虚拟助手,例如 OpenAI 的 C > + 全权重的微调,这种方式会在训练过程中对模型的所有预训练权重进行调整,但由于权重已经经过预训练,大多数情况下,微调只会对预训练权重进行微小调整,而不是大幅度改变。这种方式能够让模型保持原有的语言生成能力,同时使其在特定任务上表现得更好。 > + 冻结部分权重的微调,一般冻结低层(往往是学习到的基础语言特征),对高层的权重进行调整。这种微调方式常在需要加速训练,或者数据量较小,全权重微调可能导致过拟合的情况下使用。 -
- -
+ 如图 1.3 所示,创建 LLM 的第一步是用大量文本数据进行训练,这些数据一般被称为原始文本。这里的 "raw" 指的是这些数据只是普通文本,没有任何标注信息[^1] 。(可以进行过滤,比如去除格式字符或未知语言的文档。) @@ -122,9 +116,7 @@ LLM 的第一阶段训练被称为预训练,旨在创建一个初始的预训 大多数现代 LLM 基于 transformer 架构,这是一种深度神经网络架构,首次在 2017 年的论文《Attention Is All You Need》中提出。为了理解 LLM,我们需要简要回顾一下最初为机器翻译开发的原始 Transformer,该架构用于将英文文本翻译成德文和法文。图 1.4 显示了 Transformer 架构的简化版本。 -
- -
+ 图 1.4 中的 Transformer 架构由两个子模块组成:编码器和解码器。编码器模块处理文本输入,将其编码为一系列数值表示或向量,以捕捉输入的上下文信息。然后,解码器模块利用这些编码向量生成输出文本。例如,在翻译任务中,编码器将源语言文本编码为向量,而解码器则将这些向量解码为目标语言的文本。编码器和解码器都由多个层通过自注意力机制相连。您可能会对输入的预处理和编码过程有许多疑问,这些将在后续章节的逐步实现中详细解答。 @@ -142,17 +134,13 @@ BERT 是基于原始 Transformer 架构的编码器子模块,与 GPT 的训练 > > **个人思考:** 为什么BERT适合用于文档分类或情感预测,这主要是基于BERT的训练模式,BERT也是基于Transformer架构,但它采用的是 **masked language model (MLM)** 训练方式,即在训练过程中,它会随机遮掩输入句子中的一些词(称为“masked”),并让模型预测这些被遮掩的词。这种训练策略被称为**掩蔽词预测**。这一独特的训练方法使得 BERT 能够更好地理解句子的上下文,因为它需要根据整句话的前后部分来预测被遮掩的词。这种双向(bidirectional)的训练使得 BERT 更适合处理需要全局上下文理解的任务,而文档分类或情感预测正是两种对于上下文语义理解要求非常高的场景。 -
- -
+ 另一方面,GPT 专注于原始 Transformer 架构中的解码器部分,被设计用于需要生成文本的任务。这些任务包括机器翻译、文本摘要、小说创作和编写代码等。在本章接下来的部分,我们将更详细地讨论 GPT 架构,并在本书中从零开始实现它。 GPT 模型主要是为文本补全任务设计和训练的,但它们在能力上展现出显著的多样性。这些模型擅长执行zero-shot 和few-shot 学习任务。zero-shot 学习指的是在没有先前具体示例的情况下,能够处理完全未见过的任务。而few-shot 学习则是指模型可以从用户提供的极少量示例中进行学习,如图 1.6 所示。 -
- -
+ > [!NOTE] > @@ -166,9 +154,7 @@ GPT 模型主要是为文本补全任务设计和训练的,但它们在能力 流行的 GPT 和 BERT 类模型的大型训练数据集代表了丰富而全面的文本语料库,涵盖数十亿个单词,涉及各种主题以及自然语言和计算机语言。为了提供一个具体的例子,表 1.1 总结了用于预训练 GPT-3 的数据集,这个模型是第一版 ChatGPT 的基础。 -
- -
+ 通过表1.1能得出的主要结论是,这个训练数据集的规模和多样性使得这些模型在各种任务中表现优异,包括不同语言的语法、语义和上下文信息,甚至还可以处理一些需要通用知识的任务。 @@ -198,27 +184,15 @@ GPT 模型主要是为文本补全任务设计和训练的,但它们在能力 GPT-3 是该模型的增强版,具有更多参数,并在更大的数据集上进行训练。而在 ChatGPT 中提供的原始模型是通过在一个大型指令数据集上微调 GPT-3 而创建的,这一过程使用了 OpenAI 的 InstructGPT 论文中的方法,我们将在第 7 章“使用人类反馈进行微调以遵循指令”中详细介绍。如图 1.6 所示,这些模型在文本完成方面表现出色,并且还能够进行拼写纠正、分类和语言翻译等其他任务。考虑到 GPT 模型是在相对简单的下一个单词预测任务上进行预训练的,这一点确实非常惊人,如图 1.7 所示。 -
- -
+ 下一个单词预测任务是一种自监督学习的方法,这是一种自我标注的形式。这意味着我们不需要专门收集训练数据的标签,而是可以利用数据本身的结构:我们可以把句子或文档中的下一个单词作为模型需要预测的标签。由于下一个单词预测任务允许我们“动态”生成标签,因此我们可以利用大量未标记的文本数据集来训练 LLM,这在第 1.5 节中也有讨论,即利用大型数据集。 与我们在 1.4 节讨论的原始 Transformer 架构相比,通用 GPT 架构相对简单。实际上,它仅包含解码器部分,而没有编码器,如图 1.8 所示。由于像 GPT 这样的解码器模型是通过逐字预测生成文本,因此它们被视为一种自回归模型。自回归模型会将之前的输出作为未来预测的输入。因此,在 GPT 中,每个新词的选择都是基于之前的文本序列,这样可以提高生成文本的连贯性。 -> [!NOTE] -> -> 自回归,是一种用于`时间序列`分析的**统计技术**,它假设时间序列的`当前值`是其`过去值`的**函数**。 -> -> 自回归模型,使用类似的数学技术来确定序列中,**元素之间**的**概率相关性**。然后,它们使用所得知识,来猜测未知序列中的下一个元素。 -> -> 自相关,用于衡量序列中元素之间的相关性;一般会圈定一个时间窗口,计算窗口内元素之间的相关性。大部分场景下,窗口之前的元素,对窗口之后的元素影响较小。 - 像 GPT-3 这样的模型架构明显大于原始的 Transformer 模型。例如,原始的 Transformer 将编码器和解码器块重复了六次,而 GPT-3 具有 96 层 Transformer,总共有 1750 亿个参数。 -
- -
+ GPT-3 于 2020 年推出,按照深度学习和大语言模型(LLM)开发的标准,如今看来,已经是很久以前了。然而,像 Meta 的 Llama 模型这样的最新架构依然基于相同的基本原理,仅做了些许修改。因此,理解 GPT 的重要性依旧不减。本书将专注于实现 GPT 背后的核心架构,并提供有关其他 LLM 所采用的特定调整的参考。 @@ -232,9 +206,7 @@ GPT-3 于 2020 年推出,按照深度学习和大语言模型(LLM)开发 在本章中,我们为理解LLM打下了基础。在本书的其余部分,我们将从零开始编码一个 LLM,使用 GPT 的基本理念作为框架,并分为三个阶段进行,如图 1.9 所示。 -
- -
+ 首先,我们将学习基本的数据预处理步骤,并编写 LLM 核心的注意力机制代码。 diff --git a/cn-Book/2.处理文本数据.md b/cn-Book/2.处理文本数据.md index 0e4d2fc..1311885 100644 --- a/cn-Book/2.处理文本数据.md +++ b/cn-Book/2.处理文本数据.md @@ -25,15 +25,13 @@ -在上一章中,我们介绍了大语言模型(LLM)的基本结构,并了解到 LLM 用海量文本数据集进行`预训练`。我们特别关注仅用**解码器**(Transformer 架构下)的 LLM,这也是 ChatGPT 和其他流行 GPT 的 LLM 所依赖的模型。 +在上一章中,我们介绍了大语言模型(LLM)的基本结构,并了解到它们会基于海量的文本数据集进行预训练。我们特别关注的是仅使用通用 Transformer 架构中解码器部分的 LLM,这也是 ChatGPT 和其他流行的类似 GPT 的 LLM 所依赖的模型。 -在**预训练**阶段,LLM 逐字处理文本。通过**预测下一个单词任务**,来训练出拥有数百万到数十亿参数的 LLM,最终生成的模型具有出色的能力。随后可以进一步微调模型,以遵循指令或执行特定目标任务。然而,在我们接下来几章中实现和训练 LLM 之前,我们需要准备训练数据集,这也是本章的重点,如图 2.1 所示。 +在预训练阶段,LLM 逐字处理文本。通过使用下一个单词预测任务训练拥有数百万到数十亿参数的 LLM,最终能够生成具有出色能力的模型。这些模型随后可以进一步微调,以遵循指令或执行特定目标任务。然而,在我们接下来几章中实现和训练 LLM 之前,我们需要准备训练数据集,这也是本章的重点,如图 2.1 所示。 -
- -
+ -在本章中,您将学习如何为训练 LLM 准备输入文本。这包括将文本拆分为单个单词和子词token,并将这些token编码为 LLM 的向量表示。您还将了解一些先进的token分割方案,比如字节对编码,流行 LLM 中常用此类优化后的方案。最后,我们将实现一个采样和数据加载策略,以生成后续章节中训练 LLM 所需的输入输出数据对。 +在本章中,您将学习如何为训练 LLM 准备输入文本。这包括将文本拆分为单个单词和子词token,并将这些token编码为 LLM 的向量表示。您还将了解一些先进的token分割方案,比如字节对编码,这种方法在像 GPT 这样的流行 LLM 中得到应用。最后,我们将实现一个采样和数据加载策略,以生成后续章节中训练 LLM 所需的输入输出数据对。 @@ -43,9 +41,7 @@ 将数据转换为向量格式的过程通常被称为嵌入(Embedding)。我们可以通过特定的神经网络层或其他预训练的神经网络模型来对不同类型的数据进行嵌入,比如视频、音频和文本,如图 2.2 所示。 -
- -
+ 如图 2.2 所示,我们可以使用嵌入模型来处理多种不同的数据格式。然而,需要注意的是,不同的数据格式需要使用不同的嵌入模型。例如,专为文本设计的嵌入模型并不适用于音频或视频数据的嵌入。 @@ -76,9 +72,7 @@ 生成单词嵌入的算法和框架有很多。其中,Word2Vec是较早且最受欢迎的项目之一。Word2Vec通过预测给定目标词的上下文或反之,训练神经网络架构以生成单词嵌入。Word2Vec的核心思想是,出现在相似上下文中的词通常具有相似的含义。因此,当将单词投影到二维空间进行可视化时,可以看到相似的词汇聚在一起,如图2.3所示。 -
- -
+ 词嵌入可以具有不同的维度,从一维到数千维。如图2.3所示,我们可以选择二维词嵌入进行可视化。更高的维度可能捕捉到更细微的关系,但代价是计算效率的降低。 @@ -94,9 +88,7 @@ 本节将讨论如何将输入文本拆分为单个token,这是创建 LLM 嵌入所需的预处理步骤。这些token可以是单个单词或特殊字符,包括标点符号,具体如图 2.4 所示。 -
- -
+ 我们即将用于 LLM 训练的文本数据集是一部由 Edith Wharton 创作的短篇小说《判决》,该作品已在网上公开,因此允许用于 LLM 训练任务。该文本可在 Wikisource 上找到,网址是 [https://en.wikisource.org/wiki/The_Verdict](https://en.wikisource.org/wiki/The_Verdict),您可以将其复制并粘贴到文本文件中。我已将其复制到名为 "the-verdict.txt" 的文本文件中,以便使用 Python 的标准文件读取工具进行加载。 @@ -192,9 +184,7 @@ print(result) 如图 2.5 所示,我们的分词方案现在能够成功处理文本中的各种特殊字符。 -
- -
+ 现在我们已经有了一个基本的分词器,接下来让我们将其应用于艾迪丝·沃顿的整篇短篇小说: @@ -226,9 +216,7 @@ print(preprocessed[:30]) 为了将先前生成的token映射到token ID,我们首先需要构建一个词汇表。这个词汇表定义了每个独特单词和特殊字符与唯一整数的映射,如图 2.6 所示。 -
- -
+ 在前一章节中,我们将艾迪丝·华顿的短篇小说进行分词,并将其存储在名为 preprocessed 的 Python 变量中。现在,让我们创建一个包含所有唯一token的列表,并按字母顺序对其进行排序,以确定词汇表的大小: @@ -261,9 +249,7 @@ for i, item in enumerate(vocab.items()): 根据输出可知,词汇表包含了与唯一整数标签相关联的单个token。我们接下来的目标是利用这个词汇表,将新文本转换为token ID,如图 2.7 所示。 -
- -
+ 在本书后面,当我们想将 LLM 的输出从数字转换回文本时,我们还需要一种将token ID 转换为文本的方法。为此,我们可以创建一个词汇表的反向版本,将token ID 映射回相应的文本token。 @@ -300,9 +286,7 @@ class SimpleTokenizerV1: 使用上述的 SimpleTokenizerV1 Python 类,我们现在可以使用现有的词汇表实例化新的分词器对象,并利用这些对象对文本进行编码和解码,如图 2.8 所示。 -
- -
+ 让我们通过 SimpleTokenizerV1 类实例化一个新的分词器对象,并对艾迪丝·华顿的短篇小说中的一段文本进行分词,以便在实践中进行尝试: @@ -358,15 +342,11 @@ KeyError: 'Hello' 具体来说,我们将修改在前一节中实现的词汇表和分词器类(修改后的类命名为SimpleTokenizerV2),以支持两个新的token:<|unk|> 和 <|endoftext|>,具体见图 2.9。 -
- -
+ 如图2.9所示,我们可以修改分词器,以便在遇到不在词汇表中的单词时使用一个<|unk|> token。此外,我们还会在不相关的文本之间添加一个特殊的<|endoftext|> token。例如,在对多个独立文档或书籍进行GPT类大语言模型的训练时,通常会在每个文档或书籍之前插入一个token,以连接前一个文本源,如图2.10所示。这有助于大语言模型理解,尽管这些文本源在训练中是连接在一起的,但它们实际上是无关的。 -
- -
+ 现在,让我们修改词汇表,将这两个特殊token <|unk|> 和 <|endoftext|> 包含在内,方法是将它们添加到我们在上一节中创建的唯一单词列表中: @@ -553,9 +533,7 @@ print(strings) BPE背后的算法将不在其预定义词汇表中的单词分解为更小的子词单元甚至单个字符,使其能够处理超出词汇表的单词。因此,得益于BPE算法,如果分词器在分词过程中遇到一个不熟悉的单词,它可以将其表示为一系列子词token或字符,如图2.11所示。 -
- -
+ 如图 2.11 所示,将未知单词分解为单个字符的能力确保了分词器以及随之训练的 LLM 能够处理任何文本,即使文本中包含训练数据中不存在的单词。 @@ -607,9 +585,7 @@ BPE背后的算法将不在其预定义词汇表中的单词分解为更小的 这些输入-目标对是什么样的呢?正如我们在第一章中所学,LLM通过预测文本中的下一个单词进行预训练,如图2.12所示。 -
- -
+ 在本节中,我们将实现一个数据加载器,通过滑动窗口方法从训练数据集中提取图 2.12 所示的输入-目标对。 @@ -694,9 +670,7 @@ and established himself in ----> a 具体来说,我们的目标是返回两个张量:一个输入张量,包括 LLM 看到的文本,另一个目标张量,包含 LLM 需要预测的目标,如图 2.13 所示。 -
- -
+ 虽然图2.13展示了字符串格式的token以供说明,但代码实现将直接操作token ID,因为 BPE 分词器的 encode 方法将分词和转换为token ID 的过程合并为了一个步骤。 @@ -800,9 +774,7 @@ print(second_batch) 如果我们将第一个批次与第二个批次进行比较,可以看到第二个批次的token ID 相较于第一个批次右移了一个位置(例如,第一个批次输入中的第二个 ID 是 367,而它是第二个批次输入的第一个 ID)。步幅设置决定了输入在批次之间移动的位置数,模拟了滑动窗口的方法,如图 2.14 所示。 -
- -
+ > [!NOTE] > @@ -853,9 +825,7 @@ Targets: 为 LLM 准备训练集的最后一步是将token ID 转换为嵌入向量,如图 2.15 所示,这将是本章最后两部分的主要内容。 -
- -
+ 除了图 2.15 中概述的过程外,还需注意的是,我们首先会以随机值初始化这些嵌入权重。这一初始化为 LLM 的学习过程提供了起始点。我们将在第 5 章中优化嵌入权重,作为 LLM 训练的一部分。 @@ -869,7 +839,7 @@ Targets: > > GPT 类模型(以及其他深度神经网络)是基于大量的矩阵运算和数值计算构建的,尤其是神经元之间的连接权重和偏置在训练过程中不断更新。这些运算要求输入的数据是**数值形式的向量**,因为神经网络只能对数值数据进行有效计算,而无法直接处理原始的离散文字数据(如单词、句子)。 > -> + **向量表示**:通过将每个单词、句子或段落转换为连续向量(Embedding),可以在高维空间中表示文本的语义关系。例如,通过词嵌入(如 Word2Vec、GloVe)或上下文嵌入(如 GPT 中的词嵌入层),每个单词都被转换为一个向量,这个向量可以用于神经网络的计算。 +> + **向量表示: **通过将每个单词、句子或段落转换为连续向量(Embedding),可以在高维空间中表示文本的语义关系。例如,通过词嵌入(如 Word2Vec、GloVe)或上下文嵌入(如 GPT 中的词嵌入层),每个单词都被转换为一个向量,这个向量可以用于神经网络的计算。 > > 2. **向量嵌入的作用** > @@ -957,9 +927,7 @@ tensor([[ 1.2753, -0.2010, -0.1606], 输出矩阵中的每一行都是通过从嵌入权重矩阵进行查找操作获得的,如图2.16所示。 -
- -
+ 本节介绍了如何从token ID 创建嵌入向量。本章的下一节也是最后一节将对这些嵌入向量进行小的修改,以编码文本中token的位置信息。 @@ -971,17 +939,13 @@ tensor([[ 1.2753, -0.2010, -0.1606], 之前引入的嵌入层的工作方式是,无论token ID 在输入序列中的位置如何,相同的token ID 始终映射到相同的向量表示,如图 2.17 所示。 -
- -
+ 从原则上讲,确定性的、与位置无关的token ID 嵌入对于可重复性是有益的。然而,由于LLM的自注意力机制本身也是与位置无关的,因此向LLM注入额外的位置信息是有帮助的。 绝对位置嵌入与序列中的特定位置直接相关。对于输入序列中的每个位置,都会将一个唯一的绝对位置嵌入向量添加到token的嵌入向量中,以传达其确切位置。例如,第一个token将具有特定的位置嵌入,第二个token将具有另一个不同的嵌入,依此类推,如图2.18所示。 -
- -
+ 与关注token在序列中的绝对位置不同,相对位置嵌入强调的是token之间的相对位置或距离。这意味着模型学习的是“相隔多远”的关系,而不是“在什么确切位置”。这样的优势在于,即使模型在训练时没有接触过不同的长度,它也可以更好地适应各种长度的序列。 @@ -1076,9 +1040,7 @@ torch.Size([8, 4, 256]) 我们创建的 input_embeddings,如图 2.19 所示,现在可作为LLM的核心模块的输入嵌入。我们将在第3章开始实现这些模块。 -
- -
+ diff --git a/cn-Book/3.实现注意力机制.md b/cn-Book/3.实现注意力机制.md index 0014c8c..0054e13 100644 --- a/cn-Book/3.实现注意力机制.md +++ b/cn-Book/3.实现注意力机制.md @@ -37,17 +37,13 @@ 在本章中,我们将关注 LLM 架构中的重要组成部分,即注意力机制,如图 3.1 所示。 -
- -
+ 注意力机制是一个复杂的话题,因此我们将专门用一整章来讨论它。我们将注意力机制作为独立模块来研究,重点关注其内部的工作原理。在下一章中,我们将编写与自注意力机制相关的 LLM 的其他部分,以观察其实际运作并创建一个生成文本的模型。 本章中,我们将实现四种不同的注意力机制变体,如图 3.2 所示。 -
- -
+ 图 3.2 中展示的这些不同的注意力变体是逐步构建的,其目标是在本章末尾实现一个简单且高效的多头注意力机制,以便在下一章中可以将其整合到我们将编写的 LLM 架构中。 @@ -57,9 +53,7 @@ 在深入了解自注意力机制之前(这是大语言模型的核心),让我们先探讨一下缺乏注意力机制的架构存在哪些问题(这些架构在大语言模型之前已经存在)。假设我们想要开发一个将一种语言翻译成另一种语言的翻译模型。如图 3.3 所示,我们无法简单地逐词翻译文本,因为源语言和目标语言的语法结构往往存在差异。 -
- -
+ 为了解决逐词翻译的局限性,通常使用包含两个子模块的深度神经网络,即所谓的编码器(encoder)和解码器(decoder)。编码器的任务是先读取并处理整个文本,然后解码器生成翻译后的文本。 @@ -69,9 +63,7 @@ 在编码器-解码器架构的 RNN 网络中,输入文本被输入到编码器中,编码器按顺序处理文本内容。在每个步骤中,编码器会更新其隐状态(即隐藏层的内部值),试图在最终的隐状态中捕捉整个输入句子的含义,如图 3.4 所示。随后,解码器使用该最终隐状态来开始逐词生成翻译句子。解码器在每一步也会更新其隐状态,用于携带生成下一个词所需的上下文信息。 -
- -
+ 尽管我们不需要深入了解这些编码器-解码器架构的 RNN 的内部工作原理,但这里的关键思想在于,编码器部分将整个输入文本处理为一个隐藏状态(记忆单元)。解码器随后使用该隐藏状态生成输出。您可以将这个隐藏状态视为一个嵌入向量,这是我们在第 2 章中已讨论过的概念。 @@ -122,9 +114,7 @@ 因此,研究人员在 2014 年为 RNN 开发了所谓的 Bahdanau 注意力机制(该机制以论文的第一作者命名)。该机制对编码器-解码器架构的 RNN 进行了改进,使得解码器在每个解码步骤可以选择性地访问输入序列的不同部分,如图 3.5 所示。 -
- -
+ 有趣的是,仅仅三年后,研究人员发现构建用于自然语言处理的深度神经网络并不需要 RNN 结构,随后提出了基于自注意力机制的原始 Transformer 架构(在第 1 章中讨论),其灵感来自 Bahdanau 提出的注意力机制。 @@ -132,9 +122,7 @@ 本章将重点讲解并实现 GPT 类模型中使用的自注意力机制,如图 3.6 所示。在下一章中,我们将继续编码 LLM 的其它部分。 -
- -
+ @@ -156,9 +144,7 @@ 在本节中,我们实现了一个简化的自注意力机制版本,没有包含任何可训练的权重,如图 3.7 所示。本节的目标是先介绍自注意力机制中的一些关键概念,然后在 3.4 节引入可训练的权重。 -
- -
+ 图 3.7 显示了一个输入序列,记作 x,由 T 个元素组成,表示为 x(1) 到 x(T)。该序列通常代表文本,例如一个句子,并且该文本已被转换为 token 嵌入(不记得嵌入概念的请回顾第 2 章)。 @@ -188,9 +174,7 @@ inputs = torch.tensor( 实现自注意力机制的第一步是计算中间值 **ω**,即注意力得分,如图 3.8 所示。(请注意,图 3.8 中展示的输入张量值是截断版的,例如,由于空间限制,0.87 被截断为 0.8。在此截断版中,单词 "journey" 和 "starts" 的嵌入向量可能会由于随机因素而看起来相似)。 -
- -
+ 图 3.8 展示了如何计算查询 token 与每个输入 token 之间的中间注意力得分。我们通过计算查询 x(2) 与每个其他输入 token 的点积来确定这些得分: @@ -267,9 +251,7 @@ tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865]) 如图 3.9 所示,接下来,我们对先前计算的每个注意力分数进行归一化。 -
- -
+ 图3.9中所示的归一化的主要目的是使注意力权重之和为 1。这种归一化是一种有助于解释和保持LLM训练稳定性的惯例。以下是一种实现此归一化步骤的简单方法: @@ -345,22 +327,10 @@ Sum: tensor(1.) > + **便于优化**:在分类任务中,Softmax 输出的概率分布可与真实的标签概率进行比较,从而计算交叉熵损失。交叉熵损失的梯度较为稳定,便于模型的优化。 > + **概率解释**:Softmax 输出可以解释为“模型对每个类别的信心”,使得输出直观可理解。 > + **与交叉熵的结合**:Softmax 与交叉熵损失函数结合效果特别好,可以直接将模型预测的概率分布与真实标签比较,从而更快收敛,效果更好。 -> -> 4. **激活函数** -> -> 激活函数(`Activation Function`)是神经网络中的核心组件,它的作用类似于神经元的“**开关**”或“**过滤器**”,负责决定神经元**是否被激活**(即输出信号),以及**激活的程度**。 -> -> 在神经网络中,激活函数通常用于将输入信号转换为输出信号,从而实现**非线性变换**。 常见的激活函数包括: -> -> + **Sigmoid**:将输入信号转换为0到1之间的概率值,常用于二分类问题。 -> + **ReLU**:将输入信号转换为0到正无穷之间的值,常用于多分类问题。 -> + **Softmax**:将输入信号转换为0到1之间的概率值,常用于多分类问题。 现在我们已经计算出了归一化的注意力权重,接下来可以执行图 3.10 所示的最后一步:通过将嵌入后的输入 token x(i) 与相应的注意力权重相乘,再将所得向量求和来计算上下文向量 z(2)。 -
- -
+ 如图 3.10 所示,上下文向量 z(2) 是所有输入向量的加权和。其计算方法为将每个输入向量与对应的注意力权重相乘后相加。 @@ -386,15 +356,11 @@ tensor([0.4419, 0.6515, 0.5683]) 在前一节中,我们计算了第二个输入元素的注意力权重和上下文向量(如图 3.11 中的高亮行所示)。现在,我们将扩展该计算,以对所有输入计算注意力权重和上下文向量。 -
- -
+ 我们沿用之前的三个步骤(如图 3.12 所示),只是对代码做了一些修改,用于计算所有的上下文向量,而不仅仅是第二个上下文向量 z(2)。 -
- -
+ 如图 3.12 所示,在第 1 步中,我们添加了一个额外的 for 循环,用于计算所有输入对之间的点积。 @@ -510,9 +476,7 @@ tensor([[0.4421, 0.5931, 0.5790], 在本节中,我们将实现一种在原始 Transformer 架构、GPT 模型以及大多数其他流行的大语言模型中使用的自注意力机制。这种自注意力机制也被称为缩放点积注意力。图 3.13 提供了一个概念框架,展示了这种自注意力机制如何应用在在大语言模型的架构设计中。 -
- -
+ 如图 3.13 所示,带有可训练权重的自注意力机制是基于之前简化自注意力机制的改进:我们希望计算某个特定输入元素的嵌入向量的加权和来作为上下文向量。您将看到,与我们在 3.3 节中编码的简化自注意力机制相比,只有细微的差别。 @@ -526,9 +490,7 @@ tensor([[0.4421, 0.5931, 0.5790], 我们通过引入三个可训练的权重矩阵:Wq、Wk 和 Wv 来逐步实现自注意力机制。这三个矩阵用于将嵌入后的输入 token x(i) 映射为查询向量、键向量和值向量(如图 3.14 所示)。 -
- -
+ 在 3.3.1 节中,我们将第二个输入元素 x(2) 定义为查询(query),通过计算简化的注意力权重来得到上下文向量 z(2)。随后,在第 3.3.2 节中,我们将这一过程推广到整个输入句子 "Your journey starts with one step",为这六个词的输入句子计算所有的上下文向量 z(1) 到 z(T)。 同样地,为了便于说明,我们将先计算一个上下文向量 z(2)。接下来,我们将修改代码以计算所有的上下文向量。让我们从定义一些变量开始: @@ -600,9 +562,7 @@ values.shape: torch.Size([6, 2]) 接下来的第二步是计算注意力得分(如图 3.15 所示)。 -
- -
+ 首先,我们计算注意力得分ω22 : @@ -639,7 +599,7 @@ print(attn_score_22) > > **Kit = Wk * (Eit + Posit)** > -> 其中 **Ecat**和**Eit**是这两个词的嵌入向量,表示该词的基本语义信息,在不同的上下文中是固定的,根据公式可知,要使最终算出的**score_cat_it**与上下文语义相关,最重要的是**Wq** 和 **Wk** 这两个权重参数应该能反映出不同上下文语义的相关性。在标准的自注意力机制中,W、K、V向量都是固定的,然而,由于 GPT 模型是由多层自注意力模块堆叠而成,每一层都会根据当前输入和上下文信息,动态调整查询、键和值向量的**权重矩阵**。因此,即使初始的词嵌入和权重矩阵是固定的,经过多层处理后,模型能够生成与当前上下文相关的 Q、K、V 向量权重矩阵,最终计算出的Q、K、V 向量也就能反映出上下文的语义了。GPT多层的实现的细节后文会详述。 +> 其中 **Ecat**和**Eit**是这两个词的嵌入向量,表示该词的基本语义信息,在不同的上下文中是固定的,根据公式可知,要使最终算出的**score_cat_it**与上下文语义相关,最重要的是**Wq** 和**Wk**这两个权重参数应该能反映出不同上下文语义的相关性。在标准的自注意力机制中,W、K、V向量都是固定的,然而,由于 GPT 模型是由多层自注意力模块堆叠而成,每一层都会根据当前输入和上下文信息,动态调整查询、键和值向量的权重矩阵。因此,即使初始的词嵌入和权重矩阵是固定的,经过多层处理后,模型能够生成与当前上下文相关的 Q、K、V 向量权重矩阵,最终计算出的Q、K、V 向量也就能反映出上下文的语义了。GPT多层的实现的细节后文会详述。 我们可以再次通过矩阵乘法将其应用到所有注意力得分的计算: @@ -656,9 +616,7 @@ tensor([1.2705, 1.8524, 1.8111, 1.0795, 0.5577, 1.5440]) 第三步是将注意力得分转换为注意力权重,如图 3.16 所示。 -
- -
+ 接下来,如图 3.16 所示,我们通过缩放注意力得分并使用前面提到的 softmax 函数来计算注意力权重。与之前的不同之处在于,现在我们通过将注意力得分除以`keys`嵌入维度的平方根来进行缩放(注意,取平方根在数学上等同于指数为 0.5 的运算)。 @@ -693,9 +651,7 @@ tensor([0.1500, 0.2264, 0.2199, 0.1311, 0.0906, 0.1820]) 好了,我们只剩最后一步,也就是计算上下文向量,如图3.17所示。 -
- -
+ 与第 3.3 节中我们通过输入向量的加权和来计算上下文向量相似,现在我们通过值向量的加权和来计算上下文向量。这里,注意力权重作为加权因子,用于衡量每个值向量的重要性。与第 3.3 节类似,我们可以通过矩阵乘法一步得到输出结果: @@ -781,9 +737,7 @@ tensor([[0.2996, 0.8053], 图 3.18 概述了我们刚刚实现的自注意力机制。 -
- -
+ 如图3.18所示,自注意力机制涉及可训练的权重矩阵 Wq、Wk 和 Wv。这些矩阵将输入数据转换为查询、键和值,它们是自注意力机制的重要组成部分。随着训练过程中数据量的增加,模型会不断调整这些可训练的权重,在后续章节中我们会学习相关细节。 @@ -854,9 +808,7 @@ tensor([[-0.0739, 0.0713], 在 GPT 类大语言模型中,要实现这一点,我们需要对每个处理的 token 屏蔽其后续 token,即在输入文本中当前词之后的所有词,如图 3.19 所示。 -
- -
+ 如图 3.19 所示,我们对注意力权重的对角线上方部分进行了掩码操作,并对未掩码的注意力权重进行归一化,使得每一行的注意力权重之和为 1。在下一节中,我们将用代码实现这个掩码和归一化过程。 @@ -866,9 +818,7 @@ tensor([[-0.0739, 0.0713], 在本节中,我们将编码实现因果注意力掩码。我们首先按照图 3.20 中总结的步骤开始。 -
- -
+ 如图3.20总结,我们可以利用上一节的注意力得分和权重来实现因果注意力机制,以获得掩码后的注意力权重。 @@ -966,9 +916,7 @@ tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000], 尽管通过上文的方式我们已经完成了因果注意力的实现,但我们还可以利用 softmax 函数的数学特性,更高效地计算掩码后的注意力权重,减少计算步骤,具体实现如图 3.21 所示。 -
- -
+ Softmax 函数将输入值转换为概率分布。当一行中存在负无穷值(-∞)时,Softmax 函数会将这些值视为零概率。(从数学上讲,这是因为 e−∞ 接近于 0。) @@ -1023,9 +971,7 @@ Dropout 在深度学习中是一种技术,即在训练过程中随机忽略一 在这里,我们会在计算完注意力权重之后应用 dropout 掩码(如图 3.22 所示),因为在实际应用中这是更为常见的做法。 -
- -
+ 在以下代码示例中,我们使用了50%的 dropout 率,这意味着屏蔽掉一半的注意力权重。(在后续章节中训练 GPT 模型时,我们将使用更低的 dropout 率,比如 0.1 或 0.2) @@ -1071,11 +1017,11 @@ tensor([[2., 2., 0., 2., 2., 0.], > > 3. **缩放操作的作用** > -> 在应用 dropout 时,一部分注意力权重被随机置零(假设 dropout 率为 p)。剩余的权重会被放大,其放大倍数为 $ \frac{1}{1-p} $。放大后的权重记为 z′: +> 在应用 dropout 时,一部分注意力权重被随机置零(假设 dropout 率为 p)。剩余的权重会被放大,其放大倍数为 $` \frac{1}{1-p} `$。放大后的权重记为 z′: > > $$z_{i}^{\prime}=\frac{z_{i}}{1-p} \quad \text { (对于未被置零的权重) }$$ > -> 此时,未被置零的注意力权重 $ \mathbf{z}' $ 将作为 Softmax 的输入。因此,dropout 后的缩放对 Softmax 有两个主要影响: +> 此时,未被置零的注意力权重 z′\mathbf{z}'z′ 将作为 Softmax 的输入。因此,dropout 后的缩放对 Softmax 有两个主要影响: > > + **增大未遮盖值的相对差异**:放大剩余权重后,它们的数值相对于被置零的权重增大,从而拉大了非零元素之间的相对差异。这使得在 Softmax 计算中(通过前文提过的Softmax公式推导,输入值的**差异越大**,输出分布就会**越尖锐**;而输入值差异越小,输出分布就会越**平滑**),剩下的值之间的对比更明显。 > + **影响 Softmax 输出的分布形态**:当未被置零的权重值被放大后,它们在 Softmax 输出中会更具代表性,注意力分布会更集中(即更尖锐),让模型更关注特定的 token。 @@ -1193,9 +1139,7 @@ context_vecs.shape: torch.Size([2, 6, 2]) 图 3.23 提供了一个概念框架,总结了我们迄今为止完成的内容。 -
- -
+ 如图 3.23 所示,本节我们重点介绍了神经网络中的因果注意力的概念和实现。在下一节中,我们将进一步扩展这一概念,实现一个多头注意力模块,该模块可以并行实现多个因果注意力机制。 @@ -1217,9 +1161,7 @@ context_vecs.shape: torch.Size([2, 6, 2]) 图3.24展示了多头注意力模块的结构,该模块由多个单头注意力模块组成,如图3.18所示,彼此堆叠在一起。 -
- -
+ 如前所述,多头注意力机制的核心思想是在并行运行多个注意力机制的过程中,对输入数据(如注意力机制中的 query、key 和 value 向量)使用不同的、可学习的线性投影。具体来说,就是将这些输入数据与权重矩阵相乘,得到不同的投影结果。 @@ -1241,9 +1183,7 @@ class MultiHeadAttentionWrapper(nn.Module): 例如,如果我们使用这个 MultiHeadAttentionWrapper 类,并通过设置 num_heads=2 使用两个注意力头,同时将 CausalAttention 的输出维度 d_out 设置为 2,那么生成的上下文向量将是 4 维的(d_out*num_heads=4),如图 3.25 所示。 -
- -
+ 为了通过一个具体的例子进一步说明图 3.25,我们可以按如下方式使用 MultiHeadAttentionWrapper 类(使用方式类似于之前的 CausalAttention 类): @@ -1366,9 +1306,7 @@ class MultiHeadAttention(nn.Module): 从宏观层面上看,在之前的 MultiHeadAttentionWrapper 中,我们通过堆叠多个单头注意力层的方式来组合成一个多头注意力层。而 MultiHeadAttention 类采用了一种集成的方法:它从一个多头注意力层开始,并在内部将该层分解为各个独立的注意力头,如图 3.26 所示。 -
- -
+ 如图 3.26 所示,query、key 和 value 张量的拆分是通过张量的重塑和转置操作实现的,这些操作分别使用了 PyTorch 的 `.view` 和 `.transpose` 方法。首先,通过线性层对输入进行投影(分别生成 query、key 和 value),然后将其重塑为多个注意力头的形式。 diff --git a/cn-Book/4.从零开始实现一个用于文本生成的 GPT 模型.md b/cn-Book/4.从零开始实现一个用于文本生成的 GPT 模型.md index 41d6a5f..0b7bbb4 100644 --- a/cn-Book/4.从零开始实现一个用于文本生成的 GPT 模型.md +++ b/cn-Book/4.从零开始实现一个用于文本生成的 GPT 模型.md @@ -24,9 +24,7 @@ 在上一章中,我们学习并实现了多头注意力机制,这是大语言模型(LLM)的核心组件之一。本章将进一步实现 LLM 的其他组件,并将它们组装成一个与 GPT 类似结构的模型。我们将在下一章中训练该模型,以生成类人文本,具体过程如图 4.1 所示。 -
- -
+ 大语言模型(LLM)架构(见图 4.1)由多个模块构成,我们将在本章中实现这些模块。接下来的内容,我们首先从整体视角介绍模型架构,然后详细讲解各个组件。 @@ -36,9 +34,7 @@ LLM(如GPT,即生成式预训练 Transformer,Generative Pretrained Transformer)是一种大型深度神经网络架构,设计用于逐词(或逐 token)生成新文本。然而,尽管模型规模庞大,其结构却并没有想象中那么复杂,因为模型的许多组件是重复的(后文将对此展开说明)。图 4.2 展示了一个类 GPT 的 LLM 的整体视图,并突出了其主要组成部分。 -
- -
+ 如图 4.2 所示,我们已经在之前的章节中讲解过几个模块,如输入的分词和嵌入,以及掩码多头注意力模块。本章的重点是实现 GPT 模型的核心结构(包括 Transformer 模块)。我们将在下一章对该模型进行训练,使其能够生成类人文本。 @@ -79,9 +75,7 @@ GPT_CONFIG_124M = { 使用上述配置,我们将从本章开始实现一个GPT占位架构(DummyGPTModel),如图4.3所示。这将为我们提供一个全局视图,了解所有组件如何组合在一起,以及在接下来的章节中需要编写哪些其他组件来组装完整的GPT模型架构。 -
- -
+ 图 4.3 中显示的编号框说明了我们编写最终 GPT 架构所需理解的各个概念的顺序。我们将从第 1 步开始,这是一个我们称之为 DummyGPTModel 的 GPT 占位架构: @@ -145,9 +139,7 @@ class DummyLayerNorm(nn.Module): #E 接下来,我们将准备输入数据并初始化一个新的 GPT 模型,以展示它的用法。基于第二章实现的分词器,图 4.4 展示了数据在 GPT 模型中流入和流出的整体流程。 -
- -
+ 根据图 4.4 的步骤,我们使用第 2 章介绍的 tiktoken 分词器对包含两个文本的批量输入进行分词,以供 GPT 模型使用: @@ -236,9 +228,7 @@ tensor([[[-1.2034, 0.3201, -0.7130, ..., -1.5548, -0.2390, -0.4667], 在我们用代码实现层归一化之前,先通过图 4.5 了解一下层归一化的工作原理。 -
- -
+ 我们可以通过以下代码重现图 4.5 中的示例,其中实现了一个具有 5 个输入和 6 个输出的神经网络层,并将其应用于两个输入样本: @@ -289,9 +279,7 @@ Variance: `dim` 参数用于指定张量中进行统计计算(如均值或方差)的维度,具体如图 4.6 所示。 -
- -
+ 如图 4.6 所示,对于二维张量(如矩阵),在进行均值或方差计算等操作时,使用 `dim=-1` 等同于使用 `dim=1`,因为 `-1` 指的是张量的最后一个维度,即二维张量中的列。在后续对 GPT 模型加入层归一化时,模型会生成形状为 `[batch_size, num_tokens, embedding_size]` 的三维张量,我们依然可以使用 `dim=-1` 对最后一个维度进行归一化,而无需将 `dim=1` 改为 `dim=2`。 @@ -389,9 +377,7 @@ Variance: 在本节中,我们介绍了实现 GPT 架构所需的一个基础模块(`LayerNorm`),如图 4.7 所示。 -
- -
+ 在下一节中,我们将探讨大语言模型中使用的 GELU 激活函数,它将替代我们在本节使用的传统 ReLU 函数。 @@ -454,9 +440,7 @@ plt.show() 如图 4.8 所示,ReLU 是一个分段线性函数,输入为正时输出输入值本身,否则输出零。而 GELU 是一种平滑的非线性函数,它近似于 ReLU,但在负值上也具有非零梯度。 -
- -
+ 如图 4.8 所示,GELU 的平滑性使其在训练过程中具有更好的优化特性,能够对模型参数进行更细微的调整。相比之下,ReLU 在零点处有一个拐角,这在网络深度较大或结构复杂时可能会增加优化难度。此外,ReLU 对所有负输入的输出为零,而 GELU 对负值允许一个小的非零输出。这意味着在训练过程中,接收负输入的神经元也能对学习过程产生一定的贡献,尽管贡献程度不及正输入。 @@ -481,9 +465,7 @@ def forward(self, x): 图 4.9 展示了当我们输入数据后,这个前馈网络内部如何调整嵌入维度。 -
- -
+ 按照图 4.9 中的示例,我们初始化一个新的 FeedForward 模块,设置 token 嵌入维度为 768,并输入一个包含 2 个样本且每个样本有 3 个 token 的数据集: @@ -516,17 +498,13 @@ torch.Size([2, 3, 768]) > > 将这种理解再应用到神经网络中,扩展后的高维空间可以让模型“看到”输入数据中更多的隐藏特征,提取出更丰富的信息。然后在收缩回低维度时,这些丰富的特征被整合到了输入的原始维度表示中,使模型最终的输出包含更多的上下文和信息。 -
- -
+ 此外,输入输出维度保持一致也有助于简化架构,方便堆叠多层(在后续的章节实现),无需调整各层维度,从而提升了模型的可扩展性。 如图4.11所示,我们目前已经实现了LLM 架构中的大部分模块。 -
- -
+ 下一节,我们将介绍“快捷连接”的概念,即在神经网络的不同层之间插入的连接结构,它对于提升深度神经网络架构的训练性能非常重要。 @@ -536,9 +514,7 @@ torch.Size([2, 3, 768]) 接下来,我们来讨论快捷连接(也称跳跃连接或残差连接)的概念。快捷连接最初是在计算机视觉中的深度网络(尤其是残差网络)提出的,用于缓解梯度消失问题。梯度消失是指在训练中指导权重更新的梯度在反向传播过程中逐渐减小,导致早期层(靠近输入端的网络层)难以有效训练,如图 4.12 所示。 -
- -
+ 如图 4.12 所示,快捷连接通过跳过一层或多层,为梯度提供一条更短的流动路径,这是通过将某层的输出加到后续层的输出上来实现的。因此,这种连接方式也称为跳跃连接。在反向传播中,快捷连接对保持梯度流动至关重要。 @@ -717,9 +693,7 @@ layers.4.0.weight has gradient mean of 1.3258541822433472 本节我们将实现 Transformer 模块,它是 GPT 和其他大语言模型架构的基本模块。这个在 124M 参数的 GPT-2 架构中重复了十几次的模块,结合了多头注意力、层归一化、dropout、前馈层和 GELU 激活等多个概念,详见图 4.13。在下一节中,我们将把这个 Transformer 模块连接到 GPT 架构的其余部分。 -
- -
+ 如图 4.13 所示,Transformer 模块结合了多个组件,包括第 3 章中的掩码多头注意力模块以及我们在 4.3 节中实现的前馈网络模块。 @@ -800,9 +774,7 @@ Transformer 模块结构中保持数据形状不变并非偶然,而是其设 在本节完成了 Transformer 模块的实现后,我们已经具备了实现 GPT 架构所需的全部基础模块(如图 4.14 所示)。 -
- -
+ 如图 4.14 所示,Transformer 模块由层归一化、带有 GELU 激活函数的前馈网络和快捷连接组成,这些内容在本章前面已经讨论过。正如我们将在接下来的章节中看到的,这个 Transformer 模块将构成我们要实现的 GPT 架构的核心部分。 @@ -816,9 +788,7 @@ Transformer 模块结构中保持数据形状不变并非偶然,而是其设 在我们通过代码构建 GPT-2 模型之前,先通过图 4.15 看一下模型的整体结构,该结构结合了本章目前为止介绍的所有概念。 -
- -
+ 如图 4.15 所示,我们在 4.5 节中编写的 Transformer 模块在 GPT 架构中会重复多次。在参数量为 1.24 亿的 GPT-2 模型中,该模块重复了 12 次,这一数量通过 `GPT_CONFIG_124M` 配置字典中的`n_layers`参数指定。在 GPT-2 最大的 15.42 亿参数模型中,Transformer 模块重复了 36 次。 @@ -986,9 +956,7 @@ Total size of the model: 621.83 MB 在本章的最后一节,我们将编写代码把 GPT 模型的张量输出转回文本。在开始之前,我们先简要回顾一下像 LLM 这样的生成模型是如何逐词生成文本的,如图 4.16 所示。 -
- -
+ 如图 4.16 所示,GPT 模型在给定输入上下文(例如 ‘Hello, I am’)后,逐步生成文本。每次迭代中,输入上下文会不断扩展,使模型能够生成连贯且符合上下文的内容。在第 6 次迭代时,模型已构建出完整句子 ‘Hello, I am a model ready to help.’。 @@ -996,9 +964,7 @@ Total size of the model: 621.83 MB GPT 模型从输出张量到生成文本的过程涉及几个步骤(如图 4.17 所示)。这些步骤包括解码输出张量、根据概率分布选择 token,并将其转化为可读文本。 -
- -
+ 图 4.17 详细展示了 GPT 模型根据输入生成下一个 token 的单步过程。 @@ -1038,9 +1004,7 @@ def generate_text_simple(model, idx, max_new_tokens, context_size): #A 我们可以使用 `generate_text_simple` 函数逐步生成 token ID,每次生成一个 token ID 并将其附加到上下文中。其具体过程详见图 4.18(每次迭代生成 token ID 的步骤详见图 4.17)。 -
- -
+ 如图 4.18 所示,我们以迭代的方式逐步生成 token ID。例如,在第 1 轮迭代中,模型接收到“Hello , I am”对应的 token 作为输入,预测下一个 token(ID 为 257,对应“a”),并将其添加到输入序列中。这个过程不断重复,直到模型在第六轮迭代后生成完整的句子“Hello, I am a model ready to help.”。 diff --git a/cn-Book/5.在无标记数据集上进行预训练.md b/cn-Book/5.在无标记数据集上进行预训练.md index eaa07fd..7e81c0f 100644 --- a/cn-Book/5.在无标记数据集上进行预训练.md +++ b/cn-Book/5.在无标记数据集上进行预训练.md @@ -32,9 +32,7 @@ 在之前的章节中,我们实现了数据采样、注意力机制,并编写了 LLM 的架构。本章的核心是实现训练函数并对 LLM 进行预训练,详见图 5.1。 -
- -
+ 如图5.1所示,我们将继续学习基本的模型评估技术,以衡量生成文本的质量,这对于在训练过程中优化 LLM 是非常必要的。此外,我们将讨论如何加载预训练权重,以便为接下来的微调提供坚实的基础。 @@ -50,9 +48,7 @@ 本章开篇,我们将基于上一章的代码设置 LLM 进行文本生成,并讨论如何对生成文本质量进行评估的基本方法。而本章剩余部分的内容请参考图5.2。 -
- -
+ 如图 5.2 所示,接下来的小节我们首先简要回顾上一章末尾的文本生成过程,然后深入探讨文本评估及训练和验证损失的计算方法。 @@ -88,9 +84,7 @@ model.eval() 我们通过前一章节中介绍的 generate_text_simple 函数来使用 GPTmodel 实例,同时引入了两个实用函数:text_to_token_ids 和token_ids_to_text。这些函数简化了文本与 token 表示之间的转换,本章中我们将多次使用这种技术。图 5.3 可以帮助我们更清楚地理解这一过程。 -
- -
+ 图 5.3 展示了使用 GPT 模型生成文本的三个主要步骤。首先,分词器将输入文本转换为一系列 token ID(在第 2 章中已有讨论)。然后,模型接收这些 token ID 并生成对应的 logits(即词汇表中每个 token 的概率分布,具体见第 4 章)。最后,将 logits 转换回 token ID,分词器将其解码为可读的文本,完成从文本输入到文本输出的循环。 @@ -141,9 +135,7 @@ Output text: 图 5.4 展示了从输入文本到 LLM 生成文本的整体流程,该流程通过五个步骤实现。 -
- -
+ 图 5.4 展示了第 4 章中`generate_text_simple`函数内部的本生成过程。在后续章节中计算生成文本的质量损失之前,我们需要先执行这些初始步骤。 @@ -216,17 +208,13 @@ Outputs batch 1: Armed heNetflix 可以看到,模型生成的文本与目标文本不同,因为它尚未经过训练。接下来,我们将通过‘损失’来数值化评估模型生成文本的质量(详见图 5.5)。这不仅有助于衡量生成文本的质量,还为实现训练函数提供了基础,训练函数主要通过更新模型权重来改善生成文本的质量。 -
- -
+ 文本评估过程的一部分(如图 5.5 所示)是衡量生成的 token 与正确预测目标之间的差距。本章后面实现的训练函数将利用这些信息来调整模型权重,使生成的文本更接近(或理想情况下完全匹配)目标文本。 换句话说,模型训练的目标是提高正确目标 token ID 所在位置的 softmax 概率,如图 5.6 所示。接下来的部分中,我们还会将该 softmax 概率作为评价指标,用于对模型生成的输出进行数值化评估:正确位置上的概率越高,模型效果越好。 -
- -
+ 请注意,图 5.6 使用了一个包含 7 个 token 的简化词汇表,以便所有内容可以在一张图中展示。这意味着 softmax 的初始随机值会在 1/7 左右(约 0.14)。 @@ -263,9 +251,7 @@ Text 2: tensor([1.0337e-05, 5.6776e-05, 4.7559e-06]) 在本节剩余内容中,我们将针对`target_probas_1`和`target_probas_2`的概率得分计算损失。图 5.7 展示了主要步骤。 -
- -
+ 由于我们已经完成了图 5.7 中列出的步骤 1-3,得到了 `target_probas_1` 和 `target_probas_2`,现在进行第 4 步,对这些概率得分取对数: @@ -421,9 +407,7 @@ tensor(10.7940) 在本节中,我们首先准备训练和验证数据集,以用于后续 LLM 的训练。接着,我们计算训练集和验证集的交叉熵(如图 5.8 所示),这是模型训练过程中的重要组成部分。 -
- -
+ 为了计算训练集和验证集上的损失(如图 5.8 所示),我们使用了一个非常小的文本数据集,即伊迪丝·华顿的短篇小说《判决》,我们在第 2 章中已对此文本进行过处理。选择公共领域的文本可以避免任何关于使用权的担忧。此外,我们选择小数据集的原因在于,它允许代码示例在普通笔记本电脑上运行,即使没有高端 GPU 也能在几分钟内完成,这对于教学尤为有利。 @@ -463,9 +447,7 @@ Tokens: 5145 接下来,我们将数据集划分为训练集和验证集,并使用第二章的数据加载器为 LLM 训练准备需输入的批量数据。图 5.9 展示了该过程。 -
- -
+ 出于可视化的需要,图 5.9 将最大长度设置为 6。然而,在实际数据加载器中,我们会将最大长度设置为 LLM 支持的 256 个 token 的上下文长度,使得模型在训练时可以看到更长的文本。 @@ -618,9 +600,7 @@ Validation loss: 10.98110580444336 现在我们已经有了评估生成文本质量的方法,接下来我们将训练 LLM 以减少损失,从而提升文本生成的效果,如图 5.10 所示。 -
- -
+ 如图 5.10 所示,下一节将重点讲解 LLM 的预训练过程。在模型训练完成后,将应用不同的文本生成策略,并保存和加载预训练模型的权重。 @@ -630,9 +610,7 @@ Validation loss: 10.98110580444336 在本节中,我们将实现 LLM(基于GPTModel)的预训练代码。我们重点采用一种简单的训练循环方式来保证代码简洁易读(如图 5.11 所示)。不过,有兴趣的读者可以在附录 D 中了解更多高级技术,包括学习率预热、余弦退火和梯度裁剪等,以进一步完善训练循环。 -
- -
+ 图 5.11 中的流程图展示了一个典型的 PyTorch 神经网络训练流程,我们用它来训练大语言模型(LLM)。流程概述了 8 个步骤,从迭代各个 epoch 开始,处理批次数据、重置和计算梯度、更新权重,最后进行监控步骤如打印损失和生成文本样本。如果你对使用 PyTorch 如何训练深度神经网络不太熟悉,可以参考附录 A 中的 A.5 至 A.8 节。 @@ -793,9 +771,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses) 生成的训练损失和验证损失图表如图 5.12 所示。 -
- -
+ 如图 5.12 所示,训练损失和验证损失在第一个 epoch 开始时都有所改善。然而,从第二个 epoch 之后,损失开始出现分歧。验证损失远高于训练损失,这表明模型在训练数据上出现了过拟合。我们可以通过搜索生成的文本片段(例如“The Verdict”文件中的片段:“quite insensible to the irony”)来确认模型逐词记住了训练数据。 @@ -824,9 +800,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses) 在接下来的部分(如图 5.13 所示),我们将探讨 LLM 使用的采样方法,这些方法可以减轻记忆效应,从而生成更具新意的文本。 -
- -
+ 如图 5.13 所示,下一节将介绍适用于 LLM 的文本生成策略,以减少训练数据的记忆倾向,提升 LLM 生成文本的原创性。之后我们还会讨论权重的加载与保存,以及从 OpenAI 的 GPT 模型加载预训练权重。 @@ -974,9 +948,7 @@ plt.show() 图 5.14 展示了生成的图表: -
- -
+ 当 temperature 取 1 时,logits 在传递给 softmax 函数之前会除以 1,计算概率得分。这意味着,temperature 为 1 时相当于不进行任何缩放。在这种情况下,模型将根据原始的 softmax 概率,通过 PyTorch 中的`multinomial`函数来选择 token。 @@ -1013,9 +985,7 @@ plt.show() 在 top-k 采样中,我们可以将采样限制在最有可能的前 k 个 token 内,并通过将其他 token 的概率设为零,将它们排除在选择之外,如图 5.15 所示。 -
- -
+ 如图 5.15 所示,将所有未选中的 logits 替换为负无穷(-inf),这样在计算 Softmax 时,非 top-k 的 token 的概率为 0,剩下的概率之和为 1。(细心的读者可能记得,我们在第 3 章的因果注意力模块中使用过这种掩码技巧。) @@ -1159,9 +1129,7 @@ Every effort moves you stand to work on surprise, a one of us had gone with rand 如图 5.16 的章节概览所示,本节将介绍如何保存和加载预训练模型。然后,在接下来的部分中,我们将从 OpenAI 加载一个更强大的预训练 GPT 模型到我们的 GPTModel 实例中。 -
- -
+ 幸运的是,保存 PyTorch 模型相对简单。推荐的做法是保存模型的 `state_dict`(状态字典),这是一个字典,用于将模型的每一层映射到其对应的参数上,可以通过 `torch.save` 函数来实现,代码如下所示: @@ -1304,9 +1272,7 @@ Token embedding weight tensor dimensions: (50257, 768) 我们通过 `download_and_load_gpt2(model_size="124M", ...)` 加载了最小的 GPT-2 模型权重。此外,OpenAI 还提供了更大规模模型的权重,包括 "355M"、"774M" 和 "1558M" 等。尽管模型规模不同,但其整体架构是相同的,如图 5.17 所示。 -
- -
+ 如图 5.17 所示,不同大小的 GPT-2 模型在总体架构上保持一致,但注意力头和 Transformer 模块等组件的重复次数以及嵌入维度大小有所不同。本章的剩余代码也会兼容这些更大的模型。 diff --git a/cn-Book/6.用于分类任务的微调.md b/cn-Book/6.用于分类任务的微调.md index 19956f9..82c57b2 100644 --- a/cn-Book/6.用于分类任务的微调.md +++ b/cn-Book/6.用于分类任务的微调.md @@ -29,9 +29,7 @@ 在之前的章节中,我们实现了 LLM 的架构,进行了预训练,并学习了如何从外部来源(如 OpenAI)导入预训练权重。本章将在此基础上,通过微调 LLM 来完成特定目标任务,比如文本分类(见图 6.1)。我们将以一个具体的例子来说明如何将文本消息分类为垃圾短信或正常短信。 -
- -
+ 图 6.1 展示了微调 LLM 的两种主要方式:用于分类的微调(步骤 8)和用于指令遵循的微调(步骤 9)。在下一节中,我们将深入探讨这两种微调方式。 @@ -41,9 +39,7 @@ 微调语言模型最常见的方法是指令微调和分类微调。指令微调通过在一组任务上使用特定指令训练模型,用以提升模型对自然语言提示中任务描述的理解和执行能力,如图 6.2 所示。 -
- -
+ 下一章将讨论指令微调,相关内容在图 6.2 中有所展示。而本章的重点是分类微调,如果您有机器学习基础,可能已经对这一概念比较熟悉。 @@ -51,9 +47,7 @@ 但有一个关键点需要注意,经过分类微调的模型只能预测训练中遇到的类别。例如,它可以判断某内容是‘垃圾短信’还是‘非垃圾短信’(如图 6.3 所示),但不能对输入文本提供其他方面的信息。 -
- -
+ 与图6.3中所示的分类微调模型不同,指令微调模型通常可以执行更广泛的任务。分类微调模型可以视为高度专业化的模型,而相比之下,开发一个适用于各种任务的通用型模型通常更具挑战性。 @@ -71,9 +65,7 @@ 在本章的剩余部分,我们将对之前章节中实现并预训练的 GPT 模型进行修改和分类微调。我们从下载并准备数据集开始,如图 6.4 所示。 -
- -
+ 为了提供一个直观实用的分类微调示例,我们将采用一个包含垃圾消息和非垃圾消息的文本消息数据集。 @@ -128,9 +120,7 @@ df #A 保存的数据集如图 6.5 所示: -
- -
+ 我们来看一下数据集中类别标签的分布情况: @@ -240,9 +230,7 @@ test_df.to_csv("test.csv", index=None) 在实现细节上,我们可以在编码后的文本消息中添加与 `"<|endoftext|>"` 对应的 token ID,而不是直接将字符串 `"<|endoftext|>"` 附加到每条文本消息后,如图 6.6 所示。 -
- -
+ 图 6.6 假定 50,256 是填充 token `<|endoftext|>` 的 token ID。我们可以通过使用 tiktoken 包中的 GPT-2 分词器对 `<|endoftext|>` 进行编码来进一步验证此 token ID 是否正确(该分词器在前几章中已使用过): @@ -352,9 +340,7 @@ test_dataset = SpamDataset( 将以上的数据集作为输入,我们就可以实例化数据加载器(可以回顾第 2 章中的操作)。然而,在本例中,目标表示的是类别标签,而非文本中的下一个 token。例如,选择批量大小为 8 时,每个批次包含 8 个长度为 120 的训练样本和相应的类别标签,如图 6.7 所示。 -
- -
+ 以下代码创建了训练集、验证集和测试集的数据加载器,以批量大小为 8 加载文本消息及其标签(如图 6.7 所示): @@ -431,9 +417,7 @@ print(f"{len(test_loader)} test batches") 在本节中,我们将准备用于垃圾短信分类微调的模型。首先,我们初始化上一章使用过的预训练模型,如图 6.8 所示。 -
- -
+ 现在我们通过复用第 5 章的配置,开始进行模型准备过程: @@ -536,9 +520,7 @@ The following text 'spam'? Answer with 'yes' or 'no': 'You are a winner 本节我们将修改预训练的模型,为分类任务的微调做准备。为此,我们需要替换原始输出层,原输出层将隐层表示映射到50,257个词汇的词汇表,而我们用一个较小的输出层将其映射到两个类别:0(‘非垃圾短信’)和1(‘垃圾短信’),如图6.9所示。 -
- -
+ 如图 6.9 所示,我们使用与前几章相同的模型,唯一的不同是替换了输出层。 @@ -619,9 +601,7 @@ model.out_head = torch.nn.Linear( 此外,我们还需将最后一个 Transformer 模块以及连接该模块和输出层的 LayerNorm 模块配置为可训练,如图6.10所示。 -
- -
+ 为了让最终的 LayerNorm 和最后一个 Transformer 模块参与训练(如图 6.10 所示),我们将它们的 `requires_grad` 设置为 `True:` @@ -678,9 +658,7 @@ Outputs dimensions: torch.Size([1, 4, 2]) 请注意,我们希望微调该模型,使其能够输出一个分类标签,用于判断输入是否为垃圾短信。为实现这一点,我们不需要微调所有 4 行输出,只需聚焦于单个输出 token。具体来说,我们将重点关注最后一行对应的输出 token,如图 6.11 所示。 -
- -
+ ```python # To extract the last output token, illustrated in figure 6.11, from the output tensor, we use the following code: @@ -697,9 +675,7 @@ Last output token: tensor([[-3.5983, 3.9902]]) 在第 3 章中,我们探讨了注意力机制,该机制在每个输入 token 与其他所有输入 token 之间建立关系。随后,我们引入了因果注意力掩码的概念,这在 GPT 类模型中被广泛使用。这种掩码限制每个 token 的关注范围,使其只能关注当前位置及之前的内容,从而确保每个 token 只能受到自身及前面 token 的影响,如图 6.12 所示。 -
- -
+ 在图 6.12 所示的因果注意力掩码设置中,序列中的最后一个 token 聚合了所有前面 token 的信息。因此,在垃圾短信分类任务的微调过程中,我们会重点关注这个最后的 token。 @@ -717,9 +693,7 @@ Last output token: tensor([[-3.5983, 3.9902]]) 本章到目前为止,我们已完成了数据集准备、预训练模型的加载,以及对模型进行分类微调的修改。在微调正式开始前,还剩下一小部分工作:实现微调过程中使用的模型评估函数(如图 6.13 所示)。我们将在本节完成这一部分。 -
- -
+ 在实现评估工具之前,我们先简单讨论一下如何将模型输出转换为类别标签预测。 @@ -727,9 +701,7 @@ Last output token: tensor([[-3.5983, 3.9902]]) 模型对每个输入文本的最后一个 token 生成的输出被转换为概率得分。然后,通过查找概率得分中最高值的位置来确定对应的分类标签。请注意,由于模型尚未经过训练,目前对垃圾短信标签的预测是不准确的。 -
- -
+ 为了通过具体示例来说明图 6.14,我们来看一下前一节代码示例中的最后一个输出 token: @@ -879,9 +851,7 @@ Test loss: 2.322 在本节中,我们定义并使用训练函数,对预训练的 LLM 进行微调,以提升其垃圾短信分类的准确率。训练循环的整体结构与第 5 章中的相同(详见图 6.15),唯一的区别在于,这里计算的是分类准确率,而不是通过生成文本来评估模型。 -
- -
+ 可以看到,图 6.15 中所示的训练函数逻辑,与第 5 章中用于模型预训练的 `train_model_simple` 函数非常相似。 @@ -1032,9 +1002,7 @@ plot_values(epochs_tensor, examples_seen_tensor, train_losses, val_losses) 图6.16展示了最终的损失曲线。 -
- -
+ 从图 6.16 中陡峭的下降曲线可以看出,模型在训练数据上的学习效果很好,且没有明显的过拟合迹象,训练集和验证集的损失值几乎没有差距。 @@ -1054,9 +1022,7 @@ plot_values(epochs_tensor, examples_seen_tensor, train_accs, val_accs, label="ac The resulting accuracy graphs are shown in figure 6.17. ``` -
- -
+ 从图 6.17 的准确率曲线可以看出,模型在第 4 到 5 个训练周期后,训练和验证准确率均达到了较高水平。 @@ -1094,9 +1060,7 @@ Test accuracy: 95.67% 在前几节对模型进行微调和评估后,我们现在进入本章的最后阶段(见图 6.18):使用模型进行垃圾短信分类。 -
- -
+ 最后,我们将使用微调后的基于 GPT 的垃圾短信分类模型。以下的 `classify_review` 函数遵循了与本章之前实现的 `SpamDataset` 类似的数据预处理步骤。函数先将文本处理为 token ID,然后使用模型预测一个整数类别标签(与 6.6 节中的实现类似),并返回对应的类别名称: diff --git a/cn-Book/7.指令遵循微调.md b/cn-Book/7.指令遵循微调.md index 814d812..5171f73 100644 --- a/cn-Book/7.指令遵循微调.md +++ b/cn-Book/7.指令遵循微调.md @@ -30,9 +30,7 @@ 在之前的章节中,我们实现了 LLM 架构,完成了预训练,并将外部的预训练权重导入模型。接着,在上一章中,我们专注于对 LLM 进行特定分类任务的微调,即区分出正常短信和垃圾短信。在本章中,我们将介绍如何微调 LLM 以遵循人类指令(见图 7.1),这是开发用于聊天机器人、个人助理和其他对话任务的 LLM 的主要技术之一。 -
- -
+ 图 7.1 展示了微调 LLM 的两种主要方式:用于分类任务的微调(步骤 8)和用于指令遵循的微调(步骤 9)。上一章中我们已实现了步骤 8,本章将重点讲解如何使用指令数据集微调 LLM,具体过程将在下一节进一步说明。 @@ -46,15 +44,11 @@ 本章将专注于提升 LLM 遵循指令并生成理想回答的能力,如图 7.2 所示。 -
- -
+ 在本章的剩余部分,我们将逐步实现指令微调过程,首先从数据集准备开始,如图 7.3 所示。 -
- -
+ 数据集准备是指令微调中的关键环节,本章的大部分内容都将围绕这一过程展开。下一节将开始实现下载和格式化数据集的代码,这是数据集准备过程的第一步(如图 7.3 所示)。 @@ -130,9 +124,7 @@ antonym of 'complicated' is 'simple'."} 指令微调(instruction finetuning),也称为监督式指令微调(supervised instruction finetuning),是指在包含明确输入-输出对的数据集上对模型进行训练(例如从 JSON 文件中提取的输入-输出对)。在为大语言模型(LLM)格式化这些条目时,通常会使用多种不同的方法。图 7.4 展示了两种不同的示例格式(通常称为提示风格),这些格式常用于训练一些知名的 LLM,例如 Alpaca 和 Phi-3。Alpaca 是最早公开指令微调过程的 LLM 之一,而由微软开发的 Phi-3 则展示了提示风格的多样性。 -
- -
+ 本章其余部分将使用 Alpaca 风格的提示方式,这是最受欢迎的提示风格之一,主要是因为它帮助定义了最初的微调方法。 @@ -232,9 +224,7 @@ Test set length: 110 随着我们进入指令微调过程的实施阶段,接下来的步骤(如图 7.5 所示)将重点介绍如何高效地构建训练批次。这一步需要定义一种方法,以确保模型在微调过程中能够接收到格式化的训练数据。 -
- -
+ 在上一章中,训练批次是通过 PyTorch 的 `DataLoader` 类自动创建的,该类使用默认的`collate`函数将样本列表合并为批次。`collate ` 函数的作用是将单个数据样本列表合并成一个批次,以便模型在训练过程中能够高效处理。 @@ -242,15 +232,11 @@ Test set length: 110 本节将分几步介绍批处理过程(包括自定义`collate`函数的编写),具体内容如图 7.6 所示。 -
- -
+ 首先,为实现图 7.6 中展示的步骤 2.1 和 2.2,我们编写了一个 `InstructionDataset` 类,它应用了上一节中的 `format_input` 函数,并对数据集中的所有输入进行了预分词,类似于第 6 章中的 `SpamDataset`。这两个步骤的详细说明见图 7.7。 -
- -
+ 图 7.7 中展示的 两步操作通过 `InstructionDataset` 类的 `__init__` 构造函数实现。 @@ -293,9 +279,7 @@ The resulting token ID is 50256. 在第 6 章中,我们使用的填充方式是将数据集中的所有示例填充到相同长度。在本章中,我们将采用一种更为精细的方法,开发一个自定义的`collate`函数并传递给数据加载器。该自定义`collate`函数会将每个批次中的训练样本填充到相同长度,同时允许不同批次中的样本具有不同的长度,如图 7.8 所示。这种方法通过仅将序列扩展到每个批次中最长的序列长度,从而减少了不必要的填充,避免了对整个数据集进行冗余填充。 -
- -
+ 我们可以通过以下自定义`collate`函数来实现图 7.8 所示的填充过程: @@ -353,17 +337,13 @@ tensor([[ 0, 1, 2, 3, 4], 我们刚刚实现了自定义 `collate` 函数的第一个版本,用于从输入列表创建批次。然而,正如在第 5 章和第 6 章中所学的那样,我们还需要创建与输入 ID 批次相对应的目标 token ID 批次。图 7.9 显示了这些目标 ID,它们非常重要,因为它们代表我们希望模型生成的内容,并且在训练时用于计算损失,从而指导模型更新权重。这与之前章节的做法类似。 -
- -
+ 如图 7.9 所示,我们需要修改自定义的`collate`函数,使其在返回输入 token ID 的基础上,同时返回目标 token ID。 与第 5 章中描述的 LLM 预训练过程类似,目标 token ID 与输入 token ID 一一对应,但会右移一个位置,这种设置(如图 7.10 所示)使得 LLM 能够学习如何预测序列中的下一个 token。 -
- -
+ 以下为更新后的`collate`函数,它根据输入 token ID 生成目标 token ID(流程如图 7.10 所示): @@ -416,17 +396,13 @@ tensor([[ 1, 2, 3, 4, 50256], #B 关于这个过程的更多细节将在实施此修改后讨论。(在第 6 章中,我们无需担心这个问题,因为当时只训练了最后一个输出 token。) -
- -
+ 如图 7.11 所示,在步骤 2.4 中,我们将文本结束 token(之前用作填充 token,token ID 为 50256)在目标 token 列表中替换为 -100(选择 -100 作为替代值的原因将在后续说明)。 然而,请注意,我们在目标列表中仍保留了一个文本结束 token(ID 为 50256),如图 7.12 所示。这使得 LLM 能够学习在接收到指令时何时生成结束 token,以指示生成的响应已完成。 -
- -
+ 在以下代码中,我们修改了自定义的 `collate` 函数,将目标列表中 ID 为 50256 的 token 替换为 -100,图 7.12 展示了这一操作。此外,我们引入了一个 `allowed_max_length` 参数,用于选择性地限制样本的长度。当你使用的数据集超过 GPT-2 模型支持的 1024 个 token 的上下文长度时,这一调整将非常有用。更新后的 `collate` 函数代码如下: @@ -551,9 +527,7 @@ loss_1 == loss_3: tensor(True) 在实践中,除了遮蔽填充 token 外,还常常将指令部分对应的目标 token ID 一并遮蔽,如图 7.13 所示。 -
- -
+ 通过对指令部分对应的目标 token ID 进行掩码(如图 7.13 所示),交叉熵损失仅计算生成响应的目标 token ID,模型在训练时也会专注于生成准确的回答,而不是去记住指令内容,从而有助于减少过拟合。 @@ -571,9 +545,7 @@ loss_1 == loss_3: tensor(True) 在前一节中,我们完成了 `InstructionDataset` 类和 `custom_collate_fn` 函数的多个实现步骤。本节中,我们可以将 `InstructionDataset` 对象和 `custom_collate_fn` 函数直接传入 PyTorch 的数据加载器中(如图 7.14 所示)。加载器将自动对批次数据进行随机化和组织,为 LLM 的指令微调过程提供支持。 -
- -
+ 在我们实现图 7.14 中所示的数据加载器创建步骤之前,我们需要先简要讨论在前一节中实现的 `custom_collate_fn` 中的`device`参数设置。 @@ -677,9 +649,7 @@ torch.Size([8, 69]) torch.Size([8, 69]) 在正式开始指令微调之前,我们首先需要加载一个预训练的 GPT 模型,正如图 7.15 所示,该模型是我们希望进行微调的对象。 -
- -
+ 如 7.15 概述了完整的指令微调流程,本节重点介绍第 4 步,即加载预训练的 LLM ,作为指令微调的起点,过程与前几章类似。然而,这次我们加载的是 3.55 亿参数的中等模型,而非之前使用的 1.24 亿参数的小模型。选择更大模型的原因是 1.24 亿参数的小模型容量有限,难以通过指令微调获得令人满意的效果。” @@ -788,9 +758,7 @@ Convert the active sentence to passive: 'The chef cooks the 图 7.16 中的章节概述展示了本节的重点:对大语言模型(LLM)进行微调。我们将在上一节加载的预训练模型基础上,利用本章前面准备的指令数据集进一步训练该模型。 -
- -
+ 如前所述,我们在本章开头实现指令数据集处理时,已经完成了所有关键工作。对于微调过程本身,我们可以复用第 5 章中实现的损失计算和训练函数: @@ -830,9 +798,7 @@ Validation loss: 3.7619335651397705 表格 7.1 提供了在不同设备(包括 CPU 和 GPU)上训练每个模型的参考运行时间。在兼容的 GPU 上运行此代码无需修改代码,并且能够显著加快训练速度。对于本章展示的结果,我使用了 GPT-2 中型模型,并在 A100 GPU 上进行了训练。 -
- -
+ 模型和数据加载器准备好后,我们可以开始训练模型。以下代码设置了训练过程的各项配置,包括初始化优化器、设置训练轮次、定义评估频率,并基于之前提到的第一个验证集样本(val_data[0])来评估训练过程中生成的 LLM 响应: @@ -900,9 +866,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses) 由此生成的损失曲线如图 7.17 所示。 -
- -
+ 如图 7.17 的损失图所示,模型在训练集和验证集上的表现随着训练的进行显著提高。在初期阶段,损失的快速下降表明模型正在迅速学习数据中的有意义的模式和表示。随着训练进入第二个 epoch,损失继续减少,但速度放缓,表明模型正在微调其学习到的表示,并逐渐收敛到一个稳定的解。 @@ -920,9 +884,7 @@ plot_losses(epochs_tensor, tokens_seen, train_losses, val_losses) 在之前内容中,我们已经对 LLM 在指令数据集的训练部分进行微调,现在我们开始评估其在测试集上的表现。为此,我们首先对测试集中的每个输入生成模型的回答,并收集这些结果以便人工分析,详见图 7.18。 -
- -
+ 我们从步骤 7 开始(详见图 7.18),通过`generate`函数输出模型回答,并将其与预期的前三个测试集答案并排展示,便于进行对比: @@ -1077,9 +1039,7 @@ medium355M-sft.pth")). 之前章节中,我们通过查看模型在测试集中的 3 个示例上的响应来评估指令微调模型的性能。虽然这种方法可以提供模型表现的大致概况,但不适合用于大规模响应的评估。因此,我们在本节中实现了一种新方法(如图 7.19 的章节概览所示),利用另一个更大的大语言模型对微调模型的响应进行自动化评估。 -
- -
+ 为了实现图 7.19 中第 9 步(以自动化方式评估测试集响应),我们使用了 Meta AI 开发的一个经过指令微调的 Llama 3 模型,该模型拥有 80 亿参数,可以通过开源应用程序 Ollama 在本地运行(官网:[https://ollama.com](https://ollama.com))。 @@ -1099,9 +1059,7 @@ Ollama 是一个高效的应用程序,适用于在笔记本电脑上运行大 在实现模型评估代码之前,我们需要先下载 Llama 3 模型,并通过命令行验证 Ollama 是否正常运行。 -
- -
+ 如图 7.20 所示,在另一终端中运行 Ollama 应用程序或 Ollama 服务后,请在命令行(不是在 Python 会话中)执行以下命令来运行具有 80 亿参数的 Llama 3 模型: @@ -1428,9 +1386,7 @@ Average score: 54.16 本章总结了大语言模型(LLM)开发流程的关键步骤,包括实现 LLM 架构、预训练模型以及针对特定任务的微调,具体内容可参考图 7.21。 -
- -
+ 接下来的小节将为你提供一些思路,帮助你在完成图 7.21 中展示的关键步骤后,进一步探索下去。 diff --git a/cn-Book/附录A.PyTorch简介.md b/cn-Book/附录A.PyTorch简介.md index 512aff9..659205a 100644 --- a/cn-Book/附录A.PyTorch简介.md +++ b/cn-Book/附录A.PyTorch简介.md @@ -58,9 +58,7 @@ PyTorch 之所以如此受欢迎,部分原因在于其用户友好的界面和 PyTorch 是一个功能全面的深度学习库,快速理解它的一种方法是从它的三个核心组件入手,在图 A.1 中对这三个组件进行了总结。 -
- -
+ 首先,PyTorch 是一个张量库,它在数组导向编程库 NumPy 的基础上扩展了功能,增加了对 GPU 加速计算的支持,从而实现了 CPU 和 GPU 之间的无缝切换。 @@ -80,9 +78,7 @@ PyTorch 是一个功能全面的深度学习库,快速理解它的一种方法 机器学习是人工智能的一个子领域(如图 A.2 所示),其重点在于开发和改进学习算法。机器学习的核心思想是使计算机能够从数据中学习,并在无需编程的情况下进行预测或决策。这涉及到开发能够识别数据模式的算法,并通过更多的数据和反馈不断改进其性能。 -
- -
+ 机器学习在人工智能的发展中一直扮演着至关重要的角色,推动了包括大语言模型(LLM)在内的许多的技术进步,例如在线零售商和流媒体服务使用的推荐系统、电子邮件垃圾邮件过滤、虚拟助手中的语音识别,甚至是自动驾驶汽车。机器学习的引入和发展极大地增强了人工智能的能力,使其能够超越严格的基于规则的系统,并适应新的输入或变化的环境。 @@ -107,9 +103,7 @@ PyTorch 是一个功能全面的深度学习库,快速理解它的一种方法 下图 A.3 总结了机器学习和深度学习中典型的预测建模工作流程(也称为监督学习)。 -
- -
+ 如上图所示,模型通过使用一种学习算法在包含示例及其对应标签的训练数据集上进行训练。例如,在电子邮件垃圾邮件分类器的案例中,训练数据集包含电子邮件及其由人工标注的垃圾邮件和非垃圾邮件标签。然后,训练好的模型可以用于新的观测数据(新的电子邮件),以预测它们未知的标签(垃圾邮件或非垃圾邮件)。 @@ -145,9 +139,7 @@ pip install torch 然而,为了明确安装与 CUDA 兼容的 PyTorch 版本,通常最好指定你希望 PyTorch 兼容的 CUDA 版本。PyTorch 的官方网站 (https://pytorch.org) 提供了针对不同操作系统的、带有 CUDA 支持的 PyTorch 安装命令,如图 A.4 所示。 -
- -
+ (注意,图 A.4 中显示的命令也会安装 torchvision 和 torchaudio 库,这两个库对于本书是可选的。) @@ -197,9 +189,7 @@ True 如果你没有 GPU,有一些云计算服务提供商可以按小时收费让你使用 GPU 进行计算。一个很受欢迎的、类似于 Jupyter Notebook 的环境是 Google Colab (https://colab.research.google.com),截至本书撰写之时,它提供有时限的 GPU 使用权限。通过“运行时”菜单,你可以选择使用 GPU,如图 A.5 的截图所示。 -
- -
+ > [!NOTE] > @@ -231,9 +221,7 @@ True 张量代表一个将向量和矩阵向更高维度的推广的数学概念。换句话说,张量是可以用它们的阶(或秩)来描述的数学对象,阶(或秩)表示了张量的维度数量。例如,一个标量(就是一个数字)是 0 阶张量,一个向量是 1 阶张量,一个矩阵是 2 阶张量,如图 A.6 所示。 -
- -
+ 从计算的角度来看,张量充当数据容器。例如,它们可以存储多维数据,其中每个维度代表一个不同的特征。张量库(例如 PyTorch)可以高效地创建、操作和计算这些多维数组。在这种情况下,张量库的作用类似于数组库。 @@ -460,9 +448,7 @@ loss = F.binary_cross_entropy(a, y) 如果你不完全理解上面代码中的所有内容,不用担心。这个例子的重点不是实现一个逻辑回归分类器,而是为了说明我们如何将一系列计算视为一个计算图,如图 A.7 所示。 -
- -
+ 事实上,PyTorch 在后台构建了这样一个计算图,我们可以利用它来计算损失函数相对于模型参数(这里是 w1 和 b)的梯度,从而训练模型,这也是接下来章节的主题。 @@ -472,9 +458,7 @@ loss = F.binary_cross_entropy(a, y) 在上一节中,我们介绍了计算图的概念。如果在 PyTorch 中进行计算,默认情况下,PyTorch 通过构建计算图,并利用你设置的 `requires_grad=True` 标记,就能自动帮你计算出训练神经网络所需的关键信息——梯度,而反向传播就是利用这些梯度来更新模型参数,让模型变得更聪明。如图 A.8 所示。 -
- -
+ > [!TIP] > @@ -560,9 +544,7 @@ print(b.grad) 为了提供一个具体的例子,我们将重点介绍多层感知器,它是一种全连接神经网络,如图 A.9 所示。 -
- -
+ 在 PyTorch 中实现神经网络时,我们通常会继承 `torch.nn.Module` 类来定义我们自己的自定义网络架构。这个 `Module` 基类提供了许多功能,使得构建和训练模型更加容易。例如,它允许我们封装层和操作,并跟踪模型的参数。 @@ -780,9 +762,7 @@ tensor([[0.3113, 0.3934, 0.2952]]) 在上一节中,我们自定义了一个神经网络模型。在训练这个模型之前,我们需要简要地讨论一下如何在 PyTorch 中创建高效的数据加载器,以便在训练模型的过程中使用。PyTorch 中数据加载的总体思路如图 A.10 所示。 -
- -
+ 根据图 A.10 中的说明,在本节中,我们将实现一个自定义的 `Dataset` 类,接着使用它来创建训练数据集和测试数据集,最后用这些数据集来创建数据加载器。 @@ -946,9 +926,7 @@ Batch 2: tensor([[ 2.7000, -1.5000], 最后,让我们讨论一下 `DataLoader` 中的 `num_workers=0` 这个设置。PyTorch `DataLoader` 函数中的这个参数对于并行化数据加载和预处理至关重要。当 `num_workers` 设置为 0 时,数据加载将在主进程中完成,而不是在单独的工作进程中。这看起来可能没什么问题,但当我们使用 GPU 训练更大的网络时,可能会导致模型训练速度显著下降。这是因为 CPU 除了专注于深度学习模型的处理外,还必须花费时间来加载和预处理数据。结果,GPU 可能会在等待 CPU 完成这些任务时处于空闲状态。相反,当 `num_workers` 设置为大于零的数字时,会启动多个工作进程来并行加载数据,从而使主进程可以专注于训练你的模型并更好地利用系统的资源,如图 A.11 所示。 -
- -
+ 然而,如果我们处理的是非常小的数据集,那么将 `num_workers` 设置为 1 或更大的值可能没有必要,因为总的训练时间可能只需要几分之一秒。相反,如果你处理的是非常小的数据集或者像 Jupyter 笔记本这样的交互式环境,增加 `num_workers` 可能不会带来任何明显的加速。事实上,它们甚至可能导致一些问题。一个潜在的问题是启动多个工作进程的开销,当你的数据集很小时,这个开销可能比实际的数据加载时间还要长。 @@ -1408,9 +1386,7 @@ device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 这是如何工作的呢?PyTorch 在每个 GPU 上启动一个独立的进程,每个进程都会接收并保存模型的副本——这些副本在训练过程中会保持同步。为了说明这一点,假设我们有两个想要用来训练神经网络的 GPU,如图 A.12 所示。 -
- -
+ 如上图所示,两个 GPU 中的每一个都将接收到模型的一个副本。然后,在每个训练迭代中,每个模型都将从数据加载器接收到一个小批量(或称为批次)。我们可以使用 `DistributedSampler` 来确保在使用 DDP 时,每个 GPU 都将接收到不同的、不重叠的批次。 @@ -1432,9 +1408,7 @@ device = torch.device("cuda" if torch.cuda.is_available() else "cpu") > > 简单来说,多 GPU 训练就像是让多个“学生”(GPU)同时学习不同的“教材”(数据),然后定期交流“学习心得”(梯度),最终让每个“学生”都掌握相同的知识(更新后的模型)。 -
- -
+ 使用 DDP 的好处是,与单个 GPU 相比,它可以显著提高处理数据集的速度。除去使用 DDP 带来的设备之间微小的通信开销,理论上,使用两个 GPU 可以将一个训练 epoch 的处理时间缩短一半,而使用一个 GPU 则需要更长的时间。这种时间效率随着 GPU 数量的增加而提高,如果我们有八个 GPU,就可以将一个 epoch 的处理速度提高八倍,以此类推。 diff --git a/cn-Book/附录D.给训练循环添加高级技巧.md b/cn-Book/附录D.给训练循环添加高级技巧.md index cd1ea02..4224f25 100644 --- a/cn-Book/附录D.给训练循环添加高级技巧.md +++ b/cn-Book/附录D.给训练循环添加高级技巧.md @@ -134,9 +134,7 @@ plt.show() 结果图如图 D.1 所示。 -
- -
+ 如图 D.1 所示,学习率从一个较低的值开始,并在 20 步内逐步增加,直到在 20 步后达到最大值。 @@ -188,9 +186,7 @@ plt.show() 学习率曲线如图 D.2 所示。 -
- -
+ 如图 D.2 所示,学习率以线性预热阶段开始,在前 20 步内增加,直到在 20 步后达到最大值。在 20 步线性预热之后,余弦衰减开始起作用,逐渐降低学习率,直到达到最小值。 diff --git a/cn-Book/附录E.使用LoRA的参数高效微调.md b/cn-Book/附录E.使用LoRA的参数高效微调.md index c3dd8ba..ec168db 100644 --- a/cn-Book/附录E.使用LoRA的参数高效微调.md +++ b/cn-Book/附录E.使用LoRA的参数高效微调.md @@ -43,9 +43,7 @@ Wupdated = W + AB 图 E.1 并排展示了完整微调和 LoRA 的权重更新公式。 -
- -
+ 如果你仔细观察,你可能会注意到图 E.1 中完整微调和 LoRA 的视觉表示与之前呈现的公式略有不同。这种差异归因于矩阵乘法的分配律,该定律允许我们分离原始权重和更新后的权重,而不是将它们组合在一起。例如,在进行常规微调的情况下,以 x 作为输入数据,我们可以将计算按如下表示: @@ -314,9 +312,7 @@ Test accuracy: 48.75% 该层可以接受一个输入并计算相应的输出,如图 E.2 所示。 -
- -
+ 我们可以通过以下代码来实现图 E.2 中描述的 LoRA 层: @@ -377,9 +373,7 @@ class LoRALayer(torch.nn.Module): 在 LoRA 中,典型的目标是替换现有的线性层,从而允许将权重更新直接应用于预先存在的预训练权重,如图 E.3 所示。 -
- -
+ 为了集成图 E.3 所示的原始线性层权重,我们现在创建一个 `LinearWithLoRA` 层。该层利用了之前实现的 `LoRALayer`,旨在替换神经网络中现有的线性层,例如 `GPTModel` 中的自注意力模块或前馈模块: @@ -419,9 +413,7 @@ def replace_linear_with_lora(model, rank, alpha): 我们现在已经实现了所有必要的代码,以将 `GPTModel` 中的线性层替换为新开发的 `LinearWithLoRA` 层,从而实现参数高效微调。在接下来的章节中,我们将把 `LinearWithLoRA` 升级应用于 `GPTModel` 的多头注意力模块、前馈模块和输出层中的所有线性层,如图 E.4 所示。 -
- -
+ 在我们应用如图 E.4 所示的 `LinearWithLoRA` 层升级之前,我们首先需要冻结原始模型的参数: @@ -612,9 +604,7 @@ plot_values(epochs_tensor, examples_seen_tensor, train_losses, val_losses, label 结果如图 E.5 所示。 -
- -
+ 除了基于图 E.5 中显示的损失曲线评估模型外,我们还要计算在完整训练集、验证集和测试集上的准确率(在训练过程中,我们通过 `eval_iter=5` 设置从 5 个批次中近似计算了训练集和验证集的准确率):