分词 入门

LLM 处理文本前的第一件事,是把文本切成小块、给每一块查一个整数 ID。这些小块就叫token,这个切的过程叫分词(tokenization)。 4 个短主题:框定整个问题的字符 / 单词 / 子词的取舍;BPE(GPT 用的算法);WordPiece、SentencePiece、Unigram几个主要变种;以及为什么现代 LLM 的词表大小坐落在5 万到 20 万之间。

01

字符、单词,还是子词?

「单位」选什么,定下了之后所有代价和能力的形状。

任何「文本到数字」流水线里的第一个决策,也是后果最大的一个:什么算「一个 token」? 传统上三个答案,每个都有自己的缺陷,直到子词分词出现、悄悄把它们全换掉。

  • 字符级。每个字符是一个 token。词表小(ASCII ~100、Unicode 数千), 没有 OOV —— 任何文本都能切。代价:序列很长。100 词的句子变成 ~500 token。 序列长度对 Transformer 来说什么都贵 —— attention 是 O(n²)、KV 缓存随之线性增长。 能做,但慢。
  • 单词级。每个词是一个 token。100 词的句子就是 100 token。 单位自然、序列短。但代价不小:词表会爆炸(光英语,算上复数、屈折、复合词、人名, 就有几百万种独立词形),任何没见过的词都是「词表外(OOV)」—— 模型完全不知道该拿它怎么办。
  • 子词。混合做法:常见词保持整体,罕见词拆成可复用片段。 \"unhappiness\" 可能变成 \"un\" + \"happi\" + \"ness\"。词表保持小 (5 万 – 20 万),序列保持短,任何没见过的词都能用熟悉的片段拼出来。没有 OOV。 所有现代 LLM 都用这套。
切分:"The unhappiness ended."字符级 —— 22 个 tokenThe unhappiness ended.Theunhappinessended.字符级。词表小,但每句变成很长的序列。
1 / 3
三个层次:字符(词表小、序列很长)、单词(词表大、有 OOV 问题)、子词(现代折中)。

子词为啥赢,看另外两种各自的失败模式最清楚。纯字符级表示一切, 但 n² 的 attention 代价让长上下文推理不现实。纯单词级表示不了词表之外的任何东西, 一旦你拿网络文本去训,OOV 就爆炸了。子词是「需要的时候是字符、够用的时候是单词」。 现代 tokenizer 都是这个思路的变种。

一个细节。子词 token 的「合适大小」取决于语言。英语里一个典型 token 大约是半个词 (平均 3–4 个字符)。中文或日文里,一个 token 经常是单字、甚至一个 Unicode 字节。 代码里常常是子关键字:\"def\" + \" \" + \"function\"。多语言或多格式语料上训出的 tokenizer 会小心平衡这些 —— 平衡得不好时,你会看到诡异的 token 账单 (早期 GPT-3 上,中文 token 数能到同等英文的 3 倍)。

在 Transformer 里:tokenizer 是模型碰的第一样和最后一样东西。 输入文本 → token ID → embeddings → attention 层 → ... → 输出 logits → 下一个 token ID → 再解码回文本。Transformer 付的每一项代价 —— 显存、算力、延迟、API 价格 —— 都跟 tokenizer 切出来多少 token 成比例。 从这个意义上说,tokenizer 是「打扮成预处理步骤的架构决策」。

02

BPE —— Byte Pair Encoding

GPT 用的贪婪合并算法,1994 年原本是为数据压缩发明的。

Byte Pair Encoding 是 2026 年子词分词的主流算法。从 GPT-2 一直到 GPT-4o 全 GPT 系列、 以及大部分 LLaMA 系模型都用它。算法本身意外地小。

训练:
  1. 词表 = 语料里所有出现过的字符。
  2. 把每个词切成字符序列。
  3. 重复 target_vocab_size − len(vocab) 次:
     a. 统计语料里所有相邻 token 对。
     b. 找出最频繁的那一对 (a, b)。
     c. 合并成新 token "ab",加入词表。
     d. 在整个语料里应用这次合并。

切分(用训好的 merges):
  1. 把输入切成字符。
  2. 按训练时学到的顺序贪心应用 merges。
  3. 返回 token 序列。

整个算法就这些。在大语料上跑够久,就得到一个能用的 LLM tokenizer。

在小语料上的 BPE —— "low low low lower lowest"第 0 步 —— 字符lowlowlowlowerlowest从字符开始。数对:(l,o) 出现 5 次。
1 / 4
BPE 从字符开始,贪婪地合并最频繁的相邻字符对,一轮一轮,直到词表达到目标大小。

「byte pair encoding」里的「byte」藏了几个细节:

  • 从字节起步。GPT-2 / GPT-3 / GPT-4 用的是字节级BPE —— 从 256 个原始字节开始,不从 Unicode 字符开始。 意思是这个 tokenizer 不需要专门的「未知字符」码,任何语言的任何字节序列都是合法输入。
  • 预分词(pre-tokenization)。跑 BPE 之前,现代 tokenizer 会先按 空白和标点把文本切开,这样 BPE 就不会跨词边界做合并。OpenAI tokenizer 里有个正则 处理这件事:把连续的字母、数字、空白等分别成组。
  • 特殊 token。真实词表里还有「不是文本」的 token ——<|endoftext|>、像 <|im_start|> 这样的 对话格式标记、padding token 等等。这些不来自 BPE 合并,是手工加进来的, 在词表顶端预留几百个 ID。
  • 确定性。训好之后,tokenizer 是一个从文本到 token ID 的确定函数。 训练是随机的(语料里要做子采样),但推理不是。

从历史角度看,BPE 之所以赢,是一个关于「实用主义」的故事。在它之前也有子词方法 (Morfessor 等),但是 BPE 简单到 100 行代码就能实现、快到能在网页规模语料上训, 最后给出能用的 tokenizer。Sennrich 等(2016)把它从数据压缩搬到神经机器翻译上, 两年内成了默认。2019 年的 GPT-2 把字节级 BPE 钉死成了自回归 LLM 的标准。

在 Transformer 里:每个现代 LLM 都附带一个 BPE merges 文件, 里面就是当年学到的合并顺序的一张配对列表。tokenizer 启动时读一次、建一棵 trie 或哈希表, 然后在每一条 prompt 上贪心应用。tiktoken(OpenAI 的 tokenizer)能跑到几百 MB/s。 模型永远看不到文本 —— 只看到从这一头出去的整数 ID。

03

WordPiece、SentencePiece、Unigram

子词分词的三个变种,各自挑了一个略不同的合并准则。

BPE 不是唯一的子词配方。还有另外三种方法跟它并存,在某些场景甚至比它好用。 它们产生的 tokenizer 在性质上很相似 —— 定长词表、没有 OOV、常见词保持整体 —— 但在「词表怎么学、文本怎么切」的细节上各执一词。

  • WordPiece(Schuster 和 Nakajima,2012)。最早出自 Google 的日语 语音识别系统,被 BERT 采纳。跟 BPE 类似,但用「似然」准则替代「频率」:每一步选 那个能让训练语料(在一个 unigram 语言模型下)概率最大的合并。实战中得到的词表很像, 但 WordPiece 倾向于保留稍长一点的子词。续接片段会加 ## 前缀 —— 所以 BERT 的输出里到处是 ##ing##tion 之类。
  • Unigram(Kudo,2018)。一个概率视角的替代方案。先从一个大初始词表 (通常用 BPE 初始化)开始,然后通过 EM 优化、把最没用的 token 逐步剪掉。 一个词的「最佳切分」是「在 unigram 模型下总概率最大」的那一种。 独特之处:同一个词可以有多种合法切分。这一点在训练时做数据增强 (\"subword regularization\")很有用。
  • SentencePiece(Kudo 和 Richardson,2018)。不是新的合并算法, 而是另一种预处理:把输入当作包含空格的原始字节流。空格成了 token 的一部分 (渲染成 ,U+2581)。结果:完全语言无关,不需要单独的「分词」步骤, 对中文、日文这种没有空格的语言、还有代码,都一视同仁。SentencePiece 是一个框架,里面可以跑 BPE 或 Unigram;LLaMA 系模型一般用 SentencePiece + BPE。
单词 "unhappiness" —— 三种 tokenizerGPT(BPE)—— 2 个 tokenunhappinessunhappinessGPT 字节级 BPE。"unhappiness" → "un" + "happiness"。2 个 token。
1 / 3
不同配方挑出的合并不同,标记边界的方式也不同。"##" 表示「接上一段」、"▁" 表示「一个新词的开始」。

「##」和「▁」这套约定,踩坑之前总觉得有点随意:

  • BERT 的 WordPiece 假定输入已经按词切好。token 上的## 意思是「我接在上一个词后面、前面别加空格」。对用空格分词的语言很方便, 对不用空格的就尴尬。
  • SentencePiece 对空格不做任何假设。任意一个词如果在原文里前面有空格, 它的 token 就以 开头,否则不加。解码时只要把 标记去掉就行。对称、语言中立,代码里到处都不需要「这是不是续接片?」的特判。
  • GPT 的 BPE 居中:空格本身是 token 的一部分(第一空格经常被算作下一个 token 的开头,比如 \" the\" 是一个 token),但没有续接标记。

实际上你不太会自己挑 tokenizer;你继承基础模型用的那个。这些选择更多是历史包袱: GPT 选了字节级 BPE、BERT 选了 WordPiece、T5 和大多数多语言模型选了 SentencePiece。 差别在「跨模型家族微调」或者「给一个非标准领域训新 tokenizer」时才真正会影响你。

在 Transformer 里:tokenizer 是跟模型权重一起发布的一个独立工件。 一次典型的模型发布是(权重、tokenizer、config)三件套,tokenizer 是其中最小的 (几百 KB)。给一套权重配错 tokenizer,是深度学习里最隐蔽的失败之一 —— 模型会自信地输出垃圾。

04

现代词表大小 —— 5 万到 20 万

为什么「合适的词表大小」落在这个带里,以及在带内移动有什么代价。

LLM 时代的大部分时间(2019–2024),「词表大小」是没人动的旋钮 —— 5 万左右就是默认, 是 GPT-2 那次有影响力的发布定下来的,后来大部分模型都继承了。前沿在最近两年往上挪了, 现在大致在 5 万到 20 万这一带。下面是原因。

几个流行模型的具体数字:

  • GPT-2(2019):50,257 个 token。在以英文为主的网页文本上做字节级 BPE。 做了大约 5 年的默认值。
  • LLaMA 1 / 2(2023):32,000 个 token。为效率比 GPT 更小、用 SentencePiece BPE。 主要为英语优化,多语言支持有限。
  • GPT-4 / cl100k(2023):100,256 个 token。差不多把 GPT-2 翻倍, 对代码、非英语、现代技术名词覆盖大大改善。
  • LLaMA 3(2024):128,256 个 token。相对 LLaMA 2 一次大跳, 特别是为了改进多语言和代码覆盖。
  • GPT-4o(2024):大约 20 万个 token。又把前沿往前推了一截; 特别是非英文文本被压缩得厉害(同一段中文或日文,有时候比 GPT-4 少 4 倍 token)。
  • Claude 3 / Gemini(2024):未公开,传闻在 10 万 – 20 万这个区间。
现代 LLM 词表大小 —— 3.2 万到 20 万GPT-2 —— 50,257 个 tokenGPT-250KLLaMA 232KGPT-4 (cl100k)100KLLaMA 3128KGPT-4o200KGPT-2 在 2019 年上线时 ~50K BPE token,「标配」多年。
1 / 3
词表越大 → 每句英文用的 token 越少(推理更便宜),但 embedding 表更大、长尾里少见的 token 更多。

这些数字背后的取舍,是核心设计问题:

  • 词表越大 → 每段文本的 token 数越少。每个 token 平均覆盖更多字符。 10 万词表可以把 \"unhappiness\" 当成一个 token;3 万的就大概率拆开。 更少 token 意味着更便宜的推理(attention 计算更少、KV 缓存更小、用户 API 价更低)。
  • 词表越大 → embedding 表越大。embedding 表是 vocab_size × d_modeld_model = 4096 时,从 5 万 vocab 到 20 万 vocab, 表从 2 亿涨到 8 亿参数 —— 训练和存储上都是实打实的代价。现代 LLM 经常把 输入 embedding 和输出投影共享权重(\"tied weights\")来减一半。
  • 词表越大 → 罕见 token 的长尾越长。20 万 vocab 里会有几千个 训练中很少出现的 token。它们的 embedding 学不好,误触会在非常见输入上伤害质量 (\"glitch token\" 现象 —— 搜 SolidGoldMagikarp)。
  • 词表越大 → 非英语 / 代码覆盖越好。这是从 5 万跳到 20 万最大的单一理由。 GPT-3 上需要 1000 token 的中文段落,GPT-4o 上可能只要 250 token, 因为大词表里有许多中文专有的多字 token。代码也一样 —— 10 万+ 的词表能把printfdefconsole.log 这样的常见标识符 作为单 token 保留下来。

如果你哪天真要挑词表大小:挑「能让你关心的语言和格式拿到可接受 token/字符比」 的最小那个。只搞英语研究的话,3 万 – 5 万还能用。多语言或重代码场景, 2026 年的底线是 10 万+。没充分理由别低于这个;embedding 表很少是瓶颈, 而序列长度上的节省在每条 prompt 的每个 token 上累加。

在 Transformer 里:词表大小是把整个模型「夹起来」的那个参数。 它决定输入端的 embedding 维度和输出端分类头的维度;中间所有东西都跟它无关。 一个 32K 词表的 7B LLaMA-2 和一个 128K 词表的 7B LLaMA-3,中间的架构一模一样, 但后者在 embedding + 输出头上多了 9600 万参数。 tokenizer 的选择会一路影响到每一层。

整个前置知识栈到此为止 —— 数学、数据、优化、硬件、软件、词嵌入、分词。 所有零件都铺好了。下一篇 primer 终于开始组装它们:Transformer。