Token 是怎么被切出来的
在上一章里,我们把 tokenizer 粗略说成“把文字切成 token 的工具”。这一章稍微展开一点,看看常见 tokenizer,尤其是 BPE (Byte Pair Encoding),是怎么把一句自然语言变成模型能处理的数字序列的。
什么是 token
token 是模型处理文本时使用的基本单位。它不一定等于一个汉字,也不一定等于一个完整单词,而是 tokenizer 按照自己的规则切出来的文本片段。
比如这句话:
为什么天空是蓝色的?在人看来,它是一句完整的问题;但在模型看来,它会先被拆成一段段 token。可能是:
为什么 / 天空 / 是 / 蓝色 / 的 / ?也可能是:
为 / 什么 / 天空 / 是 / 蓝 / 色 / 的 / ?英文也是类似的。比如:
unbelievable它可能被切成:
un / believable也可能被切成:
un / believe / able所以,token 可以粗略理解成“模型词表里的文本积木”。模型不是直接按人类的字、词、句来处理文本,而是基于这些 token 接收输入、预测输出。
为什么需要 tokenizer
模型本身不直接理解“字”或“词”,它只能处理数字。也就是说,用户输入:
为什么天空是蓝色的?因为模型只能处理数字(计算机中存储的都是 0 和 1 所表示的数字),在进入模型之前,必须先被转换成类似这样的数字序列:
[123, 456, 789, 321, ...]这里每一个数字都对应词表里的一个 token。token 可以是一个字、一个词、一个词的一部分、一个标点,甚至是英文单词里的几个字母片段。
一个极简词表可以想象成这样:
token id
-------------
<bos> 1
<eos> 2
为什么 128
天空 923
是 17
蓝色 2048
的 9
? 64如果 tokenizer 把句子切成:
为什么 / 天空 / 是 / 蓝色 / 的 / ?那么查这个词表后,就会得到:
[128, 923, 17, 2048, 9, 64]tokenizer 要解决的问题就是:
原始文本 -> token 列表 -> token id 列表模型看到的是最后的 token id,而不是原始文字。
如何获取一个词表用于分词
前面那个极简词表是为了方便理解。真实模型不能靠人工手写词表,因为自然语言里有大量常用词、罕见词、数字、符号、英文、代码和混合写法。词表如果完全靠人设计,很快就会遇到两个问题:
词表太小:很多文本会被切得很碎
词表太大:模型需要记住和预测的候选 token 太多,体积和计算开销都会变大所以,实际做法通常是:先准备一批有代表性的文本语料,然后用某种分词算法从语料里自动学习出一套词表和切分规则。
这套算法要决定两件事:
词表里应该有哪些 token
一段新文本应该怎样被切成这些 token常见方法包括 BPE、WordPiece、Unigram 等。这里重点看 BPE,因为它很经典,也足够直观。
BPE:一种经典的分词方法
BPE 的全称是 Byte Pair Encoding,可以理解成“不断合并高频相邻片段”的方法。
它的作用不是让模型“理解语言”,而是从训练语料里学出一套可复用的文本片段。换句话说,BPE 负责把连续文本拆成 token,再给每个 token 分配一个 id。模型后面做推理时,看到的就是这些 id。
它的直觉很简单:
如果某些字符经常挨在一起出现,就把它们合成一个更大的片段。这个片段以后就可以作为一个 token 使用。
比如一开始,文本可能被拆得很细:
天 / 空 / 是 / 蓝 / 色 / 的如果训练语料里“天空”经常一起出现,BPE 可能会学到一条合并规则:
天 + 空 -> 天空如果“蓝色”也经常一起出现,它也可能学到:
蓝 + 色 -> 蓝色于是同一句话就可能被切成:
为什么 / 天空 / 是 / 蓝色 / 的 / ?真实 tokenizer 的切法由训练语料和词表大小决定,不一定和人类直觉完全一致。
BPE 是怎么训练出来的
BPE 训练时大致会经历几个步骤:
准备大量文本
先收集一批训练语料,例如网页、书籍、百科、问答、代码、对话数据等。
把文本拆成最小单位
最小单位可以是字符,也可以是字节。很多现代 tokenizer 会从字节级别开始,这样几乎所有文本都能被表示出来,不容易遇到“这个字符不认识”的问题。
统计相邻片段出现频率
比如在大量文本里统计:
text天 + 空 出现了很多次 蓝 + 色 出现了很多次 machine + learning 出现了很多次合并最高频的相邻片段
如果“天 + 空”出现频率很高,就把它合并成一个新 token:
text天空反复统计和合并
合并一次之后,再重新统计新的相邻片段。随着训练继续,token 会从很小的片段逐渐变成更大的常见片段。
最后得到两样东西:
text词表:记录有哪些 token 合并规则:记录哪些片段可以按什么顺序合并
推理时如何切分
训练完成后,tokenizer 的词表和合并规则就固定了。推理时不会重新学习规则,只会按已经学好的规则切分文本。
还是这句话:
为什么天空是蓝色的?tokenizer 会先把它拆成很小的基础片段,然后按照 BPE 规则,从高优先级到低优先级不断尝试合并。
可能得到:
为什么 / 天空 / 是 / 蓝色 / 的 / ?也可能得到更细的切法:
为 / 什么 / 天空 / 是 / 蓝 / 色 / 的 / ?如果词表里有“为什么”,它就可能是一个 token;如果没有,可能会拆成“为”和“什么”,甚至拆得更细。
切分完成后,每个 token 会被查表转换成 id:
为什么 -> 128
天空 -> 923
是 -> 17
蓝色 -> 2048
的 -> 9
? -> 64于是模型真正收到的是:
[128, 923, 17, 2048, 9, 64]生僻词和没见过的词怎么办
BPE 的一个好处是,它不要求词表里必须提前存在完整词。
比如模型词表里没有:
超导量子计算机它仍然可以把这个词拆成更小的片段:
超导 / 量子 / 计算 / 机如果这些片段也不全在词表里,还可以继续拆:
超 / 导 / 量 / 子 / 计 / 算 / 机字节级 BPE 甚至可以退回到更底层的字节表示,所以理论上几乎任何字符串都能被编码。代价是:越生僻、越不常见的内容,可能需要越多 token 才能表示。
token 切分会影响模型效果
tokenizer 并不是一个无关紧要的前处理工具。它怎么切文本,会直接影响模型后面要处理多少 token。
如果一句话被切得很碎,token 数就会变多。比如同样一段文本,某个 tokenizer 可能切成 20 个 token,另一个 tokenizer 可能切成 35 个 token。
token 越多,模型要读的片段就越多,生成时也要一步一步处理更多内容,速度可能会变慢。
但词表也不是越大越好。词表太大,虽然很多词可以直接作为一个 token,但模型需要记住和选择的 token 种类也会更多,整体会更复杂。
这就是 tokenizer 的取舍:
词表大:很多词可以直接表示,但整体更复杂
词表小:整体更简单,但文本可能切得更碎一个直观总结
BPE tokenizer 可以理解成一套“文本压缩规则”:
常见片段 -> 尽量合成一个 token
少见片段 -> 拆成更小 token
完全陌生的内容 -> 继续拆到底层单位它最终做的事情是:
自然语言文本
-> 按规则切成 token
-> token 查表变成 id
-> id 交给模型继续推理所以,当我们说 MiniMind 在预测“下一个 token”时,意思不是它一定在预测下一个汉字或下一个完整单词,而是在预测 tokenizer 词表里的下一个文本片段。