解密大模型的第一道关卡:深度剖析 Tokenization(BPE、WordPiece 与 SentencePiece)

当我们惊叹于 ChatGPT 的对答如流,或是感叹 LLaMA 能够吟诗作赋时,我们往往会将功劳归结于 Transformer 架构的精妙、海量训练数据的堆积以及 RLHF 等对齐技术的魔法。然而,在这些宏大的叙事之下,隐藏着一个极其基础却至关重要的环节——Tokenization(分词)

如果把大语言模型比作一个正在阅读的人类,模型底层计算的是一个个冰冷的数字,而人类看到的是有意义的文本。Tokenization 就是连接这两个世界的“巴别塔”。它不仅决定了模型如何“阅读”文本,更直接影响了模型的推理速度、上下文窗口的实际长度,甚至是模型处理多语言和拼写错误的能力。

今天,我们就来深度剖析大模型背后的 Tokenization 技术,重点探讨业界最主流的三种算法:BPE、WordPiece 与 SentencePiece


1. 为什么我们需要 Tokenization?

在深入算法之前,我们需要回答一个根本问题:为什么不直接把整段文本(字符序列)或者单个字符喂给模型,而要费尽心机去做分词?

1.1 词级别与字符级别的困境

早期的自然语言处理(NLP)通常采用词级别的切分。

  • 优点:语义清晰。
  • 缺点:词表会随着语料库的膨胀而无限扩大,遇到未登录词就完全束手无策。比如网络新词“绝绝子”,词表里没有,模型就只能输出 [UNK]

为了解决 OOV 问题,人们转向了字符级别的切分(将英文切分为 a, b, c,中文切分为单字)。

  • 优点:词表极小(英文只需 26 个字母加一些符号),永远不会有 OOV 问题。
  • 缺点:字符本身缺乏语义(字母 “a” 没有具体含义),且把句子切分成字符会导致序列长度暴增。对于基于 Transformer 的模型来说,序列长度意味着计算复杂度的平方级增长(O(N2)O(N^2)),这是不可接受的。

1.2 子词切分:寻找完美的平衡点

现代大模型普遍采用子词切分。它的核心理念是:
高频词保留,低频词拆分。

像 “apple” 这样的高频词,作为一个整体的 Token;而像 “unbelievably” 这样的低频词,则被拆分为 “un”, “believ”, “ably” 等具有一定语义的子词。这样一来:

  1. 词表大小适中:通常在几万到十几万之间。
  2. 无 OOV 问题:任何奇怪的词汇都可以被拆解为基本的字符或子词组合。
  3. 序列长度可控:在保留语义的同时,尽量缩短了输入序列的长度。

基于这个理念,业界衍生出了三大经典算法。


2. BPE (Byte-Pair Encoding):GPT 系列的基石

BPE 最早是一种数据压缩算法,在 2015 年被 Sennrich 等人引入到 NLP 领域。如今,它统治了绝大多数生成式大模型(GPT-2, GPT-3, GPT-4, LLaMA 系列均使用 BPE 或其变体)。

2.1 核心思想

BPE 的核心逻辑非常朴素:寻找语料中出现频率最高的一对相邻 Token,将它们合并为一个新的 Token,不断重复此过程,直到达到预设的词表大小。

2.2 训练过程详解

假设我们有一段极简的英文语料,经过预处理(加空格、转小写)后,我们首先按字符进行切分,并统计词频:

  • l o w </w>: 5次
  • l o w e r </w>: 2次
  • n e w e s t </w>: 6次
  • w i d e r </w>: 3次

(注:</w> 通常被加在词尾,用来区分词内的子词和作为完整词的子词)

迭代 1:
统计相邻 Token 对的出现频率(受词频加权):

  • esn e w e s t 中出现了 6 次,是最高频对。
  • 动作:将 e s 合并为 es
  • 当前状态:n es t </w> (6次)

迭代 2:
继续找最高频对。假设是 est,出现了 6 次。

  • 动作:合并为 est
  • 当前状态:n est </w> (6次)

迭代 N:
不断迭代,直到词表大小达到我们设定的目标(例如 32000 或 50000)。最终我们可能会得到诸如 low, lower, new, est 这样的高质量子词。

2.3 代码实战:使用 HuggingFace 体验 BPE

我们可以用 HuggingFace 的 tokenizers 库来直观地感受 BPE 的训练和切分过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace

# 1. 初始化一个空的 BPE 模型
tokenizer = Tokenizer(BPE(unk_token="[UNK]"))

# 2. 设定预切分规则(按空格切分)
tokenizer.pre_tokenizer = Whitespace()

# 3. 配置训练器,设定词表大小和特殊字符
trainer = BpeTrainer(vocab_size=1000, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])

# 假设我们有一些训练语料
files = ["my_corpus.txt"]

# 4. 开始训练
# tokenizer.train(files, trainer)

# 为了演示,我们直接使用 GPT-2 的预训练 BPE tokenizer
from transformers import GPT2Tokenizer
gpt2_tokenizer = GPT2Tokenizer.from_pretrained("gpt2")

text = "Unbelievable! Tokenization is magical."
tokens = gpt2_tokenizer.tokenize(text)
ids = gpt2_tokenizer.encode(text)

print(f"Original Text: {text}")
print(f"Tokens: {tokens}")
print(f"IDs: {ids}")

输出解析:
GPT-2 的输出可能是这样的:['Un', 'believ', 'able', '!', ' Token', 'ization', ' is', ' magical', '.']
你可以清晰地看到,高频词 is 被完整保留,而 Unbelievable 被拆分成了有词缀意义的 Un, believ, able。而且,由于 GPT-2 使用了 Byte-level BPE (BBPE),它永远不会出现 [UNK]


3. WordPiece:BERT 的选择

WordPiece 算法在 2012 年由 Google 提出,并在著名的 BERT、DistilBERT 等模型中被广泛使用。它看起来和 BPE 非常相似,但在合并的评判标准上有着本质的区别。

3.1 核心思想与 BPE 的区别

  • BPE:选择语料中出现频率最高的相邻 Token 对进行合并。
  • WordPiece:选择合并后能最大程度增加语言模型似然度 的相邻 Token 对。

在工程实现上,这个“似然度”通常被简化为一个巧妙的公式:

Score(A,B)=Freq(AB)Freq(A)×Freq(B)Score(A, B) = \frac{Freq(AB)}{Freq(A) \times Freq(B)}

WordPiece 不会单纯因为 AB 经常挨在一起就合并它们,而是要看合并 AB 后,相对于 AB 单独出现的概率,其互信息(Mutual Information)有多大。

3.2 为什么 BERT 选择 WordPiece?

对于以理解为主的模型(如 BERT)来说,单纯的词频可能会导致误导。例如,在特定领域的语料中,某些无意义的字符组合可能频率极高,但它们合并在一起并不能带来更好的语义表示。WordPiece 的似然度评估机制,使得合并出来的子词在语言学上更具合理性。

3.3 直观的标记:##

WordPiece 最明显的特征是使用 ## 来标记非词首的子词。
例如单词 unwanted,在 BPE 中可能被切分为 un, want, ed
但在 WordPiece 中,它会被切分为 un, ##want, ##ed。这告诉模型:##want 是一个后接片段,不能作为一个独立单词的开头。

3.4 代码实战:BERT 的 Tokenization

1
2
3
4
5
6
7
8
9
10
11
12
from transformers import BertTokenizer

# 加载 BERT 的 WordPiece 分词器
bert_tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")

text = "Unbelievable! Tokenization is magical."
tokens = bert_tokenizer.tokenize(text)
ids = bert_tokenizer.encode(text)

print(f"Original Text: {text}")
print(f"Tokens: {tokens}")
print(f"IDs: {ids}")

输出解析:
切分结果类似于:['un', '##bel', '##ie', '##va', '##ble', '!', 'token', '##ization', 'is', 'magical', '.']
注意观察,BERT 把 unbelievable 切得更碎了,并且非首位的子词都带上了 ## 前缀。


4. SentencePiece:多语言大模型的终极武器

当我们从纯英文走向多语言(特别是中日韩等亚洲语言)时,BPE 和 WordPiece 都面临一个巨大的痛点:它们都依赖于预切分。

在英文中,我们可以简单地用空格把单词分开。但在中文里,“自然语言处理”这六个字之间是没有空格的。如果强行按字切分,序列会太长;如果调用结巴分词等外部工具,又引入了额外的依赖和误差。

SentencePiece 应运而生。它是 Google 开源的一个跨语言、纯数据驱动的 Tokenization 框架。LLaMA、ChatGLM、BLOOM 等现代大模型几乎都采用了它。

4.1 核心思想:把空格也当作一种字符

SentencePiece 的革命性在于,它完全抛弃了“空格即词边界”的传统假设

它将输入的纯文本首先进行一种称为 Unicode NFKC 的规范化,然后把空格替换为一个特殊的可见字符(通常是 _<space>)。在这个阶段,所有语言都被统一看作是没有边界的连续字符流。

接着,它在底层使用一种名为 Unigram 的算法(或者 BPE)来进行子词切分。

4.2 Unigram 语言模型算法

虽然 SentencePiece 也支持 BPE,但它最经典的默认算法是 Unigram。与 BPE 自底向上的合并不同,Unigram 是自顶向下的。

  1. 初始化:用一个非常巨大的词表(比如包含所有可能的子串和字符)。
  2. 建模:计算每个子词在当前语料中的概率(通常使用期望最大化 EM 算法)。
  3. 剪枝:计算移除每个子词后对整体似然度的损失。移除那些损失最小(即最不重要)的子词。
  4. 重复:不断剪枝,直到词表缩小到目标大小。

优势:Unigram 能够为一个词输出多种切分可能性,并赋予它们不同的概率。在推理时,通常使用 Viterbi 算法寻找概率最大的那一条切分路径。

4.3 代码实战:LLaMA 的中文处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 需要安装 sentencepiece: pip install sentencepiece
import sentencepiece as spm

# 假设我们已经用spm训练好了一个模型,这里演示加载过程
# 在实际应用中,LLaMA 的 tokenizer 是基于 sentencepiece 的
from transformers import LlamaTokenizer

# 加载 LLaMA 的 tokenizer (需要有权重或本地路径)
# llama_tokenizer = LlamaTokenizer.from_pretrained("decapoda-research/llama-7b-hf")
# 为了方便没有环境的读者,这里伪代码演示其特性

text_en = "Tokenization is important."
text_zh = "分词技术非常重要。"

# 经过 SentencePiece 处理前,内部会将空格转义为 '▁' (特殊的下划线字符)
# 英文可能变成: ['▁Token', 'ization', '▁is', '▁important', '.']
# 中文可能变成: ['▁分', '词', '技术', '▁非常', '重要', '。']

print("SentencePiece 最大的好处是:无论什么语言,都不需要预分词工具。")
print("它在模型内部将文本视为纯粹的字节流,空格也是普通的字符。")

5. 终极对决:BPE vs WordPiece vs SentencePiece

为了方便大家在实际工作和选型中做出决策,我总结了这三者的核心差异对比表:

特性 Byte-Pair Encoding (BPE) WordPiece SentencePiece (Unigram)
代表模型 GPT 系列, LLaMA, RoBERTa BERT, DistilBERT, XLNet T5, ALBERT, ChatGLM, BLOOM
核心算法 自底向上,频率驱动合并 自底向上,似然度驱动合并 自顶向下,概率EM算法剪枝
合并标准 最高频的相邻字符对 互信息最大(提升似然度最大) 整体语料概率最优
依赖预分词? (依赖空格/外部工具) (依赖空格/外部工具) (空格视为普通字符 _)
多语言支持 差(需 Byte-level 弥补) 极佳 (语言无关的字节流处理)
输出确定性 唯一确定的切分结果 唯一确定的切分结果 多种切分路径(推理时取最佳)

选型建议:

  1. 如果你在做纯英文的生成式任务,直接上基于 Byte-level BPE 的 OpenAI 系列方案(如 tiktoken)。
  2. 如果你在做基于 BERT 架构的理解型任务,继续使用 WordPiece 即可。
  3. 如果你在做多语言混合或**中日韩(非空格分隔)**语言的大模型训练,SentencePiece 是几乎唯一的最优解

6. Tokenization 对大模型的影响:为什么它比你想的更重要?

掌握了具体算法后,我们需要站在更高维度来看看 Tokenization 对当前大模型生态产生的深远影响。

6.1 上下文窗口的“通货膨胀”

我们常听宣传说“某模型支持 128K 上下文”。但请注意,这 128K 是 Token 数量,而不是字符数量。
由于不同语言和不同分词器的分词效率不同:

  • 在 GPT-4 中,1 个英文单词通常约等于 1-1.3 个 Token。
  • 但是,1 个中文字符通常需要 1.5 到 2 个 Token!
    这意味着,相同的 128K 限制下,中文大模型实际能处理的文本长度要比英文短得多。这不仅关乎内存,更关乎训练和推理的成本。

6.2 为什么大模型不会拼写单词?(Spelling Issues)

你有没有发现,如果你让 GPT-4 “请把 Strawberry 这个单词倒序拼写”,它经常会出错(比如输出 “yrrabets” 而非 “yrrebarts”)?

原因就在于 BPE 分词
在 GPT-4 的 Token 眼中,“Strawberry” 可能并不是 10 个字母的组合,而是一个单一的 Token ID 46823。模型在自注意力机制中看到的是这个 ID 及其上下文,并没有在底层特征中强化“它由哪些字母按什么顺序组成”的信息。这也是为什么现在很多新的研究开始呼吁回归字符级别的模型。

6.3 弱点与安全漏洞:Token 欺骗

由于分词器是独立于大模型主体训练的,有时它会成为安全漏洞的来源。
例如,在某些分词器中,特定的罕见字符组合可能会导致 Token 被异常切分,使得原本的安全过滤词(如 “kill”)被切分成 kill,从而绕过基于字符匹配的安全审查系统。


7. 总结与展望

Tokenization 是连接人类语言与机器数字世界的桥梁。从最基础的频率统计(BPE),到引入概率语义模型,再到跨语言统一的 SentencePiece,分词技术的演进史实际上也是大语言模型走向成熟的一个缩影。

然而,Tokenization 并非完美的终局。它引入的边界模糊性、多语言的不公平性以及拼写推理能力的缺失,越来越成为模型进一步发展的掣肘。

在当前的前沿研究中,学术界已经开始探索无需 Tokenizer 的架构(如 Byte-level models,代表工作有 MegaByte 等),试图让模型直接从原始字节中学习。但在可预见的未来内,考虑到计算效率的极限,基于 BPE 和 SentencePiece 的子词分词器依然将是工业界大模型的主流配置。

理解 Tokenization,不仅是调参炼丹的第一步,更是真正看透大模型黑盒的敲门砖。希望这篇文章能为你在大模型领域的探索打下坚实的基础。