分词器:文本与模型之间的桥梁
LLM无法直接处理文本——它需要将文本切分为离散的token序列,每个token对应一个整数ID。分词器(Tokenizer)就是完成这一转换的组件。分词器的选择直接影响模型的词表大小、序列长度、多语言能力和推理效率。
2026年主流LLM使用的分词器虽然名称各异,但核心算法主要分为BPE、WordPiece和Unigram三大类。
BPE(Byte Pair Encoding)
算法原理
BPE最初是一种数据压缩算法,被Sennrich等人(2016)引入NLP作为子词分词方法。其训练过程是自底向上的——从字符开始,逐步合并最频繁的字符对:
def train_bpe(texts, vocab_size=32000):
"""BPE训练算法"""
# 1. 将文本拆分为字符序列
word_freqs = {}
for text in texts:
words = text.split()
for word in words:
chars = tuple(word)
word_freqs[chars] = word_freqs.get(chars, 0) + 1
# 2. 初始化词表为所有字符
vocab = set()
for word in word_freqs:
for char in word:
vocab.add(char)
# 3. 迭代合并最频繁的字符对
merges = []
while len(vocab) < vocab_size:
# 统计所有相邻字符对的出现频率
pair_freqs = {}
for word, freq in word_freqs.items():
for i in range(len(word) - 1):
pair = (word[i], word[i+1])
pair_freqs[pair] = pair_freqs.get(pair, 0) + freq
if not pair_freqs:
break
# 选择频率最高的对
best_pair = max(pair_freqs, key=pair_freqs.get)
new_token = best_pair[0] + best_pair[1]
# 合并
merges.append(best_pair)
vocab.add(new_token)
# 更新词频
new_word_freqs = {}
for word, freq in word_freqs.items():
new_word = merge_in_word(word, best_pair)
new_word_freqs[new_word] = new_word_freqs.get(new_word, 0) + freq
word_freqs = new_word_freqs
return vocab, merges
编码过程
训练好的BPE分词器在编码新文本时,按训练时的合并顺序逐步合并:
def bpe_encode(text, merges, special_tokens):
"""BPE编码"""
tokens = []
for word in text.split():
# 先检查是否是特殊token
if word in special_tokens:
tokens.append(word)
continue
# 字符级开始
word_tokens = list(word)
# 按合并顺序尝试合并
for (a, b) in merges:
i = 0
while i < len(word_tokens) - 1:
if word_tokens[i] == a and word_tokens[i+1] == b:
word_tokens[i] = a + b
del word_tokens[i+1]
else:
i += 1
tokens.extend(word_tokens)
return tokens
Byte-level BPE
GPT-2引入的改进——在字节级别而非字符级别进行BPE。这使得词表可以处理任意UTF-8字符,彻底解决了未登录字符(OOV)问题:
def byte_level_bpe_encode(text):
"""Byte-level BPE"""
# 将文本编码为UTF-8字节
byte_seq = text.encode('utf-8')
# 将每个字节映射到Unicode字符(避免控制字符冲突)
mapped = ''.join(byte_to_unicode[b] for b in byte_seq)
# 在映射后的字符序列上应用BPE
return bpe_encode(mapped)
Byte-level BPE的优势:
- 零OOV:任何文本都可以被编码
- 多语言支持:天然处理任何语言的UTF-8编码
- 可逆性:可以完美还原原始文本
BPE的使用者
- GPT系列(GPT-2, GPT-3, GPT-4):Byte-level BPE
- LLaMA系列:Byte-level BPE(基于SentencePiece实现)
- Qwen系列:Byte-level BPE
WordPiece
与BPE的区别
WordPiece与BPE类似,但选择合并对的准则不同:
- BPE:选择频率最高的字符对
- WordPiece:选择似然增益最大的字符对
似然增益衡量的是合并某个字符对后,语言模型似然的提升:
def wordpiece_score(pair, pair_freq, char1_freq, char2_freq, total_freq):
"""WordPiece合并分数:互信息"""
return pair_freq / (char1_freq * char2_freq)
WordPiece本质上是选择互信息最高的字符对——即这两个字符共同出现的概率远高于独立出现的概率。
编码方式
WordPiece的编码与BPE不同——它使用贪心最长匹配:
def wordpiece_encode(word, vocab, max_chars=100):
"""WordPiece编码:贪心最长匹配"""
if len(word) > max_chars:
return ['[UNK]']
tokens = []
start = 0
while start < len(word):
end = len(word)
cur_substr = None
# 从最长到最短尝试匹配
while start < end:
substr = word[start:end]
if start > 0:
substr = '##' + substr # 非首字符加##前缀
if substr in vocab:
cur_substr = substr
break
end -= 1
if cur_substr is None:
return ['[UNK]']
tokens.append(cur_substr)
start = end
return tokens
WordPiece的使用者
- BERT系列
- DistilBERT
- ELECTRA
Unigram Language Model
算法原理
Unigram分词器采用与BPE相反的方向——自顶向下。它先从一个大词表开始,逐步删除对整体似然贡献最小的子词:
def train_unigram(texts, vocab_size=32000, initial_factor=100):
"""Unigram训练算法"""
# 1. 构建初始大词表(所有可能的子串)
substrings = {}
for text in texts:
for word in text.split():
for i in range(len(word)):
for j in range(i+1, len(word)+1):
substr = word[i:j]
substrings[substr] = substrings.get(substr, 0) + 1
# 2. 保留频率最高的子串作为初始词表
vocab = sorted(substrings.items(), key=lambda x: -x[1])
vocab = [item[0] for item in vocab[:vocab_size * initial_factor]]
# 3. 迭代删除对似然贡献最小的子词
while len(vocab) > vocab_size:
# 计算当前词表的对数似然
base_likelihood = compute_likelihood(texts, vocab)
# 尝试删除每个子词,计算似然下降
deltas = []
for i, token in enumerate(vocab):
reduced_vocab = vocab[:i] + vocab[i+1:]
new_likelihood = compute_likelihood(texts, reduced_vocab)
delta = base_likelihood - new_likelihood
deltas.append((i, delta))
# 删除似然下降最小的子词(按比例删除,而非逐个)
deltas.sort(key=lambda x: x[1])
n_remove = max(1, len(vocab) // 10) # 每轮删除10%
remove_indices = set(idx for idx, _ in deltas[:n_remove])
vocab = [t for i, t in enumerate(vocab) if i not in remove_indices]
return vocab
编码方式
Unigram分词器使用维特比算法找到最优的分词方式:
def unigram_encode(word, vocab, vocab_probs):
"""Unigram编码:维特比最优分词"""
n = len(word)
# dp[i] = (对数概率, 前驱位置, token)
dp = [(-float('inf'), -1, None) for _ in range(n + 1)]
dp[0] = (0, -1, None)
for end in range(1, n + 1):
for start in range(end):
substr = word[start:end]
if substr in vocab_probs:
prob = vocab_probs[substr]
score = dp[start][0] + math.log(prob)
if score > dp[end][0]:
dp[end] = (score, start, substr)
# 回溯获取最优分词
tokens = []
pos = n
while pos > 0:
_, prev_pos, token = dp[pos]
tokens.append(token)
pos = prev_pos
return list(reversed(tokens))
Unigram的优势
- 概率模型:每个子词有概率,可以处理多种分词方式
- 鲁棒性:对噪声和拼写错误更鲁棒(可以找到次优分词)
- 多分词候选:可以输出多种分词候选,用于数据增强
Unigram的使用者
- T5、mT5
- ALBERT
- XLNet
SentencePiece
统一框架
SentencePiece不是一个新算法,而是BPE和Unigram的统一实现框架。它解决了传统分词器的两个问题:
- 空格问题:传统分词器先按空格分词再子词化,SentencePiece直接在原始文本上操作
- 可逆性:SentencePiece保证编码可以完美解码回原始文本
# SentencePiece将空格替换为特殊字符▁(U+2581)
# "Hello world" → "▁Hello▁world"
# 这意味着:
# 1. 不依赖预分词(空格切分)
# 2. 编码-解码完全可逆
# 3. 语言无关(中日韩等无空格语言同样处理)
SentencePiece + BPE
LLaMA使用SentencePiece包装的BPE:
import sentencepiece as spm
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='llama_tokenizer',
vocab_size=32000,
model_type='bpe', # 使用BPE算法
character_coverage=0.9998,
byte_fallback=True, # 字节回退
normalization_rule_name='identity', # 不做归一化
)
SentencePiece + Unigram
T5使用SentencePiece包装的Unigram:
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='t5_tokenizer',
vocab_size=32000,
model_type='unigram', # 使用Unigram算法
character_coverage=0.99995,
byte_fallback=True,
)
2026年分词器对比
词表大小趋势
| 模型 | 分词器 | 词表大小 | 特点 |
|---|---|---|---|
| GPT-4 | BPE | ~100K | 多语言,压缩率高 |
| LLaMA-3 | BPE | 128K | 扩大词表提升多语言 |
| Qwen-3 | BPE | 152K | 中英文优化 |
| Mistral | BPE | 32K | 紧凑词表 |
| T5 | Unigram | 32K | 概率模型 |
压缩效率对比
在英文文本上:
| 分词器 | 词表大小 | 压缩率(tokens/word) | 示例 |
|---|---|---|---|
| BPE (GPT-4) | 100K | 1.15 | “tokenization” → [“token”, “ization”] |
| BPE (LLaMA-3) | 128K | 1.20 | “tokenization” → [“token”, “ization”] |
| WordPiece (BERT) | 30K | 1.45 | “tokenization” → [“token”, “##ization”] |
| Unigram (T5) | 32K | 1.35 | “tokenization” → [“token”, “ization”] |
在中文文本上:
| 分词器 | 压缩率(chars/token) | 说明 |
|---|---|---|
| BPE (Qwen-3) | 2.8 | 中文优化词表 |
| BPE (LLaMA-3) | 1.5 | 通用多语言 |
| BPE (GPT-4) | 1.8 | 多语言覆盖 |
工程实践建议
- 选BPE:2026年最主流的选择,生态最完善
- Byte-level:务必使用字节级BPE,避免OOV
- 词表大小:32K-128K,多语言场景建议128K+
- 特殊token:预留特殊token位置(如
<|im_start|>,<|endoftext|>) - 归一化:谨慎使用文本归一化,避免信息丢失
- 评估:用目标语言的压缩率评估分词器质量
结语
分词器是LLM基础设施中容易被忽视但影响深远的组件。BPE以其简洁和高效成为2026年的主流选择,Unigram的概率模型提供了独特的优势。SentencePiece作为统一框架,使得不同算法可以在同一框架下公平对比和互换。随着多语言需求的增长,分词器的设计将继续演进。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。