Skip to content

分词:把可逆编码变成可学习单位 ​

tokenizer 决定模型一次预测什么。按字符分词简单,但相同词根要重复处理很多步;按词分词序列短,却难以覆盖新词、代码标识符和混合语言。字节 BPE 从全部 256 个字节出发,再把频繁相邻片段合并,在覆盖能力与序列长度之间作折中。它不理解词义,也不保证一个汉字只占一个 token。

先区分三张表 ​

词表把 token ID 映射到字节串;merge 表记录哪两个 ID 在哪一轮合并;特殊 token 表保留语义边界,例如文档结束。编码需要 merge 的顺序,不只是最终词表。解码则把 token 对应字节按序连接,最后统一按 UTF-8 解码。如果逐个 token 解码,中文字符被拆开的字节片段可能各自无效。

本地 labs/llm/bpe.py 保留“字节起点+有序合并+可逆解码”的核心,使用空白段/非空白段作预切分,频次相同时选择整数 pair 的字典序最小者。这与官方 A1 的 GPT-2 式预切分、tie-breaking 和特殊 token 约定不同;因此它是原理实验,不是官方适配器实现。差异必须在比较词表或压缩率之前说明。

训练 BPE 的完整过程 ​

以两份文档 ab ab、ab ac 为例,先把每段编码为字节 ID。初始 a=97,b=98,c=99,空格单独形成段。第一轮 (97,98) 出现三次,合并成 256;第二轮可合并 (97,99) 成 257。空白边界不参与跨段合并,两份文档也不相连。新 token 的字节串是两个旧 token 字节串的连接。

flowchart LR
  A[按文档预切分] --> B[UTF-8 字节列表]
  B --> C[统计相邻 pair 频次]
  C --> D[选择频次最高 pair]
  D --> E[非重叠替换并新增 ID]
  E --> C
  E --> F[达到 merge 数或无 pair 停止]
查看流程图文本
flowchart LR
  A[按文档预切分] --> B[UTF-8 字节列表]
  B --> C[统计相邻 pair 频次]
  C --> D[选择频次最高 pair]
  D --> E[非重叠替换并新增 ID]
  E --> C
  E --> F[达到 merge 数或无 pair 停止]

重复串要特别小心。对 aaaaa 合并 aa,统计上相邻 pair 出现四次,但从左向右非重叠替换后应得到 [aa,aa,a],实际序列只减少两个元素。不能把相邻 pair 的计数直接当作替换次数;优化计数器时尤其容易因此出现负数。

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

这是实验中的真实核心代码。训练每一轮重新扫描所有段,易读但成本高;大语料可以先把相同预切分段聚合成“段→出现次数”,统计 pair 时加权。进一步维护 pair 到受影响段的索引,仅重新计算这些段的变化;堆中的旧频次需要惰性失效或显式更新,不能默认弹出的永远有效。

编码使用训练顺序 ​

给新文本编码时,先恢复字节序列,再按已学 merge rank 执行可用合并。不能根据新文本的局部频次重新选择 pair,否则同一句话的编码会依赖正在处理的文档集合。本地实现依次遍历 merge 表并扫描每段,牺牲速度换取容易检查的确定性。对于训练产生的无环 merge 依赖,这一顺序能正确构造新 token。

流式读取文件不等于可以任意切块独立编码。若块边界切断一个可合并片段,分词结果会改变;若切断 UTF-8 字符,文本解码也可能出错。可行边界是已确认的文档或预切分边界,或在分块时保留足够上下文并消除重复。特殊 token 应先按明确策略拆开,禁止普通 BPE 跨其边界合并。

如何评估一个 tokenizer ​

首先测往返:对中文、emoji、换行、制表符和未见字符,应满足 decode(encode(text)) == text。字节级起点能覆盖未见字符,但任意采样出的字节序列不一定是有效 UTF-8;生成显示可用替换字符,数据编码往返则应严格保真。

其次测压缩:固定验证文本,报告原始字节数、token 数、bytes/token、编码吞吐和词表大小。不同 tokenizer 的每 token perplexity 不宜直接比较,因为预测单位改变了。扩大词表通常缩短序列,却增加 embedding 与输出层参数:不共享权重时约为 2Vd,logits 的临时内存也与 BTV 成正比。不能只优化 token 数。

最后测边界行为:文档分别训练与拼接训练、特殊 token、长重复串、频次平局、序列化重载。词表和 merge 表必须与模型 checkpoint 一起保存;相同 ID 在另一个 tokenizer 中可能代表完全不同的字节。

本地练习与自测 ​

运行全部测试后,用同一语料训练 num_merges=0,16,32,64,对留出的文档比较 token 数。不要在验证文档上训练 merge,再宣布压缩率提升代表泛化。默认字符模型可先跑通;BPE 模型通过 python3 labs/llm/train.py --tokenizer bpe --steps 300 启动。

如果词表含有“abc”,编码时能直接最长匹配吗?

不能普遍用最终词表最长匹配代替 BPE rank。BPE 的定义依赖有序 pair 合并,相互竞争的合并路径可能使最终分段不同。应按所选 tokenizer 的精确定义实现,并用构造的冲突例验证;“能还原原文”只证明可逆,不证明编码符合该 tokenizer。

来源:官方 A1 2025 分词实验、BPE 子词论文。下一章:Transformer。