主题
字号
CHAPTER 07 ≈ 60 MIN READ

子词分词——BPE、WordPiece、Unigram、SentencePiece

词级分词遇到生词就抓瞎,字符级分词又把序列长度炸上天。子词分词取了个折中——2026 年每个主流大模型都用它。

为什么要学这个

你的词表有 50,000 个词。用户输入了"untokenizable"。分词器直接返回 [UNK]。模型对这个词完全没有信号。更惨的是:语料库中 90% 分位的文档包含 40 个罕见词,意味着每篇文档平均丢掉 40 bit 的信息。

子词分词(Subword Tokenization)解决了这个问题。常见词保持一个 token 不动,罕见词拆成有意义的片段:untokenizableun, token, izable。训练数据能覆盖一切,因为任何字符串归根到底都是一串字节。

2026 年每个前沿大模型都用三种算法之一(BPE、Unigram、WordPiece),套在三个库之一(tiktoken、SentencePiece、HF Tokenizers)上跑。你没法在不选定分词器的情况下发布语言模型。

核心概念

BPE vs Unigram vs WordPiece,逐字符视角

BPE(Byte-Pair Encoding,字节对编码)。 从字符级词表开始。统计所有相邻字符对的频率。把最高频的那对合并成一个新 token。重复,直到达到目标词表大小。主流算法:GPT-2/3/4、Llama、Gemma、Qwen2、Mistral。

字节级 BPE(Byte-level BPE)。 同样的算法,但操作对象是原始字节(256 个基础 token)而不是 Unicode 字符。保证零 [UNK]——任何字节序列都能编码。GPT-2 使用 50,257 个 token(256 字节 + 50,000 次合并 + 1 个特殊 token)。

Unigram(一元语言模型分词)。 从一个巨大词表开始。为每个 token 分配一元概率。反复裁剪那些"移除后对语料对数似然损失最小"的 token。推理时是概率性的:可以采样不同的分词方式(对子词正则化做数据增强很有用)。T5、mBART、ALBERT、XLNet、Gemma 使用此方法。

WordPiece。 合并时选择能最大化训练语料似然的字符对,而不是单纯选频率最高的。BERT、DistilBERT、ELECTRA 使用此方法。

SentencePiece vs tiktoken。 SentencePiece 是一个训练词表的库(BPE 或 Unigram),直接在原始 Unicode 文本上操作,将空格编码为 。tiktoken 是 OpenAI 的快速编码器,使用预构建词表;它不负责训练。

经验法则:

从零实现

第一步:从零写 BPE

参见 code/main.py。核心循环:

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

第三步: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 控制稀有字符是保留还是映射为 <unk>

第四步:tiktoken 用于 OpenAI 兼容词表

import tiktoken
enc = tiktoken.get_encoding("o200k_base")
print(enc.encode("untokenizable"))        # [127340, 101028]
print(len(enc.encode("Hello, world!")))   # 4

只做编码。极快(Rust 后端)。与 GPT-4/5 分词完全一致,适合字节计数、成本估算、上下文窗口预算。

2026 年还在踩的坑

实战用法

2026 年的技术栈:

场景 选择
从零训练单语模型 HF Tokenizers(BPE)
训练多语言模型 SentencePiece(Unigram,character_coverage=0.9995
服务 OpenAI 兼容 API tiktoken(GPT-4+ 用 o200k_base
领域专用词表(代码、数学、蛋白质) 在领域语料上训练自定义 BPE,合并到基础词表
边缘推理、小模型 Unigram(小词表效果更好)

词表大小是一个随规模变化的决策,不是常数。粗略经验:<1B 参数用 32k,1-10B 用 50-100k,多语言/前沿模型用 200k+。

产出物

保存为 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 个 token?
  2. 中等。 在 100 句英文维基百科句子上,比较 cl100k_baseo200k_base 和你自己训练的 vocab=32k SentencePiece BPE 的 token 数。报告每种方案的压缩率。
  3. 困难。 用同一语料分别训练 BPE、Unigram 和 WordPiece。在一个小型情感分类器上测量各自的下游准确率。分词方式能拉开超过 1 个 F1 点的差距吗?

术语表

术语 口语说法 实际含义
BPE 字节对编码(Byte-Pair Encoding) 贪心合并最高频字符对,直到达到目标词表大小
字节级 BPE(Byte-level BPE) 永远没有未知 token 在原始 256 字节上做 BPE;GPT-2 / Llama 采用
Unigram 概率分词器 从大候选集出发,用对数似然裁剪;T5、Gemma 使用
SentencePiece 那个处理空格的库 在原始文本上训练 BPE/Unigram 的库;空格编码为
tiktoken 那个贼快的 OpenAI 的 Rust 后端 BPE 编码器,用于预构建词表。不训练
合并列表(Merge list) 那串神奇数字 有序的 (a, b) → ab 合并规则;推理时按顺序应用
字符覆盖率(Character coverage) 多罕见算太罕见? 分词器必须覆盖的训练语料字符比例;通常 ~0.9995

延伸阅读


自测题

Q1子词分词相比词级词表,核心优势是什么?
Q2GPT-2 为什么用字节级 BPE 而不是字符级 BPE?
Q3Unigram 分词器是怎么构建词表的?
Q4WordPiece 的合并标准和 BPE 有什么区别?
Q5哪个工具能直接在原始多语言 Unicode 文本上训练分词器?
Q6为什么生产环境 CI 必须对部署的 tokenizer.json 做哈希校验?
Q7单个 emoji 为什么经常占好多 token?
Q8新训单语 Transformer 的词表大小有什么经验法则?