主题
第 24 章 tokenizer 的选择,会一直留在模型里
到这里,我们已经能训练一个字节 BPE,保存合并表,处理预切分和特殊标记,并在有限测试集上与一个发布版本精确对齐。但工程上的问题还没有结束:词表应该多大?换成 SentencePiece 会怎样?模型在数数、拼写和不同语言上的困难,哪些与 tokenizer 有关,哪些不能全怪它?
本章覆盖课程后半段的 SentencePiece、词表规模、新增 token、多模态概念和 tokenization quirks。原课的强烈表述用于提醒读者不要忽视分词;本书把机制、可验证结论和未完成研究分开说明。官方课程
24.1 SentencePiece 是工具,不是一种唯一的算法
SentencePiece 支持 BPE 和 unigram 等模型。不能说“用了 SentencePiece,所以一定是 BPE”,也不能说所有叫 SentencePiece 的模型都采用完全相同的预处理。课程展示的是 SentencePiece 的 BPE 配置,并尝试接近 Llama 2 的若干选项;这不等于靠一小段 toy text 就能重建 Llama 2 的词表。
它的一条常见工作路线是直接处理 Unicode 字符,内部把空白显示为特殊符号 ▁,再学习子词。根据字符覆盖率、未知字符和 byte_fallback 设置,不常见或未见码点可能落到 <unk>,也可能退回 UTF-8 字节。与前面从一开始就把全部文本变成 bytes 的路线不同,但两者都需要精确保存模型与配置。SentencePiece v0.2.0 官方说明;SentencePiece 原论文
原 notebook 的配置项很多,不应机械地复制。下面按它们改变的行为组织,便于你知道改动的后果。
| 配置组 | 课程出现的选项 | 需要检查的行为 |
|---|---|---|
| 算法与容量 | model_type、vocab_size | 用 BPE 还是 unigram;目标词表是否可达 |
| 归一化 | normalization_rule_name、remove_extra_whitespaces | 输入是否被改写,连续空白是否保留 |
| 未知字符 | character_coverage、byte_fallback | 罕见字符保留、变 UNK 还是拆成字节 |
| 分块限制 | split_digits、split_by_number、split_by_unicode_script、split_by_whitespace | 数字、文字类别与空白边界能否跨越 |
| 片段形式 | max_sentencepiece_length、add_dummy_prefix、allow_whitespace_only_pieces | 最长片段、人工前缀、纯空白项 |
| 特殊编号 | unk_id、bos_id、eos_id、pad_id | 未知、开始、结束、填充的协议约定 |
| 输入预算 | input_sentence_size、max_sentence_length、shuffle_input_sentence | 哪些数据实际参与训练 |
| 训练资源 | seed_sentencepiece_size、num_threads | 候选初始化与资源预算,影响可复现记录 |
其中 seed_sentencepiece_size 是候选规模相关设置,不是随机种子。character_coverage 接近 1 也不能保证覆盖将来所有字符。add_dummy_prefix 可以在开头引入类似空白的内部表示,不能据显示的 ▁ 数量直接推断原文空格数量。对空白敏感的代码任务,应把空格、制表符、换行和开头结尾分别做 roundtrip 测试。v0.2.0 官方选项定义
24.2 用未知中文检查 byte_fallback
examples/part3/check_sentencepiece.py 使用 SentencePiece 0.2.0,在一份原创英文小语料上训练两个 BPE 模型。二者都使用 identity 归一化、保留多余空白、关闭人工前缀、禁用输入打乱、单线程;一个不开 byte_fallback,一个开启。两者的词表目标分别为 44 和 300,因为开启 fallback 要给全部字节保留足够位置。
对同一句 hello 中文🙂,真实结果如下。
| 检查 | 无 byte_fallback | 有 byte_fallback |
|---|---|---|
| 实际词表大小 | 44 | 300 |
| 是否出现 UNK ID | 是 | 否 |
| 未见中文和 emoji | 进入未知片段 | 拆成 <0xE4> 等字节片段 |
| decode 是否等于原文 | 否 | 是 |
无 fallback 的 decode 为 hello ⁇ ,丢失了具体未知文字;有 fallback 的 decode 完整恢复原文。查看 piece 列表时,未知片段有时仍能显示原始表面字符串,但真正送给模型的是同一个 UNK ID,所以不能用“打印出了中文”来证明信息没有丢失。详细 ID 和 pieces 在 qa/part3/sentencepiece.json。
这项实验只隔离未知字符处理,不比较 tokenizer 总体优劣,也不复现官方 notebook 的所有数值配置。它没有下载 Llama 权重或 tokenizer;“类似配置”不能代替相同训练语料和已发布模型文件。
24.3 词表变大,序列常变短,但不是免费午餐
设词表大小为
另一方面,常用长片段得到一个 token 后,同一文本可能需要更少的位置。普通注意力的分数矩阵形状为
| 选择 | 输入序列 | 词表相关参数 | 常见代价 |
|---|---|---|---|
| 原始字节 | 长 | 小 | 多字节文字需要更多位置 |
| 较小子词表 | 较长 | 较小 | 稀有词需要较多片段 |
| 较大子词表 | 常较短 | 较大 | 很多稀有 token 训练不足 |
| 字符表 | 依语言而异 | 依字符覆盖而异 | 新字符处理和词表覆盖需设计 |
同样是 4096 个 token 的上下文,不同 tokenizer、语言和文体能装下的原始文字长度不同。不要把英文的平均“一个 token 约多少字符”当成中文、代码或任意 tokenizer 的固定换算。实际预算应调用模型对应版本的 tokenizer 测量。
词表大小也不宜只凭训练集压缩率选。你需要同时看未见文本上的 token/字节比、各语言覆盖、极稀有词条比例、吞吐和内存,以及最终任务性能。因为训练目标的基本单位变了,不同 tokenizer 的“每 token 平均 loss”不能直接横向比较;可以使用一致原始文本上的总负对数似然,并按字节等共同单位归一化,且仍要交代评价方法。
24.4 给模型加一个新 token,要改的不只有 tokenizer
如果给常用提示模板分配一个新 token ID,tokenizer 可以把长字符串缩成一项。但原模型的输入 embedding 没有这一行,输出头也没有对应候选。需要扩展相关矩阵,为新行初始化参数,然后用合适数据训练,让模型学会新项的用途。
直接把多个旧 token 的 embedding 求平均作为新行,最多是一种初始化策略,不保证模型对新 token 的行为等于读取原始长串。Transformer 的位置、注意力和非线性加工使二者不必等价;尤其是一段复杂指令被压缩为一个随机新向量时,语义不会凭空保留。
同样,若用新 tokenizer 给原文生成全新编号,却保持旧网络权重,模型看到的是改变后的协议。固定词表、固定 ID、训练新增行和必要的继续训练,应视为同一次变更。把“提示压缩”说成只修改一个词表文件就能无损节省上下文,会误导读者。
24.5 拼写、算数、尾随空格:tokenizer 怎样改变题目
语言模型收到的是 token ID,不直接收到字符数组。若一个长单词是一个 token,要回答“其中有几个 r”,网络需要从学到的表示中恢复字母结构;字符级输入则把这些位置直接暴露出来。tokenization 可以解释任务为何变难,却不能单独证明某次失败完全由分词造成。训练数据、推理过程和模型容量同样影响结果。
数字也类似。把 123456 切成两个三位片段,和切成六个数字,会改变加法进位所需的表示。但换成数字级 tokenizer 也不会自动安装加法算法;还需要模型学会计算规则,并在未见数值上测试。课程让你尝试反转答案数字,是为了使生成方向更接近从个位向高位进位,而不是宣布所有算数困难已经解决。
代码缩进依赖空白。若常见四空格需要很多 token,会占用更多上下文和预测步骤;若合成一项,长度更短,但模型仍要理解缩进的语法作用。大小写、尾随空格、emoji 序列和组合字符也都可能引起切分变化。调试时应把“看到的文本”与“实际 ID 列表”同时记录。
罕见、异常或训练分布外的 token 还可能表现出古怪生成行为。tokenizer 把某段字节收进词表,只证明它有地址,不证明模型已充分见过并学好那一行参数。反过来,基础字节可表示任何良构文本,也不证明模型理解所有语言。
24.6 多模态的 token,不一定来自 BPE
课程短暂讨论了图像、音频和视频的离散表示。一个常见思路是先用编码器把输入映射为连续向量,再通过向量量化选择有限码本中的项,得到离散 ID。这样的 ID 可以送入序列模型,但其训练目标、时空结构和重建误差与文本 BPE 不同。
把图像编码成离散 token 也不代表“一个 token 是一个完整物体”。它可能对应局部块或学习到的视觉模式。现代多模态系统还可能使用连续特征投影等路线,因此不能把一次向量量化示意当作所有系统的统一实现。本书这里只解释课程涉及的概念,不提供未经运行的多模态复现。Neural Discrete Representation Learning 原论文
24.7 真要选 tokenizer,先写验收条件
如果你在使用一个已有语言模型,应使用它指定的 tokenizer 和消息格式。若在从头训练一个领域模型,可以先列出支持的语言、代码比例、精确保真要求、上下文预算和资源限制,再挑代表性语料比较。tiktoken 适合使用其发布的编码器;SentencePiece 提供训练与推理工具;minbpe 的优势是代码短、机制容易核对。本书的 Python 逐轮扫描实现用于学习,不作为大规模 tokenizer 训练的性能基准。
一个有用的最小验收集应包括:常见文本 roundtrip、未知字符、空白、空输入、组合字符、特殊 token 字面串、保存加载一致性、固定版本的 ID 金标准,以及恶意或损坏的 tokenizer 文件处理。测试的目的不是证明世界上没有边界情况,而是把本项目依赖的行为写成可重复检查的协议。
课程练习第五阶段与补充题
课程第五阶段:探索 SentencePiece。 在本书脚本中只改一个选项,例如 add_dummy_prefix 或空白归一化,然后比较 ID、piece 显示与 decode。提示:先用 hello、hello 、两个空格、制表符和换行,不要一开始就拿大语料训练。
答案应包含的检查。 明确使用 BPE 还是 unigram,固定库版本,保存训练选项,并分别验证普通文字和未见文字。若想声称兼容 Llama 2,必须取得对应 tokenizer 模型文件并逐 ID 对照,仅采用相近选项远远不够。本书未进行这项特定模型兼容验证。
补充题 1。 两个模型的平均 token loss 分别为 2 和 3,能否说明前者一定更会建模文本?
答案。 若 tokenizer 不同,不能直接比较。一个 token 对应的文本长度和候选空间不同,应对相同原始文本使用一致单位比较总概率或任务表现,同时记录上下文与评估协议。
补充题 2。 新增 1000 个 token,宽度为 512,输入输出不绑定且输出有 bias,会增加多少参数?
答案。 输入增加
补充题 3。 byte_fallback 通过了中文 roundtrip,是否说明英文语料训练出的语言模型已经能理解中文?
答案。 不能。tokenizer 证明文字可以不丢失地表示,语言模型还需要学习这些序列的规律与任务。可表示性、压缩效率和语言能力,是三个不同指标。
本章来源:SentencePiece 的具体行为以 v0.2.0 文档与本书实际实验为准;多模态部分仅为所引原论文的概念性延伸。课程末尾提到 Hugging Face 的 tokenizer 工具,本书没有对其性能或兼容性做测试,也不将其实现简单等同于 SentencePiece。