分词方法:BPE/WordPiece/SentencePiece
约 1531 字大约 5 分钟
tokenizationnlp
2025-08-30
分词(Tokenization)是将原始文本转换为模型可处理的 token 序列的过程。分词策略直接影响模型的词汇覆盖能力、序列长度和生成质量。本文详细介绍主流的子词分词算法及其工程实现。
分词粒度对比
| 粒度 | 优点 | 缺点 |
|---|---|---|
| 词级别 | 语义明确 | OOV(未登录词)问题严重,词表巨大 |
| 字符级别 | 无 OOV 问题 | 序列过长,语义信息薄弱 |
| 子词级别 | 平衡覆盖与效率 | 需要训练分词器 |
现代 LLM 几乎全部采用子词分词。
BPE(Byte Pair Encoding)
BPE 最初用于数据压缩,后被 Sennrich et al.(2016)引入 NLP。核心思想是迭代合并出现频率最高的相邻 token 对。
算法流程
- 初始化词表为所有单字符
- 统计所有相邻 token 对的出现频率
- 合并频率最高的 token 对,加入词表
- 重复步骤 2-3,直到达到目标词表大小
from collections import Counter, defaultdict
def learn_bpe(corpus, num_merges):
"""简化版 BPE 训练"""
# 初始化:将每个词拆分为字符序列 + 结束符
vocab = {}
for word, freq in corpus.items():
vocab[' '.join(list(word)) + ' </w>'] = freq
for i in range(num_merges):
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for j in range(len(symbols) - 1):
pairs[(symbols[j], symbols[j+1])] += freq
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
new_vocab = {}
bigram = ' '.join(best_pair)
replacement = ''.join(best_pair)
for word, freq in vocab.items():
new_word = word.replace(bigram, replacement)
new_vocab[new_word] = freq
vocab = new_vocab
print(f"Merge {i+1}: {best_pair} -> {replacement}")
return vocab
# 示例
corpus = Counter({'low': 5, 'lower': 2, 'newest': 6, 'widest': 3})
learn_bpe(corpus, num_merges=10)GPT 系列模型使用 Byte-level BPE,以字节(256 个基本单元)而非 Unicode 字符为基础单位,可以处理任意语言和特殊字符,彻底消除 OOV。
WordPiece
WordPiece 由 Google 提出,被 BERT 使用。与 BPE 类似但使用不同的合并策略:不是选择频率最高的对,而是选择使语言模型似然值增加最大的对。
合并标准:
score(a,b)=freq(a)×freq(b)freq(ab)
WordPiece 使用 ## 前缀表示非词首子词:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
text = "unbelievable transformations"
tokens = tokenizer.tokenize(text)
print(tokens)
# ['un', '##bel', '##ie', '##va', '##ble', 'transformations']
# 编码为 ID
input_ids = tokenizer.encode(text, add_special_tokens=True)
print(input_ids)
# [101, 4895, 17316, 7416, 3567, 3468, 18209, 102]
# 解码回文本
decoded = tokenizer.decode(input_ids)
print(decoded)
# [CLS] unbelievable transformations [SEP]Unigram(SentencePiece)
Unigram 模型(Kudo, 2018)采用与 BPE 相反的策略:从一个大词表出发,逐步删除对整体似然影响最小的 token。
训练流程:
- 初始化一个足够大的候选词表
- 用 EM 算法估计每个子词的概率
- 对每个子词计算移除后的似然损失
- 移除损失最小的子词(保留一定比例)
- 重复步骤 2-4
SentencePiece 是 Google 开发的分词工具库,支持 BPE 和 Unigram 两种算法,且直接处理原始文本(不依赖预分词),特别适合中文、日文等无空格分隔的语言。
import sentencepiece as spm
# 训练 SentencePiece 模型
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='sp_model',
vocab_size=32000,
model_type='unigram', # 或 'bpe'
character_coverage=0.9995,
pad_id=0,
unk_id=1,
bos_id=2,
eos_id=3,
)
# 加载并使用
sp = spm.SentencePieceProcessor()
sp.load('sp_model.model')
text = "自然语言处理是人工智能的重要领域"
tokens = sp.encode(text, out_type=str)
print(tokens)
# ['▁自然', '语言', '处理', '是', '人工', '智能', '的', '重要', '领域']
ids = sp.encode(text, out_type=int)
print(ids)
# 解码
decoded = sp.decode(ids)
print(decoded)词表大小权衡
| 模型 | 分词方法 | 词表大小 |
|---|---|---|
| GPT-2 | Byte-level BPE | 50,257 |
| GPT-4 | Byte-level BPE (tiktoken) | 100,256 |
| BERT | WordPiece | 30,522 |
| LLaMA | SentencePiece (BPE) | 32,000 |
| LLaMA 3 | tiktoken (BPE) | 128,256 |
| T5 | SentencePiece (Unigram) | 32,000 |
tiktoken
tiktoken 是 OpenAI 开源的快速 BPE 分词器,基于 Rust 实现,比纯 Python 实现快数十倍。
import tiktoken
# GPT-4 使用的编码
enc = tiktoken.encoding_for_model("gpt-4")
text = "Hello, how are you doing today?"
tokens = enc.encode(text)
print(f"Token IDs: {tokens}")
print(f"Token count: {len(tokens)}")
# 逐 token 解码查看
for token_id in tokens:
print(f" {token_id} -> '{enc.decode([token_id])}'")
# 中文分词
chinese_text = "大型语言模型正在改变世界"
cn_tokens = enc.encode(chinese_text)
print(f"中文 token 数: {len(cn_tokens)}")Hugging Face Tokenizers
Hugging Face 的 tokenizers 库提供了高性能的分词器训练和使用接口。
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
# 从零训练 BPE 分词器
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
trainer = trainers.BpeTrainer(
vocab_size=32000,
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"],
min_frequency=2,
)
# 从文件训练
tokenizer.train(files=["corpus.txt"], trainer=trainer)
tokenizer.save("my_tokenizer.json")
# 使用 AutoTokenizer 加载预训练分词器
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
result = tokenizer("深度学习改变了自然语言处理", return_tensors="pt")
print(result.input_ids)
print(tokenizer.convert_ids_to_tokens(result.input_ids[0]))分词对模型的影响
- 序列长度:分词粒度决定了同一段文本消耗的 token 数,直接影响推理成本和上下文窗口利用率
- 多语言能力:中文在以英文为主的分词器中每个汉字可能被拆成 2-3 个 token,导致效率低下。LLaMA 3 通过扩大词表显著改善了中文 tokenization 效率
- 数学和代码:数字的分词方式影响模型的算术能力;代码缩进的处理影响代码生成质量
- 生成质量:tokenization 边界影响模型对词义的理解和生成的流畅性
总结
分词是连接原始文本与模型的桥梁。BPE 是当前最主流的算法(GPT、LLaMA),WordPiece 在 BERT 系列中使用,Unigram 提供了概率化的替代方案。选择分词策略时需要综合考虑词表大小、多语言支持、序列长度和训练数据规模。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于