分词(Tokenization)是大语言模型处理文本的第一步,也是最容易被忽视的关键环节。将原始文本转换为 Token 序列的策略,直接影响模型的词汇覆盖率、多语言支持能力和推理效率。2016 年 Sennrich 等人提出的 BPE(Byte-Pair Encoding)和 2018 年 Kudo 等人提出的 SentencePiece,奠定了现代 LLM 分词器的基础。GPT-4 使用 BPE(cl100k_base),BERT 使用 WordPiece,LLaMA 使用 SentencePiece——不同的分词策略对模型性能有着深远影响。
理解分词器是理解 LLM 的第一步。一个不好的分词器会让模型”看”不到正确的语言单位,从而限制其理解和生成能力。
本文要点
- 分词问题的三种粒度:词级、字符级、子词级
- BPE(Byte-Pair Encoding)算法的详细原理与实现
- WordPiece(BERT 使用)的核心机制
- Unigram Language Model 与 SentencePiece
- 主流 LLM 的分词器对比:GPT、BERT、LLaMA、Qwen
- OOV 处理、多语言支持和压缩率分析
- tiktoken、sentencepiece、tokenizers 库的代码示例
- 词汇表大小与序列长度的权衡
一、分词问题的本质
0.1 三种分词粒度
0.2 各粒度的优缺点
| 粒度 | 优点 | 缺点 | 代表模型 |
|---|---|---|---|
| 词级 | 直观、语义完整 | OOV 问题严重、词汇表巨大 | 早期 NMT |
| 字符级 | 无 OOV、极小词汇表 | 序列过长、语义信息弱 | CharRNN |
| 子词级 | 平衡 OOV 和序列长度 | 需要预训练分词器 | GPT、BERT、LLaMA |
子词级分词的核心思想是:高频词保留为完整词,低频词拆分为有意义的子词单元。
二、BPE:字节对编码
0.3 算法原理
BPE(Byte-Pair Encoding)最初是一种数据压缩算法,2016 年被 Sennrich 等人引入 NLP 领域用于分词。其核心思想是:迭代地合并最频繁的字符对,构建子词词汇表。
0.4 算法步骤
0.5 详细示例
以训练语料 low lower lowest 为例:
Step 1: 初始化(字符级)
l o w </w>l o w e r </w>l o w e s t </w>Step 2: 统计字符对频率
(l, o): 3 次(o, w): 3 次(w, e): 2 次(e, r): 1 次(e, s): 1 次(s, t): 1 次Step 3: 合并最高频对 (l, o) → lo
lo w </w>lo w e r </w>lo w e s t </w>Step 4: 合并 (lo, w) → low
low </w>low e r </w>low e s t </w>Step 5: 合并 (w, e) → we…(继续直到目标词汇表大小)
0.6 编码新文本
训练好 BPE 后,对新文本编码就是按照学到的合并规则,从最优先级最高的合并开始应用:
def bpe_encode(token, merges): """将一个单词用 BPE 编码""" word = list(token) + ['</w>']
while len(word) > 1: # 找到优先级最高的合并对 pairs = [(word[i], word[i+1]) for i in range(len(word)-1)] best_pair = min(pairs, key=lambda p: merges.get(p, float('inf')))
if best_pair not in merges: break # 没有更多合并规则可用
# 应用合并 new_word = [] i = 0 while i < len(word): if i < len(word) - 1 and (word[i], word[i+1]) == best_pair: new_word.append(word[i] + word[i+1]) i += 2 else: new_word.append(word[i]) i += 1 word = new_word
return word
# 示例merges = {('l', 'o'): 0, ('lo', 'w'): 1, ...}print(bpe_encode("lowest", merges))# 输出: ['low', 'est</w>']0.7 Byte-level BPE
现代 LLM(如 GPT-2/3/4)使用 Byte-level BPE,将字节(而非字符)作为基本单元:
- 优点:天然支持所有语言和特殊字符,无需预处理
- 词汇表:256 个字节 + 学到的合并规则
- 无 OOV:任何文本都可以用 UTF-8 字节表示
# Byte-level BPE 的基本单元是字节text = "你好世界" # 中文bytes_repr = text.encode('utf-8')# b'\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xb8\x96\xe7\x95\x8c'# 每个中文字符 = 3 个字节三、WordPiece:BERT 的分词器
WordPiece 是 Google 为 BERT 开发的子词分词算法,与 BPE 类似但有重要区别。
0.8 核心区别
BPE 选择频率最高的字符对合并,而 WordPiece 选择似然增益最大的字符对合并:
# BPE: 选择频率最高的对best_pair = max(pairs, key=lambda p: count[p])
# WordPiece: 选择似然增益最大的对# Score = P(AB) / (P(A) * P(B))best_pair = max(pairs, key=lambda p: likelihood_gain(p))0.9 特殊标记
WordPiece 使用 ## 前缀表示子词的续接部分:
输入: "unbelievable"分词: ["un", "##believe", "##able"]
输入: "helloworld"分词: ["hello", "##world"]## 前缀让模型知道这个子词是前一个子词的延续,而不是独立的词。
四、Unigram Language Model
Unigram LM 是 SentencePiece 支持的另一种子词分词方法,与 BPE 的”自底向上合并”相反,Unigram LM 是”自顶向下裁剪”。
0.10 算法原理
- 初始化:从一个超大词汇表开始(如所有字符 + 常见子串)
- 计算每个子词的概率:基于 Unigram 语言模型
- 计算每个子词的重要性:移除后对整体似然的损失
- 裁剪:移除最不重要的子词(保留前 V 个)
- 迭代:重复 2-4 直到词汇表达到目标大小
0.11 关键优势
Unigram LM 的一个重要特性是:一个文本可以有多种分词方式,模型通过概率选择最优的:
# "invisible" 的可能分词# 方案1: ["in", "vis", "ible"] P = 0.3# 方案2: ["invis", "ible"] P = 0.5# 方案3: ["in", "visible"] P = 0.2# 选择概率最高的方案2这种概率性分词在处理多语言文本时特别有用。
五、SentencePiece
SentencePiece 是 Google 开源的分词工具库,支持 BPE 和 Unigram LM 两种算法。
0.12 核心特性
- 语言无关:不依赖预分词,直接处理原始文本
- 空格作为普通字符:将空格编码为特殊标记
▁(U+2581) - 可逆转换:分词结果可以无损还原为原始文本
- 子词正则化:训练时随机采样不同分词方案,提高鲁棒性
import sentencepiece as spm
# 训练 SentencePiece 模型spm.SentencePieceTrainer.train( input='corpus.txt', model_prefix='mymodel', vocab_size=32000, model_type='unigram', # 或 'bpe')
# 使用模型sp = spm.SentencePieceProcessor()sp.load('mymodel.model')
# 编码tokens = sp.encode("Hello, how are you?", out_type=str)# ['▁Hello', ',', '▁how', '▁are', '▁you', '?']
ids = sp.encode("Hello, how are you?")# [1234, 56, 789, 345, 678, 90]
# 解码(可逆)text = sp.decode(ids)# "Hello, how are you?"六、主流 LLM 分词器对比
| 模型 | 分词算法 | 词汇表大小 | 特殊设计 | 空格处理 |
|---|---|---|---|---|
| GPT-2 | Byte-level BPE | 50,257 | 字节级基础 | 保留空格 |
| GPT-3/4 | Byte-level BPE (cl100k_base) | 100,256 | 更大多词汇表 | 保留空格 |
| BERT | WordPiece | 30,000 | ## 前缀 | 预分词 |
| LLaMA | SentencePiece (BPE) | 32,000 | 字节回退 | ▁ 标记 |
| LLaMA-2/3 | SentencePiece (BPE) | 32,000 | 同上 | ▁ 标记 |
| Qwen | tiktoken (BPE) | 151,643 | 超大多语言词汇 | 保留空格 |
| ChatGLM | BPE | 130,528 | 中英双语优化 | 特殊处理 |
| Mistral | SentencePiece (BPE) | 32,000 | 标准配置 | ▁ 标记 |
0.13 压缩率对比
压缩率指每个 Token 平均能表示多少字符。压缩率越高,模型处理相同文本所需的 Token 越少:
中文压缩率的差异尤为显著:LLaMA 将每个中文字符编码为约 2-3 个 Token(因为 SentencePiece 对中文的词频统计不够),而 Qwen 的中文优化分词器将每个中文字符编码为约 1-1.5 个 Token。
0.14 词汇表大小的影响
| 词汇表大小 | 优点 | 缺点 |
|---|---|---|
| 小(30K) | 嵌入矩阵小、内存低 | 压缩率低、序列长 |
| 中(50-100K) | 平衡 | — |
| 大(150K+) | 压缩率高、序列短 | 嵌入矩阵大、内存高 |
七、代码示例
0.15 tiktoken(GPT-4 使用)
import tiktoken
# GPT-4 使用的分词器enc = tiktoken.get_encoding("cl100k_base")
# 编码tokens = enc.encode("Hello, how are you?")print(tokens) # [9906, 11, 1268, 527, 499, 30]
# 解码text = enc.decode(tokens)print(text) # "Hello, how are you?"
# 统计 Token 数量def count_tokens(text, model="gpt-4"): enc = tiktoken.encoding_for_model(model) return len(enc.encode(text))
print(count_tokens("这是一段中文文本"))0.16 sentencepiece(LLaMA 使用)
import sentencepiece as spm
# 加载 LLaMA 分词器sp = spm.SentencePieceProcessor()sp.load("llama_tokenizer.model")
# 编码tokens = sp.encode("Hello, world!")print(tokens) # [1, 15043, 29892, 1917, 29901]
# 获取子词文本pieces = sp.encode("Hello, world!", out_type=str)print(pieces) # ['▁Hello', ',', '▁world', '!']
# 解码text = sp.decode(tokens)print(text) # "Hello, world!"
# 词汇表大小print(f"Vocab size: {sp.get_piece_size()}")0.17 tokenizers(HuggingFace)
from tokenizers import Tokenizerfrom tokenizers.models import BPEfrom tokenizers.trainers import BpeTrainerfrom tokenizers.pre_tokenizers import Whitespace
# 从零训练 BPE 分词器tokenizer = Tokenizer(BPE(unk_token="[UNK]"))tokenizer.pre_tokenizer = Whitespace()trainer = BpeTrainer(vocab_size=30000, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
# 训练tokenizer.train(files=["corpus.txt"], trainer=trainer)
# 使用output = tokenizer.encode("Hello, how are you?")print(output.tokens) # ['Hello', ',', 'how', 'are', 'you', '?']print(output.ids) # [234, 56, 789, 345, 678, 90]八、分词器对模型的影响
0.18 推理成本
分词器直接影响推理成本,因为大多数 API 按 Token 计费:
同一个句子 "这是一段关于大语言模型的中文文本"- GPT-4 (cl100k_base): ~18 Tokens × $0.03/1K = $0.00054- LLaMA (SentencePiece): ~25 Tokens × 更低单价- Qwen (优化中文): ~12 Tokens × 最低单价
→ 分词效率直接影响使用成本0.19 上下文利用率
分词效率决定了有效上下文长度:
128K Token 上下文窗口- 英文(GPT-4): ~100K 单词 ≈ 1 本书- 中文(LLaMA): ~50K 字符 ≈ 半本书(因为压缩率低)- 中文(Qwen): ~80K 字符 ≈ 接近 1 本书(因为压缩率高)0.20 多语言公平性
不同分词器对非英语语言的处理效率差异巨大:
| 语言 | GPT-4 Token 效率 | LLaMA Token 效率 | Qwen Token 效率 |
|---|---|---|---|
| 英语 | 基准 1.0× | 约 0.9× | 约 1.0× |
| 中文 | 约 0.4× | 约 0.3× | 约 0.8× |
| 日语 | 约 0.4× | 约 0.3× | 约 0.6× |
| 韩语 | 约 0.4× | 约 0.3× | 约 0.6× |
| 代码 | 约 0.7× | 约 0.6× | 约 0.8× |
这意味着用 GPT-4 处理中文时,相同信息量所需的 Token 数是英语的 2.5 倍,直接导致更高的成本和更低的上下文利用率。
常见问题 FAQ
0.1 Q1: 为什么 LLaMA 的中文分词效率这么低?
LLaMA 的 SentencePiece 模型主要在英语数据上训练,词汇表中中文子词极少。大部分中文字符被编码为 UTF-8 字节(每个中文字符 3 字节),再通过字节级合并组合,导致一个汉字需要 2-3 个 Token。
0.2 Q2: BPE 和 WordPiece 哪个更好?
在现代 LLM 中,BPE 更流行。BPE 的优势是训练简单、可解释性强,且 Byte-level BPE 天然支持多语言。WordPiece 主要被 BERT 系列模型使用,其似然增益的合并策略在词汇表较小时可能更有优势。
0.3 Q3: 如何选择词汇表大小?
对于通用模型,32K-100K 是常见范围。选择时需要平衡:
- 太小:压缩率低,序列过长,影响训练效率
- 太大:嵌入矩阵占用内存大,训练和推理成本高
- 多语言模型通常需要更大的词汇表(100K+)以覆盖各语言
0.4 Q4: 分词器可以跨模型使用吗?
理论上可以,但不推荐。不同模型的分词器与其嵌入层紧密耦合。使用错误的分词器会导致 Token ID 与嵌入向量不匹配,产生无意义的输出。唯一合理的跨模型分词器使用场景是训练新模型时继承已有分词器。
0.5 Q5: 如何评估一个分词器的好坏?
主要指标:
- 压缩率:每 Token 平均字符数(越高越好)
- OOV 率:未登录词比例(应接近 0)
- 语言公平性:不同语言的压缩率差异(越小越好)
- 可逆性:能否无损还原原始文本
0.6 Q6: 什么是分词器的”毒性”问题?
分词器的毒性指某些 Token 可能被用于绕过模型安全过滤。例如,一个包含有害子串的 Token 可能被模型正确处理但绕过了简单的关键词过滤。现代分词器通过仔细设计词汇表和过滤训练数据来缓解这个问题。
小结
分词器是 LLM 的基础组件,直接影响模型的效率、成本和多语言能力:
- BPE:通过迭代合并最频繁字符对构建子词词汇表,GPT 系列使用 Byte-level BPE
- WordPiece:通过似然增益选择合并对,BERT 使用 ## 前缀标记续接
- Unigram LM:通过自顶向下裁剪构建词汇表,SentencePiece 支持此算法
- SentencePiece:语言无关的分词工具库,被 LLaMA 等模型采用
- 多语言支持:不同分词器对非英语语言的处理效率差异巨大
- 实际影响:分词效率直接影响推理成本、上下文利用率和多语言公平性
理解分词器是深入理解 LLM 的第一步。选择合适的分词器需要根据目标语言、模型规模和应用场景综合考量。
参考资料
- Neural Machine Translation of Rare Words with Subword Units — Sennrich et al., 2016(BPE 论文)
- SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing — Kudo & Richardson, 2018
- Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates — Kudo, 2018
- Byte-Pair Encoding: A Quick Introduction — Overview, 2023
- Tokenizer 审查:多语言公平性分析 — Yennie Jun, 2024
- tiktoken GitHub — OpenAI
- sentencepiece GitHub — Google
- HuggingFace tokenizers 文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






