Skip to content

Token 是怎么被切出来的

Token 是怎么被切出来的:原始文本、token 列表与 token id 列表

在上一章里,我们把 tokenizer 粗略说成“把文字切成 token 的工具”。这一章稍微展开一点,看看常见 tokenizer,尤其是 BPE (Byte Pair Encoding),是怎么把一句自然语言变成模型能处理的数字序列的。

什么是 token

token 是模型处理文本时使用的基本单位。它不一定等于一个汉字,也不一定等于一个完整单词,而是 tokenizer 按照自己的规则切出来的文本片段。

比如这句话:

text
为什么天空是蓝色的?

在人看来,它是一句完整的问题;但在模型看来,它会先被拆成一段段 token。可能是:

text
为什么 / 天空 / 是 / 蓝色 / 的 / ?

也可能是:

text
为 / 什么 / 天空 / 是 / 蓝 / 色 / 的 / ?

英文也是类似的。比如:

text
unbelievable

它可能被切成:

text
un / believable

也可能被切成:

text
un / believe / able

所以,token 可以粗略理解成“模型词表里的文本积木”。模型不是直接按人类的字、词、句来处理文本,而是基于这些 token 接收输入、预测输出。

为什么需要 tokenizer

模型本身不直接理解“字”或“词”,它只能处理数字。也就是说,用户输入:

text
为什么天空是蓝色的?

因为模型只能处理数字(计算机中存储的都是 0 和 1 所表示的数字),在进入模型之前,必须先被转换成类似这样的数字序列:

text
[123, 456, 789, 321, ...]

这里每一个数字都对应词表里的一个 token。token 可以是一个字、一个词、一个词的一部分、一个标点,甚至是英文单词里的几个字母片段。

一个极简词表可以想象成这样:

text
token     id
-------------
<bos>     1
<eos>     2
为什么    128
天空      923
是        17
蓝色      2048
的        9
?        64

如果 tokenizer 把句子切成:

text
为什么 / 天空 / 是 / 蓝色 / 的 / ?

那么查这个词表后,就会得到:

text
[128, 923, 17, 2048, 9, 64]

tokenizer 要解决的问题就是:

text
原始文本 -> token 列表 -> token id 列表

模型看到的是最后的 token id,而不是原始文字。

如何获取一个词表用于分词

前面那个极简词表是为了方便理解。真实模型不能靠人工手写词表,因为自然语言里有大量常用词、罕见词、数字、符号、英文、代码和混合写法。词表如果完全靠人设计,很快就会遇到两个问题:

text
词表太小:很多文本会被切得很碎
词表太大:模型需要记住和预测的候选 token 太多,体积和计算开销都会变大

所以,实际做法通常是:先准备一批有代表性的文本语料,然后用某种分词算法从语料里自动学习出一套词表和切分规则。

这套算法要决定两件事:

text
词表里应该有哪些 token
一段新文本应该怎样被切成这些 token

常见方法包括 BPE、WordPiece、Unigram 等。这里重点看 BPE,因为它很经典,也足够直观。

BPE:一种经典的分词方法

BPE 的全称是 Byte Pair Encoding,可以理解成“不断合并高频相邻片段”的方法。

它的作用不是让模型“理解语言”,而是从训练语料里学出一套可复用的文本片段。换句话说,BPE 负责把连续文本拆成 token,再给每个 token 分配一个 id。模型后面做推理时,看到的就是这些 id。

它的直觉很简单:

如果某些字符经常挨在一起出现,就把它们合成一个更大的片段。这个片段以后就可以作为一个 token 使用。

比如一开始,文本可能被拆得很细:

text
天 / 空 / 是 / 蓝 / 色 / 的

如果训练语料里“天空”经常一起出现,BPE 可能会学到一条合并规则:

text
天 + 空 -> 天空

如果“蓝色”也经常一起出现,它也可能学到:

text
蓝 + 色 -> 蓝色

于是同一句话就可能被切成:

text
为什么 / 天空 / 是 / 蓝色 / 的 / ?

真实 tokenizer 的切法由训练语料和词表大小决定,不一定和人类直觉完全一致。

BPE 是怎么训练出来的

BPE 训练时大致会经历几个步骤:

  1. 准备大量文本

    先收集一批训练语料,例如网页、书籍、百科、问答、代码、对话数据等。

  2. 把文本拆成最小单位

    最小单位可以是字符,也可以是字节。很多现代 tokenizer 会从字节级别开始,这样几乎所有文本都能被表示出来,不容易遇到“这个字符不认识”的问题。

  3. 统计相邻片段出现频率

    比如在大量文本里统计:

    text
    天 + 空 出现了很多次
    蓝 + 色 出现了很多次
    machine + learning 出现了很多次
  4. 合并最高频的相邻片段

    如果“天 + 空”出现频率很高,就把它合并成一个新 token:

    text
    天空
  5. 反复统计和合并

    合并一次之后,再重新统计新的相邻片段。随着训练继续,token 会从很小的片段逐渐变成更大的常见片段。

    最后得到两样东西:

    text
    词表:记录有哪些 token
    合并规则:记录哪些片段可以按什么顺序合并

BPE 的直觉:把天和空、蓝和色这样的高频相邻片段合并成更大的 token

推理时如何切分

训练完成后,tokenizer 的词表和合并规则就固定了。推理时不会重新学习规则,只会按已经学好的规则切分文本。

还是这句话:

text
为什么天空是蓝色的?

tokenizer 会先把它拆成很小的基础片段,然后按照 BPE 规则,从高优先级到低优先级不断尝试合并。

可能得到:

text
为什么 / 天空 / 是 / 蓝色 / 的 / ?

也可能得到更细的切法:

text
为 / 什么 / 天空 / 是 / 蓝 / 色 / 的 / ?

如果词表里有“为什么”,它就可能是一个 token;如果没有,可能会拆成“为”和“什么”,甚至拆得更细。

切分完成后,每个 token 会被查表转换成 id:

text
为什么 -> 128
天空   -> 923
是     -> 17
蓝色   -> 2048
的     -> 9
?     -> 64

于是模型真正收到的是:

text
[128, 923, 17, 2048, 9, 64]

生僻词和没见过的词怎么办

BPE 的一个好处是,它不要求词表里必须提前存在完整词。

比如模型词表里没有:

text
超导量子计算机

它仍然可以把这个词拆成更小的片段:

text
超导 / 量子 / 计算 / 机

如果这些片段也不全在词表里,还可以继续拆:

text
超 / 导 / 量 / 子 / 计 / 算 / 机

字节级 BPE 甚至可以退回到更底层的字节表示,所以理论上几乎任何字符串都能被编码。代价是:越生僻、越不常见的内容,可能需要越多 token 才能表示。

token 切分会影响模型效果

tokenizer 并不是一个无关紧要的前处理工具。它怎么切文本,会直接影响模型后面要处理多少 token。

如果一句话被切得很碎,token 数就会变多。比如同样一段文本,某个 tokenizer 可能切成 20 个 token,另一个 tokenizer 可能切成 35 个 token。

token 越多,模型要读的片段就越多,生成时也要一步一步处理更多内容,速度可能会变慢。

但词表也不是越大越好。词表太大,虽然很多词可以直接作为一个 token,但模型需要记住和选择的 token 种类也会更多,整体会更复杂。

这就是 tokenizer 的取舍:

text
词表大:很多词可以直接表示,但整体更复杂
词表小:整体更简单,但文本可能切得更碎

一个直观总结

BPE tokenizer 可以理解成一套“文本压缩规则”:

text
常见片段       -> 尽量合成一个 token
少见片段       -> 拆成更小 token
完全陌生的内容 -> 继续拆到底层单位

它最终做的事情是:

text
自然语言文本
-> 按规则切成 token
-> token 查表变成 id
-> id 交给模型继续推理

所以,当我们说 MiniMind 在预测“下一个 token”时,意思不是它一定在预测下一个汉字或下一个完整单词,而是在预测 tokenizer 词表里的下一个文本片段。