大模型进食的“咀嚼”艺术:深入浅出 Tokenization(BPE、WordPiece 与 SentencePiece)
当我们在与 ChatGPT、Claude 或是文心一言对话时,我们输入的是人类熟悉的自然语言文本,但在大语言模型(LLM)那庞大的神经网络背后,它并不能直接“读懂”这些文字。模型的眼中没有古诗和代码,只有高维空间中流淌的向量。
那么,人类的文字是如何跨越这道鸿沟,转化为模型能够理解的数学表示的呢?这就要归功于大模型处理文本的第一道关卡,也是至关重要的基石——Tokenization(分词)。
如果把大模型比作一个需要进食的庞然大物,那么 Tokenization 就是它咀嚼食物的牙齿。咀嚼得好不好,直接决定了营养(信息)吸收的效率。今天,我们就来深度解析大模型时代最核心的几种 Tokenization 算法:BPE、WordPiece 与 SentencePiece,揭开它们背后的设计哲学与工程实现。
一、 为什么我们需要 Tokenization?
在深入算法之前,我们需要先明白一个基础问题:为什么我们需要分词?直接把整句话或者单个字母丢给模型不行吗?
实际上,自然语言处理(NLP)历史上经历过三种主要的文本切分粒度,每种都有其明显的优缺点:
1. 词级别
最直观的方法,按空格和标点把句子切成单词。
- 优点:符合人类直觉,语义保留完好。例如
"I love apples"切分为["I", "love", "apples"]。 - 缺点:词表极其庞大。英语还好,但在中文里,词的组合千变万化。更致命的是,面对未见过的词(OOV,Out-of-Vocabulary,如拼写错误的
"appleeeee"),模型只能被迫分配一个<UNK>(未知)标记,导致信息丢失。此外,像"unfriendly"这样的词,明明带有"un-"前缀,却被当成一个毫无关联的整体。
2. 字符级别
把句子切分成单个字符,甚至字母。
- 优点:词表极小(英文只需 26 个字母加一些符号,中文常用汉字几千个),彻底消除了 OOV 问题。
- 缺点:序列过长,且缺乏高级语义。
"apple"变成了["a", "p", "p", "l", "e"],模型为了理解一个简单的单词,需要处理更长的序列,这在 Transformer 架构中会导致计算量呈平方级爆炸。
3. 子词级别—— 最优解
为了折中上述两种方法的缺陷,子词分词应运而生,也是目前大模型的绝对主流。它的核心理念是:
- 高频词保留为整词(如
"apple")。 - 低频词拆解为有意义的子词或字符(如
"unfriendly"拆分为"un","friend","ly")。 - 通过这种方式,既能控制词表大小在合理的范围内(通常在 3万 到 15万 之间),又能有效处理几乎所有的 OOV 问题。
接下来,我们将详细介绍统治大模型领域的三大子词分词算法。
二、 BPE (Byte-Pair Encoding):GPT 系列的“绝对主力”
BPE 最初是一种古老的数据压缩算法,在 2015 年被 Sennrich 等人引入到 NLP 领域。目前,OpenAI 的 GPT 系列(GPT-2, GPT-3, GPT-4)、Meta 的 LLaMA 等绝大多数主流大模型,都采用了 BPE 或其变体。
1. BPE 的核心思想
BPE 的核心逻辑非常简单:寻找文本中出现频率最高的一对相邻 Token,将它们合并为一个新的 Token,不断重复这个过程,直到达到预设的词表大小。
2. BPE 算法演练(手把手推演)
假设我们有一段微型语料库(已经进行了初始的词频统计,并按字符切分,词尾加上了 </w> 标记表示单词结束):
low出现了 5 次 ->l o w </w>: 5lower出现了 2 次 ->l o w e r </w>: 2newest出现了 6 次 ->n e w e s t </w>: 6widest出现了 3 次 ->w i d e s t </w>: 3
第一步:统计相邻字符对的频率
我们统计所有相邻字符对在整个语料库中出现的总次数:
e和s相邻出现了 6 (newest) + 3 (widest) = 9 次。s和t相邻出现了 9 次。- (其他组合如
l和o出现了 7 次等等)
第二步:合并最高频的字符对
假设 e 和 s 出现频率最高(9次),我们将它们合并为 es。此时语料库更新为:
l o w </w>: 5l o w e r </w>: 2n e w es t </w>: 6w i d es t </w>: 3
第三步:不断重复(迭代)
接下来,新的最高频组合变成了 es 和 t(它们总是相邻,出现了9次)。我们将它们合并为 est:
l o w </w>: 5l o w e r </w>: 2n e w est </w>: 6w i d est </w>: 3
如此循环往复,比如下一步可能会合并 l 和 o 得到 lo,再合并 lo 和 w 得到 low。当词表大小达到我们预设的阈值(例如 50000)时,迭代停止。
3. BPE 的编解码机制
- 编码:当遇到一个新词时,比如
"lowest",BPE 会按照训练时生成的合并规则的顺序,从左到右进行贪婪匹配。先合并e和s,再合并es和t,最终切分为["low", "est"]。 - 解码:将所有的 Token 拼接起来,并将特殊的结束标记
</w>替换为空格即可还原文本。
4. Python 代码实现:从零构建一个简单的 BPE
为了更好地理解,我们用几十行 Python 代码来模拟 BPE 的训练过程:
1 | import re |
三、 WordPiece:BERT 的“拼图游戏”
WordPiece 算法最早由 Google 团队提出,并在著名的 BERT 模型中大放异彩。它和 BPE 非常相似,也是一种基于子词的分词方法,但在合并策略上有着本质的区别。
1. BPE 与 WordPiece 的区别
- BPE 选择合并的对象:出现频率最高的相邻 Token 对。
- WordPiece 选择合并的对象:能够最大化语言模型似然度(Likelihood)的相邻 Token 对,也就是互信息最大的对。
2. 什么是“最大化似然度”?
通俗地讲,WordPiece 在考虑是否将 A 和 B 合并为 AB 时,它不是单纯看 A 和 B 在一起出现了多少次,而是看合并后的 AB 这个整体,是否比 A 和 B 单独出现时的独立性概率乘积要高出多少。
公式表达为:
为什么要这么做?
假设在语料库中,th 这个组合出现了 1000 次,er 出现了 1500 次,而 the 出现了 900 次。
如果按照 BPE 的逻辑,可能会优先合并 er。
但 WordPiece 会认为,e 和 r 单独出现的概率也很高,所以它们组合在一起并不稀奇;相反,t 和 h 组合成 th 的概率,远远大于 t 和 h 单独出现概率的乘积。这说明 th 是一个非常紧密、不可分割的语言单元,应该优先合并 t 和 h。
因此,WordPiece 倾向于找出那些**“真正应该属于一起”**的词根、前缀和后缀。
3. 词表前缀标记 ##
WordPiece 在处理文本时有一个非常明显的特征:为了区分一个 Token 是词的开头,还是词的后续部分,它使用了 ## 前缀。
例如,对于单词 "unwanted":
- 它可能会被切分为:
["un", "##want", "##ed"]。 "un"作为词的开头,没有前缀。"##want"和"##ed"表示它们不能独立作为一个单词的开头,只能附属于前面的词。
这种机制在处理英文等以空格分隔的语言时非常优雅。
四、 SentencePiece:一统多语言天下的“瑞士军刀”
无论是基础的 BPE 还是 WordPiece,它们都有一个隐含的前提假设:文本是由空格和标点天然分隔的(主要针对英语等印欧语系)。
但是,当我们面对中文、日文、韩文(CJK 字符)时,这个假设就崩塌了。中文是没有空格的!如果在训练大模型前,我们必须先调用一次 Jieba 分词或 HanLP 等外部工具对中文进行预处理,这不仅会增加工程复杂度,还会因为外部分词工具的误差导致大模型的学习受限。
为了解决这个问题,Google 开源了 SentencePiece。它是一种与语言无关的纯数据驱动的分词框架,广泛应用于 LLaMA、ChatGLM、BART 等现代多语言大模型中。
1. SentencePiece 的核心革命:把空格也当字符
SentencePiece 的最伟大之处在于,它彻底抛弃了“空格即边界”的成见。
在 SentencePiece 的眼中,无论什么语言的文本,都只是一长串纯粹的字符流。空格只不过是一个名叫 ▁ (U+2581) 的特殊字符(这个符号叫 Lower One Eighth Block,视觉上像一个小黑块或下划线)。
- 输入:
"Hello world" - SentencePiece 内部处理:
"▁Hello▁world"(去掉了原始空格,替换为特殊字符▁)
这样一来,中文的 "你好世界" 和英文的 "Hello world" 在算法面前没有任何区别,算法可以直接在统一的字符序列上进行 BPE 或 Unigram(SentencePiece 默认使用的另一种子词算法)训练,完美实现了跨语言的统一处理。
2. Unigram Language Model (ULM)
虽然 SentencePiece 支持加载 BPE 模式,但它的默认核心算法是 Unigram Language Model。
与 BPE 的“自底向上(从字符不断合并成词)”不同,ULM 是**“自顶向下”**的:
- 它首先用一个巨大的词表(比如几百万个候选子词)初始化。
- 通过 EM 算法(期望最大化)评估每个子词在现有语料上的概率。
- 不断剪枝,剔除那些对总体似然度贡献最小的子词,直到词表缩小到目标大小(如 32000)。
- 在编码时,ULM 会计算出同一个单词多种切分方式的概率,选择概率最大的那一种(通常使用 Viterbi 算法寻找最优路径)。
3. SentencePiece 代码实战
让我们看看如何用 sentencepiece 库训练一个支持中英文混合的分词器:
1 | # 安装: pip install sentencepiece |
五、 进阶探讨:大模型时代的 Tokenizer 进化
在了解了三大基础分词器后,我们来看看在当今动辄千亿参数的大模型时代,Tokenizer 有哪些值得关注的工程细节和进化方向。
1. 令人抓狂的“Tokenizer 痛点”:Token 瘟疫
你可能遇到过这样一种情况:当你让 GPT-3 数数或者拼写单词时,它表现得像个笨蛋。比如它很难数清楚单词 "strawberry" 里有几个 r。
罪魁祸首就是 Tokenizer!
在 GPT-3 的 BPE 字典里,"strawberry" 可能被完整地切分为了一个单独的 Token ["strawberry"],而不是 ["s", "t", "r", "a", "w", "b", "e", "r", "r", "y"]。既然在模型眼中它就是一个不可分割的符号,它自然就不知道里面包含了几个 r。
2. Tiktoken:OpenAI 的速度引擎
到了 GPT-3.5 和 GPT-4,OpenAI 换用了基于 BPE 的 tiktoken 库。
与传统的 SentencePiece 或 HuggingFace Tokenizer(主要基于 Rust 编写)不同,tiktoken 是一个高度优化的纯 Python 库,它的运算速度极快。它通过将所有合并规则放入一个巨大的字典映射中,牺牲了一点内存,换取了极高的吞吐量,这对于需要处理海量并发请求的 ChatGPT 来说至关重要。
3. 多语言与代码的平衡术
如果你观察 LLaMA-1 的分词器,你会发现它在处理中文时非常吃力。因为它的训练语料绝大部分是英文,导致一个中文字符往往被切分成好几个无意义的 UTF-8 字节 Token。这不仅消耗了极其宝贵的 Context Window(上下文窗口),还削弱了模型对中文的理解能力。
为了解决这个问题,后来的开源模型(如 Qwen、Baichuan、ChatGLM)在训练 Tokenizer 时,大幅增加了中文高质量语料的比例,甚至在词表中硬性增加了数万个中文字符,使得大模型对中文的压缩率大幅提升。同样的句子,使用的 Token 数量减少了,模型就能“看到”更多的上下文,智能水平也随之提升。
六、 总结
Tokenization 是连接人类语言与机器数学世界的桥梁。从基础的字符切分,到如今大模型中百花齐放的子词算法,分词技术的演进反映了 NLP 领域对“如何最高效地传递语义”这一核心问题的不断探索。
让我们用一张表来总结三大核心算法的特点:
| 算法 | 代表模型 | 合并策略 | 特点 | 空格处理 |
|---|---|---|---|---|
| BPE | GPT-2, LLaMA | 最高频的相邻字符对 | 简单高效,纯统计驱动 | 需要预分词,保留空格边界 |
| WordPiece | BERT | 基于似然度最大化(互信息) | 倾向合并语义强关联的词根 | 预分词,使用 ## 标记非词首子词 |
| SentencePiece | LLaMA-2, T5, GLM | 支持 Unigram (默认) 和 BPE | 语言无关,支持多语言统一处理 | 将空格转为特殊字符 ▁,无需预分词 |
随着 Byte-level 模型(如 MegaByte)和多模态大模型的兴起,未来我们可能会逐渐淡化传统的 Tokenizer,转而让模型直接从原始字节甚至音频波形中学习。但不可否认的是,在现阶段和可预见的未来,BPE、WordPiece 和 SentencePiece 构成的子词分词体系,依然是支撑大模型智能爆发的最坚实的基石。
理解了 Tokenization,你就真正迈出了看懂大模型底层机制的第一步。