本篇文章介绍大语言模型(LLM)中常用的几种分词(Tokenizer)方法,包括传统分词、BPE、WordPiece、SentencePiece 以及 Unigram Language Model。我们将通过原理解析、应用场景和代码示例,帮助读者理解这些分词技术在 LLM 中的作用与实现。
💡 引言:大语言模型(LLM)的基石——Tokenizer
“一切输入皆为序列”——这是所有现代深度学习模型处理数据的核心哲学,对于大语言模型(LLM)而言更是如此。
无论我们输入的是一个中文句子、一段英文代码、还是一串生动的 Emoji 表情,LLM 都无法直接理解这些人类语言的字符串(String)。它们只能接收并处理数字形式的输入。因此,我们迫切需要一座“翻译桥梁”,将人类可读的文本转化为机器可计算的数字序列,这座桥梁就是 Tokenizer(分词器)。
为什么 Tokenizer 是 LLM 的基石?
- 机器可读性(Input Conversion):Tokenizer 将文本切分成最小的语义单元——Token,并用唯一的 ID(索引号) 来表示,使之成为模型可以处理的数字输入。
- 词汇表与计算效率(Vocabulary &
Efficiency):一个理想的 Tokenizer
必须在词汇表(Vocabulary)大小和序列长度(Sequence
Length)之间找到平衡点。
- 如果词汇表过大(例如,包含所有可能的单词),模型训练将变得极其缓慢且难以泛化。
- 如果 Token 粒度过细(例如,全部分割成单字符),虽然词汇表小了,但文本序列会变得太长,Self-Attention 机制的计算成本将以序列长度的平方 O(N2) 爆炸式增长。
- 泛化能力与 OOV 问题(Generalization & OOV):Tokenizer 的切分策略直接决定了模型处理未登录词(OOV, Out-Of-Vocabulary)的能力。像 BPE 和 WordPiece 这样的子词(Subword)分词方法,通过将新词拆分成已知的词根或子词,显著增强了模型的泛化能力。
1️⃣ 传统分词(Rule-based / Word-level)
🔍 原理
传统分词基于空格或人工规则进行切分。
- 英文:直接用空格拆词。
- 中文:需要词典和规则(如结巴分词、THULAC 等)。
- 无法处理未登录词(OOV),粒度较粗。
📦 应用场景
- 传统 NLP 特征工程(TF-IDF、BM25)
- 不依赖大模型的任务(情感分析、分类)
💻 示例(jieba 中文分词)
1 | import jieba |
输出:
1 | ['我', '爱', '自然语言', '处理', ' ', 'i', ' ', 'like', ' ', 'natural', ' ', 'language', ' ', 'process'] |
2️⃣ BPE(Byte Pair Encoding)
🔍 原理
BPE 是一种 基于频率的子词切分算法。
- 从字符级开始(如 “l”, “o”, “w”, “e”, “r”)。
- 找出频率最高的相邻对(如 “l” + “o” → “lo”)。
- 合并成新的子词,不断重复,直到词汇表大小达到限制。
- 在推理时按最长匹配优先进行切分。
📦 应用场景
- GPT-2、RoBERTa、LLaMA
- 英文效果好,可应对拼写变化与新词
💻 示例(使用 tokenizers
库)
1 | from tokenizers import Tokenizer, models, trainers, pre_tokenizers |
输出:
1 | 词表大小: 50 |
- 词表太小,BPE 没法把所有长单词收录完整。
- 算法会优先保留 高频子词/词根,低频词只能拆成更小单位。
因为vocab_size=50,词表比较小,所以BPE最后生成的大部分都是词根,并且是在训练的语句中经常出现的,例如:“lower”,“New”等等。
当我们设置vocab_size=100:
1 | 词表大小: 96 |
高频单词和长单词被更多地完整保留:
lowest,lower,technologies都出现在词表中
只有一些低频、长尾词被拆分,例如:
amazing→a,m,a,in,g
分词粒度更粗,保留更多完整单词,而非拆成单字或极小子词。
在此基础上,设置min_frequency=2:
1 | 词表大小: 39 |
- 高频子词(如
low,est,lower)被保留 - 低频子词或字符被拆得更细,例如:
The→'T', 'h', 'e'technologies→'te', 'c', 'h', 'n', 'o', 'lo', 'g', 'ies'amazing→'a', 'm', 'a', 'i', 'n', 'g'
因为min_frequency=2 会
排除低频组合,于是 BPE
没法把低频长单词直接加入词表(即使词表余量充足),只能拆成更小的字母或子词组合。高频子词仍然被保留,因此
lower, low, est
等能完整出现。
3️⃣ WordPiece(基于概率的BPE变体)
🔍 原理
- BPE: 规则很简单,谁“数量多”就合并谁。它合并的是出现频率最高的相邻pair。
- WordPiece: 规则更“聪明”:它合并的是能使训练数据总“似然(Likelihood)”提升最大的相邻 pair。
我们用一个简单的例子来说明:
- 假设我们要决定是合并
A和B呢,还是合并C和D。 count(A)表示A出现的总次数。count(A, B)表示A和B相邻出现的总次数。
1. BPE 的打分方式(频率)
BPE 只看相邻次数: Score(A, B) = count(A, B)
如果 count(A, B) = 100 且
count(C, D) = 90,BPE会选择合并 A, B。
2. WordPiece 的打分方式(似然)
WordPiece 认为,仅仅看 count(A, B) 是不够的。如果
A 和 B 本身都是超级高频的字母(比如 ‘e’ 和
‘s’),它们俩相邻出现100次可能纯属巧合。
WordPiece 的打分公式(简化后)是:
Score(A, B) = count(A, B) / ( count(A) * count(B) )
这个公式在计算 A 和 B 相邻出现的概率
count(A, B)
的同时,还除以了它们各自独立出现的概率
count(A) 和 count(B)。
这实际上是在计算一个“点互信息 (Pointwise Mutual Information,
PMI)”: A 和 B
一起出现的概率,相比于它们各自独立、“碰巧”出现在一起的概率,要高出多少?
举个例子:为什么 WordPiece 更“语义相关”
假设我们的语料库非常大,我们有以下统计数据:
- Pair 1:
('t', 'h')(比如 “the”, “this”, “that”)count(t, h)= 1,000,000 (相邻出现100万次)count(t)= 5,000,000count(h)= 4,000,000
- Pair 2:
('e', 's')(比如 “these”, “makes”, “goes”…)count(e, s)= 1,200,000 (相邻出现120万次)count(e)= 10,000,000 (最常见的字母)count(s)= 8,000,000
BPE 的选择:
- Score(
t, h) = 1,000,000 - Score(
e, s) = 1,200,000 - BPE 结论:
120万 > 100万,优先合并('e', 's')。
WordPiece 的选择:
Score(
t, h) = 1,000,000 / (5,000,000 * 4,000,000) = 1 / 20,000,000Score(
e, s) = 1,200,000 / (10,000,000 * 8,000,000) = 1.2 / 80,000,000 = 1 / 66,666,667WordPiece 结论:
1/20M远远大于1/66M。('t', 'h')这个组合的“意义”远大于('e', 's')。解读:
t和h结合在一起,比它们“随机”碰在一起的概率高得多,这说明th是一个强相关的“语义单元”。而e和s虽然也经常挨在一起,但因为它们各自都太常用了,所以它们挨在一起很可能只是“巧合”,es并不是一个像th一样牢固的单元。因此,WordPiece 会优先合并
('t', 'h')。
BPE 是“频率驱动”的,简单粗暴。而 WordPiece
是“概率驱动”的,它通过除以单个 token
的频率,惩罚了那些由高频、无意义的 token
组成的“巧合”pair,从而优先保留了像
th、ing、pro
这样真正具有强内部相关性的“语义单元”。
📦 应用场景
- BERT、ALBERT、DistilBERT
- 英文任务常用,能兼顾泛化性与语义一致性
💻 示例(Hugging Face自带的WordPiece)
1 | from transformers import BertTokenizerFast |
输出:
1 | 词表大小: 30522 |
附注
## 标记是 WordPiece
算法的核心机制,用于确保分词过程的“可逆性”和“无歧义性”。
简单来说,它的作用是告诉解码器:“这个 token 是一个词的中间或后缀,它应该紧贴着前一个 token,它们之间没有空格。”
为什么这是必要的?
想象一下,如果我们没有 ## 标记,词汇表里只有
counter, pro,
ductive。那么下面两个完全不同的输入会产生完全相同的结果:
"counterproductive"(一个词) ->['counter', 'pro', 'ductive']"counter pro ductive"(三个词) ->['counter', 'pro', 'ductive']
这就导致了一个严重问题:当模型输出
['counter', 'pro', 'ductive']
时,我们无法知道应该将它重构为 “counterproductive” 还是 “counter pro
ductive”。
## 如何解决这个问题:
WordPiece 的词汇表会同时包含 pro(词首)和
##pro(非词首)。
句子 1:
"counterproductive"- 分词结果:
['counter', '##pro', '##ductive'] - 解码:
counter+ (无空格)pro+ (无空格)ductive-> “counterproductive”
- 分词结果:
句子 2:
"counter pro ductive"分词结果:
['counter', 'pro', 'ductive']解码:
counter+ (加空格)pro+ (加空格)ductive-> “counter pro ductive”
BPE(及其变体 SentencePiece,被 GPT、RoBERTa 等模型使用)也必须解决同样的可逆性问题,但它采用了逻辑上相反的标记策略。
- WordPiece (BERT): 默认所有 token 间加空格,使用
##标记“不要加空格”。- BPE (GPT): 默认所有 token 间不加空格,使用一个特殊前缀(如
或Ġ,代表空格)来标记“请在这里加一个空格”。BPE/SentencePiece 示例:
在 BPE 的词汇表里,一个词根(如 “pro”)会有两种形式:
pro(无前缀):表示这是一个词的中间或后缀(等同于 WordPiece 的##pro)。Ġpro(带前缀Ġ或):表示这是一个新词的开头(等同于 WordPiece 的pro)。我们用 BPE 来处理同样的两个句子:
句子 1:
"counterproductive"
- 分词结果:
['counter', 'pro', 'ductive'](这里的counter是词首,pro和ductive是非词首)- 解码:
counter+ (无Ġ)pro+ (无Ġ)ductive-> “counterproductive”句子 2:
"counter pro ductive"
- 分词结果:
['counter', 'Ġpro', 'Ġductive']- 解码:
counter+ (有Ġ)pro+ (有Ġ)ductive-> “counter pro ductive”两种算法都解决了歧义问题,只是标记策略相反。WordPiece 标记“非词首”,而 BPE/SentencePiece 标记“词首”。
1 | from transformers import AutoTokenizer |
4️⃣ SentencePiece(Google开发的通用分词器)
🔍 原理
在 BPE / WordPiece 的经典实现(如
huggingface 的 tokenizer 或
GPT/BERT)中,训练输入通常是:the cat sat on the mat -> [“the”,
“cat”, “sat”, “on”, “the”, “mat”] (默认用空格分割单词)。然后在这些 token
上做 BPE 统计相邻 pair
频率,逐步合并,空格已经被视为“不可合并的边界”。
SentencePiece 不依赖空格或人工分词规则,而是直接在原始字符流上学习子词单元。
将空格也视作一种符号(用
_来替换所有空格)使用两种常用模型进行分词:BPE 和 Unigram
📦 应用场景
- T5、ALBERT、mBERT
- 适用于中英混合、多语言模型
💻 示例(SentencePiece)
1 | from transformers import T5TokenizerFast |
输出:
1 | Tokens: ['▁The', '▁lowest', '▁lower', '▁technologies', '▁are', '▁amazing', '▁and', '▁counter', 'productive', '.'] |
“▁” 表示空格位置,进而也可以表示该token是不是一个单词或一句话的第一个token
当然,这里t5-base输出的中文分词粒度很粗,是因为其训练语料中中文数据很少
我们可以换几个用其他中文数据集训练的T5模型试试:
1 | from transformers import AutoTokenizer |
5️⃣ Unigram Language Model(SentencePiece默认算法)
🔍 原理
Unigram Language Model 算法的核心目标是:找到一个最优的子词集合(词汇表),使得训练语料的整体似然(Likelihood)最大化。 它不依赖于简单的频率计数(如 BPE),而是引入了概率模型。
- 模型假设: 假设任何一个句子 S 都可以被分解为一系列子词 x1, x2, …, xn。ULM 假设这些子词是独立地从词汇表 Σ 中抽样出来的(即 Unigram 假设)。
- 似然函数: 句子 S 的概率(似然)可以表示为:
其中,Splits(S) 是 S 所有可能的分词组合。算法的目标是找到词汇表 Σ 和每个子词的概率 P(x),使得 log (P(Corpus)) 最大化。
分词过程:自上而下的剪枝法
ULM 算法的训练分为三个关键阶段:初始化、迭代优化和最终分词。
阶段 I: 初始化
- 准备: 将语料库进行 SentencePiece 预处理(用
_替换空格)。 - 构建大词汇表 Σ: 收集语料库中所有长度在 1 到某个最大值(如 5)之间的所有子字符串。这个初始词汇表 Σ 非常庞大。
- 计算初始概率: 对 Σ 中的每个子词 x,计算其在语料库中的初始频率。
阶段 II: 迭代优化(剪枝)
这个阶段是 ULM 的核心,通过 EM (Expectation-Maximization) 算法的思想来优化词汇表 Σ 和子词概率 P(x)。
- E-步(计算概率): 使用当前的词汇表 Σ 和子词概率 P(x),对语料库中的每个句子 S,找到最佳分词(即概率最大的分词路径)。这通常通过 Viterbi 算法实现,因为它需要找到 max ∏P(xi)。
- M-步(计算损失 / 剪枝):
- 计算损失: 对于词汇表中的每个子词 x,暂时将其删除,然后重新计算语料库的总似然。
- 计算贡献度: 找出那些删除后对总似然影响最小(即贡献度最低)的子词。
- 剪枝: 移除词汇表中贡献度最低的 p %(例如 20%)的子词。
- 重复: 重复 E 步和 M 步,直到词汇表 Σ 达到预设的目标大小(例如 32,000)。
阶段 III: 最终分词
训练完成后,词汇表 Σ 中的每个子词 x 都有一个固定的概率 P(x)。
对于任何输入句子 S,分词器使用 Viterbi 算法,在所有可能的分词路径中,找到那个使 ∏P(xi) 最大的分词序列作为最终结果。
假设我们已经训练好了一个 ULM 词汇表,其中包含以下子词及其概率 P(x):
| 子词 x | 概率 P(x) (Log 形式) |
|---|---|
_p |
-3.0 |
_play |
-1.0 |
ing |
-1.5 |
| lay | -2.0 |
| _playing | -3.5 |
现在,我们要对句子 S = "playing"(预处理后是 _playing)进行分词。
ULM 会考虑所有可能的拆分路径:
| 路径 (Split) | 拆分 Tokens | 概率计算 (Log) | 结果 |
|---|---|---|---|
| 路径 A | _play, ing |
P(_play) + P(ing) |
(−1.0) + (−1.5) = −2.5 |
| 路径 B | _p, lay, ing |
P(_p) + P(lay) + P(ing) |
(−3.0) + (−2.0) + (−1.5) = −6.5 |
| 路径 C | _playing |
P(_playing) |
(−3.5) |
结论: 在 Log 概率中,数字越大(越接近
0)表示概率越高。路径 A (-2.5) 的概率最高,所以 ULM 最终选择:_play, ing
📦 应用场景
- mT5、XLNet(与 SentencePiece 结合)
- 适用于中英混合、多语言模型
💻 示例
上一节中的t5-base和google/mt5-small都是使用的SentencePiece+ULM
6️⃣ Byte-level BPE
🔍 原理
传统的 BPE 训练是从字符集开始的。例如,如果你的语料是英文,BPE 的起始词汇表可能是 26 个小写字母、26 个大写字母、10 个数字和一些标点符号。
然而,如果语料中出现了像 Emoji、生僻符号或非拉丁字符(如日文、俄文)时,BPE 必须将这些新的字符也添加到初始词汇表中。这导致一个问题:初始词汇表大小不固定,且可能非常巨大。 如果你需要支持全世界所有语言,初始词汇表可能包含成千上万个稀有字符。
而BBPE 抛弃了“字符集”作为起点,而是以 字节(Byte) 作为最小单位。
- 起点: 所有的文本都被视为其 UTF-8 编码的字节序列。
- 初始词汇表: 固定的 256 个字节(从 0x00 到 0xFF)。
- 训练: 传统的 BPE 合并规则应用于这个 256 个字节的初始词汇表。
BBPE 如何实现全覆盖(Universal Coverage)
现代计算机中,所有文本都是以字节形式存储的。UTF-8 编码使用 1 到 4 个字节来表示一个字符。
| 字符 | UTF-8 字节序列 |
|---|---|
| 英文 ‘A’ | 0x41 (1 字节) |
| 中文 ‘你’ | 0xE4 0xBD 0xA0 (3 字节) |
由于 BBPE 的初始词汇表包含了所有的 256 个字节,因此它理论上可以表示任何可能的文本输入,包括 Emoji、乱码、甚至是二进制数据。
这确保了 Tokenization 的零 OOV (Out-Of-Vocabulary)
问题:任何输入文本都能被拆分成一系列已知的字节或字节组合,无需使用
<UNK> 标记。
BBPE 的实际技巧:避免无意义的字节组合
虽然 BBPE 从 256
个原始字节开始,但如果直接合并这些原始字节,可能会导致一些奇怪的、无法解码的
Token。例如,如果 0xC3 和 0x28
合并了,这可能构成一个无效的 UTF-8 序列。
为了解决这个问题,BBPE 在训练时通常结合使用以下技巧:
A. 字节到字符的映射(Byte-to-Char Mapping)
为了在视觉上保持可读性(因为一个原始字节可能无法显示),BBPE 通常会将原始的 256 个字节映射到 256 个 Unicode 字符上。
- 特殊处理: 通常会处理那些控制字符(如换行符、制表符)和 ASCII 不可见字符。例如,将 ASCII 0-31 和 127-160 的控制字节映射到 Unicode 中的其他可用区段,保证所有 256 个字节都能被清晰地表示和显示。
- 好处: 这样训练的 BPE 算法是字节级别的,但输出的 Token 仍然是可读的字符串(仅限英文,中文仍是不可读)。
B. 空格编码
与 SentencePiece 类似,BBPE 也需要处理空格信息以确保可逆性。GPT-2/3
采用的方法是在空格前添加一个特殊字符,最常见的是 Ġ(Unicode 字符 U+0120,一个大写
G 上带点)。
- 输入: “Hello World”
- 内部处理: 会被编码为
ĠHelloĠWorld - 分词结果:
['ĠHello', 'ĠWorld']
📦 应用场景
- 避免了为每种语言设计一个特定字符集的问题,是构建大规模多语言模型的理想选择。
- BPE 算法直接在字节序列上运行,无需复杂的字符集预处理。
BBPE 是 BPE 算法面向现代大规模 Transformer 模型和多语言环境的进化版本。它通过将训练的最小单位固定为字节,实现了全覆盖和零OOV,代价是在非拉丁语言上会产生更长的序列。
💻 示例(使用 GPT-2 tokenizer)
1 | from transformers import GPT2TokenizerFast |
输出:
1 | ['ðŁĺ', 'Ĭ', 'Hello', 'Ġ', 'ä½', 'ł', 'å¥', '½', '11', 'ĠThe', 'Ġlowest', 'Ġlower', 'Ġtechnologies', 'Ġare', 'Ġamazing', 'Ġand', 'Ġcounterproductive', '.'] |
可见即使包含 emoji 和中文,也能稳定切分。
并且从输出可以看出,一个中文字符在utf8中常常占3个字节。
Byte-level BPE (BBPE) 是 BPE 算法的一个重要变体,由 OpenAI 率先应用于 GPT-2 模型,并被后续的 GPT-3、GPT-4 和所有现代 BPE 模型(如 RoBERTa、BART)广泛采用。
现代分词算法对比总结
| 方法 | 基本原理 | 是否数据驱动 | 是否依赖原始空格 | 优点 | 缺点 | 代表模型 |
|---|---|---|---|---|---|---|
| 传统分词 | 规则 / 词典 | 否 | 是 | 快速可解释;精确匹配已知词 | 无法处理新词(OOV);无法支持多种语言 | Jieba, TF-IDF |
| BPE (基础版) | 最频繁相邻子词合并 | 是 | 是 | 简单高效;OOV问题减轻 | 依赖空格预分割;对罕见字符支持差 | GPT-2 (使用BBPE) |
| WordPiece | 最大似然相邻子词合并 | 是 | 是 | 子词语义更强;高频词汇拆分更合理 | 依赖空格预分割;需要特殊标记
## |
BERT, ALBERT |
| SentencePiece (框架) | 通用字符流分词器框架 | 是 | 否 | 语言无关;支持中/日/韩等无空格语言;完美可逆 | 需额外的训练和推理库;Token化可能较慢 | T5, XLNet, LLaMA |
| Unigram LM | 概率剪枝;Viterbi最优分词 | 是 | 否 | 理论严谨;分词路径可采样(正则化) | 实现复杂;训练所需资源较大 | T5, mT5, Flan-T5 |
| Byte-level BPE (BBPE) | 对 UTF-8 字节流 进行 BPE 合并 | 是 | 否 | 零 OOV;最鲁棒;真正语言无关 | 序列变长(尤其中文);Token可读性稍差 | GPT-3/4, RoBERTa, BART |