本文基于
GPT_teacher-3.37M-cn这个教学项目,把分词器相关的核心概念、原理和常见疑问整理成一篇完整的解释文章。文中所有示例数据均来自本项目真实的tokenizer/tokenizer.json。
目录
- 分词器是什么
- 分词器就是一个 JSON 文件吗
- 核心数据:vocab 与 merges
- 分词是查表吗
- 分词器训练:如何从语料总结规则
- 分词器和模型的绑定关系
- 能否直接用通用分词器
- 分词器算不算一个模型
- 数据的数量与质量意味着什么
- 不同分词算法的区别
- 核心结论速览
1. 分词器是什么
分词器(Tokenizer)负责在「文本」和「token id 序列」之间做双向转换:
- 编码(encode):把人类文本切成一个个 token,再映射成整数 id,喂给模型。
- 解码(decode):把模型输出的 id 序列还原回文本。
模型本身只会处理数字(张量),不认识文字。分词器就是模型的「输入输出翻译层」。
2. 分词器就是一个 JSON 文件吗
在本项目里,分词器保存为 tokenizer/tokenizer.json(约 65KB)。但要分清:
- JSON 只是它的保存格式,本质是一个训练出来的 BPE 分词器。
- 它由
src/build_tokenizer.py用 HuggingFacetokenizers库训练得到,模型类型是 BPE,字节级预分词(ByteLevel)。
这个 JSON 的顶层字段:
1 | version, normalizer, pre_tokenizer, post_processor, decoder, model, added_tokens |
其中最重要的是 model 字段,它包含 vocab 和 merges,其余都是「周边配置」。
加载时一行代码即可还原成完整的分词器对象:
1 | from tokenizers import Tokenizer |
3. 核心数据:vocab 与 merges
分词器 JSON 里最重要的就是这两部分。
3.1 model.vocab —— 词表(token → id 映射)
一张字典表,把每个 token 映射到唯一整数 id。本项目实测共 821 条:
1 | '<bos>' -> 0 '<eos>' -> 1 '<pad>' -> 2 '<unk>' -> 3 # 特殊符号排最前 |
- 作用:编码的最后一步查这张表把 token 变 id;解码时反查。
- 注意:本项目用 ByteLevel(字节级)BPE,中文先按 UTF-8 拆成字节,再把字节映射成可打印字符。所以词表里看到的是
ï ¼ æ ĺ这类「字节片段」,而不是直接的汉字,但组合起来能还原任何中文。
3.2 model.merges —— BPE 合并规则(有序)
BPE 的灵魂,一份有序的「合并配方」,本项目实测共 715 条:
1 | ['ï', '¼'] # 第 1 条:优先把 ï 和 ¼ 合并 |
关键点:
- 顺序 = 优先级。排在前面的合并先执行,这是训练时按共现频率从高到低学出来的。
- 合并会滚雪球:先合成的子词能继续和别的合并成更大的 token。
3.3 为什么它俩最重要
- 换了它俩 = 换了整个词表和切分方式,
vocab_size一变,模型维度就得跟着变、必须重训。 - 其他字段(
normalizer / pre_tokenizer / post_processor / decoder)只是周边配置,重要性远不如这两个。
一句话:vocab 是「有哪些 token、各自编号多少」,merges 是「怎么把小片段一步步拼成这些 token 的规则和顺序」。
4. 分词是查表吗
不是单纯查表,而是「先按 merges 规则做合并计算,最后才查 vocab 表」。 查表只是最后一步。
完整编码流程(以「注意力」为例,实测切成 3 个 token):
- 拆字节:「注意力」→ UTF-8 字节 → ByteLevel 映射成基础字符(初始每个字节是独立 token)。
- 按 merges 反复合并(重点,不是查表):按优先级顺序把相邻能合的对不断合并。
- 查 vocab 表:最终每个 token 换成 id。只有这一步是纯查表。
本项目真实编码结果:
1 | '注意力' -> tokens: ['注æĦı', 'åĬ', 'Ľ'] ids: [203, 110, 99] |
从这三个例子能看出:
- 「注意」的字节被合并成了一个 token(语料里高频共现)。
- 「世界」却被拆成好几个 token(语料里没学到合并规则)——同样是中文,切法完全不同,这是「算」出来的,不是查固定表。
- 末尾
XYZ直接变成<unk>——小词表里没有大写 X/Y/Z,查不到就兜底成<unk>。
准确说法:分词 = 规则驱动的贪心合并(merges 的有序优先级)+ 末端查表(vocab 取 id、查不到给 <unk>)。更像「拼积木」而不是「查字典」。
5. 分词器训练:如何从语料总结规则
BPE 靠「数频率、贪心合并」总结规则——谁最常挨在一起,就先把谁合并成一个 token。是否成词,完全由「在语料里共现的频率」决定,而不是靠语义。
本项目真实语料统计:
| 词 | 语料中出现次数 | 结果 |
|---|---|---|
| 注意 | 80 次 | 被合并成一个 token |
| 世界 | 0 次 | 从未共现,拆成多个字节 token |
BPE 训练算法(不断重复这几步):
- 初始化:把文本拆到最小单位(本项目是字节),每个字节是一个独立 token。
- 统计相邻对频率:扫全部语料,数每一对相邻 token 出现多少次。
- 合并最高频的那一对:合并成新 token,记入
merges(所以 merges 按频率排序)。 - 重复 2-3:合并后重新统计(「注意」已是整体,可继续和「力」组队),直到达到设定 vocab_size 或无可合并对。
由此能理解几个现象:
- 换语料,同一个词的切法就变。
- 高频词往往是 1 个 token;生僻词被切成好几块。
- 英文里
the / ing / tion常是独立 token,因为它们超高频。
一句话:BPE 是纯粹的统计压缩——频率是它唯一的「老师」,它不理解「世界」是个词,只是发现(或没发现)这两个字节老爱粘在一起。
6. 分词器和模型的绑定关系
模型绑定分词器(一对一、锁死),但一个分词器可以服务多个模型(一对多、可复用)。
模型 → 分词器:强依赖、单向绑定。
模型的嵌入层和输出头都按vocab_size构建:1
2self.tok_emb = nn.Embedding(vocab_size, n_embd) # src/model.py
self.head = nn.Linear(n_embd, vocab_size, bias=False)而
vocab_size直接取自tok.vocab_size。训练好后就锁定——换分词器会导致维度对不上,best.pt直接报错或输出乱码,必须重新训练。分词器 → 模型:独立、可复用。
分词器本身不含任何模型信息,只是文本 ↔ token 的映射规则,谁都能拿来用。同一个分词器可服务于基座模型及其各种微调版本、不同规模的模型,甚至跨项目复用。
类比:分词器像「字典/编码标准」,是相对通用的基础设施;模型是「基于这套编码学出来的大脑」,一旦学成就跟这套编码绑定。字典能给很多人用,但每个大脑只认自己学习时用的那本字典。
分词器语料 ≠ 模型语料
正常情况下,分词器的训练数据不必和模型的训练数据完全一致,两者是解耦的:
- 分词器只学「字符/子词的组合统计规律」,不关心知识内容。
- 大模型实践中,分词器一般在一份「大而杂」的通用语料上先单独训一次定下词表,之后的预训练、微调可能用不同数据,但都复用这同一个分词器。
- 唯一硬约束:分词器一旦定下并开始训模型,两者就锁定,不能中途更换。
- 现实建议:分词器语料应「覆盖」模型数据的语言/领域分布,否则会大量
<unk>、切分过碎。即「不必一致,但要覆盖得上」。
7. 能否直接用通用分词器
可以,而且是正规做法,只要满足两点:
- 词表覆盖你的语言/领域:训中文模型就用中文/多语言分词器,别拿纯英文分词器切中文。
- 在训练前定好:因为
vocab_size决定模型维度,必须「先定分词器,再建模型、开训」,不能训到一半再换。
现实中大量项目直接复用开源分词器(如 GPT 系的 tiktoken、Llama/Qwen 的 SentencePiece 词表等),省去自训成本、获得更成熟的词表。
在本项目中换分词器的步骤:
- 把通用中文
tokenizer.json放到tokenizer/下(或改config.yml的tokenizer.path); vocab_size会从 821 变成如 32000,模型维度随之改变;- 必须重新训练(旧
best.pt维度对不上)。
利弊权衡:
- 优点:覆盖广、几乎不出
<unk>、无需自训、更接近真实大模型做法。 - 代价:词表变大(几百 → 几万),会显著增大模型嵌入层和输出头的参数量。对这个仅 3.37M 的教学小模型来说,可能让参数暴涨、小数据更难训好——所以本项目故意训了个 821 的迷你词表来匹配它的迷你规模。
8. 分词器算不算一个模型
它是一个「训练出来的统计模型」,但不是「神经网络模型」。 两者都叫 model,但内涵不同。
为什么算模型:它从数据训练得到、有「参数」(词表 + 合并规则),HuggingFace 里其内部结构就叫 models.BPE。
为什么和 GPT 那种模型不是一回事:
| 维度 | 分词器(BPE) | 神经网络模型(GPT) |
|---|---|---|
| 本质 | 查表 + 贪心合并规则 | 千百万浮点权重的矩阵运算 |
| 学的是 | 字符/子词的共现统计 | 语言的语义与推理 |
| 梯度下降 | 无(纯统计计数) | 有(反向传播) |
| 运行时 | 确定性查表,无浮点计算 | 大量矩阵乘法、概率采样 |
| 是否「理解」 | 不理解含义,只切分 | 建模语义 |
准确说法:广义上它是「统计模型/规则模型」;严格 AI 语境下它是预处理组件,不是神经网络模型。分词器是「学出来的编码规则表」,神经网络才是「会算会推理的大脑」。
9. 数据的数量与质量意味着什么
分词器的质量确实高度依赖语料,但要精确区分:
BPE 分词没有「准确/错误」之分——它只是压缩编码,把「世界」切成 1 个还是 7 个 token 都不算错。更准确的说法是分词的**「质量/效率」**。
语料对分词器的影响:
- 数量不足 → 词表窄、切得碎、大量
<unk>(本项目 600 条语料 → 词表仅 821)。语料越大,常见词越可能合成整块,编码越紧凑。 - 质量/分布影响 → 词表偏向语料领域。语料脏会学到无用的合并规则、浪费词表名额。想要通用分词器就得用又大又杂的语料。
分词器 vs 模型,对数据要求不同:
- 分词器:只需语料「量大、覆盖广」,不在乎内容对不对——哪怕语料是胡说八道,只要字词分布够广也能训出好用的分词器。
- 模型:才真正在乎数据的「知识正确性、答案质量」。
结论:
- 分词器的「覆盖度和编码效率」← 靠语料的数量 + 分布广度;
- 模型回答的「准确性」← 靠语料的质量 + 数量 + 多样性。
- 「变种问题答不对」主要是模型侧(参数小 + 数据少)的问题,分词器基本不背这个锅。
一句话:分词器要的是「见得多、见得杂」;模型要的是「学得对、学得广」。
10. 不同分词算法的区别
分词算法决定「用什么策略把文本切成 token」——切分思路、粒度、对生僻词/多语言的处理、编解码是否可逆。
1. BPE(本项目用的)
- 反复合并最高频的相邻对。简单高效、压缩率好;GPT 系、Llama 用它。
- 叠加 Byte-Level 后永远不会真正
<unk>(任何字符都能用字节表示),代价是中文可能切得较碎。
2. WordPiece(BERT 用)
- 合并时看「能最大提升语言模型似然」的对(概率打分,而非纯计数)。生僻词用
##前缀拼接(如play ##ing)。
3. Unigram / SentencePiece(T5、Llama、多语言常用)
- 反着来:先建大候选词表,再逐步删掉贡献最小的 token。把空格也当普通字符,天然适合中文/日文等无空格语言、多语言友好。
4. 字符级 / 字节级(本项目 fallback ByteTokenizer)
- 不做任何合并,每个字符/字节就是一个 token。词表极小(本项目 259)、绝不
<unk>,但序列超长、效率最低。
算法选择的影响维度:
| 维度 | 影响 |
|---|---|
| 切分粒度 | 同句话切成几个 token → 影响序列长度、算力、上下文容量 |
| 生僻词处理 | 字节级永不失败;WordPiece 靠 ##;纯词级会 <unk> |
| 多语言能力 | SentencePiece/字节级对无空格语言、多语言更友好 |
| 压缩效率 | BPE/Unigram 好;字符级最差 |
| 可逆性 | 字节级/SentencePiece 能无损还原;某些处理会丢空格 |
算法不改变「分词器和模型绑定、要靠语料训练」这些根本规律,它只决定切分的策略和风格。对中文而言,Byte-Level BPE 和 SentencePiece(Unigram) 是最主流的两个选择。
一句话:语料决定分词器「见过什么、切得好不好」;算法决定它「用什么套路切、怎么处理没见过的字、切完能不能还原」。
11. 核心结论速览
- 分词器负责「文本 ↔ token id」的双向转换,是模型的输入输出翻译层。
- 本项目分词器保存为
tokenizer.json,核心资产是vocab(词表)和merges(合并规则)。 - 分词 = 规则驱动的贪心合并 + 末端查表,不是单纯查字典。
- BPE 训练靠「数频率、贪心合并」,频率是唯一的老师,与语义无关。
- 模型绑定分词器(锁死),但分词器可复用于多个模型。
- 分词器语料不必等于模型语料,但要覆盖得上。
- 可以直接用通用分词器,但必须在训练前定好、且换了要重训。
- 分词器是统计模型,不是神经网络模型。
- 分词器吃「数量 + 广度」,模型吃「质量 + 数量 + 多样性」。
- 不同算法(BPE / WordPiece / Unigram / 字节级)决定切分策略、生僻词处理和多语言能力。