令牌与映射表基础
做 NLP 项目也好,搞 LLM 推理也罢,Tokenizer 这一步总归绕不过去。早年我在做文本分类时,傻傻地直接用空格把句子切开,结果模型一直学不好中文短语。后来我发现自己压根没搞懂“令牌”和“映射表”到底怎么回事。
这一章,咱们就把这两个东西彻底掰扯清楚。
令牌()到底是什么?
令牌,说白了就是你输入文本被切出来的最小单元。可以是单词,可以是子词(subword),也可以是单个字符。为什么叫“令牌”?因为它更像一个编号,指向映射表里对应的位置。
举个例子:
句子 "I love NLP" 经过 BPE 分词后,可能变成:
["I", "love", "N", "LP"]
嗯,N 和 LP 被拆开了。每个元素就是一个 。但我更习惯把它们看成“身份证号”,因为真正进出模型的是数字,不是字符串本身。
核心认知:令牌不是词,是一个编码单元。同一个单词在不同分词器下可能被切成不同数量的 。
映射表(Mapping Table)的结构
映射表本质是一张“字符串 ↔ 数字”的对照表。我习惯叫它 vocab table 或者 vocab 文件。它在 NLP 工程中的存在感极低,但一旦出错,整个模型推理结果都会乱套。
标准的映射表一般包含三列:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | 整数 | 该 的唯一数字编号 |
| 字符串 | 原始文本片段 | |
| special_flag | 布尔 | 是否是特殊标记(如 [CLS]、[SEP]) |
你想想看,如果两个不同的 被分配了相同的 id,训练出来的 embedding 层就会混淆。我在项目中就碰见过这种事故 — 某开源词表里 "[UNK]" 出现了两遍,导致部分罕见词始终学不到正确表示。
映射表的存储形式
实际工程里,映射表最常见的存储形式就这么几种:
- 文本文件(.txt/.vocab):每行一个 ,行号就是 id。简单直接,但改字段不方便。
- JSON 文件:{"id": 0, "": "[CLS]"},可读性高,但加载慢。
- Protocol Buffers / 二进制:速度快,占用小,适合生产环境。
记得有一次我写 tokenizer 加载逻辑,直接按行读取 vocab.txt 时忽略了换行符,结果 ['CLS'] 被读成了 ['CLS\n'],embedding 查表一直查不到,整整排查了一个下午。嗯,这里要注意:入库前一定要 strip()。
# 一个最简单的映射表示例
vocab = {
0: "[PAD]",
1: "[UNK]",
2: "[CLS]",
3: "[SEP]",
4: "the",
5: "a",
6: "I",
# ...
32000: "love"
}
实战建议:我建议你把 vocab 的加载和校验封装成一个独立的类。不要散落在训练脚本各处。这样做的好处是,换分词器时只需要改这个类,尽量少碰业务逻辑。
一些避坑指南
这一节我把它单独列出来,因为很多人就是在这里栽跟头。
- 我曾经的教训:vocab 中 顺序不能轻易改变。预训练模型已经记住了 id=5 对应的 embedding,你把它改成其他 ,模型性能会直接崩掉。
- 小心特殊 :[MASK]、[CLS] 等必须出现在固定位置。如果忘了加或加错了 index,下游任务几乎无法收敛。
- 编码/解码一致性:tokenizer encode 用的 vocab 和 tokenizer decode 用的必须完全一致。一个小细节:我曾经因为测试环境和训练环境的 vocab 文件版本不同,线上 F1 直接掉了 3 个点。
令牌映射表的全貌
下面这张图帮你理清 、映射表、模型三者之间的关系:
从上图你能看出,映射表就是连接人类语言和模型世界的桥梁。没有它,模型看到的只是一堆二进制乱码。
映射表的维护原则
做 tokenizer 训练时,我总结了几条实用性原则:
- 不可逆操作要谨慎:vocab 一旦确定,尽量别删 。删除某个 会导致已训练好的 embedding 矩阵中该行失效。
- 新 的添加位置:只能在已有 vocab 末尾追加,不能往中间插。否则所有 index 大于插入点的 都会偏移。
- 保存两份元数据:除了 vocab 文件外,建议额外保存一份 ↔ id 的双向映射 JSON,方便调试。
个人习惯:我通常会在项目里建一个 vocab_utils.py 文件,把加载、校验、导出、可视化都放进去。量不大,但每次换项目都能省不少时间。
最后想说一句:映射表就像神经网络的地基,地基歪了,上层建筑再漂亮也没用。多花一点时间把这步做扎实,后面会省很多心。