Transformer 解剖:从 Attention 到推理系统

第 10 章 Tokenizer 工程:BPE / WordPiece / SentencePiece

作者 杨艺韬 · 5,674 字 · 发布于 · 更新于

第 9 章我们用字符级 tokenizer 在《全唐诗》上训了一个 mini-GPT。字符级简单粗暴,对中文小数据集够用,但真实工业里的大模型几乎全都不用字符级——它们用 BPE / WordPiece / SentencePiece / Tiktoken 等子词级(subword)tokenizer。

为什么?字符级看起来简洁,但代价是序列变长——同样一段英文句子,字符级要 100+ 个 token,BPE 大约 20-30 个。attention 的计算量和序列长度的平方成正比(O(N²)),token 数变成 5 倍,attention 计算就变成 25 倍;模型其余部分(FFN、投影)则随 token 数线性增长。词级 tokenizer 序列短,但词表会爆炸(英文词形、专名、新词数以十万计,加上各种变形拼写,OOV 率永远压不下来)。

子词 tokenizer 是这两个极端的中间方案:把常见词整体当一个 token,把不常见词拆成若干个常见子词,把任何字符都能拆到字节级别兜底。这一章我们把它的来龙去脉讲清楚。

读完这章你能:

  • 解释字符级、词级、子词级三类方案各自的得失;
  • 详细推导 BPE 的合并算法,并理解 WordPiece、SentencePiece、Tiktoken 各自的微妙差别;
  • 看懂主流模型 tokenizer 的关键参数(词表大小、特殊 token、规范化策略);
  • 给一个新模型选 tokenizer 时,知道在「英文为主」「中文为主」「多语言」「代码」等场景下分别该选什么;
  • 解释为什么 GPT-4 给「strawberry 有几个 r」数错。

10.1 三类 tokenizer 的得失对照

先把三类方案的特点放一起对比:

维度 字符级 词级 子词级
词表大小 100-1000 数十万-数百万 30K-256K
序列长度(同一段英文) 长(4-5×) 短(1×) 中(1.3×)
OOV(未登录词)问题 无(任何字符都能编码) 严重 可控(拆成已知子词)
形态学信息保留 全部保留 部分丢失(变形不归一) 部分保留
中文友好度 好(一字一 token) 中(中文分词难) 中(依赖词表)
多语言友好度 好 差 中
训练成本 不需要训练(直接字符表) 需要分词器 需要训练
模型表达力 弱(字符 → 语义路径长) 强(一词一 token) 强(折中)

直观一点:

flowchart LR
  CHAR["字符级<br/>序列太长<br/>表达力弱"] -.-> SUBW["子词级 (BPE / SP)<br/>2016 年至今的主流方案"]
  WORD["词级<br/>词表爆炸<br/>OOV 严重"] -.-> SUBW
  SUBW --> BPE[BPE]
  SUBW --> WP[WordPiece]
  SUBW --> SP[SentencePiece]
  SUBW --> TT[Tiktoken]

子词 tokenizer 是 2016 年 Sennrich 等人提出 BPE 之后才成主流的,核心思想都一致:给定大量文本,统计出最频繁的「字符片段」组合,把它们合并成单独的 token。常见词整体保留("the"、"and"),不常见的词被拆开("unbelievably" → "un" + "believ" + "ably"),生僻字符仍然能通过字节级兜底。

下面我们先把 BPE 讲透——它是其他几种方案的共同基础。

10.2 BPE 算法的来龙去脉

BPE(Byte Pair Encoding,字节对编码)原本是一种通用的数据压缩算法(Gage, 1994),最早用于压缩文件。Sennrich 等人在 2016 年的论文 Neural Machine Translation of Rare Words with Subword Units 把它改造成了 NLP 的子词分词方法——这是现代 tokenizer 的起点。

BPE 训练算法

给定一个语料库(一大堆文本),目标是训练出一个大小为 V 的词表,能用最少的 token 拼出所有常见词。

步骤如下:

  1. 初始化:把所有词拆成字符序列(或字节序列)。每个词后面加一个特殊标记(例如 </w>)表示词尾,避免「子词跨词」的混淆。

    "low"     → l, o, w, </w>
    "lower"   → l, o, w, e, r, </w>
    "newest"  → n, e, w, e, s, t, </w>
    "widest"  → w, i, d, e, s, t, </w>
  2. 统计每对相邻 token 的频率。比如:

    ("l", "o"): 2 次     ("o", "w"): 2 次     ("w", "</w>"): 1 次
    ("w", "e"): 2 次     ("e", "r"): 1 次     ("r", "</w>"): 1 次
    ("n", "e"): 1 次     ("e", "w"): 1 次     ("e", "s"): 2 次
    ("s", "t"): 2 次     ("t", "</w>"): 2 次  ("w", "i"): 1 次
    ("i", "d"): 1 次     ("d", "e"): 1 次
  3. 找出频率最高的对,把它合并成一个新 token。这里有六对并列 2 次,按实现的 tie-break 规则选定其一,比如 ("e", "s"),合并成 es。所有出现这一对的地方都被替换:

    "newest"  → n, e, w, es, t, </w>
    "widest"  → w, i, d, es, t, </w>
  4. 重复 2-3 步,直到词表达到目标大小 V,或者没有可合并的对。

每次合并都会让训练语料里的 token 总数减少。最常见的字符组合先被合并("th"、"the"、"ing" 这种),最终词表里既有单字符(兜底)、又有中等长度子词(处理常见词缀)、又有完整常见词("the"、"and")。

BPE 编码(推理)

训练完得到一个有序的合并规则列表。对一个新词「unbelievably」编码时:

  1. 拆成字符:u, n, b, e, l, i, e, v, a, b, l, y
  2. 按合并规则的顺序,扫描相邻对,找到能合并的就合并。
  3. 一直合并到没有可合并的为止。
  4. 最终得到 token 序列,比如 un + believ + ably(具体取决于训练数据)。
flowchart TB
  WORD["unbelievably"] --> CHARS["u,n,b,e,l,i,e,v,a,b,l,y<br/>(12 个字符 token)"]
  CHARS --> M1["合并规则 1: u+n → un"]
  M1 --> S1["un, b, e, l, i, e, v, a, b, l, y"]
  S1 --> M2["规则 2: a+b → ab"]
  M2 --> S2["un, b, e, l, i, e, v, ab, l, y"]
  S2 --> M3["规则 3: ab+l → abl"]
  M3 --> S3["un, b, e, l, i, e, v, abl, y"]
  S3 --> DOTS[...继续合并...]
  DOTS --> FINAL["un, believ, ably<br/>(3 个 token)"]

这就是 BPE 的全部工作流程。简单、确定性、可控。

重要的工程细节

字节级 BPE(GPT-2 引入):原始 BPE 在字符级操作,但字符在 Unicode 下有 100K+ 个,BPE 的初始词表会很大。GPT-2 改用字节级——每个字节(0-255)作为最小单元,无论什么 Unicode 字符都能被字节序列表示。词表初始 256(所有字节),通过 BPE 合并后变成 50K 量级。

字节级 BPE 的好处:任何字符串都能被编码——绝不会出现 OOV。代价是中文等多字节字符会被拆成多个字节,输出 token 数偏多(一个汉字通常 2-3 个 token)。GPT-3.5 / GPT-4 都用字节级 BPE。

正则预分词(pre-tokenization):BPE 训练前一般先用规则把文本切成「词」级片段(按空格、标点、数字等切开),合并只在片段内部进行。GPT-2 的 BPE 在文本里把每个空格都当作下一个词的前缀(比如 "hello world" 会被处理成 "hello"、" world",注意 " world" 前面带空格),这样 BPE 自然地学到「单词在句子中间」和「单词在句子开头」的区别。

10.3 WordPiece:BERT 的变种

WordPiece 由 Google 提出(最早用在语音识别里,后来用在 BERT),思路与 BPE 几乎一致,主要差别在合并准则:

  • BPE:合并频率最高的对
  • WordPiece:合并能让似然增益最大的对

具体地,WordPiece 选合并对的标准是(下式是 HuggingFace 教程给出的通行写法,Google 没有公开原始训练实现):

score(t1,t2)=freq(t1t2)freq(t1)⋅freq(t2)\text{score}(t_1, t_2) = \frac{\text{freq}(t_1 t_2)}{\text{freq}(t_1) \cdot \text{freq}(t_2)}

分子是「合并后的对的频率」,分母是「两个 token 各自的频率乘积」。这相当于「相对相关性」——只有当两个 token 经常一起出现、单独出现少时,合并才有意义。

直觉对比:BPE 倾向合并最常见的对(如 "th"、"e " 这种连贯字符),WordPiece 倾向合并「相关性最强」的对(更倾向语义单元)。

WordPiece 的另一个特殊之处:子词的标记。BPE 用 </w> 标记词尾;WordPiece 在子词前加 ## 表示「这是上一个 token 的延续」:

"unbelievably" → un, ##believ, ##ably
"playing"      → play, ##ing

这种标记让你看 token 序列时能立刻判断哪些是词的开头、哪些是延续——视觉上比 BPE 更清晰。BERT、DistilBERT、ELECTRA 都用 WordPiece。

实际效果上 BPE 和 WordPiece 在大多数任务上差不多——选哪个更多是历史习惯(BERT 系用 WordPiece,GPT 系用 BPE)。

10.4 SentencePiece:处理多语言的全能选手

到了 T5 / Llama / mT5 这一代多语言模型,BPE 和 WordPiece 都遇到了一个共同问题:它们假设输入已经被空格分词了——也就是说,它们用空格作为「词边界」的硬指示。

这在英文里没问题,但在中文、日文、泰文等没有空格的语言里崩盘。中文分词本身就是一门学问(jieba / pkuseg / LAC 等都有不同分词方案),强行先做中文分词再 BPE 会引入新的不一致。

SentencePiece(Kudo & Richardson, 2018)的核心思想:直接把空格当成一个普通字符,不做预分词。算法上仍然是 BPE 或 unigram,但输入是「整段原始文本」而非「已分词的词序列」。

SentencePiece 用一个特殊符号 ▁(U+2581)替代空格——比如 "hello world" 会被表示成 "▁hello▁world"——这样空格本身也成了 token 学习对象。

原始:  "hello world 你好世界"
SP:    ▁hello ▁world ▁你 好 世 界

这个简单的改动带来三个优势:

优势一:跨语言统一。同一个 SentencePiece tokenizer 可以同时处理英文、中文、日文、阿拉伯文——不需要任何语言特定的预处理。

优势二:可逆。从 token 序列恢复原文是无损的——「▁」对应空格、其他字符直接还原。传统的 BPE / WordPiece 依赖外部分词器先按空格切词(如 Moses tokenizer),原始空格信息在这一步就丢了,恢复时要做猜测(GPT-2 的字节级 BPE 把空格留在 token 里,同样可逆)。

优势三:和模型对齐。空格作为 token 让模型显式地建模「词边界」——比如生成时模型会自己决定下一个 token 前要不要加空格。

T5、Llama 1/2、Mistral、PaLM 等模型都用 SentencePiece;Llama 3、Qwen、DeepSeek 则转向了 GPT 式的字节级 BPE(见 §10.10)。

Unigram vs BPE 模式

SentencePiece 内部支持两种算法:BPE 和 Unigram。Unigram 模型(Kudo, 2018)的训练目标是让所有训练文本的对数似然最大化,从词表里逐步删除贡献最小的 token,直到词表大小达到目标。

算法 思路 优劣
BPE 自下而上:从字符开始合并 训练快,主流
Unigram 自上而下:从全词表开始删除 概率框架更纯粹,统计意义更清晰

实际上两种模式效果接近。Llama 默认用 BPE 模式的 SentencePiece,T5 用 Unigram 模式。

10.5 Tiktoken:GPT-4 的工程加速

Tiktoken 是 OpenAI 在 GPT-3.5 / GPT-4 时引入的 BPE 实现。它的算法和 GPT-2 的字节级 BPE 大体一致,但工程上做了几项关键优化:

  1. Rust 实现:核心 BPE 编码用 Rust 写。tiktoken 自己给的口径是「比同类开源 tokenizer 快 3-6 倍」,对比对象是 HuggingFace tokenizers,不是 GPT-2 那份教学用的 Python 实现。
  2. Hashing-based merge:用哈希表加速合并规则查找。
  3. 更大词表:GPT-4 用的 cl100k_base 词表 100K,比 GPT-2 的 50K 大一倍。

更大词表的好处:单 token 表达更多内容,序列变短。GPT-4 的 100K 词表里 "function"、"return"、"def" 等编程关键字都是单 token——这让代码生成的效率显著提升。

GPT-4o 之后启用的 o200k_base 词表更激进——200K 大小,对中文友好(多数常用汉字是单 token,一些常见双字词也是单 token)。GPT-4 本身用的仍是 cl100k_base。

flowchart LR
  G2["GPT-2: 50K BPE"] --> G3["GPT-3.5/4: 100K cl100k_base"]
  G3 --> G4["GPT-4o: 200K o200k_base"]
  G2 -.中文.-> CHN1[一字 2-3 token]
  G3 -.中文.-> CHN2[一字 1-2 token]
  G4 -.中文.-> CHN3[一字基本 1 token]

10.6 词表大小:怎么选

选词表大小是 tokenizer 工程的核心决定,影响以下维度:

词表越大:

  • 序列越短(每 token 表达更多内容)→ 推理 / 训练成本下降
  • embedding 参数越多(V × d_model)→ 模型参数膨胀
  • 罕见 token 训练信号稀疏 → 难训练充分

词表越小:

  • 序列越长 → 推理 / 训练成本上升
  • embedding 参数少 → 模型紧凑
  • 每个 token 训练信号充分 → 训练更稳定

主流模型的词表大小:

模型 词表大小 备注
GPT-2 50,257 字节级 BPE
GPT-3 50,257 同上
GPT-3.5 / GPT-4 100,277 cl100k_base
GPT-4o 200,019 o200k_base,中文友好
BERT-base 30,522 WordPiece
RoBERTa 50,265 BPE
T5 32,128 SentencePiece Unigram
Llama 1/2 32,000 SentencePiece BPE
Llama 3 128,256 tiktoken 的 100K + 28K 非英语扩充
DeepSeek-V2/V3 102,400 / 129,280 自训 BPE(config 的 embedding 行数,略大于实际 token 数)
Qwen 2.5 151,936 自训 BPE(Qwen2Config 默认值;7B 的 checkpoint 配置为 152,064)
Mistral 32,000 SentencePiece

可以看到一个明显趋势:2023 年以后,新模型的词表越来越多地扩到 100K+,主要驱动是多语言(特别是中文/日文/韩文)和代码——这些场景下大词表能显著缩短序列。

词表对中文的影响

英文中一个 token 对应一个完整的常用词("the", "function", "computer"),但 32K 词表收不下多少汉字:Llama 2 的词表里单个汉字的 token 只有 700 个,常用字大多在内,落到词表外的汉字会退回字节级,按 UTF-8 拆成 3 个字节 token。比如 Llama 2 的 tokenizer 处理「机器学习」:

"机器学习" → ["▁", "机", "器", "学", "<0xE4>", "<0xB9>", "<0xA0>"]   # 「习」不在词表里,拆成 3 个字节

而 Llama 3 的 128K 词表里常用汉字都是单 token,一部分常见词也是单 token:

"机器学习" → ["机", "器", "学习"]

同一段中文,后者的 token 数大约只有前者的一半(我们拿一段 53 字的中文实测:Llama 2 为 70 个 token,Llama 3 为 36 个)——具体比例随文本而变,但对中文用户这是实打实的节省。

DeepSeek-V3 的 128K 词表更进一步针对中文优化:预分词时把连续的汉字单独切成一段,常见词(如「人工」「智能」,乃至「机器学习」「人工智能」)都是单 token。

10.7 Tokenizer 怪象:为什么 GPT-4 数不准 r

「strawberry 有几个 r」这个问题在 2024 年让 GPT-4 这一代模型闹出大笑话——它们常回答「2 个」(错的,正确是 3 个)。这在很大程度上要归因于 tokenizer。

让我们看 GPT-4 的 tokenizer(cl100k_base)怎么处理 "strawberry":

import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
ids = enc.encode("strawberry")
print(len(ids))                          # 3
print([enc.decode([i]) for i in ids])    # ['str', 'aw', 'berry']

strawberry 被拆成 3 个 token:["str", "aw", "berry"]。模型「看到」的不是 10 个字符 s,t,r,a,w,b,e,r,r,y,而是 3 个意义抽象的子词。

要让模型回答「strawberry 有几个 r」,它需要:

  1. 在 token 序列里识别「r」字符
  2. 但 "str" 里有一个 r,"berry" 里有两个 r——这些在 tokenizer 表示下是黑盒
  3. 模型的训练数据里也没几个例子告诉它「str 这个 token 含 1 个 r」

结果就是模型瞎猜——它在训练数据里见过类似的「数字母」问题,但答对率取决于巧合。

flowchart LR
  STR["人类看到的: s-t-r-a-w-b-e-r-r-y<br/>(10 个字符, 3 个 r)"] --> TOK[tokenizer]
  TOK --> TOK_OUT["模型看到的: [str], [aw], [berry]<br/>(3 个 token,r 字符不可见)"]
  TOK_OUT --> ASK["问: 几个 r?"]
  ASK --> GUESS["模型瞎猜:<br/>2? 3? 取决于训练样本"]

这就是 tokenizer 怪象。它揭示了一个深刻的事实:模型不是在「字符」层面理解语言,而是在「token」层面。凡是涉及「子 token 操作」(拼写、字符计数、字母组合)的任务,模型都先天受限。

类似的现象:

  • 代码缩进问题:GPT-2 的词表里几乎没有「连续空格」token,8 格缩进会被拆成 7 个单空格 token 加一个 " return",代码序列被白白拉长。Codex 系列用的 p50k_base 和之后的 cl100k_base 都加入了连续空格 token,同样的缩进只占 1 个 token。
  • 数字 tokenization:早期 BPE 把数字也按频率合并(「1234」可能是单 token,「1235」拆成多个 token),导致数学能力受 tokenization 偶然性的影响。新一代模型在预分词正则里就把数字切开来缓解:Llama 3 按「最多 3 位一组」切(\p{N}{1,3},见 transformers-4815a0a/src/transformers/convert_slow_tokenizer.py:1922 的 tiktoken 转换器默认 pattern),Qwen 2 更彻底,每个数字单独成 token(\p{N},见 transformers-4815a0a/src/transformers/models/qwen2/tokenization_qwen2.py:33 的 PRETOKENIZE_REGEX)。
  • 多语言不平衡:英文一词一 token,泰文一字数 token——同样长度的 prompt 在不同语言下消耗的 token 量差几倍,API 成本显著差异。

10.8 用 SentencePiece 训练自己的 tokenizer

最后我们用代码训一个 SentencePiece tokenizer,体验一下完整流程。

安装:

pip install sentencepiece

训练:

import sentencepiece as spm

spm.SentencePieceTrainer.Train(
    input='train_corpus.txt',     # 输入文本(一行一句)
    model_prefix='my_tokenizer',   # 输出文件前缀
    vocab_size=32000,              # 词表大小
    model_type='bpe',              # 'bpe' 或 'unigram'
    character_coverage=0.9995,     # 字符覆盖率(默认值;官方建议中日文这类大字符集用它)
    pad_id=0, unk_id=1, bos_id=2, eos_id=3,   # 特殊 token id
)

生成的 my_tokenizer.model 是一个二进制文件,可以加载使用:

sp = spm.SentencePieceProcessor()
sp.Load('my_tokenizer.model')

# 编码
ids = sp.EncodeAsIds("Hello world! 你好世界!")
print(ids)        # [..., ..., ...]

# 解码
text = sp.DecodeIds(ids)
print(text)       # "Hello world! 你好世界!"——默认规范化 nmt_nfkc 把全角「!」转成了半角

# 看 token 字面值
print(sp.EncodeAsPieces("Hello world!"))  # ['▁Hello', '▁world', '!']

注意 ▁ 是 SentencePiece 表示词首空格的特殊字符。

几个工程经验

经验一:词表大小要匹配数据量。如果你训练数据只有几百 MB,词表 32K 已经够大;如果有几 TB 数据(如 Common Crawl 全量),可以用 128K 或更大。词表太大、数据太少 → 罕见 token 学不动。

经验二:字符覆盖率影响多语言。character_coverage=1.0 会保证训练语料里的所有字符都进字符表;SentencePiece 官方的建议是:英文、德文这类小字符集语言用 1.0,中文、日文这类大字符集用默认的 0.9995(舍掉极罕见字符以减小词表,被舍的字符映射为 <unk>,开启 byte_fallback 时则退回字节)。

经验三:从模型特性反推 tokenizer。如果模型主要做代码生成,要确保 tokenizer 对常见编程关键字(function、return、def、class)是单 token;如果主做中文 chat,要让常见中文词是单 token。这些都需要在训练 tokenizer 时调整数据分布或显式 force 添加 token。

经验四:tokenizer 训完不宜改。词表一变,embedding 表和输出层的形状都要跟着变:transformers 的 resize_token_embeddings 能把矩阵扩出新行,但新行是未训练的,得继续训练才有用。要大规模扩词表,要么从头训模型,要么走 vocabulary expansion 这类「扩词表 + 继续训练」的路线,代价都不小。

10.9 Tokenizer 选型经验

如果你今天为一个新模型选 tokenizer,下面是经验法则:

  1. 绝大多数情况选 BPE(SentencePiece BPE 或字节级 BPE)——多语言友好、可逆、生态成熟。Llama 1/2、Mistral 走 SentencePiece BPE,Llama 3、Qwen、DeepSeek 走字节级 BPE。

  2. 纯英文场景可以用 GPT 风格 Tiktoken——cl100k_base / o200k_base 是开箱即用的成熟选项,词表已经针对代码、空格、缩进等做了优化。

  3. 代码 / 数学密集场景:考虑专门词表——常见编程关键字、运算符(==、!=、++)作为单 token。也有模型只在通用词表上做少量追加,如 Code Llama 沿用 Llama 2 的词表,7B 的 vocab_size 从 32,000 变成 32,016,多出的位置主要给代码补全(infilling)用的特殊 token。

  4. 极致中文场景:用大词表(128K+)让常用中文词单 token 化。DeepSeek 2024 之后的词表是这条路线的代表。

  5. embed 模型用什么词表?:通常和它派生的基础模型一致(BGE v1.5 的中文、英文版本和英文 E5 基于 BERT,用 WordPiece;bge-m3、multilingual-e5 基于 XLM-RoBERTa,用 SentencePiece)。这是因为 embed 模型常常是在基础模型上 fine-tune 出来的,一般不换词表。

  6. 不要自己魔改 BPE——除非你有非常具体的研究目的。SentencePiece 已经把所有踩过的坑封装好了,自己实现版本会引入新 bug。

10.10 主流模型 tokenizer 速查

把今天主流模型的 tokenizer 信息汇总:

模型族 算法 词表大小 实现库
BERT 系 WordPiece 30K HuggingFace tokenizers
GPT-2 / GPT-3 字节级 BPE 50K tiktoken / HF
GPT-3.5 / GPT-4 字节级 BPE 100K (cl100k_base) tiktoken
GPT-4o 字节级 BPE 200K (o200k_base) tiktoken
T5 SentencePiece Unigram 32K sentencepiece
Llama 1/2 SentencePiece BPE 32K sentencepiece
Llama 3 tiktoken-based BPE 128K tiktoken
Mistral SentencePiece BPE 32K sentencepiece
Qwen 2.5 字节级 BPE 152K HF tokenizers
DeepSeek-V3 字节级 BPE 129K HF tokenizers
ChatGLM3 SentencePiece BPE 65K sentencepiece
GLM-4 字节级 BPE 151K tiktoken

可以看到一个微妙的变化:开源大模型正从 SentencePiece 转向 GPT 式的字节级 BPE——Qwen、DeepSeek 起步就是这条路,Llama 3、GLM-4 也从 SentencePiece 换了过来,实现上用 tiktoken 或 HF tokenizers。底层算法仍然是 BPE,差别在字节级兜底、预分词正则和工程实现。

本章小结

  1. Tokenizer 是文本和 token id 之间的桥——它的算法、词表大小直接影响模型表达力、训练成本、推理效率。
  2. 三类方案:字符级(简单但序列长)、词级(OOV 严重)、子词级(主流)。
  3. BPE 的核心是「贪心合并最频繁的相邻对」——从字符或字节出发,反复合并最常见的对,直到词表达到目标大小。
  4. WordPiece 用「似然增益最大」代替「频率最高」——和 BPE 几乎等价,但 BERT 系用它。
  5. SentencePiece 把空格当普通字符,不做预分词——多语言场景的常用选择,T5、Llama 1/2、Mistral 都用它;新一代开源模型则多转向字节级 BPE。
  6. Tiktoken 是 GPT-4 的工程加速版 BPE——Rust 实现 + 大词表(100K-200K),其中 o200k_base 对中文明显更友好。
  7. 词表大小要在「序列长度」和「embedding 参数量」之间权衡——主流趋势从 32K 扩到 128K-200K。
  8. Tokenizer 怪象:模型在 token 层面理解语言,「数字母」「字符级操作」类任务先天受限——「strawberry 有几个 r」是经典案例。
  9. 训练自己的 tokenizer:用 SentencePiece 库即可,关键参数是 vocab_size、model_type、character_coverage。
  10. Tokenizer 训完几乎不能改——词表大小写死了 embedding 形状。

第四部分到这里完结。我们从 50 行 Self-Attention 走到了完整的 mini-GPT,再到工业级 tokenizer——「自己从零搭一个能跑的小语言模型」的完整链路已经走通。

第五部分(11、12、13 章)我们升级视角到「规模化」——Scaling Laws 告诉我们模型、数据、算力之间该怎么配比,MoE 让万亿模型成为可能,长上下文之战推动了 4K → 1M 的演化。

延伸阅读

  • Sennrich et al., Neural Machine Translation of Rare Words with Subword Units, ACL 2016——BPE 在 NLP 的经典论文。
  • Kudo & Richardson, SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing, EMNLP 2018——SentencePiece 论文。
  • Kudo, Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates, ACL 2018——Unigram 模型的提出。
  • Radford et al., Language Models are Unsupervised Multitask Learners, 2019——GPT-2 论文,字节级 BPE 的引入。
  • HuggingFace 的 tokenizers 库与教程: https://huggingface.co/docs/transformers/tokenizer_summary
  • OpenAI tiktoken: https://github.com/openai/tiktoken
  • 苏剑林博客「科学空间」的【中文分词系列】——非常透彻的中文 tokenization 工程视角。