拆解大模型底层基石:深入探究 Tokenization(BPE、WordPiece 与 SentencePiece)

如果把大语言模型(LLM)比作一台精密的跑车,那么 Transformer 架构是它的引擎,RLHF(人类反馈强化学习)是它的方向盘,而 Tokenization(分词) 则是那不可或缺的燃油喷射系统。没有它,再强大的引擎也无法将人类复杂的自然语言转化为机器可以理解的能量。

当你向 ChatGPT 输入“我爱你”时,模型真的能看懂这三个汉字吗?并非如此。在底层,这段文字被切分成了类似 [101, 2769, 4263, 1961, 102] 这样的数字序列。这个从文本到数字的转换过程,就是 Tokenization。

很多人在学习大模型时,将 90% 的精力放在了模型架构(如 Attention 机制)上,却忽略了 Tokenization。然而,大模型的很多“神奇特性”甚至“诡异缺陷”(例如不会做简单的字母数数、拼写错误敏感等),其实都根源于 Tokenization。

今天,我们就来深度剖析大模型中三种最核心的 Tokenization 算法:BPE、WordPiece 与 SentencePiece,并手把手带你用代码揭开它们的底层面纱。


一、 为什么我们需要复杂的 Tokenization?

在深度学习早期,处理文本主要有两种极端的方式:

  1. 单词级: 以空格和标点分割。
    • 缺点: 词表无限大,遇到生僻词只能用 <UNK>(未知符)代替,且形态学上的关联无法体现(如 catcats 是完全不同的两个 Token)。
  2. 字符级: 将每个字母/汉字单独切分。
    • 缺点: 序列太长,导致 Transformer 的计算复杂度呈平方级爆炸;且单个字母缺乏语义信息(如字母 a 本身没有具体含义)。

为了在“词表大小”和“序列长度”之间找到完美的平衡点,子词分割 应运而生。其核心思想是:高频词保留,低频词拆解。

例如,对于词汇 unfriendly,我们可能没有完整的词,但可以将其拆分为 un, friend, ly。这三个片段都在词表中,既控制了词表大小,又保留了前缀和词根的语义。


二、 BPE (Byte Pair Encoding):GPT 系列的“心脏”

BPE 最初是一种数据压缩算法,在 2015 年被 Sennrich 等人引入到 NLP 领域,随后成为了 GPT-2、GPT-3、GPT-4 以及 Meta 的 LLaMA 等绝大多数主流大模型的标配。

2.1 BPE 算法核心思想

BPE 的核心逻辑非常朴素:不断地将当前语料库中出现频率最高的一对相邻 Token 合并成一个新的 Token。

训练阶段(构建词表):

  1. 初始化:将语料库中的所有单词拆分为单个字符(并在末尾加上特殊的结束符 </w>,以区分词内和词尾)。初始词表就是所有字符的集合。
  2. 统计频率:统计语料库中所有相邻字符对的出现频率。
  3. 合并:找出频率最高的字符对(比如 th),将它们合并为一个新的 Token(th),并更新语料库。
  4. 循环:重复步骤 2 和 3,直到达到预设的词表大小。

编码阶段(推理):
给定一个新单词,按照训练时生成的合并规则,从前往后依次尝试合并字符。

2.2 Python 手写简易 BPE 算法

为了让你彻底搞懂,我们用几十行 Python 代码来模拟 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
32
33
34
35
36
37
38
39
40
41
42
import re
from collections import defaultdict

def format_word(word, end_of_word='</w>'):
"""将单词拆分为字符序列,并加上结束符"""
return list(word) + [end_of_word]

def get_pairs(vocab):
"""统计词典中相邻Token对的出现频率"""
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[(symbols[i], symbols[i+1])] += freq
return pairs

def merge_vocab(pair, v_in):
"""将最高频的Token对合并,并更新词典"""
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out

# 1. 准备初始语料 (包含单词和频率)
vocab = {'low </w>': 5, 'lower </w>': 2, 'newest </w>': 6, 'widest </w>': 3}

print("初始词典:", vocab)

# 2. 模拟 10 次合并
num_merges = 10
for i in range(num_merges):
pairs = get_pairs(vocab)
if not pairs:
break
# 找出频率最高的 pair
best = max(pairs, key=pairs.get)
# 更新词典
vocab = merge_vocab(best, vocab)
print(f"第 {i+1} 步合并: {best} -> {''.join(best)} | 词典更新: {vocab}")

运行这段代码,你会亲眼目睹 es 首先合并,因为 newestwidestes 成了最高频的配对。这就是 BPE 的魅力。

2.3 BBPE (Byte-level BPE)

GPT-2 引入了一个绝妙的改进:Byte-level BPE
传统的 BPE 依赖于基础的字符集(如 Unicode),这在处理多语言时依然会产生庞大的基础词表。BBPE 直接将文本视为原始字节流(0-255),而不是 Unicode 字符。
这意味着 GPT-2 的基础词表只有 256 个基本字节,所有的多语言文字、甚至代码、Emoji,统统在这 256 个字节的基础上进行合并。这就是为什么 ChatGPT 能够无缝处理中日韩英等几十种语言的底层奥秘。


三、 WordPiece:BERT 的“拼图游戏”

WordPiece 算法最初由 Google 团队为解决语音识别系统的 OOV(未登录词)问题而提出,后来随着 BERT 模型的爆火而广为人知。国内百度的 ERNIE 模型等也多采用此算法。

3.1 WordPiece 与 BPE 的核心区别

从宏观上看,WordPiece 和 BPE 非常相似,都是自底向上的子词合并算法。但它们有一个本质的区别:合并的标准不同。

  • BPE: 基于频率。谁挨着的次数多,我就把谁合并。
  • WordPiece: 基于语言模型似然度。它不仅看两个片段挨得近不近,更看重合并之后,能不能让整体的概率最大。在实际操作中,这通常转化为计算互信息

WordPiece 选择的合并对是:能够最大化以下公式的对:

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

这就意味着,如果片段 A 和 B 单独出现的频率很高,但它们连在一起出现的频率一般,WordPiece 是不会轻易把它们合并的。只有当 A 和 B 是“天作之合”(紧密相关)时,才会合并。

3.2 识别特征:## 前缀

如果你读过 BERT 的分词结果,一定会对 ## 符号印象深刻。这是 WordPiece 在前处理时的一个显著特征。
在 WordPiece 中,如果一个子词在单词的开头,它就保持原样;如果它在单词的中间或结尾,它就会加上 ## 的前缀。

例如单词 unwanted
它会被切分为 ['un', '##want', '##ed']
这向模型传达了一个重要信息:wanted 不能独立存在,它们必须依附于其他词根。

3.3 代码示例:使用 Hugging Face Tokenizers 库体验 WordPiece

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

# 加载 BERT 的 base 中文模型分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')

text = "深度学习改变世界"
tokens = tokenizer.tokenize(text)
token_ids = tokenizer.encode(text)

print(f"原文本: {text}")
print(f"WordPiece 切分: {tokens}")
print(f"转换为 ID: {token_ids}")

运行结果解析:
对于中文,BERT 通常会以单个汉字为基本单位。如果遇到一些生僻字或者特殊的符号,它也会fallback到字级别甚至更小的片段,并加上 ## 标记。


四、 SentencePiece:大模型时代的“大一统”框架

无论是 BPE 还是 WordPiece,它们都有一个隐含的假设:输入的文本是已经被空格分割好的单词序列(例如英文要先用空格切分,再用子词算法)。
但是,对于**中文、日文、韩文(CJK字符)**等词与词之间没有明显空格的语言,或者当我们要训练一个跨语种的大型语言模型时,这种依赖“空格分词”的预处理就成了巨大的障碍。

为了解决这个问题,Google 开源了 SentencePiece。需要特别澄清的是:SentencePiece 不是一个分词算法,而是一个分词框架/工具。

4.1 SentencePiece 的革命性创新

SentencePiece 把空格也当作一种特殊字符。它不依赖外部的预分词工具,直接将原始的未处理文本流作为输入。

  1. 语言无关性: 它不管你是英文、中文还是火星文,统统视为字节流或字符流。完全省去了特定语言的预处理逻辑。
  2. 直接处理空格: 在 SentencePiece 中,空格被特殊标记为 (Unicode 字符 U+2581,下划线变体)。这样,“Hello World”和“HelloWorld”在底层表示上是完全不同的。
  3. 内置多种算法: SentencePiece 内部支持了 BPEUnigram 两种核心算法。

4.2 Unigram 语言模型(SentencePiece 的默认算法)

当你在 SentencePiece 中不指定 BPE 时,它默认使用 Unigram Language Model。这与前面讲的自底向上的 BPE/WordPiece 完全相反,是一种自顶向下的算法。

  1. 初始化: 用一个巨大的词表(比如包含所有可能的子串或字节对)初始化。
  2. EM 训练: 通过期望最大化(EM)算法,计算每个 Token 在给定语料下的概率。
  3. 剪枝: 计算如果从词表中移除某个 Token,对整体似然度的损失有多大。移除那些损失最小(即最不重要)的 Token。
  4. 循环: 不断缩小词表,直到达到目标大小。

由于 Unigram 是基于概率的,它在解码时,对于一个句子,可以输出多种切分方式(例如 Viterbi 算法寻找全局概率最大的那一条路径),这为模型提供了更好的鲁棒性。

4.3 代码实战:用 SentencePiece 训练 LLaMA 风格的词表

LLaMA 模型使用的就是基于 SentencePiece 训练的 BPE 算法。下面展示如何用 SentencePiece 训练你专属的大模型分词器:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 首先需要安装 sentencepiece: pip install sentencepiece

import sentencepiece as spm

# 假设我们有一个巨大的训练语料文件 'train_data.txt'
# 里面包含了中英混杂的原始文本
# 在这里我们使用 BPE 算法,词表大小设为 32000 (常见的 LLM 配置)

# (在实际运行前,请确保当前目录下有一个名为 train_data.txt 的文件)
# with open('train_data.txt', 'w', encoding='utf-8') as f:
# f.write("这是一个用于测试 SentencePiece 的语料库。\n")
# f.write("SentencePiece supports various languages like English and 中文。\n")

spm.SentencePieceTrainer.train(
input='train_data.txt', # 输入语料
model_prefix='my_llm_tokenizer', # 输出模型前缀
vocab_size=32000, # 词表大小
model_type='bpe', # 算法选择:'bpe' 或 'unigram'
character_coverage=0.9995, # 字符覆盖度,中文通常设为 0.9995
num_threads=4, # 训练线程数
pad_id=0, # 设置特殊 Token 的 ID
unk_id=1,
bos_id=2,
eos_id=3,
)

print("训练完成!生成了 my_llm_tokenizer.model 和 my_llm_tokenizer.vocab")

# --- 使用训练好的模型进行分词 ---
sp = spm.SentencePieceProcessor()
sp.load('my_llm_tokenizer.model')

text = "大模型正在改变世界!"
print(f"原文本: {text}")

# 编码为文本片段
pieces = sp.encode_as_pieces(text)
print(f"SentencePiece 切分: {pieces}")

# 编码为 ID 序列
ids = sp.encode_as_ids(text)
print(f"转换为 ID: {ids}")

# 解码回文本
decoded_text = sp.decode_pieces(pieces)
print(f"解码回文本: {decoded_text}")

你会注意到,中文的每个汉字或词组前面都会带有 符号,这代表了句子中真实的边界或空格,极大地增强了模型处理多语言的能力。


五、 三种技术横评:大厂如何选型?

为了方便大家在实际工作和面试中进行对比,我总结了以下表格:

特性 BPE (Byte Pair Encoding) WordPiece SentencePiece (框架)
核心合并标准 最高出现频率 最高互信息/似然度 内置 BPE 或 Unigram (概率)
依赖空格分词 否 (直接处理原始文本)
标志性前缀 无特殊前缀 (GPT用全小写等规则) 词尾/词中带 ## 前缀 空格用 表示
代表大模型 GPT 系列、LLaMA、Mistral BERT、ERNIE、DistilBERT LLaMA、T5、ALBERT、BLOOM
语言适应性 多语言需结合 Byte-level 中英等需定制预处理 原生完美支持多语言
底层思维方式 自底向上 (从字符拼到词) 自底向上 (从字符拼到词) 支持自顶向下 (Unigram大词表剪枝)

为什么最新的开源大模型(如 LLaMA 3、Qwen 等)几乎清一色转向了基于 SentencePiece 的 BPE (或其变体 Fast Tokenizer)?
原因在于现代大模型的训练数据动辄达到数 TB 级别,包含了全网的几十种语言。如果使用传统的预处理去切分空格、过滤标点,不仅耗时耗力,还容易丢失信息。SentencePiece 将“规范化”和“切分”统一起来,直接把脏乱差的原始文本映射成整数 ID,成为了工业界唯一的高效解。


六、 避坑指南:Tokenization 的暗坑

在了解了底层原理后,我们就能解释大模型中一些常见的“反常识”现象了。在大模型应用开发(如 RAG、Agent)中,Tokenizer 的这些特性至关重要:

  1. 为什么大模型做不好简单的字母数数?
    如果你问 GPT-3.5:“How many 'r’s in the word ‘strawberry’?”,它大概率会回答 2。
    这是因为在 BPE 算法下,strawberry 可能被切分成 [str, aw, berry] 甚至是一个完整的 Token。模型在训练时根本没见过独立的 r r r 拼在一起,它是在“词块”的维度上理解语言,而不是“字母”维度。
  2. 为什么提示词要拼接到一起才能攻击(越狱)?
    大模型分词器对于带有空格的独立单词和未带空格的单词,切分结果截然不同。比如 apple 可能是 Token ID 233,而前面的空格加单词 apple 可能是 Token ID 89。这种分词的脆弱性经常被黑客用来构造 Prompt 注入。
  3. 多语言付出的 Token 代价
    OpenAI 官方承认,由于 BPE 词表是基于英文字节训练的,处理一段中文文本消耗的 Token 数通常是英文的 2-3 倍。在 RAG(检索增强生成)系统中设计 Chunking(文本块大小)策略时,一定要通过 tiktoken 实际计算 Token 长度,而不是按字符数或字数切分!
1
2
3
4
5
6
7
8
9
10
# 体验 OpenAI 的 tiktoken 库对中英文切分的差异
import tiktoken

enc = tiktoken.get_encoding("cl100k_base") # GPT-4 使用的分词器
en_text = "Hello World"
zh_text = "你好世界"

print("English tokens:", len(enc.encode(en_text)), enc.encode(en_text))
print("Chinese tokens:", len(enc.encode(zh_text)), enc.encode(zh_text))
# 你会发现,同等语义下,中文消耗的 token 数可能更多

七、 总结

回顾整篇文章,我们经历了从最基础的字符分割,到 BPE 的频率合并,再到 WordPiece 的似然度合并,最后见证了 SentencePiece 一统多语言天下的演进历程。

如果说 Transformer 让大模型具备了思考的能力,那么 Tokenization 则定义了模型看世界的“视界”。模型表现出的诸多能力与缺陷,往往就隐藏在这第一步的文本切分之中。

随着 GPT-4o 等模型对音频和视觉的原生支持,业界也正在探索类似 Byte Latent Transformer (BLT) 这样直接绕过 Tokenizer、以原始字节为粒度训练的新架构。但在可预见的未来,BPE、SentencePiece 及其变种,仍将是绝大多数 AI 应用的核心基石。

深入理解 Tokenization,不仅能让我们在开发大模型应用时避开暗坑,更是从“API 调包侠”走向“大模型系统架构师”的必经之路。希望这篇文章能为你构建坚实的底层认知。