子词分词


文档摘要

子词分词 本节摘要:词分词器卡在没见过的词上,字符分词器把序列长度吹爆,子词分词器取折中——2026 每个 LLM 都跑在某一种上。词表五万,用户敲了个「untokenizable」,分词器吐回 ,模型对这个词毫无信号;更糟的是,语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。子词分词解决之:常用词保持单 token,罕见词拆成有意义的碎片( → 、 、 ),训练数据覆盖一切,因为任何字符串归根到底是字节序列。

子词分词

本节摘要:词分词器卡在没见过的词上,字符分词器把序列长度吹爆,子词分词器取折中——2026 每个 LLM 都跑在某一种上。词表五万,用户敲了个「untokenizable」,分词器吐回 [UNK],模型对这个词毫无信号;更糟的是,语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。子词分词解决之:常用词保持单 token,罕见词拆成有意义的碎片(untokenizableuntokenizable),训练数据覆盖一切,因为任何字符串归根到底是字节序列。本节从零实现 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/子词)。

学习目标

阅读完本节,你应当能够:

  1. 讲清 BPE、Unigram、WordPiece 三种算法的核心循环与各自取舍。
  2. 从零实现 BPE 的训练与编码,并解释字节级 BPE 如何保证零 [UNK]
  3. 区分 SentencePiece、tiktoken、HF Tokenizers 三个库的分工(训 vs 编码 vs 两者)。
  4. 为给定语料与部署目标选词表大小与覆盖率,并识别多语欠训、emoji 分裂等生产陷阱。

一、问题与直觉

你的词表有五万词。用户敲了个「untokenizable」。分词器吐回 [UNK],模型对这个词毫无信号。更糟的是:语料第 90 百分位的文档有 40 个罕见词,意味着每篇丢 40 比特信息。

子词分词解决之。常用词保持单 token,罕见词拆成有意义的碎片:untokenizableuntokenizable。训练数据覆盖一切,因为任何字符串归根到底是字节序列。

2026 每个前沿 LLM 都跑在三种算法(BPE、Unigram、WordPiece)之一上,包在三个库(tiktoken、SentencePiece、HF Tokenizers)之一里。不选一个,就发布不了语言模型。

  • BPE(字节对编码):从字符级词表开始,数每个相邻对,把最高频对合并成新 token,重复到目标词表大小。主导算法:GPT-2/3/4、Llama、Gemma、Qwen2、Mistral。
  • 字节级 BPE:同样算法,但在原始字节(256 个基础 token)上跑,而非 Unicode 字符。保证零 [UNK]——任何字节序列都能编码。GPT-2 用 50,257 个 token(256 字节 + 50,000 次合并 + 1 个特殊)。
  • Unigram:从巨大词表开始,给每个 token 一个 unigram 概率,迭代地剪掉「移除后对语料对数似然增加最小」的 token。推理时是概率性的:可对分词采样(对子词正则化的数据增强有用)。用于 T5、mBART、ALBERT、XLNet、Gemma。
  • WordPiece:合并「最大化训练语料似然」的对,而非原始频率。用于 BERT、DistilBERT、ELECTRA。

💡 SentencePiece vs tiktoken:SentencePiece 是训练词表(BPE 或 Unigram)的库,直接在原始 Unicode 文本上训,把空白编码为 ;tiktoken 是 OpenAI 的只编码的快库,针对预构建词表,不训练。要训新词表用 SentencePiece 或 HF Tokenizers;要对 GPT 词表快速推理用 tiktoken(cl100k_baseo200k_base);要训加服务一体的用 HF Tokenizers。

二、从零实现

第 1 步:从零训 BPE

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」(前缀)保持区分;频率加权让高频对早赢;合并列表有序——推理时按训练顺序应用合并。

第 2 步:用学到的合并编码

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)用合并优先级的优先队列查找,近线性时间。

第 3 步:实战 SentencePiece

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>

第 4 步:为 OpenAI 兼容词表用 tiktoken

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+。

2026 仍在生产的陷阱

  • 分词漂移:在词表 A 上训,部署到词表 B。token ID 不同,模型输出垃圾。CI 里查 tokenizer.json 哈希。
  • 空白歧义:BPE 的「hello」与「 hello」产出不同 token。总显式指定 add_special_tokensadd_prefix_space
  • 多语欠训:英语主导的语料产出把非拉丁文字拆成 510 倍 token 的词表。同一句提示在日语/阿拉伯语上比 GPT-3.5 贵 510 倍。o200k_base 部分修好了。
  • emoji 分裂:一个 emoji 可能占 5 个 token。预算上下文时点检 emoji 处理。

四、可复用产物

保存为 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.

五、练习

  1. 基础:在 code/main.py 的小语料上训一个 500 次合并的 BPE,编码三个留出词。多少恰好产出 1 个 token,多少产出 >1 个?
  2. 进阶:在 100 句英语维基上,对比 cl100k_baseo200k_base、与你训的词表 32k 的 SentencePiece BPE 的 token 数,报告各自的压缩比。
  3. 挑战:在同一语料上分别用 BPE、Unigram、WordPiece 训,测量各自用在一个小情感分类器上的下游准确率。这个选择是否把 F1 拨动超过 1 个点?

本节要点回顾

  1. 子词取折中:常用词单 token,罕见词拆成有意义的碎片,任意字节序列可编码。
  2. BPE 贪心合并最高频对:</w> 标词尾,合并列表有序,推理按训练顺序应用。
  3. 字节级 BPE 保证零 [UNK]:在 256 字节上跑,GPT-2/Llama 用此,50,257 token。
  4. Unigram 是概率剪枝:从大词表剪掉移除后似然增加最小的 token,推理可采样,用于 T5/Gemma。
  5. WordPiece 最大化似然而非原始频率,用于 BERT/DistilBERT/ELECTRA。
  6. SentencePiece 训词表,把空白编码为 ,无需预分词,多语首选。
  7. tiktoken 只编码不训练:Rust 后端、快、与 GPT-4/5 精确匹配,用于计费与上下文预算。
  8. 词表大小是扩展决策:<1B 用 32k,110B 用 50100k,多语/前沿 200k+。
  9. CI 里查 tokenizer.json 哈希:分词漂移会让模型静悄悄输出垃圾。
  10. 多语欠训 + emoji 分裂:同一提示在日语/阿拉伯语上可能贵 5~10 倍,emoji 可占 5 token。

下一节,我们转向「让模型说我们要的话」——进入「结构化输出与受约束解码」,看如何用 JSON 模式、语法约束与 logits 屏蔽,把自由生成的 LLM 锁进我们想要的格式。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U