跳转至

Tokenizer 设计与训练:BPE、多语言、多模态 Token

更新日期:2026-04-14

本文目标:能设计和训练一个适合目标模型的 tokenizer,理解 vocab size / 多语言 / 多模态 token 的权衡。


一、为什么 Tokenizer 如此重要

影响维度 说明 为什么
训练效率 Token 数 = 实际训练量。同样的文本,好的 tokenizer 用更少 token 表示 → 等效训练更多"内容" 训练 cost 正比于 token 数 × 步数。压缩率高的 tokenizer 让模型在固定 FLOPs 预算内看到更多语义内容,等价于用更少算力获得更好的 loss
推理成本 推理按 token 计费。token 效率高意味着同等语义输出所需的 autoregressive decoding 步骤更少 → 延迟和 API 费用同步下降 KV-cache 占用与序列长度线性相关,更少的 token 直接减少 GPU 显存占用和每次 attention 的计算量
多语言公平性 中文/日文等 1 个字可能需要 2-4 个 token,英文 1 个词通常 1-2 个 → 中文用户付更多钱 BPE 是频率驱动的:英文占训练语料主体时,高频英文 n-gram 优先合并成完整 token,而中日韩字符因频率低被碎片化,导致 fertility 不对等
模型能力 Tokenizer 的粒度影响模型对字符/字节的理解 粒度过粗(整词 token)会让模型无法做字符级推理(如拼写纠错、回文判断);粒度过细(byte-level)会拉长序列使 self-attention 成本暴增。需在语义粒度和序列长度间取平衡
模型大小 vocab_size × d_model = embedding 参数量。vocab 越大,embedding layer 和 output projection 矩阵越大 以 LLaMA-3 为例:128K × 4096 ≈ 5.2 亿参数仅用于 embedding,占 8B 模型总参数的 ~6.5%。vocab 翻倍则该开销翻倍,且 softmax 归一化的计算量也线性增长

二、Tokenizer 算法对比

2.1 BPE 训练过程

def train_bpe(corpus, vocab_size):
    # 初始 vocab: 所有单字符 (或 bytes)
    vocab = set(all_chars_in_corpus)

    # 将文本表示为字符序列
    # "hello" → ['h', 'e', 'l', 'l', 'o']
    sequences = [list(word) for word in corpus]

    while len(vocab) < vocab_size:
        # 统计所有相邻 pair 的频率
        pair_counts = Counter()
        for seq in sequences:
            for i in range(len(seq) - 1):
                pair_counts[(seq[i], seq[i+1])] += 1

        # 找最频繁的 pair
        best_pair = max(pair_counts, key=pair_counts.get)
        # 例如: ('l', 'l') 出现最多

        # 合并: 创建新 token
        new_token = best_pair[0] + best_pair[1]  # 'll'
        vocab.add(new_token)

        # 更新所有序列: 将 pair 替换为新 token
        for seq in sequences:
            merge_pair(seq, best_pair, new_token)
        # ['h', 'e', 'l', 'l', 'o'] → ['h', 'e', 'll', 'o']

    return vocab, merge_rules  # merge_rules 记录合并顺序

2.2 Byte-level BPE

标准 BPE 的问题在于遇到未知字符会产生 OOV (Out-of-Vocabulary)。Byte-level BPE 以 256 个 byte 为基础 vocab,任何 UTF-8 文本都能用 byte 序列表示,因此永远不会 OOV。

示例"你好" → UTF-8 bytes: [0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]。经 BPE 合并后,如果训练语料有足够中文,可能变成 ['你', '好'] 两个 token。 SentencePiece 提供 byte_fallback=True 选项:优先使用学到的 subword,遇到未知字符时回退到 byte 表示。


三、Vocab Size 选择

3.1 Vocab Size 的权衡

3.2 经验法则

Vocab Size 建议: - 纯英文: 32K-64K 够用 - 中英双语: 64K-128K - 全球多语言: 128K-200K - 代码能力需求高: 额外加 token 给代码关键词

判断方法: Token Fertility (每个词需要多少 token) - 英文: 好的 tokenizer fertility ≈ 1.2-1.5 - 中文: 好的 tokenizer fertility ≈ 1.5-2.0 (每个字 1-2 token) - 差的 tokenizer: 中文 fertility > 3 (每个字 3+ token) → 不公平


四、多语言 Token 分配

4.1 核心问题

BPE 训练是频率驱动的:高频对先合并。如果训练语料英文占 80%,那么大部分 vocab 会被分配给英文 subword → 中文等语言 token 效率极差。

4.2 解决方案

# 训练多语言 tokenizer 的最佳实践
# 1. 从各语言采样相同量的文本 (不是按原始比例!)
import sentencepiece as spm

spm.SentencePieceTrainer.train(
    input='balanced_multilingual_corpus.txt',  # 各语言等量采样
    model_prefix='multilingual_tokenizer',
    vocab_size=128000,
    model_type='bpe',
    byte_fallback=True,
    character_coverage=0.9999,  # 高覆盖率
    split_by_unicode_script=True,
    # 关键: input_sentence_size 控制训练用的句子数
    input_sentence_size=10_000_000,
    shuffle_input_sentence=True,
)

# 2. 验证 token fertility
for lang in ['en', 'zh', 'ja', 'ko', 'de', 'fr']:
    text = load_test_text(lang, 1000_sentences)
    tokens = tokenizer.encode(text)
    words = count_words(text, lang)
    fertility = len(tokens) / words
    print(f'{lang}: fertility = {fertility:.2f}')
    # 好的 tokenizer: 各语言 fertility 差距不超过 2x

五、多模态 Token

5.1 图像 Token

5.2 统一 Tokenizer(前沿方向)

传统方案:文本和图像使用不同的 tokenizer——文本走 BPE 得到 token ID,图像走 ViT 得到连续特征向量。

统一方向:所有模态共用一个离散 tokenizer。代表工作是 Meta 的 Chameleon挑战:图像的 codebook 需要足够大才能保留细节。典型配置:8192 个 visual codes + 128K text vocab。


六、Tokenizer 对训练的影响

影响 详细 为什么
训练步数 相同文本,token 数不同 → 实际看到的"内容量"不同 以 "Chinchilla 定律" 为例,最优训练需要 token 数与参数量匹配。token 效率高的 tokenizer 让模型在固定步数内看到更多语义内容,等价于增加训练数据量
数学能力 数字是否逐字符 tokenize 影响数学推理 若 "137" 被分为 ["1", "37"] 或 ["13", "7"],模型难以学习位值对齐和进位规则。逐字符 tokenize("1","3","7")让模型可以学到每一位的独立含义,但序列变长;一些模型特意为数字添加逐字符规则
代码能力 缩进是否保留、关键词是否完整 tokenize 影响代码生成 Python 对缩进敏感:若 4 空格被 tokenize 为不同组合(如 " "+" " vs " "),模型难以学到一致的缩进模式。完整保留缩进 token 和将 def/class 等关键词作为单独 token 可提升代码生成的语法正确率
中文能力 fertility 高 → 中文在训练中的有效权重低 若中文 fertility=3(每字 3 token),则同等长度的中文文本占 3 倍 token 预算。在固定上下文窗口下,模型能看到的中文语义内容仅为英文的 ⅓,导致中文理解和生成能力系统性偏弱
训练速度 vocab 大 → embedding lookup 和 LM head softmax 的计算量随 vocab size 线性增长 LM head 的矩阵乘法为 [batch×seq, d_model] × [d_model, V],V 从 32K→128K 使该层 FLOPs 增加 4 倍。实践中通过 tied embedding(共享 input/output 权重)和 softmax 近似(如 adaptive softmax)缓解

七、追问延伸

问题 方向 为什么
LLaMA-3 为什么从 32K 扩到 128K vocab? 提升多语言效率,尤其代码和非拉丁语言 Meta 的实验数据显示 32K vocab 下中文 fertility ~3.5,扩展到 128K 后降至 ~1.5。同时代码 token(如 functionreturnimport)成为完整 token,代码 benchmark(HumanEval)提升约 5%。额外 embedding 参数开销(约 4 亿)对 70B+ 模型影响可忽略
Tokenizer 训练需要多少数据? 通常 100M-1B 个句子的采样子集就够 BPE 合并只依赖 n-gram 频率统计,100M 句子已足够让高频模式的频率估计收敛(更多数据边际收益递减)。关键不是数据总量而是语言分布的均衡性——10M 句均匀采样的多语言数据好于 1B 句英文数据
训练完后能修改 tokenizer 吗? 可以扩展(加新 token),但不建议修改已有 token 的 mapping 已训练好的模型的 embedding 和 LM head 权重与 token ID 绑定。修改已有 token 的 ID mapping 会使对应权重失效。扩展(append)新 token 是安全的:新 token 的 embedding 可用已有 subword 的均值初始化,再通过少量 continued pretraining 学习
BPE vs Unigram 哪个好? BPE 更主流,Unigram 在多语言上有优势但使用较少 BPE 的确定性分词在工程上更简单(便于缓存、调试、复现),且多数大模型实证表明 BPE 和 Unigram 的下游性能差异小于 0.5%。Unigram 的优势在于 subword regularization 带来的鲁棒性,但实践中 BPE + dropout 也能实现类似效果

参考链接


上级 · B. 数据工程