Elea Notes.

词条 · AI · 核心

分词

把字符串切成模型认识的最小单位。切法不按字也不按词,而是按统计上常一起出现的字节片段。

也称:分词、tokenization、tokenizer、分词器、byte-pair encoding、字节对编码

先看一个麻烦

你想让一个只会做数字运算的系统读一句话。数字运算的部分不用管,先解决一个更基础的问题:这句话得先变成数字。

一句中文有多少个「单位」?「今天天气不错」是六个字,还是「今天 / 天气 / 不错」三个词?换成英文,unhappiness 是一个单位,还是 un + happiness 两个?这个选择听起来像个无关紧要的实现细节,但它决定了系统能表示什么、表示得多贵,以及在什么地方会突然出错。

而且它非常难躲开。任何把文字喂给模型的系统,都必须先在这里做出一个决定。

几个朴素猜测

猜测一:一个字符一个数字。 简单、无歧义,任何文本都能表示,不会遇到「没见过的字」。

撞墙的地方是长度。模型每次要回顾前面所有单位,成本随长度增长得很快。按字符切会让一句普通英文变成上百个单位,而这上百个单位里绝大多数信息量极低,看到 t h,下一个是 e 的概率高得几乎不用算。系统把大部分算力花在猜拼写上,而不是猜意思。

猜测二:一个词一个数字。 长度问题立刻解决了,每个单位也确实携带意义。

撞墙的地方是词表。英文的词形变化、专有名词、拼写错误、代码里的标识符,加起来是无穷的。词表只能取有限大,于是任何没进词表的词都变成同一个 <UNK>。一个专门讨论某个库的对话里,那个库的名字可能整段都是 <UNK>,模型看不见它到底是什么。中文更麻烦:分词本身就有歧义,而且需要一个额外的分词器,那个分词器自己也会错。

猜测三:折中,取常见词进词表,剩下的按字符拆。 这个方向是对的,但「哪些算常见」不能靠人手定。语料一换(中文、代码、日志),该常见的东西就完全不同了。

真正的问题变成:能不能让数据自己决定切在哪里?

机制

BPE(byte-pair encoding,字节对编码)的做法是从最细的粒度出发,反复合并最常一起出现的一对。

先把文本看成字节序列。然后统计所有相邻字节对,把出现次数最多的那一对合并成一个新单位,记进合并表。重复这个过程固定次数,比如三万次,就得到三万条有序的合并规则。

初始:      l o w e r   n e w e r   w i d e r
最常见对:  e+r  ->  er
合并后:    l o w er   n e w er   w i d er
最常见对:  w+er ->  wer            (三处都出现)
合并后:    l o wer    n e wer   w i d er   ...

结果有两个性质。第一,常见的东西被合成一个单位,罕见的东西自动退化成更细的碎片,长度和词表大小之间的折中是数据算出来的,不是人定的。第二,因为底座是字节,任何输入都能表示,永远不会出现 <UNK>

实际的分词器在 BPE 前面还有一步,叫 pre-tokenization(预切分):先用一段正则把文本切成小片(通常在空格和标点处断开),再对每一片独立跑合并。这一步的存在是为了不让合并跨过词边界产生 the_quick 这种单位。

这两级结构带来一个容易被忽略的后果:

文本:  ...pipe          再追加:  line
                 |
                 v
分开切:  [_pipe] [line]     两个单位
合起来切: [_pipeline]        一个单位

同一段字符,在右边有没有后续文本的情况下,切法不同。文本中任意一个位置都不保证是单位边界,因为预切分的片边界会因为右边来了新字符而移动,片内的合并顺序随之改变。

为什么值得知道

这套机制解释了几件平时会觉得莫名其妙的事。

为什么按 token 计费而不按字。 因为 token 才是模型实际处理的单位。同样一句话,中文和英文的 token 数可以差两三倍:英文常见词往往一词一 token,而中文的一个字经常要两到三个字节、并不总能合并成一个单位。同一段内容翻成两种语言,价格不一样,原因在这里。

为什么模型数不清一个词里有几个字母。 它看到的 strawberry 可能就是两三个不可分的单位,字母层面的结构在输入端已经被抹掉了。这不是推理能力问题,是它压根看不到你说的那个东西。

为什么代码和日志特别费 token。 缩进、下划线、罕见标识符都不在高频合并里,会碎成很多小单位。同样的屏幕面积,代码的 token 数远高于散文。

为什么「只给新增的部分分词然后拼上去」是错的。 上面那个 _pipeline 的例子说明,追加文本会改掉旧文本尾部的切分。而 token ID 序列往往正是缓存的键,切分一漂移,之前的复用就静默失效了,不报错,只是变慢变贵。任何做增量处理的系统都得正面处理这个边界问题,而不能假设边界稳定。

收束

回到最初那个「一句话有多少个单位」的问题:没有先验答案,但可以让语料统计出一个。BPE 给出的答案是一个折中,常见片段合成一块以压缩长度,罕见片段留在字节层面以保证任何输入都能表示。

代价是切分依赖上下文。位置不等于边界,同一段文本在不同的右邻居下会切得不一样。这一条决定了缓存、增量更新、跨请求复用这些工程手段能做到什么程度,也是 KV Cache 这类优化在会话续写场景里最容易踩到的坑。

来源

  1. Neural Machine Translation of Rare Words with Subword Units · arXiv
  2. Language Models are Unsupervised Multitask Learners · OpenAI