English interface selected · this article is currently written in Chinese.
文本怎样变成 token:分词、词表与训练成本
English overview →具备清晰骨架、初步数学推导或工程验证,持续补充论据与实测。
数字花园
🌿 什么是数字花园与思想演进?
遵循 Maggie Appleton 与 Andy Matuschak 的常青笔记理念:这里不是按时间沉底的流水账,而是持续修剪打磨的知识生态。文章按成熟度经历:
- 🌱 萌芽 (Seedling): 发散灵感、读书摘录、开放猜想
- 🌿 演进 (Budding): 结构渐明、初步代码推导、积极修正
- 🌳 常青 (Evergreen): 逻辑闭环、持久经得起检验的基石
- 🔬 实验待证 (Speculative): 结合沙盒与仿真探针的理论探索
第一卷结束时,我们已经知道怎样准备一份可追溯的语料。现在拿出其中一句话:
梯度累积不会减少模型参数占用的内存。
人看到的是一句中文,模型接收的却是一串整数。在它们之间,需要一个分词器:把文本拆成有限词表里已有的片段,再把片段换成编号。
token 是模型处理文本的单位,不一定是一个词,也不一定是一个字。 同样一句话,换一个分词器,长度和编号都可能变化。这件事会一路影响上下文长度、数据比例、训练成本和模型兼容性。
本篇先用自己设计的小例子说明分词为什么这样做,再讨论真实项目应该检查什么。例子里的切分不代表某个实际模型的输出。
为什么不直接把每个汉字编号
这样当然能做。假设只处理少量常见中文,每个字一个编号,是容易检查的起点。
问题出在“只处理”这三个字上。技术语料包含英文标识符、数字、空格、代码、表情和生僻字。只收常见汉字会漏内容;为所有常见整词分配编号,又会遇到没见过的新词和拼写变体。
还有长度问题。把 optimizer 拆成九个字母,模型需要九个位置;如果词表里有这个完整片段,就可能只需一个位置。反过来,为每个偶尔出现一次的长字符串都建一个词条,词表会很大,许多条目还缺少足够训练机会。
子词方法采取一个折中:常见片段可以较长,不常见内容拆得更细。最初把 BPE 用于神经机器翻译的研究,正是为了改善罕见词和开放词表的处理。那是特定翻译实验的研究动机与结果,不能直接当作“任意大模型用 BPE 都最好”的证据。Sennrich 等,2016
用三条文本手算一次 BPE
BPE 的直觉是:相邻片段经常一起出现,就尝试给它们一个共同编号。
为了能手算,我们先人为规定:每条文本独立处理,初始单位是汉字,不跨条目合并。语料只有下面三种字符串,次数也是假设的:
| 文本 | 出现次数 | 初始切分 |
|---|---|---|
| 数据库 | 5 | 数 / 据 / 库 |
| 数据集 | 3 | 数 / 据 / 集 |
| 库存 | 2 | 库 / 存 |
先数相邻对。数 + 据 出现八次,据 + 库 五次,据 + 集 三次,库 + 存 两次。所以第一轮合并 数 + 据 → 数据。
合并后,前两条变成 数据 / 库 和 数据 / 集。重新计数,数据 + 库 出现五次,成为下一轮合并对象。若此时停止,词表里有初始的五个字,也有新增的 数据 和 数据库,共七个条目。
原始语料的总长度是:
第一次合并后减少八个位置,剩二十个;第二次再减少五个,剩十五个。这里计算的是这份教学语料的 token 数,没有加入任何特殊标记。
# 教学验证:三条文本的 BPE 相邻片段统计与首轮合并 from collections import Countercorpus = [ (“数 据 库”, 5), (“数 据 集”, 3), (“库 存”, 2), ]
pairs = Counter() for text, freq in corpus: words = text.split() for i in range(len(words) - 1): pairs[(words[i], words[i + 1])] += freq
print(“初始相邻对出现频次:”) for (w1, w2), count in pairs.most_common(): print(f” {w1} + {w2} -> {count} 次”)
best_pair, best_count = pairs.most_common(1)[0] print(f”\n最佳首轮合并对: {best_pair[0]} + {best_pair[1]}(出现 {best_count} 次)”)
更短的序列不是凭空得到的:我们为此增加了两个词表条目。实际训练分词器时还要规定基础符号、边界、词表容量,以及频次相同怎样处理。
对于新文本 数据库存,按照这两条已学会的规则,在允许合并的片段范围内会得到 数据库 / 存。这未必符合人想表达的“数据 / 库存”。分词器学习的是统计规则,不负责保证每个片段都是语义上的完整词。
训练 BPE 是学习合并规则;使用它则是遵循既有规则及其优先次序。不能给每段新文本重新统计并另选一套合并方式,否则同一个编号体系就失去稳定意义。Hugging Face BPE 教程
算法前后还有几步会改变结果
真实分词流程通常还涉及文本规范化、预切分,以及添加特殊标记。只说“用了 BPE”,不足以复现一次切分。Tokenizers 流程说明
可以把这些环节理解为四个不同问题:
| 环节 | 要决定什么 | 应检查的例子 |
|---|---|---|
| 规范化 | 输入是否先改写 | 大小写、全角字符、组合字符 |
| 预切分 | 哪些边界不能继续合并 | 空格、标点、数字和换行 |
| 子词切分 | 在允许的范围内怎样拆分 | 长英文标识符、中英混排 |
| 特殊标记处理 | 是否额外添加模型约定的标记 | 开始、结束、角色边界 |
假设我们把所有英文都改成小写。自然语言里某些区别可能不重要,但代码里的 UserID 和 userid 未必等价。后面的分词即使可以完全还原规范化后的文本,也不能恢复被前一步抹掉的大小写。
因此,“可以解码”要问清楚还原到哪里:是原始输入,还是规范化后的输入?是否保留空格和换行?解码时是否跳过了特殊 token?
SentencePiece 是一种分词工具,不能与某一种切分算法画等号。它的论文强调直接处理原始句子和语言无关的处理方式,并支持不同子词模型。讨论实验时要同时写清工具、算法和训练配置。Kudo 与 Richardson,2018
这些区别对于第一卷的清洗流程也有影响。如果上游已经合并空白,下游再宣称“分词器保留空白”,并不能补回原始代码缩进。
字节覆盖解决了什么,又没有解决什么
字符词表可能遇到没收录的字符。另一种做法是从字节出发,再学习较长片段。UTF-8 文本可以表示成字节序列,因此包含完整字节基础单位的设计,能够避免仅仅因为没收录某个字符就无法表示它。Hugging Face BPE 教程
但“能表示”不等于“表示得紧凑”,更不等于模型理解它。
一个汉字可能由多个字节组成。若没有适合的合并片段,它可能占多个 token。反过来,常见中文片段也可能合并得很好。不能背诵一个固定的“中文一字等于几个 token”,然后用它估算所有模型。
另外,单个 token 可能只含一个字符的部分字节。逐个解码时看到不完整字符,不一定代表整个序列已经损坏;应同时检查完整序列的解码结果。若实际流程使用了有损规范化或清理,字节基础单位本身也不能保证从头到尾无损。
这给我们一个明确的检查方式:拿真实任务的文本看长度与还原结果,别只根据分词器名字下判断。
词表大小怎样进入模型成本
词表不仅是一张文本到整数的对照表。模型通常要为每个 token 保存一个输入向量,还要为词表中的候选 token 计算输出分数。
设词表有 个条目,隐藏维度为 ,一个普通嵌入矩阵有 个参数。以教学配置 为例:
| 词表大小 | 一个嵌入矩阵的参数量 |
|---|---|
| 16,000 | 8,192,000 |
| 32,000 | 16,384,000 |
这是矩阵形状的计算,不是某个模型的实际占用。如果输入嵌入和输出权重共享,这部分权重可以复用;如果不共享,输出矩阵还要另算。梯度、优化器状态、激活和实现细节会继续影响训练内存。
扩大词表可能缩短序列,也会增加相关矩阵的大小。对小模型,词表参数占比尤其值得检查。不能只看到“同样文本少了两成 token”,就宣布训练成本必然下降两成。
一个更合理的决策至少同时看三项:同一批原始文本被切成多长、整个模型因此增加多少参数、实际硬件上的吞吐与内存怎样变化。前两项可以先离线统计,第三项需要真实测量。
中英文比例为什么会跟着分词器变化
第一卷讨论过数据混合比例。这里能看出,分词器也是比例定义的一部分。
假设一份固定文本集合,在分词器 A 下,中文部分得到八十万个 token,英文部分得到四十万个;中文 token 占比是三分之二。
再假设分词器 B 把同一批中文切成四十万个 token,英文仍然是四十万个。原始文件一字没变,中文 token 占比却变成一半。
这些是假设数字,只用于解释计量单位变化。如果训练按 token 预算取样,换分词器之后需要重新算每种来源实际进入训练多少文本。不能直接沿用旧表里的百分比。
同样,比较两个分词器时,“各训练一亿 token”与“各读取同一批完整文档”回答的是不同问题。前者固定预测位置的数量,后者固定原始内容;两者一般不能同时固定。报告里应明确选了哪个预算,再补充另一个口径的实际消耗。
后训练最容易出错的是编号兼容性
如果从已有预训练模型开始 SFT,分词器通常应该与该模型配套。
原因不神秘。假设模型训练时编号 100 对应某个片段,它的嵌入向量和输出参数已经围绕这个含义学习。如果我们换一份词表,让编号 100 指向别的内容,即使词表大小相同,整数也不会自己携带原来的意思。
特殊 token 同样如此。结束标记可以是模型学习预测的目标,但它不是一种自动切断注意力的屏障。角色标记是否有效,也取决于模板、编号和模型曾经学到的用法。
因此,迁移时至少要核对:
- 分词器文件和模型版本是否配套,编号映射有没有变化。
- 模板是否已经添加开始或结束标记,后续编码是否又加了一遍。
- 新增 token 后,模型的相关矩阵是否扩展,新条目是否得到训练。
- 训练与推理是否采用同一套角色和结束约定。
这些是接口一致性问题。只检查“程序没有报词表越界”远远不够。
第一轮实验先检查表示,不急着训练模型
本篇建议先完成一个小型分词审计。这是实验设计,不是已完成的分词器排名。
从已经划分好的验证文本中抽取中文说明、英文说明、代码、中英混排和罕见字符各一组。若要自行训练分词器,只使用训练部分学习词表;验证文本用于检查,不能反过来反复调整后仍当作未见测试集。
对每个候选保存原文、token ID、可读片段、总长度和完整解码文本。再看以下问题:
| 问题 | 记录方式 | 不能直接推出什么 |
|---|---|---|
| 是否损坏内容 | 逐项比较原文与解码,解释允许的规范化 | 能还原不代表模型理解 |
| 哪类文本切得长 | 分类别报告总 token、字符数和长度分布 | 平均更短不代表所有类别更短 |
| 是否容易截断 | 固定上下文窗口,统计超长文档与被截断内容 | 不截断不代表学得更好 |
| 边界是否一致 | 检查空白、结束标记、角色模板 | 格式一致不代表训练目标正确 |
字符数应明确按什么定义,例如 Python 字符串长度与用户看见的字形数量并不总一致。跨语言汇总时也别让数量最多的一类掩盖其他类别。
如果审计已经发现代码被改坏,或中英文比例发生明显变化,就先修复这些问题。只有表示层面过关,后续模型比较才有清楚的起点。
两个值得自己检查的问题
第一,BPE 教学例子里,第一轮减少八个 token,为什么不能说“模型参数也减少了八个”?试着分别说出序列长度、词表大小和嵌入矩阵大小的关系。
第二,两个分词器在同一批中文文本上分别产生一万和七千个 token。你会怎样设计后续比较,区分“序列更短带来的预算变化”与“模型实际预测能力改善”?
接下来讨论语言模型究竟在预测什么。分词得到整数以后,还要明确每个位置用什么信息预测哪个目标;否则损失能下降,也可能是在学错任务。
文章概念网络
从章节进入正文概念。实线表示所属层级,虚线连接跨章节重复出现的概念,不代表因果关系。
67 / 67 个节点 · 正文标记术语与关键词概念
文本怎样变成 token:分词、词表与训练成本
回到原文 ↗最后更新于 2026-10-20
引用本文
@article{2026-10-20-llm-training-pretrain-01-tokenization,
author = {枫语},
title = {文本怎样变成 token:分词、词表与训练成本},
journal = {枫语},
year = {2026},
month = {oct},
url = {https://meiluosi.github.io/posts/2026-10-20-llm-training-pretrain-01-tokenization/}
}