← 返回

ai /

Tokenizer 定义与实现

基本概念

Tokenizer 是连接原始文本LLM 数值输入的编码系统,其基本任务是:

文本
→ Token 序列
→ Token ID 序列
→ LLM

例如:

"unbelievable"
→ ["un", "believ", "able"]
→ [412, 8271, 539]

一个完整的 Tokenizer 通常包含:

  • VocabularyToken ↔ ID 的映射;
  • Tokenization Rules:文本如何被切分为 Token;
  • Normalization:Unicode、空格等文本规范化规则;
  • Special Tokens:如 <BOS><EOS><PAD>
  • Decoder:Token 序列如何恢复为文本。

因此,Tokenizer 并不只是一个映射表。tokenizer.json 只是将这些规则和数据序列化保存下来。

为什么需要 Tokenizer

LLM 不能直接处理字符串,只能处理数值向量,因此首先需要把文本转换为离散 ID。

最直接的两种粒度都有明显问题。

字符 / Byte 级

例如:

unbelievable
→ u n b e l i e v a b l e

优点:

  • 词表很小;
  • 可以表示任意文本;
  • 几乎不存在 OOV(未知词)问题。

缺点:

  • 序列很长;
  • LLM 需要进行更多次计算。

Word 级

例如:

unbelievable
→ [unbelievable]

优点:

  • 序列较短。

缺点:

  • 词表会非常大;
  • unbelievableunbelievably 等需要分别存储;
  • 无法良好处理新词、代码和多语言文本。

因此现代 LLM 通常采用 Subword

unbelievable
→ un + believ + able

Tokenizer 本质上是在:

小词表 + 长序列
        ↕
大词表 + 短序列

之间寻找合适的计算折中。

Tokenizer 如何产生

所谓“训练 Tokenizer”,通常不是神经网络意义上的梯度训练,而是:

准备代表性语料
      ↓
确定预处理规则
      ↓
选择 Tokenizer 算法
      ↓
统计 / 优化语料
      ↓
Vocabulary + Rules
      ↓
保存 Tokenizer

例如选择:

algorithm = BPE
vocab_size = 32000

算法会根据训练语料自动学习哪些文本片段值得成为独立 Token。

因此可以将 Tokenizer 训练类比为:

LLM:
数据 + 网络结构 + 优化算法
→ Model Weights

Tokenizer:
语料 + Tokenizer 算法 + 超参数
→ Vocabulary + Rules

Tokenizer 一般在 LLM 正式训练前确定,并在训练和推理期间保持一致。

Tokenizer 基本算法

BPE(Byte Pair Encoding)

BPE 的核心思想是:

> 不断合并语料中最常共同出现的相邻片段。

例如训练语料:

low
lower
lowest

初始拆分:

l o w
l o w e r
l o w e s t

统计相邻组合:

(l, o) → 3
(o, w) → 3
(w, e) → 2
...

于是执行:

l + o → lo

得到:

lo w
lo w e r
lo w e s t

继续统计:

lo + w → low

最终可能得到:

Vocabulary:
l
o
w
lo
low
er
est
...

Merge Rules:
l  o  → lo
lo w  → low
e  r  → er
...

简化后的 BPE 训练算法:

vocab = initial_units(corpus)

while len(vocab) < target_vocab_size:
    pairs = count_adjacent_pairs(corpus)
    a, b = most_frequent(pairs)

    new_token = a + b

    corpus = merge(corpus, a, b)
    vocab.add(new_token)

因此 BPE 可以理解为一种根据语料自动学习压缩字典的方法。

常见片段:

the
ing
tion
人工
智能
return
function

更容易成为独立 Token。

Unigram

Unigram 的思路与 BPE 相反。

BPE 从很小的词表开始:

字符 / Byte
→ 不断添加 Token

Unigram 通常从一个较大的候选词表开始:

大量候选 Token
→ 不断删除价值较低的 Token

每个 Token 都带有一个概率:

Token       Probability
un          0.08
believe     0.03
able        0.05
...

对于:

unbelievable

可能存在多种切分:

un + believable
un + believe + able
u + n + believe + able

Unigram 根据整条 Token 序列的概率选择较优方案:

𝑃(𝑡1,,𝑡𝑛)=𝑖𝑃(𝑡𝑖)

训练过程中逐渐删除贡献较小的 Token,直到词表达到目标大小。

其特点是:

  • 不依赖固定的 BPE Merge 顺序;
  • 同一字符串可以存在多个候选切分;
  • 可以通过概率模型选择更合适的 Token 序列。

WordPiece

WordPiece 与 BPE 类似,也通过 Subword 构建词表。

区别主要在于 Token 的选择标准不同:

  • BPE 主要根据相邻片段出现频率;
  • WordPiece 更关注加入某个 Token 后对语言建模能力的改善。

BERT 是典型的 WordPiece 使用者。

例如:

playing
→ play + ##ing

其中 ## 表示该 Token 出现在单词内部。

Tokenizer 的设计

设计 Tokenizer 时主要需要决定以下内容:

训练语料
   ↓
基础单位:Byte / Character
   ↓
Normalization
   ↓
Pre-tokenization
   ↓
BPE / Unigram / WordPiece
   ↓
Vocabulary Size
   ↓
Special Tokens

其中最重要的几个因素是:

训练语料

Tokenizer 会直接反映训练数据分布。

如果大量数据是代码,可能学习:

return
const
def
::
==

如果大量数据是中文,则可能学习:

中国
人工智能
模型
系统

因此 Tokenizer 本身也是一种针对数据分布的编码设计

Vocabulary Size

词表越大:

文本
→ 更少 Token

但 Embedding 和输出层参数更多。

对于隐藏维度 𝑑

𝑁embedding=𝑉×𝑑

其中 𝑉 为词表大小。

因此:

Vocabulary ↑
Sequence Length ↓
Embedding Parameters ↑

不存在无限增大词表的收益。

评价指标

Tokenizer 不应只看“能否编码文本”,还应关注:

  • 平均 Token 数;
  • bytes / token
  • 中文、英文等不同语言的编码效率;
  • Code Tokenization 效率;
  • Vocabulary Size;
  • 对未知字符和异常输入的鲁棒性。

Tokenizer 的局限

传统 Tokenizer 有一个根本特点:

> Token 的划分规则在 LLM 训练之前就已经固定。

例如 BPE 可能始终将某个字符串编码为:

un + believable

而不会根据上下文动态改变计算粒度。

它本身并不知道:

  • 当前 Token 的语义;
  • 当前任务是否困难;
  • 哪些区域值得更多计算;
  • 哪些区域可以被高度压缩。

因此 Tokenizer 实际上人为规定了 LLM 的基础计算粒度

未来研究方向

Learnable Tokenizer

一种直接思路是:

> 让一个神经网络决定文本应该如何分块。

例如:

Bytes
  ↓
Small Neural Network
  ↓
Dynamic Chunks
  ↓
Large Language Model

与固定 BPE 不同,Chunk 可以根据上下文动态变化。

简单区域可以使用较大的 Chunk:

[The cat is sitting]

复杂区域则使用更细粒度:

[x] [9] [Q] [@] [7]

此时 Tokenization 不再是独立的数据预处理,而成为模型的一部分。

无固定 Tokenizer

进一步可以完全取消传统 Vocabulary:

UTF-8 Bytes
     ↓
Local Encoder
     ↓
Latent Patches
     ↓
Global Model

其中:

  • Local Model 处理字符、拼写等局部结构;
  • Global Model 处理语义、知识与推理。

核心思想从:

如何设计一个更好的 Tokenizer?

变成:

模型应该如何自动决定自己的计算粒度?

这类方法的优势包括:

  • 不依赖人工固定词表;
  • 无 OOV;
  • 对多语言更加自然;
  • 可以动态分配计算资源;
  • Global Model 可以在更短的序列上工作。

主要问题则集中在输出侧。

层次化计算

传统 LLM 基本在单一 Token 粒度上进行计算:

token
token
token
token
  ↓
Large Transformer

但语言本身具有天然层次:

Byte
 ↓
Character
 ↓
Subword
 ↓
Word
 ↓
Phrase
 ↓
Sentence
 ↓
Concept

因此一种可能的未来架构是:

Bytes
  ↓
Tiny Local Model
  ↓
Lexical Chunks
  ↓
Small Model
  ↓
Semantic Chunks
  ↓
Large Global Model

低层模型负责:

字符
拼写
局部语法

高层模型负责:

语义
知识
推理
规划

其目的不是单纯“去掉 Tokenizer”,而是避免让昂贵的大模型对所有低层信息进行同等级计算。

输出与 Diffusion

无 Tokenizer 或 Latent 模型的一个核心困难是输出。

传统自回归模型:

token_1
→ token_2
→ token_3
→ ...

优点是质量高、因果关系明确,但必须逐步生成。

Diffusion 可以同时更新多个位置:

[MASK] [MASK] [MASK] [MASK]
        ↓
[word] [MASK] [word] [MASK]
        ↓
[word] [word] [word] [word]

因此它有机会提高并行生成能力。

但 Diffusion 也存在:

  • 需要多次 Denoising;
  • 每一步可能重新计算整段序列;
  • 输出长度处理更困难;
  • 更少 Denoising Step 通常意味着质量下降。

因此目前的核心问题仍是:

Generation Speed
        ↕
Generation Quality

一种值得关注的结构是:

        Global Model
             ↓
      Semantic Latent
             ↓
     Local Diffusion Model
             ↓
     Token / Byte Sequence

Global Model 负责低频、高层的推理;

Local Decoder 负责高速展开具体语言。

从 Tokenizer 到 Latent Thought

更进一步,可以质疑另一个假设:

> 推理本身是否必须以语言 Token 的形式进行?

传统 Chain-of-Thought:

内部表示
→ 语言 Token
→ 内部表示
→ 语言 Token
→ ...

模型每进行一步推理,都必须把内部状态转换成人类语言。

但人的思考体验往往并不完全表现为逐词生成:

问题
 ↓
模糊的概念 / 联想 / 候选方案
 ↓
形成表达意图
 ↓
组织语言
 ↓
逐字、逐词说出

因此可以考虑:

Input
  ↓
Encoder
  ↓
Continuous Latent Thought
  ↓
Reasoning / Planning
  ↓
Communicative Intent
  ↓
Language Decoder
  ↓
Text

此时语言只是智能系统的一种输入输出形式,而不是推理本身。

这种架构可能允许连续表示同时保留多个候选方案,而不必像 Token Generation 一样过早做离散选择。

最终问题也从:

如何设计 Tokenizer?

逐渐演化为:

模型应该使用什么内部表示进行思考?

不同计算粒度之间应该如何组织?

语言是否只是内部认知状态的一种表达形式?

这已经从 Tokenizer 问题进一步扩展为 LLM 表示与推理架构问题