模型内部 · 分词器

分词器

字节级 BPE · GPT-2 · 真实合并规则

基础符号
256 个字节
合并规则
50,000
词表
50,257 256 + 50,000 + <|endoftext|>
这段文本
18 个字节 → 5 个词元

全部步骤

  1. 预分词

    为什么用子词:按单个字符切,序列会非常长;按整词切,词表会非常大,而且仍然会遗漏新词;BPE 介于两者之间。首先,一个正则表达式把文本切成单词、数字和标点,每一段都保留它前面的空格。在下方输入你自己的文本试试。

    5 个片段

  2. 字节

    每个片段变成它的 UTF-8 字节。GPT-2 为每个字节值指定一个看得见的替身字符,所以空格写作 Ġ。

    18 个字节

  3. BPE 合并

    在每个片段内部,查出每一对相邻符号的序号,把序号最小的一对融合。如此重复,直到没有任何一对在表中;罕见的词最后会变成好几个词元。

    13 次合并生效

  4. 词表 id

    最终的每个符号都是词表中的一项。合并得到的词元,其 id 等于 256 加上它的合并序号。id 0–255 是 256 个字节符号,按 GPT-2 自己的顺序排列,而不是按字节值(“.” 是 13,Ġ 是 220),50256 是 <|endoftext|>。

    ids [464, 3797, 3332, 319, 262]

  5. 解码

    解码是反过来的:每个 id 查出它的字节串,把字节拼起来,按 UTF-8 读取。一个词元可能在一个字符的中间结束(试试 “café 你好”),所以流式输出会先扣住这些字节,直到字符完整。

    id → 字节 → 文本

代码

import regex as re                  # pip install regex (\p{L} needs it)
pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")

def encode(text):
    ids = []
    for piece in pat.findall(text):
        word = [byte_char[b] for b in piece.encode("utf-8")]    # " " → "Ġ"
        while len(word) > 1:
            pairs = list(zip(word, word[1:]))
            best = min(pairs, key=lambda p: ranks.get(p, float("inf")))
            if best not in ranks: break                         # no rule left
            word = merge(word, best)                            # every a, b → ab
        ids += [vocab[s] for s in word]                         # 256 + merge rank
    return ids

def decode(ids):
    data = b"".join(token_bytes[i] for i in ids)        # a lookup and a join
    return data.decode("utf-8", errors="replace")

延伸阅读