大模型 Tokenizer:从字符到 Byte,再到大词表
Tokenizer 是大语言模型处理文本的第一步。
简单来说,它负责把一句话转换成模型能够理解的 Token。
例如:
text
我喜欢机器学习
↓
[我] [喜欢] [机器] [学习]
↓
[Token ID]
Tokenizer 的核心问题其实很简单:
怎么让文本既能被完整表示,又尽量使用更少的 Token?
一、为什么不能直接按词切?
最简单的方法是按照完整单词切分:
text
I love machine learning
↓
[I] [love] [machine] [learning]
这种方法很直观,但有一个明显的问题:新词怎么办?
如果词表里没有某个词,就会出现 OOV(Out-of-Vocabulary,登录外词)。
所以后来出现了 Character-level Tokenizer,把词拆成一个个字符:
text
machine
↓
[m] [a] [c] [h] [i] [n] [e]
这样基本不会遇到新词问题,但 Token 数量会明显增加,导致模型处理序列变长。
于是就有了折中方案:Subword Tokenizer 。
它既不把整个词作为一个 Token,也不把所有东西拆成单个字符,而是学习常见的词片段。
二、BPE 是怎么做的?
BPE(Byte Pair Encoding) 是目前非常经典的一种 Subword 方法。
它的基本思路就是:常见的组合,就把它们合并起来。
例如:
text
a + b → ab
ab + c → abc
训练 Tokenizer 时,它会根据训练数据学习这些合并规则。到了真正使用的时候,就按照已经学好的规则进行切分。
除了 BPE,还有 WordPiece 和 Unigram 等方法,它们的训练方式不同,但目的类似:找到一套合适的 Token,让文本表示更加高效。
三、为什么又出现 Byte-level?
Subword 已经解决了很多 OOV 问题,但还有一个问题:Unicode 字符非常多。
如果把大量 Unicode 字符直接作为基础单元,词表会变得很复杂。
UTF-8 提供了一个更简单的办法。
一个 Byte 只有 256 种可能:
0 ∼ 255 0 \sim 255 0∼255
因此,可以先把文本转换成 UTF-8 Byte,再在 Byte 上进行 BPE。
例如:
text
中
↓
UTF-8
↓
E4 B8 AD
所以:
Character ≠ Byte ≠ Token \boxed{\text{Character} \neq \text{Byte} \neq \text{Token}} Character=Byte=Token
"中"是一个字符,E4 B8 AD 是它的 UTF-8 编码,而 Token 是 Tokenizer 最终生成的离散单位。
四、什么是 BBPE?
BBPE 就是 Byte-Level BPE。
它的过程可以简单理解为:
text
文本
↓
UTF-8 Byte
↓
BPE 合并
↓
Token
↓
Token ID
它最大的优势是:
- Byte:作为一种通用的基础表示,让各种 UTF-8 文本都有基本的表示路径,解决基础表示问题。
- BPE:把常见的 Byte 序列组合成更大的 Token,从而减少 Token 数量,提高文本压缩效率。
五、为什么现在词表越来越大?
当 Tokenizer 能够表示各种文本之后,另一个问题就出现了:能不能用更少的 Token 表示相同的内容?
例如,如果一个常见词组总是一起出现,那么把它作为一个 Token,显然比拆成很多 Token 更高效。
因此,现在一些大模型的词表从早期的几十 K,逐渐扩大到 100K 甚至更大。
大词表的好处是:
text
词表更大
↓
可以容纳更多常见模式
↓
Token 数量可能减少
↓
文本表示更加紧凑
特别是在中文、多语言、代码和数字等场景中,大词表可能带来更好的 Token 压缩效果。
六、大词表是不是越大越好?
当然不是。
词表越大,也意味着更高的系统成本。
例如 Embedding 参数量大致为:
V × d model V \times d_{\text{model}} V×dmodel
其中 V V V 是词表大小。
所以:
text
词表变大
↓
可以容纳更多高频文本模式
↓
在合适的训练条件下,Token 数量可能减少
↓
但 Embedding / LM Head 等成本增加
因此,Tokenizer 实际上一直在做一个平衡:
覆盖能力 + Token 压缩效率 ↔ 词表成本 \boxed{\text{覆盖能力} + \text{Token 压缩效率} \leftrightarrow \text{词表成本}} 覆盖能力+Token 压缩效率↔词表成本
七、总结
从 Word-level 到 Character-level,再到 Subword、Byte-level BPE,Tokenizer 的发展可以简单理解为:
text
按词
↓ (解决语义粒度问题)
按字符
↓ (减少 OOV)
Subword
↓ (在词和字符之间寻找平衡)
Byte-level
↓ (提供通用的基础表示)
大词表
↓ (进一步减少 Token 数量)
最终,Tokenizer 要解决的并不是"怎么分词"这么简单,而是一个很实际的工程问题:
如何用尽可能少的 Token,稳定、高效地表示尽可能多的文本,同时控制词表带来的成本。