minbpe:BPE算法的极简实现
github 地址:karpathy/minbpe
1.BPE 算法
BPE(Byte Pair Encoding) 是大模型的 tokenizer 常用的算法。它对输入文本的字节进行编码。
该算法因 GPT-2 的论文和代码而被广泛使用于 LLM。Sennrich et al. 2015被认为是 BPE 在 NLP 应用中的原始参考。
简单来说,bpe 把文本看作 utf-8 编码的字节,然后将出现次数最多的相邻字节合并,生成一个新的编码。如此反复操作。
2.minbpe 简介
2.1quickstart
from minbpe import BasicTokenizer
tokenizer = BasicTokenizer()
text = "aaabdaaabac"
# 训练
tokenizer.train(text, 256 + 3) # 256tokens, 3merges
# 编码
print(tokenizer.encode(text))
# 解码
print(tokenizer.decode([258, 100, 258, 97, 99]))
# 保存
tokenizer.save("toy")
# writes two files: toy.model (for loading) and toy.vocab (for viewing)
英语字母一个字母对应一个字节。对于"aaabdaaabac",先计算相邻两字节的出现次数,然后选择次数最多的进行合并(“aa”,4次)。
“a”“a”合并为“aa”,编码为256。
然后再计算相邻两字节的出现次数,再合并。
在 toy.vocab 中可以看到所有字符及对应的编码。
toy.vocab
......
[a][a] -> [aa] 256
[aa][a] -> [aaa] 257
[aaa][b] -> [aaab] 258
2.2minbpe和GPT-4分词器功能相同
# 1.证明RegexTokenizer与GPT-4的分词器性能一致
text = "hello123!!!? (안녕하세요!) 😉"
# pip install 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]
from minbpe import GPT4Tokenizer
tokenizer = GPT4Tokenizer()
print(tokenizer.encode(text))
# [15339, 4513, 12340, 30, 320, 31495, 230, 75265, 243, 92245, 16715, 57037]
# 2.标记特殊token
text = "<|endoftext|>hello world"
print(enc.encode(text, allowed_special="all"))
# [100257, 15339, 1917]
# ours
print(tokenizer.encode(text, allowed_special="all"))
# [100257, 15339, 1917]
调用 encode 时必须显示声明处理特殊标记。allowed_special 参数可以设置为"all"、“none"或一个特殊token列表。
防止恶意注入
风险特殊令牌(如<|endoftext|>、<|user|>等)通常在模型中有特殊含义(如终止序列、角色区分)。如果分词器默认解析用户输入中的特殊令牌,攻击者可能会在用户输入中恶意插入这些令牌,诱导模型执行非预期行为(例如提前终止生成、切换角色)。显式声明allowed_special参数(如限制为 "none" 或指定安全列表),可以避免将用户可控数据中的特殊字符误解析为功能性令牌,本质上是一种输入验证机制。
避免意外的语义干扰
即使没有恶意攻击,用户输入中也可能包含与特殊令牌相同的字符串(例如自然文本中恰好出现<|end|>)。如果分词器默认解析这些字符串为特殊令牌,会破坏原始文本的语义,导致模型处理出错(如错误截断、角色混淆)。通过显式控制,开发者可以确保只有预期的特殊令牌被解析,其他类似字符串仅作为普通文本处理。
3.训练
minbpe 可以在语料上训练自己的分词器。
# 仿照openai,使用正则方法来拆分文本
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
如果要添加special tokens:
# 前256为原始字节token,接下来32768-256为合并的token,后面的32768是特殊token
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")
3.项目解析
3.1项目结构
项目中有两个分词器,它们都可以执行分词器的三个主要功能:1)在给定文本上训练分词器词汇并执行合并,2)将文本编码为tokens,3)将tokens解码为文本。仓库的文件如下:
- minbpe/base.py: 实现了
Tokenizer类,它是基础类。它包含train、encode和decode、save/load功能,还有一些常见的工具函数。这个类不是直接使用的,而是用来继承的。 - minbpe/basic.py: 实现了
BasicTokenizer,这是 BPE 算法最简单的实现,直接在文本上运行。 - minbpe/regex.py: 实现了
RegexTokenizer,通过正则表达式模式进一步分割输入文本,这是一个预处理阶段,在分词之前按类别分割输入文本。这确保了不会在类别边界处进行合并。这个方法在 GPT-2 论文中被引入,并且截至 GPT-4 仍在使用。这个类还处理特殊标记(如果有)。 - minbpe/gpt4.py: 实现了
GPT4Tokenizer。这个类是RegexTokenizer的一个轻量级包装器,精确地重现了 tiktoken 库中 GPT-4 的分词。
最后,脚本 train.py 在输入文本 tests/taylorswift.txt 上训练了两个主要的分词器,并将词汇表保存到磁盘以供可视化。
3.2代码分析
class 代码分析(https://zerolovesea.github.io/2024/03/09/MiniBPE%EF%BC%9A%E6%8E%A2%E7%A9%B6Github%E4%B8%8A%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84BPE%E5%AE%9E%E7%8E%B0%E4%BB%A3%E7%A0%81/#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95):
def __init__(self):
super().__init__()
BasicTokenizer 在 encode 时,先将文本转换为utf-8编码,然后计算stats,即文本中相邻字节的频率。
pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))
在 merges 中 寻找 stats ,找不到则设为inf。然后取最小值,因为值越小,说明该token越处于 merge 的早期,这样能实现逐渐聚合。
RegexTokenizer 在训练阶段进行改进:
- 使用正则表达式将输入文本拆分为
text_chunks(例如,将"Hello, world!"拆分为["Hello", ",", " world", "!"])。 - 将每个文本块编码为 UTF-8 字节序列。例如,
"Hello"编码为字节b'Hello',再转为[72, 101, 108, 108, 111]。 - 在每个文本块内部计算stats,这一点和basic不同。这样计算相邻字符的频率时不会跨单词。
special token 的处理:
register_special_tokens将指定的 special token 加入 vocab 中。- 在
encode时,如果allowed_special="all",先使用正则表达式将文本中的 special token 分离出来,得到special_chunks。- 对于不含 special token 的 part,使用
encode_ordinary分割、转换为编码。 - special token 直接转换为编码,实现优先处理。
- 对于不含 special token 的 part,使用
- 如果
allowed_special="none",则整个文本使用encode_ordinary处理。