跳转至

分词与词汇表(Tokenization)

分词(Tokenization)是 LLM 处理文本的第一步——将人类写的文字切分成模型能理解的最小单元(Token),并映射为数字 ID。分词策略的好坏直接影响模型的理解能力和计算效率。


为什么需要分词?

神经网络只能处理数字,不能直接看懂"你好世界"这样的文字。我们需要一套规则,把文字变成数字序列:

"你好世界" → 分词 → ["你好", "世界"] → 查表 → [3456, 7890]

这个"规则"就是分词器(Tokenizer),而"表"就是词汇表(Vocabulary)


三种分词粒度

1. 词级别(Word-Level)

最直觉的方式——按空格或标点切分。

"I love AI" → ["I", "love", "AI"]

问题

  • 词汇表爆炸:英语有几十万个单词,还有各种变形(loves, loved, loving...),词汇表极其庞大
  • OOV 问题:遇到训练时没见过的词(Out-of-Vocabulary),模型完全不认识,只能标记为 <UNK>
  • 中文灾难:中文没有天然的空格分隔,需要额外的分词工具,且效果不稳定

2. 字符级别(Character-Level)

把每个字符(字母/汉字)当作一个 Token。

"love" → ["l", "o", "v", "e"]

优点

词汇表极小(英语只有 26 个字母 + 标点),永远不会遇到 OOV 问题。

问题

  • 序列太长:一个单词就要拆成好几个 Token,计算成本暴增
  • 语义丢失:单个字符几乎没有语义,模型需要耗费大量算力去"拼"出单词的意思

3. 子词级别(Subword-Level) ⭐ 主流方案

核心思想: 既不按完整的词切分,也不拆到字符级别,而是找到一种介于两者之间的最优切分方式

"unhappiness" → ["un", "happy", "ness"]
"playing"     → ["play", "ing"]
  • 高频词保留完整(如 "the", "is")
  • 低频词拆分为有意义的子词片段(如 "un" + "happy" + "ness")

为什么子词法最好?

  1. 词汇表可控:通常 3 万~10 万个 Token 就够了
  2. 无 OOV:任何生僻词都能拆成已知的子词组合
  3. 保留语义:子词本身有意义("un" 表示否定,"ing" 表示进行时)

主流子词分词算法

BPE(Byte Pair Encoding)

BPE 是目前最主流的分词算法,被 GPT 系列、LLaMA、Qwen 等模型广泛使用。

核心思想

从最小的字符出发,反复合并出现频率最高的相邻字符对,逐步构建词汇表。

训练过程(构建词汇表)

假设我们的训练语料只有三个词及其出现频率:

"hug"  出现 10 次
"pug"  出现 5 次
"bug"  出现 12 次

第 0 步:初始化

将每个词拆到字符级别,初始词汇表为所有出现过的字符:

词汇表 = {b, g, h, p, u}
语料表示:h u g (×10), p u g (×5), b u g (×12)

第 1 步:统计所有相邻字符对的频率

(h, u) → 10 次
(u, g) → 27 次  ← 最高!
(p, u) → 5 次
(b, u) → 12 次

合并 频率最高的 (u, g) → 新 Token ug

词汇表 = {b, g, h, p, u, ug}
语料表示:h ug (×10), p ug (×5), b ug (×12)

第 2 步:继续统计

(h, ug) → 10 次
(p, ug) → 5 次
(b, ug) → 12 次  ← 最高!

合并 (b, ug) → 新 Token bug

词汇表 = {b, g, h, p, u, ug, bug}
语料表示:h ug (×10), p ug (×5), bug (×12)

如此反复,直到词汇表达到预设大小(如 32000 个 Token)。

分词过程(使用词汇表)

给定一个新词,按照训练时的合并顺序依次尝试合并:

"hugs" → 先拆到字符:h u g s
       → 发现 (u,g) 可合并:h ug s
       → 没有更多可合并的了
       → 最终结果:["h", "ug", "s"]

WordPiece

WordPiece 被 BERT 使用,思路和 BPE 类似,但合并策略不同。

对比 BPE WordPiece
合并依据 频率最高的字符对 使语言模型似然提升最大的字符对
标记方式 直接合并 非首子词用 ## 前缀标记

WordPiece 的分词示例:

"playing" → ["play", "##ing"]
"unhappy" → ["un", "##happy"]

## 前缀表示"这个子词不是一个新词的开头,而是前一个词的延续"。

SentencePiece

SentencePiece 不是一种新的分词算法,而是一个与语言无关的分词工具包

关键创新

SentencePiece 将空格也视为普通字符(用 表示),而不是把空格当作天然的分词边界。这使得它对任何语言(包括中文、日文等没有空格的语言)都能统一处理。

"I love AI" → ["▁I", "▁love", "▁AI"]
"我喜欢AI" → ["▁我", "喜欢", "AI"]

SentencePiece 内部支持 BPE 和 Unigram 两种算法,被 LLaMA、T5 等模型广泛使用。


Byte-Level BPE:现代 LLM 的标配

GPT-2 起,OpenAI 引入了 Byte-Level BPE,它将 BPE 的基础单位从字符进一步下沉到字节(Byte)

  • 任何文本(无论什么语言、什么符号)本质上都是字节序列(0~255)
  • 基础词汇表只有 256 个字节,完全无需预处理
  • 通过 BPE 合并,从字节逐步学到子词、词、甚至短语

优势

  • 真正的零 OOV:任何 Unicode 字符都能分解为字节
  • 语言无关:中英文、Emoji、代码、数学公式……全部统一处理
  • 无需预分词:不依赖空格或任何语言特定规则

GPT-3/4、LLaMA 2/3 等现代主流模型均采用这种方案。


特殊 Token

分词器除了普通的文本 Token 外,还会定义一些特殊 Token,用于标记结构性信息:

特殊 Token 含义 用途
<BOS> / <s> 序列开始(Beginning of Sequence) 告诉模型"从这里开始"
<EOS> / </s> 序列结束(End of Sequence) 告诉模型"到这里结束,停止生成"
<PAD> 填充(Padding) 将不等长的序列补齐到相同长度
<UNK> 未知词(Unknown) 极少使用(子词法几乎不会 OOV)
<|im_start|> 对话角色开始 用于区分 system / user / assistant

词汇表大小的权衡

词汇表大小是一个关键的设计选择:

词汇表小(如 8K) 词汇表大(如 128K)
序列长度 更长(同一句话拆的 Token 更多) 更短(更紧凑)
Embedding 参数 更少 更多
语义粒度 更细 更粗(可能一整个词就是一个 Token)
罕见词处理 拆得更碎,但都认识 可能整个保留

现代 LLM 的词汇表大小通常在 32K ~ 150K 之间:

模型 词汇表大小 分词算法
GPT-2 50,257 Byte-Level BPE
GPT-4 ~100,000 Byte-Level BPE (cl100k)
LLaMA 2 32,000 SentencePiece (BPE)
LLaMA 3 128,256 Byte-Level BPE (tiktoken)
Qwen 2 151,643 Byte-Level BPE

为什么中文模型偏好大词汇表?

中文字符多、词组丰富。如果词汇表太小,一个中文词可能被拆成 3-4 个 Token,既浪费上下文窗口长度,也增加了推理成本。扩大词汇表让常见中文词能作为独立 Token 存在,显著提升中文处理效率。