令牌与映射表基础

做 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) 序列 [id₁, id₂, id₃, …] 映射表(Vocab / Mapping Table) 存储结构示例: id:0 → "[PAD]" | id:1 → "[UNK]" | id:2 → "[CLS]" id:3 → "[SEP]" | id:4 → "the" | id:5 → "a" 模型输出 文本 → 分词 → 查表映射 → 数字序列 → 模型 → 输出

从上图你能看出,映射表就是连接人类语言和模型世界的桥梁。没有它,模型看到的只是一堆二进制乱码。

映射表的维护原则

做 tokenizer 训练时,我总结了几条实用性原则:

  1. 不可逆操作要谨慎:vocab 一旦确定,尽量别删 。删除某个 会导致已训练好的 embedding 矩阵中该行失效。
  2. 新 的添加位置:只能在已有 vocab 末尾追加,不能往中间插。否则所有 index 大于插入点的 都会偏移。
  3. 保存两份元数据:除了 vocab 文件外,建议额外保存一份 ↔ id 的双向映射 JSON,方便调试。

个人习惯:我通常会在项目里建一个 vocab_utils.py 文件,把加载、校验、导出、可视化都放进去。量不大,但每次换项目都能省不少时间。

最后想说一句:映射表就像神经网络的地基,地基歪了,上层建筑再漂亮也没用。多花一点时间把这步做扎实,后面会省很多心。