From 9a52f4a0eb335433cb84b00bbbabe3fdf79fedaa Mon Sep 17 00:00:00 2001 From: skindhu Date: Sat, 26 Oct 2024 19:01:12 +0800 Subject: [PATCH] add second chapter --- Book/2.处理文本数据.md | 66 ++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/Book/2.处理文本数据.md b/Book/2.处理文本数据.md index bce65f4..cd426c2 100644 --- a/Book/2.处理文本数据.md +++ b/Book/2.处理文本数据.md @@ -23,25 +23,25 @@ -在上一章中,我们介绍了大语言模型(LLMs)的基本结构,并了解到它们基于海量的文本数据集进行预训练。我们特别关注的是仅使用通用 Transformer 架构中解码器部分的 LLMs,这也是 ChatGPT 和其他流行的类似 GPT 的 LLM 所依赖的模型。 +在上一章中,我们介绍了大语言模型(LLM)的基本结构,并了解到它们会基于海量的文本数据集进行预训练。我们特别关注的是仅使用通用 Transformer 架构中解码器部分的 LLM,这也是 ChatGPT 和其他流行的类似 GPT 的 LLM 所依赖的模型。 -在预训练阶段,LLMs 逐字处理文本。通过使用下一个单词预测任务训练拥有数百万到数十亿参数的 LLMs,最终能够生成具有出色能力的模型。这些模型随后可以进一步微调,以遵循指令或执行特定目标任务。然而,在我们接下来几章中实现和训练 LLMs 之前,我们需要准备训练数据集,这也是本章的重点,如图 2.1 所示。 +在预训练阶段,LLM 逐字处理文本。通过使用下一个单词预测任务训练拥有数百万到数十亿参数的 LLM,最终能够生成具有出色能力的模型。这些模型随后可以进一步微调,以遵循指令或执行特定目标任务。然而,在我们接下来几章中实现和训练 LLM 之前,我们需要准备训练数据集,这也是本章的重点,如图 2.1 所示。 -在本章中,您将学习如何为训练 LLM 准备输入文本。这包括将文本拆分为单个单词和子词token,并将这些token编码为 LLM 的向量表示。您还将了解一些先进的token分割方案,比如字节对编码,这种方法在像 GPT 这样的流行 LLM 中得到应用。最后,我们将实现一个采样和数据加载策略,以生成后续章节中训练 LLM 所需的输入输出对。 +在本章中,您将学习如何为训练 LLM 准备输入文本。这包括将文本拆分为单个单词和子词token,并将这些token编码为 LLM 的向量表示。您还将了解一些先进的token分割方案,比如字节对编码,这种方法在像 GPT 这样的流行 LLM 中得到应用。最后,我们将实现一个采样和数据加载策略,以生成后续章节中训练 LLM 所需的输入输出数据对。 ## 2.1 理解词嵌入 -深度神经网络模型,包括 LLM,往往无法直接处理原始文本。这是因为文本是分类数据,它与实现和训练神经网络所需的数学运算不兼容。因此,我们需要一种方法将单词表示为连续值向量。(对计算中向量和张量不熟悉的读者,可以在附录 A 的 A2.2 节中了解更多关于张量的内容。) +深度神经网络模型,包括 LLM,往往无法直接处理原始文本。这是因为文本是离散的分类数据,它与实现和训练神经网络所需的数学运算不兼容。因此,我们需要一种方法将单词表示为连续值向量。(对计算中向量和张量不熟悉的读者,可以在附录 A 的 A2.2 节中了解更多关于张量的内容。) -将数据转换为向量格式的过程通常被称为嵌入(embedding)。我们可以通过特定的神经网络层或其他预训练的神经网络模型来对不同类型的数据进行嵌入,比如视频、音频和文本,如图 2.2 所示。 +将数据转换为向量格式的过程通常被称为嵌入(Embedding)。我们可以通过特定的神经网络层或其他预训练的神经网络模型来对不同类型的数据进行嵌入,比如视频、音频和文本,如图 2.2 所示。 -如图 2.2 所示,我们可以使用嵌入模型来处理多种不同的数据格式。然而,值得注意的是,不同的数据格式需要使用不同的嵌入模型。例如,专为文本设计的嵌入模型并不适用于音频或视频数据的嵌入。 +如图 2.2 所示,我们可以使用嵌入模型来处理多种不同的数据格式。然而,需要注意的是,不同的数据格式需要使用不同的嵌入模型。例如,专为文本设计的嵌入模型并不适用于音频或视频数据的嵌入。 > [!TIP] > @@ -56,17 +56,17 @@ 嵌入的本质是将离散对象(如单词、图像或整个文档)映射到连续向量空间中的点。嵌入的主要目的是将非数值数据转换为神经网络能够处理的格式。 -虽然单词嵌入是最常用的文本嵌入形式,但也存在句子、段落或整篇文档的嵌入。句子和段落嵌入常被用于检索增强生成技术。检索增强生成结合了文本生成与从外部知识库中检索相关信息的过程,这是一种超出本书讨论范围的技术。由于我们希望训练类似于GPT的LLM,这些模型以逐字的方式生成文本,因此本章将重点放在单词嵌入上。 +虽然单词嵌入是最常用的文本嵌入形式,但也存在句子、段落或整篇文档的嵌入。句子和段落嵌入常被用于检索增强生成技术。检索增强生成结合了文本生成与从外部知识库中检索相关信息的过程,这是一种超出本书讨论范围的技术。由于我们希望训练类似于GPT的LLM,这类模型以逐字的方式生成文本,因此本章将重点放在单词嵌入上。 > [!TIP] > -> **个人思考:** 这里聊一下检索增强技术(RAG),目前已经广泛应用于特定领域的知识问答场景。尽管GPT在文本生成任务重表现强大,但它们依赖的是预训练的知识,这以为着它们的回答依赖于模型在预训练阶段学习到的信息。这就导致了几个问题: +> **个人思考:** 这里聊一下检索增强技术(RAG),目前已经广泛应用于特定领域的知识问答场景。尽管GPT在文本生成任务中表现强大,但它们依赖的是预训练的知识,这意味着它们的回答依赖于模型在预训练阶段学习到的信息。这种方式导致了几个问题: > > + **知识的有效性:** 模型的知识基于它的预训练数据,因此无法获取最新的信息。比如,GPT-3 的知识截止到 2021 年,无法回答最新的事件或发展。 > + **模型大小的限制:** 即使是大型模型,所能存储和运用的知识也是有限的。如果任务涉及特定领域(如医学、法律、科学研究),模型在预训练阶段可能没有涵盖足够的信息。 > + **生成的准确性:** 生成模型可能会凭空编造信息(即“幻觉现象”),导致生成内容不准确或虚假。 > -> 而检索增强技术正是为了解决上述不足,它大致原理为将外部知识库(如文档、数据库、互联网等)进行向量化后存入到向量数据库中。当用户提交一个查询时,首先将这个查询也编码成一个向量,然后去承载外部知识库的向量数据种检索(检索技术有很多种)与问题相关的信息。检索到的信息被作为额外的上下文信息输入到LLM中,LLM会将这些外部信息与原始输入结合起来,以更准确和丰富的内容生成回答。想要进一步了解RAG技术及其应用,可以参考:[RAG 专区](https://waytoagi.feishu.cn/wiki/PUUfwNkwqielBOkbO5RcjnTQnUd) +> 而检索增强技术正是为了解决上述不足,它大致原理为将外部知识库(如文档、数据库、互联网等)进行向量化后存入到向量数据库中。当用户提交一个查询时,首先将这个查询也编码成一个向量,然后去承载外部知识库的向量数据库中检索(检索技术有很多种)与问题相关的信息。检索到的信息被作为额外的上下文信息输入到LLM中,LLM会将这些外部信息与原始输入结合起来,以更准确和丰富的内容生成回答。想要进一步了解RAG技术及其应用,可以参考:[RAG 专区](https://waytoagi.feishu.cn/wiki/PUUfwNkwqielBOkbO5RcjnTQnUd) 生成单词嵌入的算法和框架有很多。其中,Word2Vec是较早且最受欢迎的项目之一。Word2Vec通过预测给定目标词的上下文或反之,训练神经网络架构以生成单词嵌入。Word2Vec的核心思想是,出现在相似上下文中的词通常具有相似的含义。因此,当将单词投影到二维空间进行可视化时,可以看到相似的词汇聚在一起,如图2.3所示。 @@ -74,13 +74,13 @@ 词嵌入可以具有不同的维度,从一维到数千维。如图2.3所示,我们可以选择二维词嵌入进行可视化。更高的维度可能捕捉到更细微的关系,但代价是计算效率的降低。 -虽然我们可以使用预训练模型(例如 Word2Vec)为机器学习模型生成嵌入,但 LLMs 通常会生成自己的嵌入,这些嵌入是输入层的一部分,并在训练过程中进行更新。将嵌入作为 LLM 训练的一部分进行优化,而不直接使用 Word2Vec,有一个明确的优势,就是嵌入能够针对特定的任务和数据进行优化。我们将在本章后面实现这样的嵌入层。此外,LLMs 还能够创建上下文化的输出嵌入,这一点我们将在第三章中讨论。 +虽然我们可以使用预训练模型(例如 Word2Vec)为机器学习模型生成嵌入,但 LLM 通常会生成自己的嵌入,这些嵌入是输入层的一部分,并在训练过程中进行更新。将嵌入作为 LLM 训练的一部分进行优化,而不直接使用 Word2Vec,有一个明确的优势,就是嵌入能够针对特定的任务和数据进行优化。我们将在本章后面实现这样的嵌入层。此外,LLM 还能够创建上下文化的输出嵌入,这一点我们将在第三章中讨论。 高维嵌入在可视化中面临挑战,因为我们的感官感知和常见的图形表示本质上只限于三维或更少的维度,这也是图 2.3 采用二维散点图展示二维嵌入的原因。然而,在处理 LLMs 时,我们通常使用的嵌入的维度远高于图 2.3 所示的维度。对于 GPT-2 和 GPT-3,嵌入的大小(通常称为模型隐状态的维度)会根据具体的模型变体和大小而有所不同。这是性能与效率之间的权衡。以具体示例为例,最小的 GPT-2 模型(117M 和 125M 参数)使用 768 维的嵌入大小,而最大的 GPT-3 模型(175B 参数)则使用 12,288 维的嵌入大小。 本章接下来的部分将系统地介绍准备 LLM 使用的嵌入所需的步骤,这些步骤包括将文本拆分为单词、将单词转换为token,以及将token转化为嵌入向量。 -​ + ## 2.2 文本分词 @@ -108,7 +108,7 @@ I HAD always thought Jack Gisburn rather a cheap genius--though a good fellow en it was no ``` -我们的目标是将这篇 20,479 个字符的短篇小说拆分为单词和特殊字符,然后在接下来的章节中将这些token转换为 LLM 训练所需的嵌入。 +我们的目标是将这篇 20,479 个字符的短篇小说拆分为单词和特殊字符(统称为token),然后在接下来的章节中将这些token转换为 LLM 训练所需的嵌入。 > [!NOTE] > @@ -148,7 +148,7 @@ print(result) ['Hello', ',', '', ' ', 'world', '.', '', ' ', 'This', ',', '', ' ', 'is', ' ', 'a', ' ', 'test', '.', ''] ``` -一个剩余的小问题是列表仍然包含空白字符。我们可以安全地按如下方式删除这些多余的字符: +一个剩余的小问题是列表仍然包含空白字符。我们可以按如下方式安全地删除这些多余的字符: ```python result = [item for item in result if item.strip()] @@ -251,7 +251,7 @@ for i, item in enumerate(vocab.items()): 在本书后面,当我们想将 LLM 的输出从数字转换回文本时,我们还需要一种将token ID 转换为文本的方法。为此,我们可以创建一个词汇表的反向版本,将token ID 映射回相应的文本token。 -让我们在 Python 中实现一个完整的分词器类,其中包含一个 encode 方法,该方法将文本拆分为token,并通过词汇表进行token字符串到整数(token ID)的映射,以通过词汇表生成token ID。此外,我们还将实现一个 decode 方法,该方法进行整数到字符串的反向映射,将token ID 转换回文本。 +让我们在 Python 中实现一个完整的分词器类,其中包含一个 encode 方法,该方法负责将文本拆分为token,并通过词汇表进行token字符串到整数(token ID)的映射,以通过词汇表生成token ID。此外,我们还将实现一个 decode 方法,该方法则负责进行整数到字符串的反向映射,将token ID 转换回文本。 该分词器的代码实现如下: @@ -295,9 +295,9 @@ ids = tokenizer.encode(text) print(ids) ``` -上面的代码打印出以下token ID: +上面的代码打印出以下token ID(`这里原始英文书籍中没有输出打印结果,读者可以自己运行代码查看结果`): -下来,让我们看看能否通过 decode 方法将这些token ID 转换回文本: +接下来,让我们看看能否通过 decode 方法将这些token ID 转换回文本: ```python print(tokenizer.decode(ids)) @@ -331,7 +331,7 @@ KeyError: 'Hello' -## 2.4 添加特殊上下文tokens +## 2.4 添加特殊上下文token 在上一节中,我们实现了一个简单的分词器,并将其应用于训练集中的一段文本。在本节中,我们将修改这个分词器来处理未知单词。 @@ -339,7 +339,7 @@ KeyError: 'Hello' -如图2.9所示,我们可以修改分词器,以便在遇到不在词汇表中的单词时使用一个<|unk|> token。此外,我们在不相关的文本之间添加一个token。例如,在对多个独立文档或书籍进行GPT类大语言模型的训练时,通常会在每个文档或书籍之前插入一个token,以跟随前一个文本源,如图2.10所示。这有助于大型语言模型理解,尽管这些文本源在训练中是连接在一起的,但它们实际上是无关的。 +如图2.9所示,我们可以修改分词器,以便在遇到不在词汇表中的单词时使用一个<|unk|> token。此外,我们还会在不相关的文本之间添加一个特殊的<|endoftext|> token。例如,在对多个独立文档或书籍进行GPT类大语言模型的训练时,通常会在每个文档或书籍之前插入一个token,以连接前一个文本源,如图2.10所示。这有助于大语言模型理解,尽管这些文本源在训练中是连接在一起的,但它们实际上是无关的。 @@ -355,7 +355,7 @@ print(len(vocab.items())) 基于上述打印语句的输出,新词汇表的大小为1161(上一节的词汇表大小为1159)。 -作为额外的快速检查,让我们打印更新后词汇的最后5个条目: +为了快速检查,让我们打印更新后词汇表的最后5个条目: ```python for i, item in enumerate(list(vocab.items())[-5:]): @@ -372,7 +372,7 @@ for i, item in enumerate(list(vocab.items())[-5:]): ('<|unk|>', 1160) ``` -根据上述代码输出,我们可以确认这两个新的特殊token确实成功地被纳入了词汇表。接下来,我们相应地调整代码清单2.3中的分词器,如清单2.4所示: +根据上述代码的输出,我们可以确认这两个新的特殊token确实成功地被纳入了词汇表。接下来,我们相应地调整代码清单2.3中的分词器,如清单2.4所示: ```python # Listing 2.4 A simple text tokenizer that handles unknown words @@ -401,7 +401,7 @@ class SimpleTokenizerV2: #B 在指定标点符号前替换空格 ``` -与我们在上一节的代码清单 2.3 中实现的 SimpleTokenizerV1 相比,新的 SimpleTokenizerV2 用 <|unk|> tokens 替换未知词。 +与我们在上一节的代码清单 2.3 中实现的 SimpleTokenizerV1 相比,新的 SimpleTokenizerV2 用 <|unk|> token 替换未知词。 ```python text1 = "Hello, do you like tea?" @@ -443,7 +443,7 @@ print(tokenizer.decode(tokenizer.encode(text))) 通过将上面的去token化文本与原始输入文本进行比较,我们可以得知训练数据集,即艾迪丝·华顿的短篇小说《判决》,并不包含单词 "Hello" 和 "palace"。 -到目前为止,我们已经讨论了token化作为处理文本输入到 LLMs 中的重要步骤。根据不同的 LLM,一些研究人员还考虑其他特殊token,例如以下几种: +到目前为止,我们已经讨论了分词作为处理文本输入到 LLMs 中的重要步骤。根据不同的 LLM,一些研究人员还考虑其他特殊token,例如以下几种: + [BOS](序列开始):这个token表示文本的起始位置,指示 LLM 内容的开始。 + [EOS](序列结束):这个token位于文本的末尾,在连接多个无关文本时特别有用,类似于 <|endoftext|>。例如,在合并两个不同的维基百科文章或书籍时, [EOS] token指示一篇文章结束和下一篇文章开始。 @@ -473,7 +473,7 @@ print(tokenizer.decode(tokenizer.encode(text))) ## 2.5 字节对编码(Byte pair encoding) -我们在前面的章节中实现了一个简单的分词方案以作说明。本节将介绍一种基于字节对编码(BPE)概念的更复杂的分词方案。本节中介绍的BPE分词器曾用于训练大语言模型,如GPT-2、GPT-3以及最初用于 ChatGPT 的 LLM。 +我们在前面的章节中实现了一个简单的分词方案以作说明。本节将介绍一种基于字节对编码(BPE)概念的更复杂的分词方案。BPE分词器曾用于训练大语言模型,如GPT-2、GPT-3以及最初用于 ChatGPT 的 LLM。 由于从零开始实现BPE可能相对复杂,我们将使用一个名为tiktoken的现有Python开源库([https://github.com/openai/tiktoken](https://github.com/openai/tiktoken)),该库基于Rust中的源代码非常高效地实现了BPE算法。与其他Python库类似,我们可以通过Python的pip安装程序从终端安装tiktoken库: @@ -542,7 +542,9 @@ BPE背后的算法将不在其预定义词汇表中的单词分解为更小的 > [!TIP] > -> **个人思考:** 字节对编码是一种基于统计的方法,它会先从整个语料库中找出最常见的字节对(byte pair),然后把这些字节对合并成一个新的单元。让我们用一个具体的case来描述这个过程: +> **个人思考:** 字节对编码是一种基于统计的方法,它会先从整个语料库中找出最常见的字节对(byte pair),然后把这些字节对合并成一个新的单元。让我们用一个具体的示例来描述这个过程: +> +> 假如有句子:“The cat drank the milk because it was hungry” > > 1. **初始化:BPE会先将句子中每个字符视为一个单独的token** > @@ -665,7 +667,7 @@ and established himself in ----> a -虽然图2.13展示了字符串格式的token以供说明,但代码实现将直接操作token ID,因为 BPE 分词器的 encode 方法将分词和转换为token ID 的过程合并为一个步骤。 +虽然图2.13展示了字符串格式的token以供说明,但代码实现将直接操作token ID,因为 BPE 分词器的 encode 方法将分词和转换为token ID 的过程合并为了一个步骤。 为了实现高效的数据加载器,我们将使用 PyTorch 内置的 Dataset 和 DataLoader 类。有关安装 PyTorch 的更多信息与指导,请参见附录 A 中的 A.1.3 节,安装 PyTorch。 @@ -702,7 +704,7 @@ class GPTDatasetV1(Dataset): #D 从数据集中返回指定行 ``` -清单 2.5 中的 GPTDatasetV1 类继承自 PyTorch Dataset 类,定义了如何从数据集中提取单行,其中每行由多个token ID(基于 max_length)组成,并赋值给 input_chunk 张量。target_chunk 张量则包含相应的目标。我建议继续阅读,以了解将此数据集与 PyTorch DataLoader 结合时返回的数据的样子——这将让我们更清晰的了解运作原理。 +清单 2.5 中的 GPTDatasetV1 类继承自 PyTorch Dataset 类,定义了如何从数据集中提取单行,其中每行由多个token ID(基于 max_length)组成,并赋值给 input_chunk 张量。target_chunk 张量则包含相应的目标。请继续阅读,以了解将此数据集与 PyTorch DataLoader 结合时返回的数据的样子——这将让我们更清晰的了解运作原理。 以下代码将使用刚创建的 GPTDatasetV1 类,通过 PyTorch DataLoader 以批量方式加载输入: @@ -775,9 +777,9 @@ print(second_batch) > > 为了更好地理解数据加载器的工作原理,请尝试使用不同的设置进行测试,例如 `max_length=2` 和 `stride=2`,以及 `max_length=8` 和 `stride=2`。 -迄今为止,我们从数据加载器中采样的批量大小都为1,主要用于说明运作原理。如果你有深度学习的经验,你可能知道,小批量大小在训练时消耗内存较少,但会导致模型更新变得更加嘈杂。就像在常规深度学习中一样,批量大小的设置是一个权衡,它作为超参数需要在训练 LLM 过程中进行实验和调整。 +迄今为止,我们从数据加载器中采样的批次大小都为1,这主要用于说明运作原理。如果你有深度学习的经验,你可能知道,小批次大小在训练时消耗内存较少,但会导致模型更新变得更加困难。就像在常规深度学习中一样,批次大小的设置是一个权衡,它作为超参数需要在训练 LLM 过程中进行实验和调整。 -在我们继续本章最后两节之前(最后两节专注于从token ID 创建嵌入向量),先简要了解一下如何使用数据加载器以大于 1 的批量大小进行采样: +在我们继续本章最后两节之前(最后两节专注于从token ID 创建嵌入向量),先简要了解一下如何使用数据加载器以大于 1 的批次大小进行采样: ```python dataloader = create_dataloader_v1(raw_text, batch_size=8, max_length=4, stride=4) @@ -816,11 +818,11 @@ Targets: ## 2.7 构建词嵌入层 -为训练 LLM 准备训练集的最后一步是将token ID 转换为嵌入向量,如图 2.15 所示,这将是本章最后两部分的主要内容。 +为 LLM 准备训练集的最后一步是将token ID 转换为嵌入向量,如图 2.15 所示,这将是本章最后两部分的主要内容。 -除了图 2.15 中概述的过程外,还需注意的是,我们会以随机值初始化这些嵌入权重,作为准备步骤。这一初始化为 LLM 的学习过程提供了起始点。我们将在第 5 章中优化嵌入权重,作为 LLM 训练的一部分。 +除了图 2.15 中概述的过程外,还需注意的是,我们首先会以随机值初始化这些嵌入权重。这一初始化为 LLM 的学习过程提供了起始点。我们将在第 5 章中优化嵌入权重,作为 LLM 训练的一部分。 对于GPT类大语言模型(LLM)来说,连续向量表示(Embedding)非常重要,原因在于这些模型使用深度神经网络结构,并通过反向传播算法(backpropagation)进行训练。如果你不熟悉神经网络是如何通过反向传播进行训练的,请参阅附录 A 中的 A.4 节,《自动微分简易教程》。 @@ -936,7 +938,7 @@ tensor([[ 1.2753, -0.2010, -0.1606], 从原则上讲,确定性的、与位置无关的token ID 嵌入对于可重复性是有益的。然而,由于LLM的自注意力机制本身也是与位置无关的,因此向LLM注入额外的位置信息是有帮助的。 -绝对位置嵌入与序列中的特定位置直接相关。对于输入序列中的每个位置,都会将一个唯一的嵌入添加到token的嵌入中,以传达其确切位置。例如,第一个token将具有特定的位置嵌入,第二个token将具有另一个不同的嵌入,依此类推,如图2.18所示。 +绝对位置嵌入与序列中的特定位置直接相关。对于输入序列中的每个位置,都会将一个唯一的绝对位置嵌入向量添加到token的嵌入向量中,以传达其确切位置。例如,第一个token将具有特定的位置嵌入,第二个token将具有另一个不同的嵌入,依此类推,如图2.18所示。 @@ -1035,7 +1037,7 @@ torch.Size([8, 4, 256]) -​ + ## 2.9 总结