1. 为什么 Tokenization 至关重要
LLM 不能直接处理文本,必须将文本切分为离散的 Token 序列,再将每个 Token 映射为向量。Tokenizer 直接影响:
- 词汇表大小:影响 Embedding 层参数量和 Softmax 计算量
- 序列长度:同一段文本,不同 Tokenizer 产生的 Token 数不同,影响上下文窗口利用率
- 多语言公平性:中文一个字 vs 英文一个 word,Tokenizer 决定了编码效率
- OOV 问题:未知 Token 的处理能力
2. BPE (Byte Pair Encoding)
2.1 算法思想
BPE 最初是数据压缩算法,被 Sennrich 等人(2016)引入 NLP。核心思路:从一个字符级词汇表开始,反复合并出现频率最高的相邻 Token 对。
2.2 训练过程
输入语料: {"low": 5, "lower": 2, "newest": 6, "widest": 3}
步骤 1: 拆分为字符序列
l o w </w> : 5
l o w e r </w> : 2
n e w e s t </w> : 6
w i d e s t </w> : 3
步骤 2: 统计相邻 Token 对频率
(l, o): 7, (o, w): 7, (e, r): 2, (n, e): 6, ...
步骤 3: 合并最高频对 (l, o) → "lo"
lo w </w> : 5
lo w e r </w> : 2
n e w e s t </w> : 6
w i d e s t </w> : 3
重复直到词汇表达到目标大小
2.3 编码过程
对新文本,按训练时的合并规则顺序应用:
import re
from collections import Counter, defaultdict
class BPETokenizer:
def __init__(self):
self.merges = [] # 训练得到的合并规则序列
self.vocab = set()
def train(self, corpus, vocab_size=1000):
# 初始化:将每个词拆分为字符
word_freqs = Counter()
for text in corpus:
for word in text.split():
word_freqs[' '.join(list(word)) + ' </w>'] += 1
# 转为字符级表示
splits = {word: word.split() for word in word_freqs}
while len(self.vocab) < vocab_size:
# 统计相邻 pair 频率
pair_freqs = defaultdict(int)
for word, freq in word_freqs.items():
symbols = splits[word]
for i in range(len(symbols) - 1):
pair_freqs[(symbols[i], symbols[i+1])] += freq
if not pair_freqs:
break
# 合并最高频 pair
best_pair = max(pair_freqs, key=pair_freqs.get)
self.merges.append(best_pair)
new_token = best_pair[0] + best_pair[1]
self.vocab.add(new_token)
# 更新所有 splits
for word in word_freqs:
splits[word] = self._merge_pair(splits[word], best_pair, new_token)
def _merge_pair(self, symbols, pair, new_token):
i = 0
result = []
while i < len(symbols):
if i < len(symbols) - 1 and (symbols[i], symbols[i+1]) == pair:
result.append(new_token)
i += 2
else:
result.append(symbols[i])
i += 1
return result
def encode(self, text):
# 对新文本按 merges 顺序逐步合并
symbols = list(text) + ['</w>']
for pair, new_token in [(m, m[0]+m[1]) for m in self.merges]:
symbols = self._merge_pair(symbols, pair, new_token)
return symbols
2.4 BBPE (Byte-level BPE)
GPT-2/3/4 使用 Byte-level BPE:先 将文本编码为 UTF-8 字节序列(256 个基础 Token),再在字节上做 BPE。好处是天然支持所有语言和符号,无 OOV 问题。
3. WordPiece
3.1 与 BPE 的区别
WordPiece(Google,用于 BERT)与 BPE 类似,但合并准则不同:
- BPE 选择频率最高的 pair
- WordPiece 选择似然增益最大的 pair
合并 pair $(A, B)$ 的评分:
$$ \text{score}(A, B) = \frac{P(AB)}{P(A) \cdot P(B)} $$
这等价于 PMI(Pointwise Mutual Information),选择共现概率远高于独立出现概率的 pair。
3.2 编码差异
WordPiece 编码时使用最长前缀匹配(贪婪左到右),而非 BPE 的按合并规则顺序应用:
词汇表: ["play", "##ing", "##ed", "un", "##believ", "##able"]
输入: "unbelievable"
→ "un" + "##believ" + "##able"
## 前缀表示该 Token 是词的中间/结尾部分,不能出现在词首。
4. Unigram Language Model
4.1 核心思想
Unigram LM(SentencePiece 的一种模式)采用概率生成模型视角:假设每个 Token 独立生成,选择使语料似然最大的词汇表。
4.2 训练过程
- 用所有可能的子串初始化候选词汇表
- 用 EM 算法估计每个 Token 的概率 $P(t)$
- 计算移除每个 Token 对语料似然的损失
- 移除损失最小的若干 Token(保留基础字符)
- 重复 2-4 直到词汇表达到目标大小
语料似然:
$$ L = \prod_{s \in \text{corpus}} P(s) = \prod_{s} \sum_{t \in V} \prod_{i} P(t_i) $$
其中每个句子 $s$ 有多种分词方式,取最优分词(Viterbi 解码)。
4.3 编码
对于新文本,Unigram LM 通过 Viterbi 算法找概率最大的分词方式:
def viterbi_segment(text, token_probs):
n = len(text)
dp = [0.0] * (n + 1)
back = [None] * (n + 1)
for i in range(1, n + 1):
for j in range(i):
token = text[j:i]
if token in token_probs:
score = dp[j] + math.log(token_probs[token])
if score > dp[i] or dp[i] == 0:
dp[i] = score
back[i] = j
# 回溯
tokens = []
i = n
while i > 0:
j = back[i]
tokens.append(text[j:i])
i = j
return tokens[::-1]
5. SentencePiece
5.1 设计目标
SentencePiece(Google)解决了一个关键问题:语言无关的分词。传统分词依赖空格预分词,对中文/日语等无空格语言不友好。SentencePiece 直接在原始 Unicode 字符串上操作。
5.2 核心特性
- 不依赖空格预分词:直接处理原始文本
- 可逆性:
encode → decode完全还原原文 - 支持 BPE 和 Unigram 两种模式
- 特殊 Token 管理:
<unk>,<s>,</s>等
import sentencepiece as spm
# 训练
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='tokenizer',
vocab_size=32000,
model_type='unigram', # 或 'bpe'
character_coverage=0.9995,
# 特殊 token
pad_id=0, unk_id=1, bos_id=2, eos_id=3
)
# 使用
sp = spm.SentencePieceProcessor(model_file='tokenizer.model')
tokens = sp.encode('Hello, 世界!', out_type=str)
ids = sp.encode('Hello, 世界!', out_type=int)
decoded = sp.decode(ids)
5.3 多语言分词的挑战
不同语言的编码效率差异巨大。以 LLaMA-2 的 Tokenizer 为例:
| 语言 | 每词平均 Token 数 | 相对中文比例 |
|---|---|---|
| 英文 | 1.2-1.5 | - |
| 中文 | 1.5-2.5 | 1.7x |
| 日文 | 2.0-3.0 | 2.0x |
| 俄文 | 2.0-2.5 | 1.7x |
| 阿拉伯文 | 2.5-3.5 | 2.3x |
这意味着同样上下文窗口下,中文用户实际可用的信息量比英文少约 40-70%。这也是多语言模型需要特别优化词汇表的原因。
6. 各方案对比
| 特性 | BPE/BBPE | WordPiece | Unigram LM |
|---|---|---|---|
| 合并准则 | 频率最高 | PMI 最大 | 似然损失最小 |
| 编码方式 | 按规则顺序 | 贪婪前缀匹配 | Viterbi 最优 |
| 逆向兼容 | 是 | 是(##标记) | 是 |
| 多语言 | BBPE 好 | 需 SentencePiece | 好 |
| 代表模型 | GPT 系列 | BERT | T5, LLaMA |
7. 工程实践建议
- 新语言模型:推荐 SentencePiece + Unigram 或 BBPE,训练前确保语料覆盖目标语言
- 词汇表大小:32K-128K 是常见范围,多语言建议 64K+
- 特殊 Token:预留
<pad>,<unk>,<bos>,<eos>以及领域特殊 Token - 评估指标:Fertility(每词 Token 数)、Compression Ratio、OOV Rate
- 迭代优化:在特定领域语料上增量训练 Tokenizer 可以提升编码效率
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
