AI 基建
0%
第一部分 · 基座模型的形成 · 第 7 章

分词

作者Changkun Ou
阅读时长约 14 分钟

第 6 章 已经确定训练过程会从哪些文本中采样。在文本进入模型前,分词器还要把它转换成整数 ID。这套映射会影响序列长度、多语言覆盖、数字和代码的结构、按词元索引的模型参数形状,以及此后每个检查点和数据分片的兼容性。

分词不只是构造词表。生产环境中的分词器还要规定规范化、边界规则、字节处理、特殊词元、自动添加的前后缀和解码方式。这些决定共同构成带版本的模型接口,应该像架构或数据混合分布一样,经过测量后再冻结。

这件产物不只是一张词表

模型消耗的是 ID,而生成这些 ID 需要依次执行多项转换:

A 输入契约 Unicode 文本或原始字节 B 规范化 N Unicode / 大小写 / 空白 A->B C 边界规则 预分词器 + 受保护片段 B->C D 切分模型 BPE 排名或 Unigram 分数 C->D E ID 映射 + 特殊词元 BOS / EOS / PAD / 控制 ID D->E F 词元流 ID + 偏移 + 解码策略 E->F
图 7.1. 分词器契约。规范化与边界规则决定切分模型看到的输入流;词表、特殊词元策略、后处理器和解码器再共同决定 ID 及其含义。

分词器(tokenizer)指的是这份完整契约,而不只是切分模型。即使两个系统看起来拥有相同的片段,只要缺少规范化器、边界规则、特殊词元 ID 或解码器,同一份文本就可能得到不同的 ID 序列。

这些 ID 会索引模型参数。若词表规模为 VV,模型宽度为 dd,输入嵌入就是矩阵 ERV×dE\in\mathbb{R}^{V\times d}。如果输出投影不与输入嵌入绑定,还会有另一张按词元索引的矩阵 WoutRV×dW_{\mathrm{out}}\in\mathbb{R}^{V\times d};绑定权重的模型则复用 EE。因此,改变已有 ID 的含义,就等于改变检查点中已有一行参数的含义。这是分词器最核心的兼容性约束。

边界规则在学习任何合并前就已经生效。它决定一个片段能否跨过空白、标点、书写系统变化或受保护片段。最早用于神经机器翻译的 BPE 只在外部切好的词内运行。GPT-2 风格的字节级 BPE 会先用正则表达式,按字母、数字、标点和空白等类别切分输入。SentencePiece 分词器(SentencePiece) 可以直接从原始句子训练,不依赖特定语言的外部分词器,但它仍会执行配置好的规范化和边界规则 (Sennrich et al. 2016; Kudo and Richardson 2018; Radford et al. 2019)。

BPE 学到的是合并优先级

Sennrich、Haddow 和 Birch 在 2016 年把原本用于压缩的字节对编码(BPE)改造成开放词表神经机器翻译方法 (Sennrich et al. 2016)。字节对编码(BPE)是这套构造的主力:它反复合并语料中最常见的相邻符号。BPE 从基础符号开始,不断连接高频相邻对,最终得到一张有序合并表。编码时,排名越靠前的合并优先级越高。

概念上的训练算法如下:

1. 按冻结的规范化和边界规则处理分词器训练语料。
2. 把每个预词元表示成基础符号,并附上所需的边界标记。
3. 按预词元频率加权,统计相邻符号对。
4. 用确定性的平局处理规则选出计数最高的一对。
5. 替换语料中这对符号所有互不重叠的出现位置。
6. 把这对符号追加到有序合并表。
7. 重复第 3 至第 6 步,直到达到目标词表规模。

这里的“预词元”是边界规则产生的一段输入;基础符号可以是字符、字节或另一套保证完备的起始字母表;合并表既记录选中的符号对,也记录它们的排名。许多语料都存在频率相同的符号对,因此平局处理规则也是可复现性契约的一部分。

训练和编码是两种不同的算法。训练会统计频率,并在选定每次合并后改变语料的表示。编码时不会重新统计输入频率,而是从基础符号出发,在不跨越受保护边界的前提下,反复应用当前排名最高的已学习符号对。教学用的朴素训练器可能为每次合并重新扫描全部位置;生产训练器通常维护符号对出现位置的索引,只更新受影响的相邻位置。因此,复杂度取决于具体实现,不能只凭“BPE”这个名字判断。

下面的交互示例使用 low low low lo。其中 (l, o) 出现四次,(o, w) 出现三次,所以第一步没有隐藏的平局。

把每个词当成独立的预词元,基础符号为 l、o 和 w。
符号对 (l, o) 出现四次,(o, w) 出现三次。
把 (l, o) 合并成 lo,并把它记为第 1 名。
完成替换后,(lo, w) 成为唯一出现次数最多的符号对。
把 (lo, w) 合并成 low,并把它记为第 2 名。
图 7.2. 一个没有平局的 BPE 示例。每轮都在当前表示中统计符号对,替换选中的符号对,并记录其排名,供之后编码使用。

下面的代码训练一个小型字符 BPE,冻结合并顺序,再编码训练时未见过的词。词尾标记阻止合并跨越词边界。最后一个例子刻意包含未见字符,用来说明字符 BPE 并不会自动消除未知词元。

from collections import Counter

EOW = "</w>"
UNK = "<unk>"

def merge_pair(symbols, pair):
    left, right = pair
    out, i = [], 0
    while i < len(symbols):
        if i + 1 < len(symbols) and symbols[i] == left and symbols[i + 1] == right:
            out.append(left + right)
            i += 2
        else:
            out.append(symbols[i])
            i += 1
    return out

training_words = "low low low lo lower newest widest".split()
base_symbols = set("".join(training_words))
encoded_training = [list(word) + [EOW] for word in training_words]
merges = []

for _ in range(8):
    counts = Counter()
    for symbols in encoded_training:
        counts.update(zip(symbols, symbols[1:]))
    if not counts:
        break
    # 先按计数降序,再按字典序处理平局,保证结果确定。
    pair = min(counts, key=lambda candidate: (-counts[candidate], candidate))
    merges.append(pair)
    encoded_training = [merge_pair(symbols, pair) for symbols in encoded_training]

def encode_word(word):
    symbols = [char if char in base_symbols else UNK for char in word] + [EOW]
    for pair in merges:
        symbols = merge_pair(symbols, pair)
    return symbols

print("merge ranks:", [left + "+" + right for left, right in merges])
for word in ["low", "lower", "lowest", "lobster"]:
    pieces = encode_word(word)
    print(f"{word:7s} -> {' '.join(pieces)}  ({len(pieces)} pieces)")

print("UTF-8 bytes for 🐍:", list("🐍".encode("utf-8")))

字节覆盖不等于高效覆盖

一般的子词模型只对其基础字母表开放。若某个字符不在基础字母表中,字符 BPE 仍可能输出未知词元。要保证完备性,就需要穷尽所有输入的基础表示,或者提供回退路径。

字节级 BPE 把全部 256 个字节值作为基础字母表。有效的 Unicode 字符串会先编码成 UTF-8 字节,因此文本接口接受的任何字符串都有一种表示,不会在编码端产生未知词元。GPT-2 等实现会先把字节映射成可逆的占位字符,再学习合并;另一些实现则直接处理字节值 (Radford et al. 2019; Wang et al. 2020)。但覆盖保证并不意味着编码一定很短。罕见书写系统、emoji 序列或近似二进制的片段,可能退化成许多单字节片段。

字节回退与字节级 BPE 有关,但两者并不相同。带回退的字符 BPE 或 Unigram 会先学习普通字符片段,只有遇到无法表示的字符时才输出字节词元;字节级 BPE 的整套合并系统都建立在字节基础上。正确配置时,两者都能避免未知词元,却会产生不同的切分结果。

解码器还必须规定错误策略。有效输入会变成有效 UTF-8 字节,但模型任意采样出的序列可能停在一个多字节字符中间,也可能把若干字节组合成无效 UTF-8。替换、严格失败和保留字节显示,是三种不同的契约。

规范化会改变往返转换能保留什么

NN 为声明的规范化器。对于确定性分词器,若解码器没有有损清理,预期不变量为

Decode(Encode(x))=N(x).\operatorname{Decode}(\operatorname{Encode}(x))=N(x).

其中,xx 是输入字符串,N(x)N(x) 是规范化后的形式,Encode\operatorname{Encode}Decode\operatorname{Decode} 是冻结的分词与解码函数。只有在 NN 保留相关差异时,结果才会等于原始的 xx。Unicode 兼容性规范化、大小写折叠或空白折叠,都可能有意把不同的原始字符串映射为同一个规范化字符串。

SentencePiece 会让空白对切分模型可见,因此能无歧义地重建规范化后的输入流。但这不代表在任何规范化和空白配置下,都能逐字节还原原始输入 (Kudo and Richardson 2018)。代码缩进、组合附加符号、全角形式、从右到左的文本、emoji 连接符和不以空格分词的书写系统,都应进入规范化测试。

Unigram 为完整切分打分

Kudo 在 2018 年提出 Unigram 语言模型分词器,作为按合并排名的 BPE 的概率化替代 (Kudo 2018)。它先建立较大的候选词表,估计片段概率,再逐步删除那些对语料似然影响最小的片段。它建模的是有序切分,不是无序的片段集合。

对规范化输入 XX,模型为

P(z)=j=1mp(zj),z=argmaxzS(X)P(z),L=XDlog ⁣zS(X)P(z).\begin{aligned} P(z) &= \prod_{j=1}^{m}p(z_j), \\ z^* &= \arg\max_{z\in\mathcal{S}(X)}P(z), \\ \mathcal{L} &= \sum_{X\in D}\log\!\sum_{z\in\mathcal{S}(X)}P(z). \end{aligned}

其中,z=(z1,,zm)z=(z_1,\ldots,z_m)XX 的一种有序切分,zjz_j 是其中第 jj 个片段,p(zj)p(z_j) 是该片段学到的概率,S(X)\mathcal{S}(X) 是当前词表下 XX 的所有有效切分,zz^* 是概率最高的切分,DD 是分词器训练语料。动态规划可以求出 zz^*;训练过程则交替估计概率并裁剪词表。

因为模型为完整切分分配概率,训练时可以采样其他切分,而不是总取 zz^*。Kudo 把这种方法称为子词正则化,并报告了它在神经机器翻译中的数据增广作用 (Kudo 2018)。它仍然需要完备的基础字母表或字节回退。

BPE 和 Unigram 因此提供了不同的控制方式。BPE 确定性地应用学到的合并优先级;Unigram 为切分格打分,也可以从中采样路径。两者都不具有普适优势,应该在相同语料、词表预算、模型算力和评估套件下比较。

词表规模同时改变序列成本和模型成本

扩大词表通常能让分布内文本用更少词元表示,但也会增加按词元索引的参数,并让稠密输出投影在每个位置上为更多候选打分。忽略偏置项时,按词元索引的参数量为

Ptoken={Vd,输入与输出权重绑定时,2Vd,输入与输出权重不绑定时.P_{\mathrm{token}}= \begin{cases} Vd, & \substack{\text{输入与输出}\\\text{权重绑定时}},\\ 2Vd, & \substack{\text{输入与输出}\\\text{权重不绑定时}}. \end{cases}

其中,VV 是词表规模,dd 是模型宽度,PtokenP_{\mathrm{token}} 统计输入嵌入和输出投影中的参数。绑定权重时,两者复用同一张矩阵。这里不包含优化器状态、量化、分片开销和输出投影计算。

图 7.3 精确画出了模型宽度为 4,096、每个参数占两字节时的这层关系。它没有声称存在普适的最优词表规模。

2026-08-03T20:50:18.225337 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ 0 50000 100000 150000 200000 250000 词表规模(词元) 0 1 2 3 4 词元索引参数存储量(GiB) 输入 / 输出权重绑定 输入 + 输出权重不绑定
图 7.3. 模型宽度为 4,096、每个参数占两字节时,按词元索引的参数存储量。输入与输出权重绑定时需要 Vd 个参数;不绑定时需要 2Vd 个参数。图中不含优化器状态与输出投影计算量。

序列长度会改变模型其余部分的工作量,词表规模则同时改变存储和 logits 计算。扩大词表是否更省成本,取决于模型宽度、是否绑定权重、硬件、序列长度、按词元还是按文档固定预算,以及实际服务负载。Tao 等人在受控扩展实验中发现了随算力变化的最优点,而不是一个可以迁移到所有训练任务的词表规模 (Tao et al. 2024)。

分词器的训练混合分布和 VV 同样重要。若按原始字节占比采样英文网页、源代码、数学文本和低资源语言,绝大部分学到的片段可能都被最大来源占用。项目可以专门设计分词器专用的训练混合分布,主动平衡这些领域,但必须记录该策略及其下游影响。罕见片段也需要在模型训练中出现足够多次,才能学到有用的参数行。

词元级困惑度不能直接比较两个分词器,因为两者定义了不同的度量单位。比较使用不同分词器的语言模型时,应换成共同的来源单位,例如每个 UTF-8 字节的比特数、每个字符的比特数,或另一种明确说明的单位。

下层约束

词表规模和 ID 映射固定了 第 8 章 中输入嵌入和输出投影每一行的含义。架构可以决定是否绑定这两组权重、怎样分片,却必须使用分词器给出的准确 VV 和稳定 ID 分配。

不只测压缩率,还要测语言和领域

“每个词对应多少词元”不是公平的通用指标,因为不同语言对词边界的定义并不相同。平行内容能提供更清楚的比较。设平行条目为 ii,语言为 \ell,参考语言为 rr,分词器为 TT,定义词元溢价

p,i=T(s,i)T(sr,i).p_{\ell,i}=\frac{|T(s_{\ell,i})|}{|T(s_{r,i})|}.

其中,s,is_{\ell,i}sr,is_{r,i} 用语言 \ellrr 表达同一内容,T(s)|T(s)| 是分词器输出的词元数,p,ip_{\ell,i} 是相对参考语言的句子级溢价。应报告整个分布,包括中位数和高分位数,而不是只挑一句示例。

Petrov 等人把这类比较用于平行译文,发现多个分词器在不同语言间会产生很大的词元数差异,其中也包括专为多语言设计的系统 (Petrov et al. 2023)。这种差异直接影响按词元定价和可用上下文。若某一平行内容切片的溢价为 pp,在扣除提示格式开销前,固定的 CC 词元窗口大约只能容纳参考语言的 1/p1/p。实际延迟还取决于批处理、算子、模型架构,以及额外词元出现在预填充还是串行生成阶段。

图 7.4 展示的是审计流程,而不是虚构的词元数量。

2026-08-03T20:50:18.720715 image/svg+xml Matplotlib v3.11.0, https://matplotlib.org/ 固定平行文本 内容相同 一种固定的 规范化策略 同一个分词器 产物 按语言 + 领域统计 词元溢价分布 p_l = 语言词元数 / 参考词元数 报告中位数、p95,以及部署上下文上限下的截断率
图 7.4. 分词器公平性审计流程:从固定的平行内容开始,应用同一种规范化策略,用同一个分词器产物编码,并按语言与领域报告词元溢价分布。

词元溢价只是一项指标,不是质量分数。Rust 等人发现,即使控制预训练数据,分词器选择也可能造成多语言下游表现差异,但数据覆盖、词法形态、书写系统、训练目标和模型容量同样重要 (Rust et al. 2021)。至少应报告:

  • 每个 Unicode 标量值、字素簇和 UTF-8 字节对应的词元数,并明确写出所用单位;
  • 未知词元率和字节回退率;
  • 序列长度的中位数、p95 和最大值,以及在部署上下文上限处的截断率;
  • 在代表性硬件上的编码和解码吞吐量;
  • 散文、代码、数字、公式、URL、emoji 和混合书写系统等切片;
  • 规范化碰撞和来源到词元的偏移准确率。

数字需要单独做消融实验。逐位、从左分组、从右分组和固定宽度方案,会产生不同的位置分组。Singh 和 Strouse 的实验表明,数字分词方向会改变算术行为,但没有证明存在一种普适最佳方案 (Singh and Strouse 2024)。代码也需要针对缩进、标识符、运算符和长字面量做类似测试。

冻结兼容性契约

特殊词元是模型的控制面输入。序列开头、序列结尾、填充、文档边界、角色、工具和模态标记,都需要稳定的拼写和 ID。契约还必须说明编码器是否自动插入这些词元、怎样转义或拒绝与标记相同的普通文本、哪些词元参与损失计算,以及怎样掩码填充位置。把控制标记当成普通用户文本,可能同时改变模型行为和安全边界。

机器可读的分词器清单至少应包含:

tokenizer:
  schema_version: <清单版本>
  artifact_sha256: <序列化分词器的摘要>
  library: <名称、版本和构建信息>
input:
  type: <Unicode 文本或原始字节>
  encoding: <例如 UTF-8>
  invalid_input_policy: <拒绝、替换或保留>
normalization:
  unicode_version: <固定版本>
  form: <不变、NFC、NFKC 或命名的自定义转换>
  case_and_whitespace_rules: <有序配置>
boundaries:
  pretokenizer: <正则表达式或模型及准确版本>
  protected_spans: <合并不得跨越的规则>
model:
  type: <BPE 或 Unigram>
  vocabulary_sha256: <片段到 ID 的映射>
  ranks_or_scores_sha256: <BPE 排名或 Unigram 概率>
  byte_mapping_or_fallback: <完整配置>
special_tokens:
  ids: <BOS、EOS、PAD、文档、角色、工具和模态 ID>
  literal_text_policy: <作为普通文本、转义、允许或拒绝>
postprocessor_and_decoder:
  automatic_insertions: <有序规则>
  cleanup_and_error_policy: <准确行为>
training:
  corpus_manifest_sha256: <来源混合分布和样本>
  trainer_flags_and_seed: <完整的确定性配置>
tests:
  golden_vectors_sha256: <原始输入、规范化结果、ID、偏移和解码输出>
compatibility:
  model_config_sha256: <检查点侧的分词器契约>
  stable_id_prefix: <含义永远不得改变的 ID>

黄金向量应覆盖组合附加符号、兼容字符、重复空白、从右到左的文本、emoji 连接符、无空格书写系统、代码缩进、数字、URL、与控制词元相同的普通字符串,以及策略规定的畸形输入。分词器摘要应固定写入语料分片、模型配置、训练检查点、服务镜像和评估报告。

更换分词器是一项迁移

替换文本到 ID 的函数并非直接可用的预处理更新,但也不是绝对不可能。只追加的扩展可以保留所有旧 ID,同时为按词元索引的参数增加新行;但仍需要初始化新增行并继续训练。Wang 等人展示了怎样为预训练多语言模型扩展词表 (Wang et al. 2019)。

若重新映射已有 ID,就必须精确重排相应的嵌入行和输出行。替换分词器还会改变整个数据分布上的切分方式,通常需要更强的适配或重新训练。零样本分词器迁移研究表明,在某些设置中,模型改造和继续训练可以降低这项成本 (Minixhofer et al. 2024)。迁移需要证据,不能当作普通配置修改。

只要未来可能重新分词,就应保留来源文本或获准保留的规范化表示。仅有词元 ID 分片,无法恢复规范化、未知词元或解码清理已经抹掉的差异。

无分词器模型只是移动了边界

无分词器方案(tokenizer-free)通常表示没有一套独立训练的子词词表把文本映射成变长片段,并不意味着模型完全不做表示或压缩。不同系统只是把这部分工作移到了不同位置:

系统 基础输入 怎样处理长序列
ByT5 UTF-8 字节 不学习分块,由字节到字节的 Transformer 直接承担更长序列 (Xue et al. 2022)
CANINE Unicode 字符 在深层编码器前用跨步卷积下采样 (Clark et al. 2022)
MEGABYTE 字节 用固定大小的块分开全局计算与局部计算 (Yu et al. 2023)
BLT 字节 用下一字节熵模型选择动态分块边界 (Pagnoni et al. 2025)
H-Net 字节 由层级模型端到端学习随内容变化的分块边界 (Hwang et al. 2025)

原始字节流和字符流通常比子词流长。层级模型试图把昂贵的全局计算放在块或分组层级,只用较小的局部模块处理字节。实际质量、FLOPs、内存和延迟仍取决于架构与工作负载。BLT 报告称,在截至其实验中的 80 亿参数设置里,它能在 FLOP 对齐的条件下取得有竞争力的扩展表现;H-Net 则报告了相对算力与数据匹配基线的改进。两项结果都不能证明它普遍优于固定分词。

争议所在

固定子词词表具有成熟工具和较强压缩能力,却会把依赖语料的边界冻结在模型之外。直接处理原始单位的系统去掉了这套学习得到的词表,但要承担更长的基础序列和新的层级结构。它们仍可能继承 Unicode 编码、规范化、训练数据、分块规则和架构中的偏差。问题在于切分和压缩应该放在哪里,而不是哪一种设计完全没有表示选择。

在模型训练前完成验证

在生成最终词元 ID 分片前,应完成以下检查:

  1. 重放产物。 从固定样本训练两次,确认词表、排名或分数、ID 和序列化摘要完全相同。
  2. 运行黄金向量。 针对每类必要输入,核验规范化、ID、偏移、特殊词元插入、解码和错误行为。
  3. 审计覆盖率。 按语言、领域、书写系统和分数区间测量未知词元率与字节回退率。
  4. 审计长度。 报告共同单位下的压缩率和平行内容的词元溢价分布,包括部署上下文上限处的截断情况。
  5. 基准测试系统。 测量训练时分词吞吐量、在线编码与解码吞吐量、输出投影成本和端到端延迟。
  6. 消融模型质量。 在数据与算力匹配的条件下,比较候选分词器的留出损失和目标任务,其中包括多语言、代码和数字切片。
  7. 测试控制词元。 确认与标记相同的普通文本、填充、文档边界、角色、工具和模态词元都遵守声明的策略。
  8. 核验交接。 检查语料清单、检查点、评估和服务系统是否都写明同一个分词器摘要。

这些检查通过后,分词器就成为训练分布与模型架构之间的稳定接口。日后仍可更换,但那会是显式的检查点迁移,而不是无声的预处理更新。

延伸阅读

  • Sennrich et al., “Neural Machine Translation of Rare Words with Subword Units” (BPE), 2016. arXiv:1508.07909
    本文提出将 BPE(字节对编码)用于稀有词的子词分割,使神经机器翻译无需回退词典即可实现开放词表翻译。
  • Kudo, “Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates” (一元语言模型分词), 2018. aclanthology.org
    子词正则化通过在训练时对多个概率采样的子词切分方案进行边际化,并提出基于一元语言模型的分词器作为 BPE(字节对编码)的概率替代方案,以提升神经机器翻译的鲁棒性。
  • Kudo & Richardson, “SentencePiece: A Simple and Language Independent Subword Tokenizer and Detokenizer for Neural Text Processing,” 2018. aclanthology.org
    SentencePiece 是一种语言无关的子词分词器,直接从原始句子训练,支持字节对编码(BPE)和 unigram 语言模型,无需预分词即可实现端到端文本处理。
  • Pagnoni et al., “Byte Latent Transformer: Patches Scale Better Than Tokens” (BLT,无分词器), 2025. arXiv:2412.09871
    BLT 用依据下一字节熵选择的动态字节块取代固定子词分词,并在其最高 8B 参数的测试范围内报告了有竞争力的 FLOP 对齐扩展结果。
  • Hwang et al., “Dynamic Chunking for End-to-End Hierarchical Sequence Modeling” (H-Net,学习式动态切块), 2025. arXiv:2507.07955
    H-Net 在层级字节模型内部学习依内容而定的切块边界;其单阶段配置在论文的算力与数据对齐实验中超过了 BPE 基线。
  • Petrov et al., “Language Model Tokenizers Introduce Unfairness Between Languages,” 2023. proceedings.neurips.cc
    论文在平行译文上发现,某些分词器与语言组合的词元数量差异可达 15 倍,造成成本、延迟与可用上下文方面的不平等。

评论

登录后评论