拓展阅读

教学 BPE:字符怎样合并成 Token

观察一组简单语料如何决定字符对合并。训练规则与应用规则分开,保留原文本可逆。例子刻意不实现真实模型的字节编码、特殊Token和预处理。

状态:自检已实际通过 · 环境:Python标准库 · 性质:合成/模拟教学实验
对应讲义 1.2 · 保存Python脚本

沿着对象与状态理解本章

先沿着实际对象追踪:用户文字不是消息包,消息包不是Token数组,编号也不是Embedding。把这四种对象分别认出来,后面的shape和API角色才有落点。下图的词表是独立教学约定;不要把它当成所有Tokenizer的真实结果。

下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。

动态图解 · F03

文字、字节、Token、编号是四件事

为什么不能把“一个汉字”当成“一次生成”?

Token是分词规则选出来的片段,编号是它在词表中的地址。一个Token可以跨越多个字符,也可能只覆盖字符编码的一部分。教学中把“学习”合成一个Token,只是明确的演示规则,不能当成所有模型的实际分词。

字节存储和传输文本的单位;UTF-8里的汉字通常占多个字节。
Token分词器处理的片段单位。
词表片段与编号的对照。

输入与适用条件:本图使用人为词表:PAD0、我1、爱2、学习3、你4、!5;不演示真实BPE训练。

1 / 6 · 原文字

第1步,共6步

窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。

文字、字节、Token、编号是四件事:原文字原文字我爱学习 / 你爱文本片段我/爱/学习;你/爱词表对应我1、爱2、学习3、你4整数序列[1,2,3] 与 [4,2]解码校验按同一规则还原片段明确边界不等于按字分词,也不是语义打分教学流程;高亮表示当前步骤,不表示真实耗时比例。

先确定待编码原文,两句之间不能直接接起来当作同一人的上下文。

本步状态由图形与说明共同给出。

默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。

为什么需要

弄清计数单位,可避免上下文预算、输出长度和界面刷新混为一谈。

怎样实现

用目标模型配套分词器编码和解码,并分开观察字符数、字节数、Token数。

可能的代价与误解

更换分词器后沿用旧编号,会读到错误的词表项。

如何判断是否做对

对实际输入做一次往返编解码,并按所用分词器规则处理特殊Token。

怎样观察与验证

先运行默认样例,再改变语料或合并次数。相同文本可能得到不同教学Token数,说明计数依赖分词器。不要把结果用于商业模型计费。

运行命令

python3 reading-notes/experiments/01_tokenization.py

完整教学代码

"""教学级字符 BPE:只演示合并与编码,不是商业模型的 Tokenizer。"""
from collections import Counter
from typing import Iterable


def apply_merge(tokens: list[str], pair: tuple[str, str]) -> list[str]:
    out, i = [], 0
    while i < len(tokens):
        if i + 1 < len(tokens) and (tokens[i], tokens[i + 1]) == pair:
            out.append(tokens[i] + tokens[i + 1])
            i += 2
        else:
            out.append(tokens[i])
            i += 1
    return out


def train_bpe(texts: Iterable[str], num_merges: int = 8) -> list[tuple[str, str]]:
    if isinstance(texts, (str, bytes)):
        raise TypeError("texts 必须是字符串集合,不是单个字符串")
    if not isinstance(num_merges, int) or isinstance(num_merges, bool) or num_merges < 0:
        raise ValueError("num_merges 必须是非负整数")
    data = list(texts)
    if any(not isinstance(t, str) for t in data):
        raise TypeError("训练样本必须是字符串")
    corpus = [list(t) for t in data]
    merges = []
    for _ in range(num_merges):
        counts = Counter(pair for seq in corpus for pair in zip(seq, seq[1:]))
        if not counts:
            break
        # 相同频次时固定排序,保证演示可复现。
        pair = min(counts, key=lambda p: (-counts[p], p))
        if counts[pair] < 2:
            break
        merges.append(pair)
        corpus = [apply_merge(seq, pair) for seq in corpus]
    return merges


def encode(text: str, merges: list[tuple[str, str]]) -> list[str]:
    if not isinstance(text, str):
        raise TypeError("text 必须是字符串")
    result = list(text)
    for pair in merges:
        result = apply_merge(result, pair)
    return result


def self_test() -> None:
    merges = train_bpe(["路径路径", "路径过滤", "路径规范"], 5)
    for text in ["路径过滤", "未知字符 x\n", ""]:
        assert "".join(encode(text, merges)) == text
    assert len(encode("路径路径", merges)) < len("路径路径")
    assert train_bpe([], 3) == []
    try:
        train_bpe("错误输入")
    except TypeError:
        pass
    else:
        raise AssertionError("应拒绝单个字符串作为语料集合")


def run_demo() -> dict:
    texts = ["exclude_paths", "exclude_path", "exclude_files", "include_paths"]
    merges = train_bpe(texts, 10)
    sample = "exclude_paths"
    tokens = encode(sample, merges)
    return {"性质": "字符级教学 BPE,非真实模型计费", "合并规则": merges,
            "文本": sample, "字符数": len(sample), "教学Token": tokens,
            "教学Token数": len(tokens), "可逆": "".join(tokens) == sample}


if __name__ == "__main__":
    import json
    self_test()
    print(json.dumps(run_demo(), ensure_ascii=False, indent=2))

运行结果示例

以下结果由随包脚本产生。它们不是LLM或产品性能数据。

{
  "性质": "字符级教学 BPE,非真实模型计费",
  "合并规则": [
    [
      "c",
      "l"
    ],
    [
      "cl",
      "u"
    ],
    [
      "clu",
      "d"
    ],
    [
      "clud",
      "e"
    ],
    [
      "clude",
      "_"
    ],
    [
      "a",
      "t"
    ],
    [
      "at",
      "h"
    ],
    [
      "clude_",
      "p"
    ],
    [
      "clude_p",
      "ath"
    ],
    [
      "e",
      "x"
    ]
  ],
  "文本": "exclude_paths",
  "字符数": 13,
  "教学Token": [
    "ex",
    "clude_path",
    "s"
  ],
  "教学Token数": 3,
  "可逆": true
}

输入关键词即可检索。

↑ ↓ 选择 Enter 打开Esc 关闭 · 仅本地搜索

阅读设置

阅读设置仅保存在本机浏览器,不上传任何数据。