Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tokenizer: 文本如何变成 token id?

这一节准备先回答下面几个问题:

  1. 字符串是如何被输入到 LLM 中的?
  2. token 到底是什么?它和“字”或“词”是什么关系?
  3. 为什么今天的大模型通常不直接按 word level 处理,而更常见是 subword level?

Q1: 字符串是如何被输入到 LLM 中的?

这是这一节最核心的问题。

模型本身并不能直接理解字符串。
从计算的角度看,神经网络最终能处理的是:

  • 数值
  • 向量
  • 张量

而原始文本首先只是字符序列。
所以在把文本送入模型之前,必须先把它变成可以计算的离散符号,再进一步变成数值表示和连续向量。

如果把这条链路写完整,可以先记成:

字符串 -> tokenizer -> token -> token id -> embedding -> 模型主体

这里面其实包含了几层不同的对象:

  1. 原始字符串
  2. tokenizer 切分后的 token
  3. token 在词表中的编号,也就是 token id
  4. token id 经过 embedding 层后得到的向量

这几层非常容易混在一起,所以最好拆开看。

第一步: 字符串先被切成 token

tokenizer 的第一件事,是把原始字符串切成模型使用的离散单位,也就是 token。

例如,一段文本不会直接进入 Transformer,而是先被 tokenizer 拆成一串 token。
这些 token 不一定等于“一个字”,也不一定等于“一个完整单词”,它们只是 tokenizer 定义出来的基本处理单位。

第二步: token 再映射成 token id

模型内部不会直接处理 token 字符串,而会处理它们在词表中的编号。

这里就会引出一个很重要的概念:词表,也就是 vocab。

词表可以理解成:

  • tokenizer 允许使用的那一套离散符号集合
  • 以及这些符号到整数 id 的映射关系

也就是说,词表本质上是在维护:

token <-> token id

例如,一个非常简化的词表可以写成:

"我"    -> 0
"喜欢"  -> 1
"学习"  -> 2
"AI"    -> 3
"<unk>" -> 4

这里:

  • 左边是 token
  • 右边是 token 对应的整数 id

所以 tokenizer 输出的通常不是 embedding,而是一串 token id。

第三步: token id 再通过 embedding 层变成向量

到了这一步,离散的 token id 才会进入 embedding 层,被映射成连续向量。

如果词表大小记作 $V$,embedding 维度记作 $d$,那么 embedding 矩阵通常可以写成:

$$ E \in \mathbb{R}^{V \times d} $$

其中:

  • $V$ 表示词表大小
  • $d$ 表示 embedding 维度

这个式子的直觉非常重要。它表示:

  • 词表里一共有 $V$ 个 token
  • 每个 token 都对应 embedding 矩阵中的一行向量

所以从实现角度看,一个 token id 进入 embedding 层,本质上就是在 embedding 矩阵中“查一行”。

例如,如果一个 token 的 id 是 17,那么 embedding 层返回的就是 embedding 矩阵第 17 行对应的向量。

vocab 和 embedding 的关系到底是什么?

这部分非常值得单独说清楚,因为很多时候最容易混淆的就是这里。

可以简单概括成:

  • vocab 决定“有哪些 token,以及它们分别编号是多少”
  • embedding 决定“这些 token 被表示成什么向量”

也就是说:

  • vocab 是离散符号空间
  • embedding 是连续表示空间

两者一一对应,但不是一回事。

更进一步说:

  • tokenizer 负责把字符串变成 token
  • vocab 负责把 token 变成 token id
  • embedding 层负责把 token id 变成向量

所以 tokenizer 的工作并不是“直接产生语义向量”,而是先把文本送进模型可以处理的离散接口中。

为什么 embedding 层本身也很重要?

虽然 embedding 层看起来像“查表”,但它其实已经是模型参数的一部分。

因为 embedding 矩阵中的每一行向量,都会在训练过程中被更新。
也就是说,模型不仅在学习“怎么组合 token”,也在学习:

  • 哪些 token 的表示更接近
  • 哪些 token 在语义或统计关系上更相似

所以如果用一句话总结这个核心问题,我觉得可以写成:

字符串先经过 tokenizer 被切成 token,再通过词表映射成 token id,最后经过 embedding 层变成模型真正处理的连续向量。

一个最小例子

如果只看抽象定义,token -> token id -> embedding 这条链很容易停留在概念层面。
所以不妨先看一个非常简化的例子。

假设原始输入字符串是:

我喜欢AI

现在假设某个 tokenizer 把它切成了下面三个 token:

["我", "喜欢", "AI"]

再假设它对应的词表是:

"我"    -> 0
"喜欢"  -> 1
"AI"    -> 2
"<unk>" -> 3

那么这一步之后,字符串就会被表示成 token id 序列:

[0, 1, 2]

接下来,假设 embedding 维度是 4,那么 embedding 矩阵可以写成:

$$ E \in \mathbb{R}^{4 \times 4} $$

其中:

  • 第一个 $4$ 表示词表大小
  • 第二个 $4$ 表示 embedding 维度

为了方便说明,假设这个 embedding 矩阵的每一行分别是:

$$ E = \begin{bmatrix} 0.2 & -0.1 & 0.5 & 0.7 \ 0.9 & 0.3 & -0.4 & 0.1 \ -0.2 & 0.8 & 0.6 & -0.5 \ 0.0 & 0.0 & 0.0 & 0.0 \end{bmatrix} $$

那么:

  • token id 0 对应 "我",取出第 0 行
  • token id 1 对应 "喜欢",取出第 1 行
  • token id 2 对应 "AI",取出第 2 行

于是输入序列 [0, 1, 2] 进入 embedding 层之后,就会变成下面三个向量:

$$ \begin{aligned} e_0 &= [0.2, -0.1, 0.5, 0.7] \ e_1 &= [0.9, 0.3, -0.4, 0.1] \ e_2 &= [-0.2, 0.8, 0.6, -0.5] \end{aligned} $$

到了这一步,模型真正看到的就不再是字符串 "我喜欢AI",也不再是离散编号 [0, 1, 2],而是这些连续向量组成的矩阵。

如果把它写成更紧凑的形式,就是:

$$ X \in \mathbb{R}^{3 \times 4} $$

其中:

  • $3$ 表示序列长度,也就是 token 个数
  • $4$ 表示 embedding 维度

这个例子虽然非常简化,但它已经把最关键的几层关系串起来了:

  • 原始字符串
  • token 序列
  • token id 序列
  • embedding 向量序列

后面进入 Transformer 之前,模型真正处理的就是这些向量,而不是原始文本本身。

Q2: token 到底是什么?

token 可以先粗略理解成:
模型在文本处理中使用的基本离散单位。

但这个单位不一定等于“一个字”,也不一定等于“一个完整单词”。

例如在不同 tokenizer 里,一个 token 可能是:

  • 一个汉字
  • 一个英文单词
  • 一个单词的一部分
  • 一个标点
  • 一个空格相关的片段

所以更准确地说,token 是 tokenizer 根据某种规则切分出来的文本片段。

这也是为什么“token 数”和“字数”或“单词数”通常并不相等。

Q3: 为什么今天通常是 subword,而不是直接按词切分?

如果完全按单词切分,看起来很自然,但会遇到很多问题:

  1. 词表会非常大
  2. 生僻词、新词、拼写变化很难处理
  3. 多语言场景下词表管理会非常麻烦

所以现在更常见的做法,是让 tokenizer 处理到 subword 级别,也就是“比单词更小,但又不是纯字符”的单位。

这样做的好处通常包括:

  • 词表规模更可控
  • 未登录词更容易被拆解处理
  • 可以在表达能力和词表大小之间做折中

例如一个较长的英文单词,可能会被拆成多个更常见的片段;而中文里,很多 tokenizer 的行为又会更接近字级别,但不一定严格等于“每个汉字一个 token”。

所以从实际效果上看,subword tokenizer 更像是一种工程上非常有效的折中方案。

Q4: 这一节之后最重要的收获是什么?

如果只保留最关键的几件事,我觉得是:

  1. 字符串进入 LLM 之前,要先经过 token -> token id -> embedding 这条链路。
  2. token 不等于“字”或“词”,它是 tokenizer 定义出来的离散单位。
  3. vocab 是 token 到整数 id 的映射,它决定了系统内部的离散符号空间。
  4. embedding 层负责把 token id 变成连续向量,它和 vocab 一一对应,但和 tokenizer 不是同一个层次的东西。
  5. 今天的大模型通常更常用 subword tokenizer,因为它在词表大小和表达能力之间更平衡。

下一节再继续回到模型结构本身,讨论一个预训练模型通常由哪些部分组成。