【大模型】- 从零构建分词器
从零构建分词器
第 01 课给你的是玩具。这课给你的是武器。
类型: 学习 | 语言: Python | 🏷 前置:《分词器:BPE、WordPiece、SentencePiece》(本系列第 1 篇)
学习目标
- 构建处理 Unicode、空格规范化和特殊 token 的生产级 BPE 分词器
- 实现字节级回退,使分词器可以编码任何输入(包括 emoji、CJK 和代码)而没有未知 token
- 添加预分词正则表达式模式,在应用 BPE 合并之前按词边界分割文本
- 在语料库上训练自定义分词器并评估其在多语言文本上与 tiktoken 的压缩比
问题
你从第 01 课得到的 BPE 分词器适用于英语文本。现在给它扔日语。或 emoji。或带有混合制表符和空格的 Python 代码。
它坏了。
不是因为 BPE 错了——是因为实现不完整。生产分词器处理任何编码的原始字节、在分割前规范化 Unicode、管理永不合并的特殊 token、将预分词与子词分割链接,并且所有这些都足够快,不会成为处理 15 万亿 token 的训练管道的瓶颈。
GPT-2 的分词器有 50,257 token。Llama 3 有 128,256。GPT-4 大约 100,000。这些不是玩具数字。这些词汇表背后的合并表在数百 GB 文本上训练,而周围的机制——规范化、预分词、特殊 token 注入、聊天模板格式化——是将处理 “hello world” 的分词器与处理整个互联网的分词器区分开来的东西。
你将构建那个机制。
概念
完整管道
生产分词器不是一个算法。它是五个阶段的管道,每个阶段解决不同的问题。
每个阶段有特定的工作:
| 阶段 | 做什么 | 为什么重要 |
|---|---|---|
| 规范化 | NFKC Unicode、可选小写、可选去除重音 | “fi” 连字变成 “fi”(两个字符)。没有这个,同一个词会获得不同的 token。 |
| 预分词 | 在 BPE 之前将文本分成块 | 防止 BPE 跨词边界合并。”the cat” 永远不应产生 token “e c”。 |
| BPE 合并 | 将学习的合并规则应用于字节序列 | 核心压缩。将原始字节变成子词 token。 |
| 特殊 token | 注入 BOS、EOS、PAD、聊天模板 token | 这些 token 有固定 ID。它们永不参与 BPE 合并。 |
| ID 映射 | 将 token 字符串转换为整数 ID | 模型看到整数,不是字符串。 |
字节级 BPE
字节级 BPE 通过将每个可能的字节值(0-255)视为有效 token 来解决问题。你的基础词汇表恰好有 256 个条目。任何文件都可以分词而不会产生未知 token。
GPT-2 添加了一个技巧:将每个字节映射到可打印的 Unicode 字符,使词汇表保持人类可读。
真正的威力:字节级 BPE 处理地球上的每种语言。中文字符每个 3 个 UTF-8 字节。日文可以是 3-4 个字节。阿拉伯文、天城文、emoji——全部只是字节序列。
预分词
在 BPE 接触你的文本之前,你需要将其分成块。这防止合并算法创建跨词边界的 token。
GPT-2 使用正则表达式模式分割文本,在缩写、带可选前导空格的词、数字、标点和空格处分割。
选择很重要。GPT-2 的正则表达式防止分词器学习跨词边界的合并。SentencePiece 允许这样做,有时产生更高效的压缩但更难解释的 token。
特殊 token
每个生产分词器为结构 token保留 token ID。特殊 token 永远不会被 BPE 分割。它们在合并算法运行之前被精确匹配,替换为固定 ID。
聊天模板
这是大多数人困惑和大多数实现崩溃的地方。模型看不到 JSON。它看到一个扁平的 token 序列。聊天模板使用特殊 token 将消息转换为该扁平序列。每个模型都这样做不同。搞错模板,模型就会产生垃圾输出。
速度
Python 对生产分词来说太慢了。tiktoken 用 Rust 编写,HuggingFace tokenizers 也是 Rust。SentencePiece 是 C++。这些比纯 Python 快 10-100 倍。
构建
步骤 1:字节级编码
基础。将任何字符串转换为字节序列,将每个字节映射到可打印字符用于显示,并反转该过程。
“hello” 是 5 个字节。”你好” 是 6 个字节(每个字符 3 个)。fire emoji 是 4 个字节。字节级分词器不在乎它是什么语言。字节就是字节。
步骤 2:带正则表达式的预分词器
使用 GPT-2 正则表达式模式将文本分成块。每个块由 BPE 独立分词。
regex 模块支持 Unicode 属性转义。标准库 re 模块不支持,所以我们回退到 ASCII 字符类。对于生产多语言分词器,安装 regex。
前导空格保持附着在词上。缩写在撇号处分割。标点成为自己的块。BPE 永远不会跨这些边界合并 token。
步骤 3:字节序列上的 BPE
第 01 课的核心算法,但现在独立地在预分词块上操作。
步骤 4:特殊 token 处理
特殊 token 需要精确匹配和固定 ID。它们完全绕过 BPE。
步骤 5:完整分词器类
链接所有内容:规范化、按特殊 token 分割、预分词、BPE 合并、映射到 ID。
步骤 6:多语言测试
真正的测试。扔英语、中文、emoji 和代码给它。
中文字符每个产生 3 个字节。emoji 产生 4 个字节。这些都不会使分词器崩溃。都不会产生未知 token。这就是字节级 BPE 的威力。
使用
比较真实分词器
加载 Llama 3、GPT-4 和 Mistral 的实际分词器。看看每个如何处理相同的多语言段落。
你会看到相同文本的不同 token 数。128K 词汇表的 Llama 3 在合并常见模式上更积极。100K 的 GPT-4 居中。32K 的 Mistral 产生更多 token 但有更小的embedding 层。
权衡始终相同:更大的词汇表意味着更短的序列但更多参数。
交付
本课产出一个构建和调试生产分词器的 prompt。见 outputs/prompt-tokenizer-builder.md。
练习
- 添加一个
get_token_bytes(id)方法,显示任何 token ID 的原始字节。用它检查你最常见的合并 token 实际代表什么。 - 实现 Llama 风格的预分词器,在空格和数字处分割但保留前导空格。在相同语料库上将其词汇表与 GPT-2 正则表达式方法比较。
- 添加一个聊天模板方法,接受消息列表并为 Llama 3 聊天格式生成正确的 token 序列。对照 HuggingFace 实现测试。
关键术语
| 术语 | 人们怎么说 | 实际含义 |
|---|---|---|
| 字节级 BPE | 在字节上工作的分词器 | 基础词汇表为 256 个字节值的 BPE——处理任何输入而没有未知 token |
| 预分词 | BPE 之前的分割 | 正则表达式或基于规则的分割,防止 BPE 跨词边界合并 |
| NFKC 规范化 | Unicode 清理 | 规范分解后跟兼容性组合 |
| 聊天模板 | 消息如何变成 token | 将角色/内容消息列表转换为扁平 token 序列的精确格式 |
| 特殊 token | 控制 token | 绕过 BPE 的保留 token ID |
| Fertility | 每个词的 token 数 | 输出 token 与输入词的比率 |
| tiktoken | OpenAI 分词器 | 带 Python 绑定的 Rust BPE 实现 |
| 合并表 | 词汇表 | 训练期间学习的字节对合并的有序列表 |
延伸阅读
- OpenAI tiktoken source – GPT-3.5/4 使用的 Rust BPE 实现
- HuggingFace tokenizers – 支持 BPE、WordPiece、Unigram 的 Rust 分词器库
- Llama 3 paper (Meta, 2024) – 128K 词汇表和分词器训练的细节
- SentencePiece (Kudo & Richardson, 2018) – 与语言无关的分词
- GPT-2 tokenizer source – 原始字节到 Unicode 映射
📚 本文改编自 AI Engineering from Scratch(MIT License · 作者 Rohit Ghumare),中文内容来自官方中文镜像。原课程共 503 课 · 20 阶段 · 免费开源,教程网站见 aiengineeringfromscratch.com。
📝 自我检查(课程配套测验)
Q1(学前) 为什么基础 BPE 分词器在多语言或代码输入上会失效?
A. BPE 本质上是单语的
B. 没有正确的 Unicode 处理、字节回退和预分词正则,会产生不正确或低效的 token 序列
C. 多语言文本无法分词
D. BPE 只适用于 ASCII
答案: B 解析: 朴素的 BPE 实现可能无法正确处理多字节 Unicode 字符,可能在词边界处错误合并,且可能对训练词表外的字符缺少字节级回退。
Q2(学前) 生产级分词器中预分词正则的作用是什么?
A. 移除标点
B. 在 BPE 合并前于词边界切分文本,防止跨空格和词边界合并
C. 压缩空白
D. 将文本转为小写
答案: B 解析: 预分词正则将文本切成块(通常在词边界、数字和标点处),使 BPE 合并只在块内发生。否则 BPE 可能把 ‘end’ 与下一词前的空格合并。
Q3(学后) 什么是特殊 token,为什么分词器要专门处理它们?
A. 出现频率高的 token
B. 如 <|endoftext|> 或
C. 嵌入值最高的 token
D. 仅评估时使用的 token
答案: B 解析: 特殊 token 有结构用途:标识文档边界、填充序列、指示生成起止。必须识别并编码为精确 ID,不能拆成子词。
Q4(学后) 如何评估自定义分词器是否良好?
A. 检查能否分词你的名字
B. 在多样化文本上测量压缩比(每字符 token 数),并与 tiktoken 等成熟分词器比较
C. 统计词表大小
D. 仅测量编码速度
答案: B 解析: 压缩比(每 token 字节数或每词 token 数)衡量效率。好的分词器对相同文本产生更少 token,意味着更多内容能放入上下文窗口。跨语言和领域比较。
Q5(学后) 为什么现代 LLM 偏好字节级 BPE 而非词级分词?
A. 更快
B. 能表示任何输入而无未知 token,同时为常见模式学习高效子词合并
C. 产生更小词表
D. 词级分词更准确
答案: B 解析: 词级分词器无法处理未见词(产生
- 标题: 【大模型】- 从零构建分词器
- 作者: 宋
- 创建于 : 2026-08-19 09:02:00
- 更新于 : 2026-08-21 16:20:11
- 链接: https://sxl-space.tk/2026/08/19/010_LLM/010_LLM-02-BuildingATokenizer/
- 版权声明: 版权所有 © 宋,禁止转载。