子词分词 本节摘要:词分词器卡在没见过的词上,字符分词器把序列长度吹爆,子词分词器取折中——2026 每个 LLM 都跑在某一种上。词表五万,用户敲了个「untokenizable」,分词器吐回 ,模型对这个词毫无信号;更糟的是,语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。子词分词解决之:常用词保持单 token,罕见词拆成有意义的碎片( → 、 、 ),训练数据覆盖一切,因为任何字符串归根到底是字节序列。
本节摘要:词分词器卡在没见过的词上,字符分词器把序列长度吹爆,子词分词器取折中——2026 每个 LLM 都跑在某一种上。词表五万,用户敲了个「untokenizable」,分词器吐回
[UNK],模型对这个词毫无信号;更糟的是,语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。子词分词解决之:常用词保持单 token,罕见词拆成有意义的碎片(untokenizable→un、token、izable),训练数据覆盖一切,因为任何字符串归根到底是字节序列。本节从零实现 BPE 与字节级 BPE,讲透 Unigram 的概率剪枝与 WordPiece 的似然最大化,并理清 SentencePiece(训词表的库)与 tiktoken(只编码的快库)的分工,最后点名 2026 仍在生产的陷阱:分词漂移、空白歧义、多语欠训、emoji 分裂——以及为何词表大小是个扩展决策,而非常数。
对应原课程:Phase 5 · Lesson 19 ·
subword-tokenization(原英文phases/05-nlp-foundations-to-advanced/19-subword-tokenization/docs/en.md)。前置依赖:第 01 节(文本处理)、第 04 节(GloVe/FastText/子词)。
阅读完本节,你应当能够:
[UNK]。你的词表有五万词。用户敲了个「untokenizable」。分词器吐回 [UNK],模型对这个词毫无信号。更糟的是:语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。
子词分词解决之。常用词保持单 token,罕见词拆成有意义的碎片:untokenizable → un、token、izable。训练数据覆盖一切,因为任何字符串归根到底是字节序列。
2026 每个前沿 LLM 都跑在三种算法(BPE、Unigram、WordPiece)之一上,包在三个库(tiktoken、SentencePiece、HF Tokenizers)之一里。不选一个,就发布不了语言模型。
[UNK]——任何字节序列都能编码。GPT-2 用 50,257 个 token(256 字节 + 50,000 次合并 + 1 个特殊)。💡 SentencePiece vs tiktoken:SentencePiece 是训练词表(BPE 或 Unigram)的库,直接在原始 Unicode 文本上训,把空白编码为
▁;tiktoken 是 OpenAI 的只编码的快库,针对预构建词表,不训练。要训新词表用 SentencePiece 或 HF Tokenizers;要对 GPT 词表快速推理用 tiktoken(cl100k_base、o200k_base);要训加服务一体的用 HF Tokenizers。
def train_bpe(corpus, num_merges): vocab = {tuple(word) + ("</w>",): count for word, count in corpus.items()} merges = [] for _ in range(num_merges): pairs = Counter() for symbols, freq in vocab.items(): for a, b in zip(symbols, symbols[1:]): pairs[(a, b)] += freq if not pairs: break best = pairs.most_common(1)[0][0] merges.append(best) vocab = apply_merge(vocab, best) return merges
算法编码了三件事。</w> 标词尾,让「low」(后缀)与「lower」(前缀)保持区分;频率加权让高频对早赢;合并列表有序——推理时按训练顺序应用合并。
def encode_bpe(word, merges): symbols = list(word) + ["</w>"] for a, b in merges: i = 0 while i < len(symbols) - 1: if symbols[i] == a and symbols[i + 1] == b: symbols = symbols[:i] + [a + b] + symbols[i + 2:] else: i += 1 return symbols
朴素的 O(n·|merges|)。生产实现(tiktoken、HF Tokenizers)用合并优先级的优先队列查找,近线性时间。
import sentencepiece as spm spm.SentencePieceTrainer.train( input="corpus.txt", model_prefix="my_tokenizer", vocab_size=8000, model_type="bpe", # 或 "unigram" character_coverage=0.9995, # CJK 要更低(英文 0.9995,日文 0.995) normalization_rule_name="nmt_nfkc", ) sp = spm.SentencePieceProcessor(model_file="my_tokenizer.model") print(sp.encode("untokenizable", out_type=str)) # ['▁un', 'token', 'izable']
注意:无需预分词;空白编码为 ▁;character_coverage 控制多激进地保留罕见字符 vs 映射到 <unk>。
import tiktoken enc = tikencoding("o200k_base") print(enc.encode("untokenizable")) # [127340, 101028] print(len(enc.encode("Hello, world!"))) # 4
只编码,不训练。快(Rust 后端),与 GPT-4/5 分词精确匹配,用于字节计数、成本估算、上下文预算。
2026 的栈:
| 情形 | 选 |
|---|---|
| 从零训单语模型 | HF Tokenizers(BPE) |
| 训多语模型 | SentencePiece(Unigram,character_coverage=0.9995) |
| 服务 OpenAI 兼容 API | tiktoken(GPT-4+ 用 o200k_base) |
| 领域专用词表(代码、数学、蛋白质) | 在领域语料上训自定义 BPE,与基词表合并 |
| 边缘推理、小模型 | Unigram(更小的词表工作得更好) |
💡 词表大小是扩展决策,不是常数:粗略经验是 <1B 参数 32k,110B 用 50100k,多语/前沿 200k+。
tokenizer.json 哈希。add_special_tokens 与 add_prefix_space。o200k_base 部分修好了。保存为 outputs/skill-bpe-vs-wordpiece.md:
--- name: tokenizer-picker description: Pick tokenizer algorithm, vocab size, library for a given corpus and deployment target. version: 1.0.0 phase: 5 lesson: 19 tags: [nlp, tokenization] --- Given a corpus (size, languages, domain) and deployment target (training from scratch / fine-tuning / API-compatible inference), output: 1. Algorithm. BPE, Unigram, or WordPiece. One-sentence reason. 2. Library. SentencePiece, HF Tokenizers, or tiktoken. Reason. 3. Vocab size. Rounded to nearest 1k. Reason tied to model size and language coverage. 4. Coverage settings. `character_coverage`, `byte_fallback`, special-token list. 5. Validation plan. Average tokens-per-word on held-out set, OOV rate, compression ratio, round-trip decode equality. Refuse to train a character-coverage <0.995 tokenizer on corpora with rare-script content. Refuse to ship a vocab without a frozen `tokenizer.json` hash check in CI. Flag any monolingual tokenizer under 16k vocab as likely under-spec.
code/main.py 的小语料上训一个 500 次合并的 BPE,编码三个留出词。多少恰好产出 1 个 token,多少产出 >1 个?cl100k_base、o200k_base、与你训的词表 32k 的 SentencePiece BPE 的 token 数,报告各自的压缩比。</w> 标词尾,合并列表有序,推理按训练顺序应用。[UNK]:在 256 字节上跑,GPT-2/Llama 用此,50,257 token。▁,无需预分词,多语首选。tokenizer.json 哈希:分词漂移会让模型静悄悄输出垃圾。下一节,我们转向「让模型说我们要的话」——进入「结构化输出与受约束解码」,看如何用 JSON 模式、语法约束与 logits 屏蔽,把自由生成的 LLM 锁进我们想要的格式。