从零开始构建大语言模型(MEAP)(1)https://developer.aliyun.com/article/1510678
二、使用文本数据
本章涵盖内容
- 为大型语言模型训练准备文本
- 将文本分割成单词和子单词标记
- 字节对编码作为一种更高级的文本标记化方式
- 使用滑动窗口方法对训练样本进行抽样
- 将标记转换为输入大型语言模型的向量
在上一章中,我们深入探讨了大型语言模型(LLMs)的一般结构,并了解到它们在大量文本上进行了预训练。具体来说,我们关注的是基于变压器架构的解码器专用 LLMs,这是 ChatGPT 和其他流行的类 GPT LLMs 的基础。
在预训练阶段,LLM 逐个单词处理文本。利用亿万到数十亿参数的 LLM 进行下一个词预测任务的训练,可以产生具有令人印象深刻能力的模型。然后可以进一步微调这些模型以遵循一般指示或执行特定目标任务。但是,在接下来的章节中实施和训练 LLM 之前,我们需要准备训练数据集,这是本章的重点,如图 2.1 所示
图 2.1 LLM 编码的三个主要阶段的心理模型,LLM 在一般文本数据集上进行预训练,然后在有标签的数据集上进行微调。本章将解释并编写提供 LLM 预训练文本数据的数据准备和抽样管道。
在本章中,您将学习如何准备输入文本以进行 LLM 训练。这涉及将文本拆分为单独的单词和子单词标记,然后将其编码为 LLM 的向量表示。您还将学习有关高级标记方案,如字节对编码,这在像 GPT 这样的流行 LLM 中被使用。最后,我们将实现一种抽样和数据加载策略,以生成后续章节中训练 LLM 所需的输入-输出对。
2.1 理解词嵌入
深度神经网络模型,包括 LLM,无法直接处理原始文本。由于文本是分类的,所以它与用于实现和训练神经网络的数学运算不兼容。因此,我们需要一种将单词表示为连续值向量的方式。(不熟悉计算上下文中向量和张量的读者可以在附录 A,A2.2 理解张量中了解更多。)
将数据转换为向量格式的概念通常被称为嵌入。使用特定的神经网络层或其他预训练的神经网络模型,我们可以嵌入不同的数据类型,例如视频、音频和文本,如图 2.2 所示。
图 2.2 深度学习模型无法直接处理视频、音频和文本等原始格式的数据。因此,我们使用嵌入模型将这些原始数据转换为深度学习架构可以轻松理解和处理的稠密向量表示。具体来说,这张图说明了将原始数据转换为三维数值向量的过程。需要注意的是,不同的数据格式需要不同的嵌入模型。例如,专为文本设计的嵌入模型不适用于嵌入音频或视频数据。
在其核心,嵌入是从离散对象(如单词、图像,甚至整个文档)到连续向量空间中的点的映射——嵌入的主要目的是将非数值数据转换为神经网络可以处理的格式。
虽然单词嵌入是文本嵌入的最常见形式,但也有针对句子、段落或整个文档的嵌入。句子或段落嵌入是检索增强生成的流行选择。检索增强生成结合了生成(如生成文本)和检索(如搜索外部知识库)以在生成文本时提取相关信息的技术,这是本书讨论范围之外的技术。由于我们的目标是训练类似 GPT 的 LLMs,这些模型学习逐词生成文本,因此本章重点介绍了单词嵌入。
有几种算法和框架已被开发用于生成单词嵌入。其中一个较早和最流行的示例是Word2Vec方法。Word2Vec 训练神经网络架构以通过预测给定目标词或反之亦然的单词的上下文来生成单词嵌入。Word2Vec 背后的主要思想是在相似上下文中出现的单词往往具有相似的含义。因此,当投影到二维单词嵌入进行可视化时,可以看到相似术语聚集在一起,如图 2.3 所示。
图 2.3 如果单词嵌入是二维的,我们可以在二维散点图中绘制它们进行可视化,如此处所示。使用单词嵌入技术(例如 Word2Vec),与相似概念对应的单词通常在嵌入空间中彼此靠近。例如,不同类型的鸟类在嵌入空间中彼此比国家和城市更接近。
单词嵌入的维度可以有不同的范围,从一维到数千维不等。如图 2.3 所示,我们可以选择二维单词嵌入进行可视化。更高的维度可能捕捉到更加微妙的关系,但会牺牲计算效率。
虽然我们可以使用诸如 Word2Vec 之类的预训练模型为机器学习模型生成嵌入,但 LLMs 通常产生自己的嵌入,这些嵌入是输入层的一部分,并在训练过程中更新。优化嵌入作为 LLM 训练的一部分的优势,而不是使用 Word2Vec 的优势在于,嵌入被优化为特定的任务和手头的数据。我们将在本章后面实现这样的嵌入层。此外,LLMs 还可以创建上下文化的输出嵌入,我们将在第三章中讨论。
不幸的是,高维度嵌入给可视化提出了挑战,因为我们的感知和常见的图形表示固有地受限于三个或更少维度,这就是为什么图 2.3 展示了在二维散点图中的二维嵌入。然而,当使用 LLMs 时,我们通常使用比图 2.3 中所示的更高维度的嵌入。对于 GPT-2 和 GPT-3,嵌入大小(通常称为模型隐藏状态的维度)根据特定模型变体和大小而变化。这是性能和效率之间的权衡。最小的 GPT-2(117M 参数)和 GPT-3(125M 参数)模型使用 768 维度的嵌入大小来提供具体的例子。最大的 GPT-3 模型(175B 参数)使用 12288 维的嵌入大小。
本章的后续部分将介绍准备 LLM 使用的嵌入所需的步骤,包括将文本分割为单词,将单词转换为标记,并将标记转换为嵌入向量。
2.2 文本分词
本节介绍了如何将输入文本分割为单个标记,这是为了创建 LLM 嵌入所必需的预处理步骤。这些标记可以是单独的单词或特殊字符,包括标点符号字符,如图 2.4 所示。
图 2.4 在 LLM 上下文中查看本节涵盖的文本处理步骤。在这里,我们将输入文本分割为单个标记,这些标记可以是单词或特殊字符,如标点符号字符。在即将到来的部分中,我们将把文本转换为标记 ID 并创建标记嵌入。
我们将用于 LLM 训练的文本是 Edith Wharton 的短篇小说《The Verdict》,该小说已进入公有领域,因此可以用于 LLM 训练任务。文本可在 Wikisource 上获得,网址为en.wikisource.org/wiki/The_Verdict
,您可以将其复制粘贴到文本文件中,我将其复制到一个名为"the-verdict.txt
"的文本文件中,以便使用 Python 的标准文件读取实用程序加载:
列表 2.1 将短篇小说作为文本示例读入 Python
with open("the-verdict.txt", "r", encoding="utf-8") as f: raw_text = f.read() print("Total number of character:", len(raw_text)) print(raw_text[:99])
或者,您可以在本书的 GitHub 存储库中找到此"the-verdict.txt
"文件,网址为github.com/rasbt/LLMs-from-scratch/tree/main/ch02/01_main-chapter-code
。
打印命令打印出字符的总数,然后是文件的前 100 个字符,用于说明目的:
Total number of character: 20479 I HAD always thought Jack Gisburn rather a cheap genius--though a good fellow enough--so it was no
我们的目标是将这篇短篇小说的 20,479 个字符标记成单词和特殊字符,然后将其转换为 LLM 训练的嵌入。
文本样本大小
请注意,在处理 LLM 时,处理数百万篇文章和数十万本书——许多吉字节的文本——是很常见的。但是,出于教育目的,使用小型文本样本,如一本书,就足以说明文本处理步骤背后的主要思想,并且可以在消费类硬件上合理的时间内运行。
我们如何最好地分割这段文本以获得标记列表? 为此,我们进行了小小的探索,并使用 Python 的正则表达式库re
进行说明。 (请注意,您无需学习或记忆任何正则表达式语法,因为我们将在本章后面过渡到预构建的标记器。)
使用一些简单的示例文本,我们可以使用re.split
命令及以下语法来在空格字符上拆分文本:
import re text = "Hello, world. This, is a test." result = re.split(r'(\s)', text) print(result)
结果是一系列单词、空格和标点字符:
['Hello,', ' ', 'world.', ' ', 'This,', ' ', 'is', ' ', 'a', ' ', 'test.']
请注意,上述简单分词方案通常可将示例文本分隔成单词,但是有些单词仍然与我们希望作为单独列表项的标点字符连接在一起。
让我们修改在空格(\s
)和逗号、句号([,.]
)上的正则表达式分割:
result = re.split(r'([,.]|\s)', text) print(result)
我们可以看到单词和标点字符现在是作为我们想要的分开的列表条目:
['Hello', ',', '', ' ', 'world.', ' ', 'This', ',', '', ' ', 'is', ' ', 'a', ' ', 'test.']
一个小问题是列表仍然包括空白字符。可选地,我们可以安全地按如下方式删除这些多余的字符:
result = [item.strip() for item in result if item.strip()] print(result)
去除空格字符后的输出如下:
['Hello', ',', 'world.', 'This', ',', 'is', 'a', 'test.']
是否去除空白
在开发简单的标记器时,是否将空白字符编码为单独的字符或仅将其删除取决于我们的应用程序和其要求。去除空格减少了内存和计算需求。但是,如果我们训练的模型对文本的精确结构敏感(例如,对缩进和间距敏感的 Python 代码),保留空格可能会有用。在这里,为了简化标记化输出的简洁性,我们移除空白。稍后,我们将转换为包括空格的标记方案。
我们上面设计的标记方案在简单的示例文本上运行良好。让我们进一步修改它,使其还可以处理其他类型的标点符号,例如问号,引号以及我们在 Edith Wharton 的短篇小说的前 100 个字符中先前看到的双破折号,以及其他额外的特殊字符。
text = "Hello, world. Is this-- a test?" result = re.split(r'([,.?_!"()\']|--|\s)', text) result = [item.strip() for item in result if item.strip()] print(result)
结果输出如下:
['Hello', ',', 'world', '.', 'Is', 'this', '--', 'a', 'test', '?']
根据总结在图 2.5 中的结果,我们的标记方案现在可以成功处理文本中的各种特殊字符。
图 2.5 我们目前实施的标记化方案将文本分割为单个单词和标点字符。在本图所示的特定示例中,样本文本被分割成 10 个单独的标记。
现在我们已经有了一个基本的标记器工作,让我们将其应用到爱迪丝·沃顿的整个短篇小说中:
preprocessed = re.split(r'([,.?_!"()\']|--|\s)', raw_text) preprocessed = [item.strip() for item in preprocessed if item.strip()] print(len(preprocessed))
上面的打印语句输出了4649
,这是这段文本(不包括空格)中的标记数。
让我们打印前 30 个标记进行快速的视觉检查:
print(preprocessed[:30])
结果输出显示,我们的标记器似乎很好地处理了文本,因为所有单词和特殊字符都被很好地分开了:
['I', 'HAD', 'always', 'thought', 'Jack', 'Gisburn', 'rather', 'a', 'cheap', 'genius', '--', 'though', 'a', 'good', 'fellow', 'enough', '--', 'so', 'it', 'was', 'no', 'great', 'surprise', 'to', 'me', 'to', 'hear', 'that', ',', 'in']
2.3 将标记转换为标记 ID
在上一节中,我们将爱迪丝·沃顿的短篇小说标记化为单个标记。在本节中,我们将这些标记从 Python 字符串转换为整数表示,以生成所谓的标记 ID。这种转换是将标记 ID 转换为嵌入向量之前的中间步骤。
要将之前生成的标记映射到标记 ID 中,我们必须首先构建一个所谓的词汇表。这个词汇表定义了我们如何将每个唯一的单词和特殊字符映射到一个唯一的整数,就像图 2.6 中所示的那样。
图 2.6 我们通过对训练数据集中的整个文本进行标记化来构建词汇表,将这些单独的标记按字母顺序排序,并移除唯一的标记。然后将这些唯一标记聚合成一个词汇表,从而定义了从每个唯一标记到唯一整数值的映射。为了说明的目的,所示的词汇表故意较小,并且不包含标点符号或特殊字符。
在前一节中,我们标记化了爱迪丝·沃顿的短篇小说,并将其分配给了一个名为preprocessed
的 Python 变量。现在让我们创建一个包含所有唯一标记并按字母顺序排列的列表,以确定词汇表的大小:
all_words = sorted(list(set(preprocessed))) vocab_size = len(all_words) print(vocab_size)
通过上面的代码确定词汇表的大小为 1,159 后,我们创建词汇表,并打印其前 50 个条目以作说明:
列表 2.2 创建词汇表
vocab = {token:integer for integer,token in enumerate(all_words)} for i, item in enumerate(vocab.items()): print(item) if i > 50: break
输出如下:
('!', 0) ('"', 1) ("'", 2) ... ('Has', 49) ('He', 50)
如上面的输出所示,字典包含与唯一整数标签相关联的单独标记。我们的下一个目标是将这个词汇表应用到新文本中,以将其转换为标记 ID,就像图 2.7 中所示的那样。
图 2.7 从新的文本样本开始,我们对文本进行标记化,并使用词汇表将文本标记转换为标记 ID。词汇表是从整个训练集构建的,并且可以应用于训练集本身以及任何新的文本样本。为了简单起见,所示的词汇表不包含标点符号或特殊字符。
在本书的后面,当我们想要将 LLM 的输出从数字转换回文本时,我们还需要一种将标记 ID 转换成文本的方法。为此,我们可以创建词汇表的反向版本,将标记 ID 映射回相应的文本标记。
让我们在 Python 中实现一个完整的标记器类,它具有一个encode
方法,将文本分割成标记,并通过词汇表进行字符串到整数的映射,以产生标记 ID。另外,我们实现了一个decode
方法,进行反向整数到字符串的映射,将标记 ID 转回文本。
这个标记器实现的代码如下所示,如列表 2.3 所示:
列表 2.3 实现一个简单的文本标记器
class SimpleTokenizerV1: def __init__(self, vocab): self.str_to_int = vocab #A self.int_to_str = {i:s for s,i in vocab.items()} #B def encode(self, text): #C preprocessed = re.split(r'([,.?_!"()\']|--|\s)', text) preprocessed = [item.strip() for item in preprocessed if item.strip()] ids = [self.str_to_int[s] for s in preprocessed] return ids def decode(self, ids): #D text = " ".join([self.int_to_str[i] for i in ids]) text = re.sub(r'\s+([,.?!"()\'])', r'\1', text) #E return text
使用上述的SimpleTokenizerV1
Python 类,我们现在可以通过现有词汇表实例化新的标记对象,然后可以用于编码和解码文本,如图 2.8 所示。
图 2.8 标记器实现共享两个常见方法:一个是编码方法,一个是解码方法。编码方法接受示例文本,将其拆分为单独的标记,并通过词汇表将标记转换为标记 ID。解码方法接受标记 ID,将其转换回文本标记,并将文本标记连接成自然文本。
让我们从SimpleTokenizerV1
类中实例化一个新的标记对象,并对爱迪丝·沃顿的短篇小说中的段落进行分词,以尝试实践一下:
tokenizer = SimpleTokenizerV1(vocab) text = """"It's the last he painted, you know," Mrs. Gisburn said with pardonable pride.""" ids = tokenizer.encode(text) print(ids)
上面的代码打印了以下标记 ID:
[1, 58, 2, 872, 1013, 615, 541, 763, 5, 1155, 608, 5, 1, 69, 7, 39, 873, 1136, 773, 812, 7]
接下来,让我们看看是否可以使用解码方法将这些标记 ID 还原为文本:
tokenizer.decode(ids)
这将输出以下文本:
'" It\' s the last he painted, you know," Mrs. Gisburn said with pardonable pride.'
根据上面的输出,我们可以看到解码方法成功地将标记 ID 转换回原始文本。
目前为止,我们已经实现了一个能够根据训练集中的片段对文本进行标记化和解标记化的标记器。现在让我们将其应用于训练集中不包含的新文本样本:
text = "Hello, do you like tea?" tokenizer.encode(text)
执行上面的代码将导致以下错误:
... KeyError: 'Hello'
问题在于“Hello”这个词没有在The Verdict短篇小说中出现过。因此,它不包含在词汇表中。这突显了在处理 LLMs 时需要考虑大量和多样的训练集以扩展词汇表的需求。
在下一节中,我们将进一步测试标记器对包含未知单词的文本的处理,我们还将讨论在训练期间可以使用的额外特殊标记,以提供 LLM 更多的上下文信息。
从零开始构建大语言模型(MEAP)(3)https://developer.aliyun.com/article/1510681