LLM 标记化中常用的(字节级)字节对编码 (BPE) 算法的最小、干净的代码。BPE 算法是“字节级”的,因为它在 UTF-8 编码的字符串上运行。
该算法通过 GPT-2 论文和 OpenAI 发布的相关 GPT-2代码在 LLM 中得到推广。[森里奇等人,2015 年]被引用为 BPE 在 NLP 应用中使用的原始参考文献。如今,所有现代 LLM(例如 GPT、Llama、Mistral)都使用此算法来训练他们的标记器。
该存储库中有两个 Tokenizer,它们都可以执行 Tokenizer 的 3 个主要功能:1)训练 tokenizer 词汇并合并给定文本,2)从文本编码到令牌,3)从令牌解码到文本。存储库的文件如下:
- minbpe/base.py:实现该类
Tokenizer
,该类是基类。它包含train
、encode
、 和decode
存根、保存/加载功能,还有一些常见的实用功能。这个类不应该直接使用,而是要继承。 - minbpe/basic.py:实现
BasicTokenizer
,直接在文本上运行的 BPE 算法的最简单实现。 - minbpe/regex.py:实现
RegexTokenizer
,通过正则表达式模式进一步分割输入文本,这是一个预处理阶段,在标记化之前按类别(例如:字母、数字、标点符号)分割输入文本。这确保不会发生跨类别边界的合并。这是在 GPT-2 论文中引入的,并从 GPT-4 开始继续使用。此类还处理特殊标记(如果有)。 - minbpe/gpt4.py:实现
GPT4Tokenizer
,该类是RegexTokenizer
的一个轻量级包装器,它精确地再现了tiktoken库中 GPT-4 的标记化。包装处理有关恢复标记生成器中的精确合并的一些细节,以及处理一些不幸的(并且可能是历史的?)1 字节标记排列。
最后,脚本train.py在输入文本tests/taylorswift.txt(这是她的维基百科条目)上训练两个主要分词器,并将词汇保存到磁盘以进行可视化。该脚本在我的 (M1) MacBook 上运行大约需要 25 秒。
上面的所有文件都非常短且注释详尽,并且在文件底部还包含使用示例。
快速开始
作为最简单的例子,我们可以复制维基百科关于 BPE 的文章如下:
from minbpe import BasicTokenizer tokenizer = BasicTokenizer() text = "aaabdaaabac" tokenizer.train(text, 256 + 3) # 256 are the byte tokens, then do 3 merges print(tokenizer.encode(text)) # [258, 100, 258, 97, 99] print(tokenizer.decode([258, 100, 258, 97, 99])) # aaabdaaabac tokenizer.save("toy") # writes two files: toy.model (for loading) and toy.vocab (for viewing)
根据 Wikipedia,对输入字符串“aaabdaaabac”运行 bpe 进行 3 次合并会生成字符串:“XdXac”,其中 X=ZY、Y=ab 和 Z=aa。需要注意的棘手之处在于,minbpe 始终将 256 个单独字节分配为令牌,然后根据需要合并字节。所以对于我们来说 a=97、b=98、c=99、d=100(它们的ASCII值)。然后,当 (a,a) 合并到 Z 时,Z 将变为 256。同样,Y 将变为 257,X 变为 258。因此,我们从 256 字节开始,进行 3 次合并以获得上面的结果,预期输出为[258、100、258、97、99]。
推理:GPT-4比较
我们可以验证RegexTokenizer
与tiktoken的 GPT-4 分词器具有相同的功能,如下所示:
text = "hello123!!!? (안녕하세요!) 😉" # tiktoken import tiktoken enc = tiktoken.get_encoding("cl100k_base") print(enc.encode(text)) # [15339, 4513, 12340, 30, 320, 31495, 230, 75265, 243, 92245, 16715, 57037] # ours from minbpe import GPT4Tokenizer tokenizer = GPT4Tokenizer() print(tokenizer.encode(text)) # [15339, 4513, 12340, 30, 320, 31495, 230, 75265, 243, 92245, 16715, 57037]
(你必须执行pip install tiktoken
才能运行)。在底层,GPT4Tokenizer
只是RegexTokenizer
的一个轻量级的包装器,传递 GPT-4 的合并和特殊标记。我们还可以确保特殊令牌得到正确处理:
text = "<|endoftext|>hello world" # tiktoken import tiktoken enc = tiktoken.get_encoding("cl100k_base") print(enc.encode(text, allowed_special="all")) # [100257, 15339, 1917] # ours from minbpe import GPT4Tokenizer tokenizer = GPT4Tokenizer() print(tokenizer.encode(text, allowed_special="all")) # [100257, 15339, 1917]
请注意,就像 tiktoken 一样,我们必须在编码调用中显式声明使用和解析特殊令牌的意图。否则,这可能会成为一个主要的枪口,无意中用特殊令牌标记攻击者控制的数据(例如用户提示)。allowed_special
参数可以设置为“all”、“none”或允许的特殊标记列表。
训练
与 tiktoken 不同,此代码允许您训练自己的标记器。原则上,据我所知,如果您在词汇量为 100K 的大型数据集上进行RegexTokenizer
训练,您将重现 GPT-4 分词器。
您可以遵循两条路径。首先,您不希望使用正则表达式模式分割和预处理文本的复杂性,并且您也不关心特殊标记。在这种情况下,请访问BasicTokenizer
。您可以对其进行训练,然后进行编码和解码,例如如下所示:
from minbpe import BasicTokenizer tokenizer = BasicTokenizer() tokenizer.train(very_long_training_string, vocab_size=4096) tokenizer.encode("hello world") # string -> tokens tokenizer.decode([1000, 2000, 3000]) # tokens -> string tokenizer.save("mymodel") # writes mymodel.model and mymodel.vocab tokenizer.load("mymodel.model") # loads the model back, the vocab is just for vis
如果您想效仿 OpenAI 的文本标记器,最好采用他们使用正则表达式模式按类别分割文本的方法。GPT-4 模式默认带RegexTokenizer
,因此您只需执行以下操作即可:
from minbpe import RegexTokenizer tokenizer = RegexTokenizer() tokenizer.train(very_long_training_string, vocab_size=32768) tokenizer.encode("hello world") # string -> tokens tokenizer.decode([1000, 2000, 3000]) # tokens -> string tokenizer.save("tok32k") # writes tok32k.model and tok32k.vocab tokenizer.load("tok32k.model") # loads the model back from disk
当然,您希望根据数据集的大小来更改词汇表大小。
特殊令牌。最后,您可能希望向标记生成器添加特殊标记。使用register_special_tokens
函数注册它们。例如,如果您使用 vocab_size 为 32768 进行训练,则前 256 个标记是原始字节标记,接下来的 32768-256 个标记是合并标记,在这些标记之后您可以添加特殊标记。最后一个“真实”合并令牌的 id 为 32767 (vocab_size - 1),因此您的第一个特殊令牌应该紧随其后,其 id 恰好为 32768。所以:
from minbpe import RegexTokenizer tokenizer = RegexTokenizer() tokenizer.train(very_long_training_string, vocab_size=32768) tokenizer.register_special_tokens({"<|endoftext|>": 32768}) tokenizer.encode("<|endoftext|>hello world", allowed_special="all")
当然,您也可以根据需要添加更多令牌。最后,我想强调,我努力保持代码本身的干净、可读和可修改。您不应该害怕阅读代码并理解它是如何工作的。这些测试也是寻找更多使用示例的好地方。
测试
我们使用 pytest 库进行测试。它们全部位于该tests/
目录中。如果您还没有安装 pytest,首先执行pip install pytest
,然后:
$ pytest -v .
运行测试。(-v 是详细的版本,稍微漂亮一些)。
练习
对于那些尝试学习 BPE 的人,这里是建议的进阶练习,帮助您逐步构建自己的 minbpe。请参阅exercise.md。
讲座
我在这个 YouTube 视频的存储库中构建了代码。您还可以在lecture.md中找到本讲座的文本形式。