下面讨论如何将输入文本分割成独立的 token,这是为 LLM 创建嵌入所需的预处理步骤。这些 tokens 要么是单独的词语,要么是特殊字符,包括标点符号,如图 2.4 所示。
图 2.4 显示了在 LLM 背景下文本处理步骤的视图。这里,我们将输入文本分割成独立的 token,这些 tokens 要么是词语,要么是特殊字符,如标点符号。
此处用于训练 LLM 的文本是伊迪斯·沃顿的短篇小说《The Verdict》,该作品已进入公共领域,因此可以用于 LLM 训练任务。该文本可以在 Wikisource 上找到,网址为 https://en.wikisource.org/wiki/The_Verdict,将其复制粘贴到一个文本文件中,以下演示中,将此文本文件命名为“the-verdict.txt”的文本文件中。
或者,在本系列的 GitHub 仓库中找到这个“the-verdict.txt”文件,网址为 https://mng.bz/Adng。使用以下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 的读文件方法加载这个文件。
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
命令打印出了文件的总字符数以及前 99 个字符。以下是输出结果:
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 个字符的短篇小说《The Verdict》分成单独的词语和特殊字符,然后将它们转换成可用于 LLM 训练的嵌入。
注意:在处理 LLM 时,通常会处理数以百万计的文章和数十万本书——即许多千兆字节的文本。然而,出于教育目的,使用较小的文本样本(如一本书)来说明文本处理步骤背后的主要思想,并使得在消费者级别的硬件上能够在合理时间内运行就足够了。
如何最好地分割这段文本以获得 token 列表(list,Python语言中的内置对象类型)?为此,须简要介绍并使用 Python 的正则表达式库 re
。(你不需要学习或记忆任何正则表达式的语法,因为我们之后会转向使用预构建的 token 工具。)
用简短的文本作为示例,使用 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', '.']
注意:当开发一个简单的 token 分词器时,是否应将空字符或空格编码为单独的字符或者直接移除它们,取决于应用程序及其要求。移除空字符可以减少内存和计算需求。然而,如果所训练的模型需要非常完整的文本结构,即文本中的空格对文本内容是有意义的,例如,Python 语言中以空格实现的缩进,这时就应该保留这些空格。以上演示中,为了简化和缩短输出的分词,我们移除了它们。稍后,会切换到一种包括空字符和空格的分词方案。
上述的这种分词方案在简单的文本上运行良好。下面再稍微修改它,以便也能处理其他类型的标点符号,如问号、引号以及在伊迪斯·沃顿短篇小说开头 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 个独立的 token。
现在有了一个实现基本功能的分词器,下面将其应用于伊迪斯·沃顿的整个短篇小说:
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(len(preprocessed))
打印语句的输出是 4690,这是该文本中的 token 数量(不包括空格)。打印前 30 个 token 看一看:
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']
原文:Sebastian Raschka. Build a Large Language Model(From Scratch),此处为原文的中文翻译,为了阅读方便,有适当修改。