本节将介绍如何将输入文本分割为独立的词元,这是为大语言模型生成嵌入向量所必需的预处理步骤。词元既可以是单个单词,也可以是包括标点符号在内的特殊字符,如图2-4 所示。
图2-4 大语言模型中文本处理步骤概览。在这一步骤图中,我们将输入文本分割成了单独的词元,这些词元既可以是单词,也可以是诸如标点符号之类的特殊字符
为了训练大语言模型,我们将使用 Edith Wharton 的短篇小说 The Verdict 作为分词文本。由于这部小说已公开发表,因此可以用于大语言模型的训练。该文本可以在维基文库中找到,你可以将其复制并粘贴到文本文件中。我已经将其复制到了一个名为 the-verdict.txt 的文本文件中。
此外,你也可以在本书的 GitHub 仓库中找到 the-verdict.txt 文件,然后可以使用以下 Python 代码下载该文件。
import urllib.request url = ("https://raw.githubusercontent.com/rasbt/" "LLMs-from-scratch/main/ch02/01_main-chapter-code/" "the-verdict.txt") file_path = "the-verdict.txt" urllib.request.urlretrieve(url, file_path)
接下来,可以使用 Python 的标准文件读取工具来加载 the-verdict.txt 文件,如代码清单 2-1 所示。
代码清单 2-1 通过 Python 读取短篇小说 The Verdict 作为文本样本
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])
打印(
print
)命令首先输出该文件的字符总数,然后展示文件的前 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 个字符的短篇小说分割为独立的单词和特殊字符,以便在后续章节中将它们转换为嵌入向量,进而用于大语言模型训练。
注意 构建大语言模型需要处理数百万篇文章和成千上万本图书,通常多达数千兆字节(GB)数据。不过,出于教学目的,使用小规模的文本样本(如一本书)就足以说明文本处理步骤的核心思想,并且可以在合理时间内在消费级硬件上完成运行。
为了获取词元列表,应该如何有效地分割这段文本?为此,我们将进行一个小练习,使用 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 for item in result if item.strip()] print(result)
去除空白字符后的输出如下所示。
['Hello', ',', 'world', '.', 'This', ',', 'is', 'a', 'test', '.']
注意 在开发简易分词器时,是将空白字符单独编码还是直接移除,取决于具体的应用场景和需求。移除空白字符可以减轻内存和计算的负担。然而,如果训练的模型需要对文本的精确结构保持敏感,那么保留空白字符就显得尤为重要(例如,Python 代码对缩进和空格具有高敏感性)。为了简化和缩短分词的输出,我们暂时选择移除空白字符。稍后,我们将改为采用保留空白字符的分词方案。
我们设计的分词方法在处理简单文本时表现良好。接下来,让我们再修改一下,使其能够处理其他类型的标点符号,比如问号、引号,以及短篇小说 The Verdict 的前 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 个独立的词元
现在,我们已经构建了一个简易分词器,让我们将其应用于短篇小说 The Verdict 的全文:
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text) preprocessed = [item.strip() for item in preprocessed if item.strip()] print(len(preprocessed))
上述打印语句的输出是
4690
,这是该文本的词元数量(不包括空白字符)。为了快速查看分词效果,可以打印前 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']