大模型通信的基石:万字深度解析 Tokenization(BPE、WordPiece 与 SentencePiece)
引言:大模型是如何“阅读”文字的?
当我们向 ChatGPT、LLaMA 或 GLM 提问时,我们输入的是人类自然语言(如中文、英文)。然而,对于这些拥有千亿参数的大型语言模型(LLM)来说,它们的世界里没有“文字”,只有“数字”。
将人类的自然语言转换为模型能够理解的数字序列,这个过程就是 Tokenization(分词)。
很多初学者甚至部分开发者往往将注意力全部集中在 Transformer 架构、注意力机制或 RLHF(基于人类反馈的强化学习)上,而忽视了 Tokenization 的重要性。事实上,Tokenization 是大模型与真实世界交互的咽喉要道。一个糟糕的 Tokenizer 会导致模型无法处理多语言、无法进行基本的拼写检查、对数学公式逻辑混乱,甚至产生严重的“乱码”问题(例如早年模型无法正确拼写特定的单词,往往就是 Tokenization 挖的坑)。
在这篇文章中,我们将深入剖析大模型时代最主流的 Tokenization 算法:BPE(Byte-Pair Encoding)、WordPiece 以及 SentencePiece。我们将探讨它们的核心思想、数学原理、代码实现,并横向对比它们在现代大模型中的应用。
一、 Tokenization 的演进:从词到子词
在深入算法之前,我们需要理解为什么需要这些复杂的算法。
1. 传统方法的困境
在 NLP 的早期,分词方法非常直观:
- 词级别: 以空格和标点为界限切分(如
I love AI->["I", "love", "AI"])。- 痛点: 词表会无限膨胀,遇到生僻词只能用
<UNK>(未知词)代替;词与词之间的形态学关系(如run和running)被完全割裂。
- 痛点: 词表会无限膨胀,遇到生僻词只能用
- 字符级别: 将每个字母/汉字单独切分(如
apple->["a", "p", "p", "l", "e"])。- 痛点: 序列变得极其漫长,模型难以捕捉长距离的语义依赖;且单个字符往往没有实际意义(如字母
a无法表达完整含义)。
- 痛点: 序列变得极其漫长,模型难以捕捉长距离的语义依赖;且单个字符往往没有实际意义(如字母
2. 子词分词的崛起
为了平衡“词级别”和“字符级别”的优缺点,子词分词 成为了现代 LLM 的绝对主流。
其核心思想是:高频词保留为完整词,低频词拆分为有意义的子词或字符。
例如,单词 unbelievable 可能会被切分为 ["un", "believ", "able"]。这样做的好处是:
- 彻底解决 OOV(Out-Of-Vocabulary)问题:任何新词都可以用有限的字符集拼凑出来。
- 大幅缩小词表规模:从词级别的几十万,缩减到 3 万到 10 万左右,降低模型 Embedding 层的计算压力。
目前,主流大模型(如 GPT-4、BERT、LLaMA)都在使用基于“子词”的 Tokenizer。
二、 BPE (Byte-Pair Encoding):GPT 系列的“魔法”
BPE 最初是一种简单的数据压缩算法,用来将经常一起出现的字节对进行合并。后来被 Alan DJ 等人引入到 NLP 领域,成为了现代大模型(特别是 OpenAI 的 GPT 系列)最广泛使用的 Tokenization 算法。
1. BPE 的核心思想
BPE 是一种自底向上的贪心算法。
给定一个语料库,BPE 的步骤如下:
- 在词的末尾添加一个特殊的结束符(如
</w>),以区分词内的字符和词尾的字符。 - 将所有词拆分为最基本的字符序列(英文就是 a-z)。
- 统计语料中相邻字符对的出现频率。
- 找出频率最高的字符对,将它们合并成一个新的单一 Token。
- 重复步骤 3 和 4,直到达到预设的词表大小或设定的迭代次数。
2. BPE 算法示例推导
假设我们的语料非常简单,只有四个单词(已经统计了频率):
low (5), lower (2), newest (6), widest (3)
第一步:拆分并添加结束符
l o w </w>: 5l o w e r </w>: 2n e w e s t </w>: 6w i d e s t </w>: 3
第二步:统计相邻字符对的频率
e和s在newest和widest中相邻,总频率为 6 + 3 = 9。s和t的频率也是 9。
(假设我们选择合并e和s)
第三步:合并最高频字符对 (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。
以此类推,最终 est 甚至 lowest 可能会被合并成一个完整的 Token。
3. BPE 代码实现(Python 简易版)
为了真正理解 BPE,没有什么比自己手写一遍代码更好了。以下是一个极其简化但核心逻辑完备的 BPE 训练代码:
1 | import re |
4. GPT 系列的终极武器:Byte-level BPE (BBPE)
传统的 BPE 有一个致命弱点:它依赖于基础的字符集(如英文字母)。如果遇到中文、日文、Emoji 表情,传统 BPE 会把它们当成极其罕见的字符,导致词表爆炸或乱码。
OpenAI 在 GPT-2 中引入了 Byte-level BPE (BBPE)。
其核心哲学非常硬核:放弃所有 Unicode 字符集的概念,一切皆字节。
在计算机底层,所有的文本最终都是以 UTF-8 编码的字节形式存储的。BBPE 直接将基础的 256 个 Byte(字节)作为初始词表。
- 对于英文,基础字节就是 ASCII 码。
- 对于中文,一个汉字会被编码为 3 个字节(如
中->\xe4\xb8\xad),BBPE 会直接以这 3 个字节为单位进行学习和合并。
BBPE 的优势:
- 绝对的多语言支持:无论是日文、韩文还是颜文字,底层都是字节,模型可以通用。
- 极其稳定的基础词表:基础词表大小被永远固定为 256,不会因为语料的语言变化而改变。
三、 WordPiece:BERT 的选择
WordPiece 算法最早在 2012 年由微软提出,并在著名的 BERT 模型中大放异彩。从宏观上看,它和 BPE 非常相似,都是通过迭代合并字符对来构建词表。但它们的合并策略(核心评分函数)截然不同。
1. BPE vs WordPiece
- BPE: 选择频率最高的字符对进行合并。
- WordPiece: 选择结合后使得语言模型似然概率(Likelihood)提升最大的字符对进行合并。
2. 似然概率的数学解释
假设我们将字符 A 和字符 B 合并为新 Token AB。WordPiece 并不单纯看它们出现的次数,而是评估合并它们对整个语料库的“合理性”提升有多大。
其评分公式可以简化理解为:
这其实是计算 A 和 B 的点互信息。
- 如果 A 和 B 经常一起出现,而在其他地方很少单独出现,那么 就会很高。
- 相比于 BPE 的纯频率统计,WordPiece 更倾向于合并那些“内部凝聚力强”的字符。
3. 前缀标记 ##
在使用 WordPiece 时,最明显的视觉特征是它使用了 ## 前缀来表示子词的延续。
例如,单词 unwanted 可能被分词为:["un", "##want", "##ed"]。
un是词的开头。##want表示这不是一个独立的词,而是接在un后面的子词。
这种设计的目的是为了让模型在输入层面就能区分:一个 Token 是作为词的开头,还是作为词的中间/结尾部分。
4. WordPiece 的推理(反向最大匹配)
训练 WordPiece 词表后,在实际推理(对新文本进行分词)时,算法与 BPE 略有不同。
BPE 是按照训练时学到的合并规则从前往后应用。而 WordPiece 通常采用贪婪最长匹配策略:
从句子的第一个字符开始,在词表中寻找匹配到的最长子词,然后继续处理剩余部分。
四、 SentencePiece:多语言大模型的“瑞士军刀”
SentencePiece 是 Google 开源的一个极其强大的语言无关的 Tokenizer 框架。如今,绝大多数开源大模型(如 LLaMA、ChatGLM、BLOOM、Mistral 等)都在使用 SentencePiece 或基于它的改进版。
1. 为什么需要 SentencePiece?
无论是基础的 BPE 还是 WordPiece,它们都有一个隐含的假设:文本已经有空格作为预分词的边界。
这在英文等西方语言中没问题,但在中文、日文等 CJK(中日韩)语言中,词与词之间是没有空格的。
如果不做特殊处理,传统的 Tokenizer 通常有两种粗暴的做法:
- 按字切分: 中文词表巨大,且丧失了词级别的语义。
- 前置分词器: 在 Tokenization 前,先引入 Jieba 等分词工具。这会导致 NLP 流程极其复杂,且严重依赖外部工具。
SentencePiece 的核心革命在于:它将语言视为纯粹的字符流,将空格也视为一种特殊的字符(通常被编码为 ▁,即 Unicode 字符 U+2581),从而彻底摆脱了对预分词的依赖。
2. SentencePiece 的两大核心算法
SentencePiece 本质上是一个框架,它主要支持两种底层算法:
Unigram Language Model (ULM)
与 BPE 自底向上的贪心合并不同,Unigram 是一种自顶向下的算法。
- 初始化:从一个极其庞大的候选子词词表开始(例如所有可能的子串,或者用 BPE 先跑出一个大词表)。
- 建模:假设每个子词都是相互独立的,通过 EM 算法(期望最大化)估计出每个子词在语料库中出现的概率。
- 剪枝:计算移除每个子词后,对整个语料库似然函数的损失。移除那些“对整体似然贡献最小”的子词。
- 迭代:不断重复 2 和 3,直到词表缩减到目标大小。
ULM 的最大优势是概率化。 它不是硬性规定一种分词方式,而是对一句话可以给出多种分词路径及其概率。在推理时,通常采用 Viterbi 算法寻找概率最大的分词路径。
Character BPE
SentencePiece 也支持 BPE,但由于它剥离了语言特性(空格处理),这里的 BPE 实现更为纯粹。
3. 代码示例:使用 SentencePiece 训练自己的 Tokenizer
在实际的 LLM 开发中,SentencePiece 极其容易上手。
首先安装库:pip install sentencepiece
假设我们有一个纯文本的训练语料 train.txt(其中包含中文和英文混合):
1 | import sentencepiece as spm |
注意 SentencePiece 输出中的 ▁ 符号,它代表了原始文本中的空格或词的起始位置。
五、 横向对比与实战考量
为了方便大家在阅读论文或选择技术栈时快速判断,我们将上述三种主流方案进行总结和对比。
1. 算法特性对比总结
| 特性 | BPE / BBPE | WordPiece | SentencePiece (Unigram/BPE) |
|---|---|---|---|
| 核心策略 | 频率统计,贪心合并相邻对 | 语言模型似然概率,PMI 最大化 | 纯粹的字符流处理,支持 BPE 和 Unigram 剪枝 |
| 合并方向 | 自底向上(从字符合并到词) | 自底向上 | Unigram 是自顶向下(从大词表剪枝) |
| 多语言支持 | GPT-2 的 BBPE 彻底支持多语言 | 需要复杂的预分词处理,对中文不友好 | 极佳,原生支持所有语言,无空格依赖 |
| 标志性代表 | GPT 系列, LLaMA (基于 BBPE) | BERT, DistilBERT | T5, ChatGLM, LLaMA (基于其底层框架) |
| 词表标记 | 无特殊标记 | 使用 ## 标记非词首子词 |
使用 ▁ 标记空格/词首 |
2. 大模型实战中的考量点
当我们在训练一个全新的大型语言模型时,Tokenizer 的设计至关重要。以下是几个必须考虑的工程实战问题:
A. 词表大小的权衡
目前开源大模型的词表大小通常在 32k 到 128k 之间(GPT-4 的词表增加到了约 100k,LLaMA-2 为 32k,LLaMA-3 则剧增到了 128k)。
- 太小(如 30k): 导致常见长单词或中文短语被切碎,序列变长,增加 Transformer 解码的计算负担(推理变慢,计算成本变高)。
- 太大(如 200k): Embedding 层参数量激增,占用极大的显存,且容易导致欠拟合。
B. Tokenizer 的“公平性”问题
这是一个经常被忽略但极其重要的问题。如果你的 Tokenizer 主要在英文语料上训练,它处理英文时一个单词可能就是一个 Token;但处理中文时,一个汉字可能要 2 到 3 个 Token(因为 UTF-8 编码下中文被视作少见字符)。
这就导致了:
- 成本不公: 同样的意思,中文消耗的 Token 数是英文的好几倍,使用 OpenAI API 时中国人要付出更高的成本。
- 上下文缩减: 如果模型上下文长度是 4096 Tokens,中文能承载的实际信息量大打折扣。
这也是为什么后来很多国产大模型(如 ChatGLM、百川、Qwen)必须使用海量中文语料重新训练 Tokenizer,以保证中文的高效编码。
C. 分词中的“数字与代码”陷阱
早期的 Tokenizer 对数字和代码处理极差。比如 123456 可能被切分为 ["12", "34", "56"],导致模型学习数理逻辑异常困难。
现在的大模型(如 GPT-4、LLaMA)通常会在 Tokenization 前加入正则预处理,强制将所有连续的数字切分为固定长度(如每 3 个数字一组),或者将代码中的缩进、括号进行特殊保护。
我们可以看看 OpenAI GPT 系列(及各类开源复现版)在调用底层 Regex 切分时的经典正则表达式:
1 | import regex |
这个正则的巧妙之处在于,它优先保留了英文缩写,剥离了独立的字母/数字块,并把连续的空白符单独剥离,极大提升了代码和数字的分词质量。
六、 HuggingFace 实战:一网打尽 Tokenizer 使用
在现代大模型开发中,我们极少从零手写上述算法,而是直接使用 tokenizers 或 transformers 库。HuggingFace 重新用 Rust 编写的 tokenizers 库具有极高的性能。
以下是使用 HuggingFace 快速加载和使用各类主流大模型 Tokenizer 的代码示例:
1 | from transformers import AutoTokenizer |
七、 总结
Tokenization 是大模型这座摩天大楼的地基。了解它的前世今生,不仅能帮助我们解决日常大模型调用中遇到的“莫名其妙”的 Bug(如字符串包含错误、字符串截断导致乱码、API 计费异常),更能指导我们在训练垂直领域大模型时做出更优的架构选择。
让我们用几句话总结全文:
- 子词分词 是大模型平衡语义与词表大小的最优解。
- BPE 采用高频优先合并,结合 Byte-level 技术后成为了 GPT 等闭源模型的基石。
- WordPiece 采用概率提升优先合并,借助
##标记成为了 BERT 时代的标配。 - SentencePiece 是一套强大的工程框架,通过将空格也视作字符,完美解决了多语言(特别是中日韩)的预处理痛点,成为了当代开源大模型(LLaMA 等)的绝对主力。
随着大模型向着“无限上下文”、“多模态”以及“极致推理加速”的方向演进,Tokenizer 也在不断进化。也许在未来的某一天,大模型能够彻底抛弃现有的文本 Tokenization 方案,直接在音频波形或像素级别进行端到端的学习,但就目前而言,熟练掌握和优化 BPE、SentencePiece,依然是每一位大模型算法工程师和自然语言处理研究者的必修课。