Tokenizer 分词器详解
分词器(Tokenizer)是 LLM 与人类语言之间的翻译官——它把文本切分成模型能处理的最小单元(token),再把模型输出的 token 还原成文字。它是 LLM 应用生态概览的基础设施层,所有 LLM 的输入输出都离不开它。本页梳理从字符级到 BPE、WordPiece、Unigram 等主流分词算法的完整谱系。
Tokenizer = 把句子拆成积木块。
LLM 不认识文字,只认识数字。Tokenizer 就是”拆字+编号”的翻译官:
- 字符级(Character-level):逐个字母切分。“hello” 拆成 h-e-l-l-o。永远不会遇到生词,但序列太长——一个单词 5 个 token,模型难以学到单词级别的语义。
- 词级(Word-level):按空格/标点切分成完整词。“I love cats” 拆成 [“I”, “love”, “cats”]。序列短,但词汇表爆炸——英语有数十万词,还有各种变体(run/runs/running/unran),生词(OOV,Out-of-Vocabulary,即词汇表中不存在的词)直接抓瞎。
- 子词级(Subword):折中方案。常见词保留完整(“love”),罕见词拆成有意义的片段(“unhappiness” → “un” + “happiness” 或 “un” + “happy” + “ness”)。既不会序列太长,也几乎不会遇到生词——这是当今所有 LLM 的标准做法。
核心哲学:高频词完整保留,低频词拆成碎片。这样词汇表大小可控(通常 3 万到 20 万),同时能表示任意文本——即使是从没见过的新词,也能用已有子词拼出来。
Tokenizer 在 LLM 架构中的位置
Section titled “Tokenizer 在 LLM 架构中的位置”理解 Tokenizer 在整个 LLM 流水线中的角色非常重要:
用户输入文本 │ ▼┌──────────────┐ ┌─────────────────┐ ┌──────────────┐│ Tokenizer │─────▶│ Token ID 序列 │─────▶│ Embedding ││ (分词+编号) │ │ [15496, 995, …] │ │ (查表→向量) │└──────────────┘ └─────────────────┘ └──────┬───────┘ │ ▼ ┌──────────────┐ │ Transformer │ │ (模型主体) │ └──────┬───────┘ │ ▼┌──────────────┐ ┌─────────────────┐ ┌──────────────┐│ Tokenizer │◀─────│ Token ID 序列 │◀─────│ 输出概率分布 ││ (ID→文本) │ │ [输出 IDs] │ │ (softmax) │└──────────────┘ └─────────────────┘ └──────────────┘关键点:Tokenizer 决定了模型的”阅读能力”边界。如果一个中文字在词汇表中不存在,模型就永远无法”看到”这个字——它只能以更碎的字节片段来理解。Tokenizer 的质量直接影响模型的多语言能力、代码理解能力、以及推理成本。
BPE(Byte Pair Encoding,字节对编码)
Section titled “BPE(Byte Pair Encoding,字节对编码)”BPE 是最主流的子词分词算法,GPT 系列、LLaMA 等都使用它的变体。核心思想极其朴素:反复合并出现频率最高的相邻字节对。BPE 最初是 1994 年提出的一种数据压缩算法,2016 年被 Sennrich 等人引入 NLP 领域。
训练过程详解
Section titled “训练过程详解”训练过程(在语料上):
- 初始化:把所有词拆成单个字符序列,词尾加特殊标记(如
</w>表示词尾)。比如 “low” 拆成l o w </w>。同时统计每个词在语料中出现的次数。 - 统计频率:统计所有相邻字符对的频率,找到出现次数最多的一对。
- 合并:把这对字符合并成一个新 token,更新所有词。
- 重复:重复步骤 2-3,直到达到预设的合并次数(即词汇表大小)。
一个完整的训练示例(假设语料为:low low low low low lower lower newest newest newest newest newest newest widest widest):
初始状态(每个字符独立): 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> ×2
第 1 轮:频率最高的对是 (e, s) 出现 8 次 → 合并为 es l o w </w> ×5 l o w e r </w> ×2 n e w es t </w> ×6 w i d es t </w> ×2
第 2 轮:频率最高的对是 (es, t) 出现 8 次 → 合并为 est l o w </w> ×5 l o w e r </w> ×2 n e w est </w> ×6 w i d est </w> ×2
第 3 轮:频率最高的对是 (est, </w>) 出现 8 次 → 合并为 est</w> l o w </w> ×5 l o w e r </w> ×2 n e w est</w> ×6 w i d est</w> ×2
第 4 轮:频率最高的对是 (l, o) 出现 7 次 → 合并为 lo lo w </w> ×5 lo w e r </w> ×2 n e w est</w> ×6 w i d est</w> ×2
第 5 轮:(lo, w) 出现 7 次 → 合并为 low low </w> ×5 low e r </w> ×2 ...几轮之后,常见词 low 和后缀 est 已经被合并为完整的 token。经过数千轮合并,词汇表就包含了所有高频词、常见词根和词缀。
编码过程(对新文本):按训练时学到的合并规则表(一个有序的 (pair → merged_token) 列表),从单字符开始,按优先级(训练时的合并顺序)逐一尝试合并,直到不能再合并。
输入:"lowest"初始: l o w e s t规则: (l,o) → lo → lo w e s t规则: (lo,w) → low → low e s t规则: (e,s) → es → low es t规则: (es,t) → est → low est结果: ["low", "est"]如果输入包含训练时没见过的字符,BPE 会输出 [UNK](未知 token)——这就是经典 BPE 的局限性,也是 Byte-level BPE 要解决的问题。
WordPiece
Section titled “WordPiece”Google 为 BERT 设计,与 BPE 非常相似,区别在于选择标准:
- BPE 选择频率最高的字节对。
- WordPiece 选择使语料似然函数最大化的字节对——即合并后整体语言模型概率提升最大的那一对。
具体而言,WordPiece 的评分公式为:
这个分数衡量的是 x 和 y 共现(一起出现)的”密度”——如果 x 和 y 总是成对出现,分数高;如果 x 和 y 各自也常单独出现,分数被稀释。这与 PMI(Pointwise Mutual Information,逐点互信息,衡量两个事件关联程度的指标)的思想一致。
实际效果上,WordPiece 和 BPE 分词结果差别不大,但 WordPiece 更”贪心”地选择对整体概率贡献最大的合并。BERT、DistilBERT 等使用 WordPiece。
WordPiece 和 BPE 的编码结果都用
##前缀标记子词片段的后缀部分,如 “playing” 可能被切分为["play", "##ing"],其中##ing表示它是接在前面的词后面的。
Unigram Language Model
Section titled “Unigram Language Model”SentencePiece 库支持的另一种策略,思路”反着来”:
- 初始化:先初始化一个巨大的候选词汇表(所有可能的子串,可能有数百万个)。
- 估计概率:用 EM 算法(Expectation-Maximization,期望最大化算法,一种迭代求解最大似然估计的经典方法)估计每个子词的概率,构建一个 Unigram 语言模型。
- 逐步删减:逐步删掉对语料似然贡献最小的子词,缩小词汇表到目标大小。
- 编码:编码时,对同一文本选择概率最高的分词方式(使用 Viterbi 算法动态规划求解最优切分)。
优点:对同一文本可能有多种分词方式,编码时自然选择概率最高的——这在某些语言(日语、中文等无空格语言)上更灵活。
Unigram 与 BPE 的本质区别:BPE 是贪心增长的(从字符出发逐步合并),Unigram 是贪心缩减的(从大全出发逐步删除)。BPE 每一步只考虑局部频率,Unigram 每一步考虑全局似然——理论上更优,但训练更慢。
子词正则化(Subword Regularization):Unigram 的一个独特优势是可以采样多种分词方式,在训练时随机选不同的分词结果,起到数据增强的效果。这被称为”子词正则化”,由 Kudo (2018) 提出,能提升模型对分词噪声的鲁棒性。
SentencePiece
Section titled “SentencePiece”SentencePiece 不是分词算法,而是一个分词框架/工具库,它把文本当作原始字节流处理,不依赖空格分词。它支持 BPE 和 Unigram 两种算法。
关键优势:语言无关。英语靠空格分词,但中文、日语、韩语没有空格——SentencePiece 直接在 Unicode 字节流上操作,对所有语言一视同仁。LLaMA、T5、ChatGLM 等多语言模型都用 SentencePiece。
核心设计决策:
- 将空格编码为特殊符号(
▁,U+2581):这样空格信息不会在分词过程中丢失,解码时可以完美还原原文。 - 直接在 Unicode 字符上操作:不依赖任何语言的空格规则,对中文、日语等无空格语言同样适用。
- 可逆性保证:
decode(encode(text)) == text,不丢失任何信息。
文本: "Hello 世界"预处理: "▁Hello▁世界" (空格被替换为 ▁)分词: ["▁Hello", "▁世界"] 或 ["▁He", "llo", "▁世", "界"]解码: "▁Hello▁世界" → "Hello 世界" (▁ 替换回空格)tiktoken 与 Byte-level BPE
Section titled “tiktoken 与 Byte-level BPE”OpenAI 为 GPT 系列开发的高效 BPE 分词器库。核心改进:直接在 UTF-8 字节上操作(Byte-level BPE),而非 Unicode 字符。好处是任何文本——包括 emoji、生僻字、代码符号——都能被分词,不会出现”无法表示的字符”。
Byte-level BPE 为什么重要:经典 BPE 在 Unicode 字符层面操作,如果遇到训练时没见过的字符(如某个生僻汉字),就只能输出 [UNK]。Byte-level BPE 先将所有文本转换为 UTF-8 字节(每个字符变成 1-4 个字节),然后在字节层面做 BPE。UTF-8 只有 256 种字节值,所以永远不可能遇到”未知字节”——从根本上消除了 OOV 问题。
GPT 系列编码演进:
| 模型 | 编码名称 | 词汇表大小 | 特点 |
|---|---|---|---|
| GPT-2 / GPT-3 | r50k_base / p50k_base | ~50K | Byte-level BPE |
| GPT-3.5 (ChatGPT) | cl100k_base | ~100K | 扩充词汇表,优化多语言和代码 |
| GPT-4o | o200k_base | ~200K | 大幅优化非英语语言,中文效率提升约 1.7 倍 |
GPT-4o 的 o200k_base 是一个重要里程碑——它将中文的 token 效率(每个 token 承载的中文字符数)相比 cl100k_base 提升了约 1.7 倍,意味着同样的中文文本在 GPT-4o 下消耗的 token 更少、成本更低。
主流模型的 Tokenizer 对比
Section titled “主流模型的 Tokenizer 对比”不同模型的 Tokenizer 设计差异很大,直接影响其在不同语言和任务上的效率:
| 模型 | 算法 | 词汇表大小 | 中文效率 | 备注 |
|---|---|---|---|---|
| GPT-4 (cl100k) | Byte-level BPE | ~100K | 一般 | 英文优化 |
| GPT-4o (o200k) | Byte-level BPE | ~200K | 好 | 中文效率大幅提升 |
| LLaMA-2 | SentencePiece BPE | 32K | 差 | 词汇表太小,中文一个字常需 2-3 token |
| LLaMA-3 | tiktoken BPE | 128K | 好 | 大幅扩充词汇表 |
| Qwen 2.5 | tiktoken BPE | 152K | 优秀 | 中文优化,每字约 0.6 token |
| DeepSeek-V2 | Byte-level BPE | 100K | 优秀 | 中文优化 |
| Claude 3 | 专有 | ~100K | 好 | Anthropic 专有实现 |
中文效率差异的影响:LLaMA-2 的 32K 词表中中文占比极少,导致中文文本被切碎成大量单字甚至字节,一个中文字常需 2-3 个 token。这直接导致中文场景的成本翻倍、上下文窗口缩小。Qwen、DeepSeek 等国产模型在词表中专门加入了大量中文 token,大幅降低了中文场景的 token 消耗。
分词算法谱系
Section titled “分词算法谱系”BPE 分词流程
Section titled “BPE 分词流程”BPE 训练与编码的关系
Section titled “BPE 训练与编码的关系”使用 tiktoken 查看 GPT 的分词结果
Section titled “使用 tiktoken 查看 GPT 的分词结果”import tiktoken
# 加载 GPT-4 的分词器编码enc = tiktoken.encoding_for_model("gpt-4")
# 查看分词结果(数字 ID)和对应的文本片段text = "我喜欢学习人工智能和LLM"tokens = enc.encode(text)for tid in tokens: print(f"ID={tid:6d} 片段={enc.decode_single_token_bytes(tid)}")print(f"\n总 token 数: {len(tokens)}")
# 中文和英文的 token 效率对比zh = enc.encode("我爱自然语言处理")en = enc.encode("I love natural language processing")print(f"中文: {len(zh)} tokens") # 通常比英文多print(f"英文: {len(en)} tokens")对比不同模型的 Tokenizer 效率
Section titled “对比不同模型的 Tokenizer 效率”import tiktoken
# 加载不同编码enc_cl100k = tiktoken.get_encoding("cl100k_base") # GPT-4enc_o200k = tiktoken.get_encoding("o200k_base") # GPT-4o
text_zh = "人工智能是计算机科学的一个分支,它致力于研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统。"text_en = "Artificial intelligence is a branch of computer science that aims to create machines that can think and act like humans."text_code = "def quicksort(arr):\n if len(arr) <= 1:\n return arr\n pivot = arr[len(arr)//2]\n left = [x for x in arr if x < pivot]\n return quicksort(left) + [pivot] + quicksort(right)"
for label, text in [("中文", text_zh), ("英文", text_en), ("代码", text_code)]: c = len(enc_cl100k.encode(text)) o = len(enc_o200k.encode(text)) print(f"{label}: cl100k={c:4d} tokens, o200k={o:4d} tokens, 提升={c/o:.1%}")用 HuggingFace tokenizers 训练 BPE 分词器
Section titled “用 HuggingFace tokenizers 训练 BPE 分词器”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=5000, # 目标词汇表大小 special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"])
# 在一组文本文件上训练(这里用列表模拟)texts = ["hello world", "machine learning is fun", "tokenizers split text"]tokenizer.train_from_iterator(texts, trainer)
# 编码与解码output = tokenizer.encode("hello machine learning")print(f"Tokens: {output.tokens}")print(f"IDs: {output.ids}")print(f"解码: {tokenizer.decode(output.ids)}")用 SentencePiece 训练中文分词器
Section titled “用 SentencePiece 训练中文分词器”# 前提:pip install sentencepieceimport sentencepiece as spm
# 训练 Unigram 模型(适合中文等无空格语言)spm.SentencePieceTrainer.train( input="corpus.txt", # 训练语料文件 model_prefix="m", # 输出模型前缀 vocab_size=32000, # 词汇表大小 model_type="unigram", # 也可选 "bpe" character_coverage=0.9995, # 覆盖多少比例的 Unicode 字符 input_sentence_size=1000000,)
# 加载并使用sp = spm.SentencePieceProcessor(model_file="m.model")tokens = sp.encode("自然语言处理很有趣", out_type=str)ids = sp.encode("自然语言处理很有趣", out_type=int)print(f"分词: {tokens}") # ['▁', '自然', '语言', '处理', '很', '有趣']print(f"ID: {ids}")print(f"解码: {sp.decode(ids)}") # 完美还原原文- Token 数量直接决定成本和延迟:API 按 token 计费,上下文窗口按 token 限制。中文一个字通常是 1-2 个 token(比英文贵),代码和 emoji 的 token 开销更大。估算时:1 个英文单词约 1.3 个 token,1 个中文字约 1.5-2 个 token(GPT-4 cl100k 编码),GPT-4o(o200k 编码)中文约 0.7-1 个 token/字。
- 不同模型的 Tokenizer 不通用:GPT-4 的 “hello” 是 token 31373,但在 Claude 的 Tokenizer 里是完全不同的 ID。混用会得到乱码。务必使用目标模型对应的 Tokenizer 做长度估算。
- 词汇表大小是多语言权衡:词汇表越大,每个 token 承载的信息越多(序列短、推理快),但 Embedding 层(将 token ID 映射为向量的查找表)参数更多,训练数据更容易稀疏。GPT-4 约 10 万词表,LLaMA-2 仅 3.2 万(因此中文效率差),Qwen/DeepSeek 等中文优化模型通常扩充到 15 万+。
- 特殊 token 的处理:
[BOS](序列开头)、[EOS](序列结尾)、[PAD](填充)、[UNK](未知词)等特殊 token 的设计因模型而异。微调时如果添加了新的特殊 token(如<|im_start|>),需要正确扩展 Embedding 层。 - 压缩率是核心指标:好的 Tokenizer 能用更少的 token 表示同样的文本,直接降低成本、增大有效上下文。评估 Tokenizer 时关注 bytes-per-token(每 token 承载的字节数)——越高越好。一个好的多语言 Tokenizer 的 bytes-per-token 应在 4-6 之间。
- Tokenizer 决定了模型的能力上限:如果词汇表中缺少某些语言/领域的 token,模型在这些领域的表现会受限。这就是为什么 LLaMA-2 在中文任务上先天不如 Qwen——不是因为模型架构差,而是因为 Tokenizer 没有中文优化,大量信息在字节层面丢失了。
- Tokenizer 是模型不可分割的一部分:一个训练好的 LLM 和它的 Tokenizer 是绑定的。不能给一个模型换另一个 Tokenizer——Token ID 和 Embedding 层的对应关系是固定的。如果必须换 Tokenizer(如合并两个模型),需要重新训练 Embedding 层。
- Pre-tokenization 的影响:在子词合并之前,文本通常会先被空格或正则规则预切分(pre-tokenization)。不同的预切分策略会显著影响最终分词效果。例如 GPT-4 的预切分将
'hello和world分开,但不会拆开don't中的't——这些细节影响模型的实际行为。
- 所有 LLM 的输入输出处理:从 GPT 到 LLaMA 到 Claude,每一次对话都先经过 Tokenizer 编码再送入模型,输出再解码。详见LLM 推理优化。
- Token 计费与配额管理:OpenAI/Anthropic 等 API 按 token 计费,应用层必须用 Tokenizer 预估 token 数来控制成本和上下文窗口。详见上下文工程。
- 数据预处理与词表对齐:微调模型时,训练数据的 Tokenizer 必须和基础模型完全一致,否则 Embedding 层对不上。详见LLM 微调技术。
- Embedding 模型:嵌入模型的输入也需先分词。详见嵌入模型。
- 多语言模型评估:评估模型在不同语言上的表现时,需要考虑 Tokenizer 效率差异。同样长度的中文和英文文本,LLaMA-2 消耗的 token 数差异可达 3-5 倍——这直接影响公平比较。
- Tokenizer 压缩研究:2024-2025 年的研究热点之一是”Tokenizer 压缩”——在不降低模型性能的前提下减小词汇表,从而减小 Embedding 层参数量和模型文件体积。byte-level 训练和动态词表(如 MegaByte、Mamba 的选择性机制)是两个方向。
典型类库与工具
Section titled “典型类库与工具”| 类库 | 语言 | 说明 |
|---|---|---|
| tiktoken | Python | OpenAI 开源的高效 BPE 分词器,支持 GPT-3.5/4 的 cl100k_base 和 GPT-4o 的 o200k_base 编码 |
| sentencepiece | Python/C++ | Google 开源的分词框架,支持 BPE 和 Unigram,语言无关,LLaMA/T5/ChatGLM 使用 |
| tokenizers | Python/Rust | HuggingFace 开源的高性能分词库,Rust 实现,支持 BPE/WordPiece/Unigram |
| transformers AutoTokenizer | Python | HuggingFace Transformers 内置的统一接口,自动匹配模型对应的 Tokenizer |
| js-tiktoken | JavaScript | tiktoken 的 JavaScript 移植,适合前端/Node.js 场景的 token 估算 |
| 术语 | 英文 | 解释 |
|---|---|---|
| 分词器 | Tokenizer | 将文本切分为 token(子词单元)并映射到数字 ID 的组件 |
| 词元 | Token | 分词器输出的最小文本单元,可能是完整词、子词或单个字符 |
| 词汇表 | Vocabulary | 分词器所有可用 token 的集合,每项对应一个唯一 ID |
| 字节对编码 | BPE, Byte Pair Encoding | 反复合并频率最高的相邻字节对来构建词汇表的子词算法 |
| 词片 | WordPiece | 基于似然最大化选择合并对的子词算法,BERT 使用 |
| Unigram 语言模型 | Unigram LM | 通过概率模型选择最优分词方案的算法,支持多种分词方式 |
| 句子片段 | SentencePiece | 语言无关的分词框架,直接在 Unicode/字节流上操作 |
| 词外词 | OOV, Out-of-Vocabulary | 词汇表中不存在的词,词级分词器会遇到的问题,子词分词器基本解决 |
| 字节级 BPE | Byte-level BPE | 直接在 UTF-8 字节上运行 BPE,能表示任意字符(GPT-4 使用) |
| 压缩率 | Compression Rate | 每 token 承载的字节数,衡量 Tokenizer 效率的核心指标 |
| 预切分 | Pre-tokenization | 子词合并前对文本的初步切分(如按空格),影响最终分词效果 |
| 子词正则化 | Subword Regularization | 训练时随机采样不同分词方式,增强模型鲁棒性的技术 |
- Sennrich et al.,「Neural Machine Translation of Rare Words with Subword Units」(ACL 2016):BPE 分词的原始论文,将字节对编码从数据压缩领域引入 NLP,奠定了现代子词分词的基础。
- Kudo,「SentencePiece: A simple and language independent subword tokenizer」(2018):SentencePiece 的论文,提出语言无关的分词框架,T5/LLaMA 等模型的标准工具。
- Kudo,「Subword Regularization: Improving Neural Network Translation Models」(ACL 2018):Unigram 语言模型分词 + 子词正则化,提出多种分词采样提升鲁棒性。
- OpenAI tiktoken 文档:tiktoken 开源仓库 README,包含 GPT 系列编码方式说明和性能对比,practical 使用首选。
- HuggingFace NLP Course - Chapter 2:Tokenizer 章节,通过代码示例讲解 BPE/WordPiece/Unigram 的实际操作,入门首选。
- Petrov et al.,「Language Model Models Are Secretly Rabbits」(2024):深入分析 Tokenizer 对模型行为的隐含影响,揭示分词策略如何微妙地影响模型的推理能力。
- GPT-4o tokenizer 改进:OpenAI 在 GPT-4o 中将词汇表扩展到约 20 万 token,大幅优化非英语语言的 token 效率,官方博客有详细说明。