人人都会AI编程

7.4 训练数据 Token 化:Tokenizer 的训练方法与词表构建

更新时间:2026-07-09

在 1.1 节中我们提到,LLM 接收的并非原始字符串,而是被切分后的 Token。如果把预训练比作给模型“喂饭”,那么 Tokenizer 就是厨房里的切菜工——它决定了食材以什么粒度、什么编码进入神经网络。词表(Vocabulary)则是这份工作的最终产出:一份从 Token 到整数的映射字典。

Tokenizer 的训练是一次性的,但它的设计会锁定整个模型周期的效率边界:上下文窗口实际能装多少汉字、嵌入层吃掉多少显存、多语言混合训练是否公平,乃至推理时的首 Token 延迟,都与之相关。本节从工程视角,把 Tokenizer 的训练流程、算法选型与词表构建的关键权衡讲清楚。


一、Token 化的本质:从字符到整数的契约

神经网络只能对张量做矩阵运算,不能直接消化“你好 world”。Tokenization 的本质是将任意文本无损(或有损)地映射为整数序列,并保证这个过程可逆——模型输出的整数序列也能被还原为文本。

按粒度划分,历史上出现过三种路线:

| 粒度 | 做法 | 优点 | 缺点 |
|------|------|------|------|
| 字符级 | 每个字母/汉字一个 ID | 词表极小(<1k),无 OOV(未登录词) | 序列极长,丢失语义单元,计算浪费 |
| 词级 | 按空格/词典切词,每个词一个 ID | 语义完整 | 词表爆炸(英文百万级),OOV 严重,形态变化无法泛化 |
| 子词级(Subword) | 介于字符与词之间:popular → pop + ular | 词表可控,能覆盖新词,泛化能力强 | 实现复杂,对缩略语/专有名词可能过度拆分 |

现代大模型全部使用子词级 Tokenization。它用有限的词表(通常 3 万–25 万)表达无限的语言组合,是压缩率与泛化性的最佳平衡点。


二、主流算法对比:BPE、WordPiece、SentencePiece、Unigram

目前工业界主流的词表训练算法有四种,它们在合并策略对空白字符的处理上存在差异:

| 算法 | 核心机制 | 代表模型 | 关键特点 |
|------|----------|----------|----------|
| BPE (Byte-Pair Encoding) | 贪心合并最高频的相邻子词对 | GPT 系列、Llama、Qwen | 简单高效,适合代码和英文 |
| WordPiece | 合并能最大程度提升训练数据似然的子词对 | BERT、DistilBERT | 与 BPE 类似,但选择标准基于概率而非频率 |
| SentencePiece | 将文本视为原始字符流(空格编码为 ▁),不依赖预分词 | T5、Llama 2/3、Baichuan | 对多语言和中文极友好,端到端无预分词偏差 |
| Unigram | 从一个超大种子词表出发,迭代剪枝低贡献 Token | XLNet、Albert | 更适合多语言,但训练速度较慢 |

工程选型建议

  • 如果你从零训练一个以中文、日文、代码为主的基座模型,SentencePieceBPE + Byte-level fallback 是最稳妥的选择。它能避免“英文按空格切、中文按字切”带来的分布偏差。
  • 如果你主要做英文或代码生成,BPE(如 tiktoken 的实现)已经足够高效。
  • 不要混用算法:选定一种后,从词表训练到模型预训练必须严格保持一致,中途更换词表意味着嵌入层矩阵维度全变,历史权重无法复用。

三、词表构建的工程流程(以 BPE/SentencePiece 为例)

Tokenizer 的训练不是“跑个脚本就完事”,它需要与预训练数据的分布高度对齐。标准流程如下:

步骤 1:准备代表性语料子集

从最终预训练语料中采样(通常 100MB–10GB 量级),分布必须与预训练一致。如果你的预训练数据是 60% 中文 + 30% 代码 + 10% 英文,那么 Tokenizer 训练语料也应保持这个比例。否则,用纯英文语料训出来的 Tokenizer 切中文时,会把每个汉字拆成 2–3 个 byte,导致序列长度膨胀 30%–50%。

步骤 2:确定预分词与正则化策略(Pre-tokenization & Normalization)

这是最容易引发“训练-推理不一致”的暗坑:

  • Unicode 规范化:是否将全角字符转为半角?是否使用 NFC/NFD 合并?建议在训练前统一规范化,并在推理时严格复现同一规则。
  • 数字处理:是否将连续数字拆分为单个数字(如 12345 → 1 2 3 4 5)?这对数学推理任务影响显著。
  • 空格处理:SentencePiece 将空格编码为 (U+2581),使得缩进敏感的 Python 代码能被精确还原;标准 BPE 则可能在预分词阶段就按空格切开,丢失前导空格信息。

步骤 3:初始化基础词表

  • 基于 Byte(字节级):初始词表为 256 个字节(覆盖所有 UTF-8 编码),理论上零 OOV。GPT-2、Llama 系列采用此路线。
  • 基于字符:根据语料中出现的 Unicode 字符初始化,词表更小,但遇到罕见字符需要回退到 <unk> 或 byte fallback。

步骤 4:迭代合并或剪枝

以 BPE 为例:

  1. 将语料初始化为字符/字节序列;
  2. 统计所有相邻 Token 对的出现频率;
  3. 合并频率最高的那一对,加入词表;
  4. 重复 2–3 步,直到词表达到预设大小(如 32,000、50,000、100,000)。

步骤 5:定义特殊 Token(Special Tokens)

在词表中预留固定 ID 给以下标记:

  • <s> / </s>(BOS / EOS):序列起止;
  • <pad>:填充对齐;
  • <unk>:未登录词(现代 byte-level 模型已极少触发);
  • 预留位:如用于 <mask>、多模态占位符(<image>)、工具调用标记(如 Llama 3 中的 <|eot_id|><|tool_call_begin|> 等)。

关键工程纪律:特殊 Token 的 ID 必须在模型架构代码中硬编码或显式传入,不要在训练中途追加,否则会导致旧 checkpoint 的嵌入矩阵错位。

步骤 6:输出与封装

最终产出通常包括:

  • vocab.json:Token → ID 的映射表;
  • merges.txt(BPE):合并规则的有序列表;
  • tokenizer.model(SentencePiece):二进制模型文件;
  • tokenizer_config.json:特殊 Token 定义、Pre-tokenizer 正则规则等。

四、词表大小的权衡:嵌入层的隐形成本

词表大小不是越大越好。每增加一个 Token,都会带来全模型生命周期的开销

显存与参数量

Embedding 层参数量 = vocab_size × hidden_size
输出头(LM Head)参数量 ≈ vocab_size × hidden_size  (通常与输入嵌入共享或独立)

以 Llama 2 70B 为例,hidden_size=8192,vocab_size=32k,仅输入嵌入就占约 256M 参数;若将词表盲目扩到 256k(如部分多语言模型),仅嵌入层就会膨胀到 2B 参数。对于小模型(如 1B–3B),过大的词表会严重挤压 Transformer 主体的表达能力。

常见词表规模参考

| 模型/系列 | 词表大小 | 备注 |
|----------|----------|------|
| GPT-3 | 50,257 | 经典 BPE,英文为主 |
| Llama 2 | 32,000 | SentencePiece,多语言但较克制 |
| Llama 3 | 128,256 | 显著扩容以覆盖更多语言 |
| Qwen2 | 151,643 | 中-英-代码均衡设计 |
| DeepSeek V2 | 102,400 | 兼顾多语言与代码 |
| Gemini 1.5 | ~256,000 | 大词表 + 多模态标记 |

压缩率指标(Chars per Token)

  • 优质英文文本:~4 个字符 / Token;
  • 中文(若词表充分优化):~1.5–1.8 个字符 / Token;
  • 中文(若词表对中文支持差,退化为 byte-level):~0.5–0.8 个字符 / Token。

这意味着,在 4K 上下文窗口中,前者能塞进约 7000 个汉字,后者只能塞进约 2000 个。Tokenizer 的质量直接决定了你的长上下文产品能处理多少字


五、多语言与代码场景的实战要点

1. 中文不是“二等公民”

很多早期开源 Tokenizer 用英文语料为主训练,导致中文被过度拆分。例如,“人工智能”可能被切成 ["人", "工", "智", "能"] 甚至更碎的 byte 组合,而英文 "artificial intelligence" 可能仅占 2 个 Token。这种语言间的不公平,会让中文样本的序列长度翻倍,变相降低中文在预训练中的有效学习权重。

对策:在 SentencePiece 训练中显式提升中文语料比例,或使用 byte_fallback=False 配合充足的中文种子字符。

2. 代码中的空格与缩进

Python 的缩进是语法的一部分。标准按空格预分词的 BPE 会把前导空格当成分隔符丢弃,导致 print(x) print(x)(带缩进)在 Token 层面无法区分。SentencePiece 的 编码或专用代码 Tokenizer(如 CodeLlama 的定制版)会显式保留空格信息。

3. 数字与数学公式

如果词表把 36000 作为一个独立 Token,模型可能学会它的数值概念;如果被拆成 ["3", "6", "0", "0", "0"],模型需要更多训练步数才能建立数字位值的抽象。部分数学专用模型(如 LLEMMA)会针对数字做预分词优化。


六、工程工具链与避坑指南

推荐工具

  • Hugging Face tokenizers:Rust 实现,训练速度快,支持 BPE、WordPiece、Unigram,推荐作为首选。
  • Google sentencepiece:C++ 实现,对多语言和原始文本处理最稳定,Llama 系列的标准选择。
  • OpenAI tiktoken:面向推理的快速编码库,但不支持训练新词表。

四个必须避开的坑

  1. 训练-推理 Tokenizer 不一致:前后端使用不同版本的 Normalization 规则,导致同一字符串在训练时 ID 为 100,推理时 ID 为 101。这种 bug 极难排查。
  2. 词表泄露(Vocabulary Leakage):词表中出现了带有具体信息的高频字符串(如训练语料中反复出现的某个 URL 或人名),模型可能通过单 Token 直接“背诵”该信息,影响泛化。
  3. 特殊 Token 与文本 Token 冲突:如果 <|endoftext|> 的某部分恰好也出现在普通文本的 BPE 合并结果中,可能导致解码异常。应确保特殊 Token 在 BPE 中不会被拆分。
  4. 忽视序列长度分布:训练好 Tokenizer 后,务必用大规模语料跑一遍长度统计。如果发现 90% 的中文文档被切成了超过 8K 的序列,而你的计划上下文只有 4K,那说明词表设计或预分词策略需要回炉。

七、小结

Tokenizer 是大模型与真实世界之间的第一条契约。它决定了文本如何被压缩、不同语言是否被公平对待、以及模型在推理时每一层计算的序列长度。

关键 takeaway:

  • 词表是训练出来的,不是拍脑袋定的:必须与预训练语料同分布训练,且算法选型(BPE vs SentencePiece)需匹配语言特性;
  • 词表大小是架构决策:它直接线性影响嵌入层参数量和显存占用,小模型尤其要谨慎;
  • 特殊 Token 的设计要前置:BOS/EOS/PAD 以及未来的工具调用标记,都应在预训练开始前固化;
  • 一致性大于一切:Tokenizer 的任何改动(哪怕只是一个 Unicode 规范化规则)都会导致模型权重失效。

完成 Token 化后,原始的文本语料被转换为整齐的整数序列,接下来就要进入真正的预训练工程环节——模型初始化、数据加载、分布式并行与训练监控。在 8.1 节“预训练环境准备”中,我们将讨论如何把这套 Token 化后的数据,高效地喂给千卡集群上的 GPU。