TT Lab
开始
学习 学习路径 课程

Transformer — 手算一遍注意力

同一句话,token 数却不一样

在 TT Lab 中继续学习

一句话总结

令牌(token)既不是字符,也不是单词。它是制作词表时确定的片段,而词表因模型而异,所以同一句话的令牌数也因模型而异。

为什么需要它

价目表和上下文上限的单位都是令牌。但在屏幕上能数的只有字符,所以一段时间内都是用“字符数乘以多少”来估算。问题是这种估算什么时候会失灵。

失灵的地方总是一样。放进韩语文档,发现比预想的贵得多。同样意思的英语文档却便宜。把日志原样放进去,明明文字很短,令牌却暴增。只有混入了表情符号的用户输入,长度计算才会对不上。

全都是同一个原因。令牌不是以字符为单位的。把哪些片段算作一个,写在词表里,而那个词表是根据训练资料制作的。如果是用英语居多的资料做的词表,英语的常见片段就是一个令牌,而韩语会被切得很碎。

更糟的是,这个差别是悄无声息的。不会报错。只是在账单和上下文上限上才显现出来。

为什么从字节开始

旧的方式是设一本单词词典,把词典里没有的单词统统当成一个 <UNK>。词典里没有的名字、拼写错误、新流行语、表情符号全都变成同一个令牌,也就无法还原了。

现在用的方式是从字节开始。把文本写成 UTF-8,任何字符都会变成 1 到 4 个字节,而字节从 0 到 255 只有 256 种。把这 256 个作为初始词表,就根本不会出现词表之外的字符。

"A".encode("utf-8")      # b'A'          → 1바이트
"가".encode("utf-8")     # b'\xea\xb0\x80' → 3바이트

韩语的成本就这样显现出来。一个谚文音节用 UTF-8 是 3 个字节。词表很小、可合并的规则没几条时,一个谚文字符就变成三个令牌。英文字母是 1 个字节,同样条件下一个字母就是一个令牌。起跑线就差了三倍。

比较同样五个字符的字节数的图。在两个谚文字符(一句问候语)之后再加上空格、h 和 i,这五个字符用 UTF-8 分别是 3、3、1、1、1 个字节,共 9 个字节,而 hello 共 5 个字节。没有任何合并规则时,一个字节就是一个令牌,所以分别是 9 个令牌和 5 个令牌

词表是怎么做出来的

BPE(Byte Pair Encoding) 所做的事,一句话就能写完——把最常相邻出现的两个合并成一个,这件事一直重复到词表达到想要的大小。

一轮是三步。

  1. 数一数当前列表中相邻的两个各出现了多少次。
  2. 挑出现次数最多的一对。
  3. 把这一对出现的位置换成一个新编号。新编号从 256 开始逐个增加。
ids = [104, 101, 108, 108, 111]      # "hello"
counts = {(104,101):1, (101,108):1, (108,108):1, (108,111):1}
# 전부 1회라 동점이다 — 동점을 어떻게 깰지 정해 두지 않으면
# 같은 글로 돌려도 어휘가 매번 달라진다.

平局的处理看起来微不足道,实际上很重要。如果训练不是确定性的,昨天做的词表和今天做的词表就会不同,那么昨天编码的数据,今天的模型会读成不同的内容。Hugging Face 的 BPE 说明也在同一个位置把规则钉死了。

停下的时刻就是词表大小。在 256 停下,就没有任何合并;在 5 万停下,合并规则就有 4 万 9 千多条。词表大小是制作模型时确定的值,改了它,嵌入表的大小也会随之改变。

不过词表大小只是上限。可合并的对用完了,就停在那里。因为把只出现一次的对合并,只会占掉一个词表格子,令牌一个也减少不了。资料少的话,即使要求 5 万,也会在几千处结束——把词表大小调大,与词表真的变大,是两回事。

编码要按学到的顺序

把规则都学完之后,对新文本编码时,必须原样遵守学到的顺序。

原因是后面的规则把前面规则的结果当作材料。先运行创建 256 号的规则,257 号的规则才能看到 256 号。如果把顺序打乱,即使规则列表相同,也会得到不同的令牌,用那种状态训练的模型,在服务中读到的就是另外的文本。

解码则相反。把新编号拆成两个,其中如果又有新编号,就再拆。没有剩余时只剩字节,把它按 UTF-8 读出来就是原文。和原文有一个字符不同都不行——这个往返一旦破裂,模型读了什么,就再也没有人能追溯。

在现场相遇的样子

第一,韩语服务的账单是预期的两三倍。因为按英语标准估算的每字符令牌数被原样套用来做了报价。修复的方法只有一个——用自己服务的真实句子来测量。

第二,毫无预告地撞上上下文上限。按字符数还有很大余地,按令牌却已经超了。如果截断长文档的代码是按字符计算的,在有些语言里会截得太多,在有些语言里又截不够。

第三,内容相同,只换了格式,令牌就增加了。缩进很深的 JSON、换行很多的日志、用空格对齐的表格文档,就是这样。空格和换行也是字节,词表里没有这个片段的话,每一个都会成为令牌。

第四,在表情符号和罕见字符上只是长度计算对不上,并不会报错。因为是字节层面,不会崩坏。只不过一个表情符号是 4 个字节,词表里没有的话,会吃掉三四个令牌。

第五,换分词器就必须重新训练模型。分词器不是模型之外的预处理工具,而是模型的一部分。编号变了,嵌入表的行就变了,那就是另一个模型。

实际工作中真正重要的事

下一项实验要做什么

一步步扩展 /root/work/tf-token/bpe.py。不是调用真实模型的分词器,而是只用标准库亲手实现同样的算法——这个 Pod 里没有 transformers,没有 tokenizers,没有 tiktoken,连 numpy 也只在 /opt/onnx-lab/bin/python 里才有。所以这里出现的数字,全都是用你做的词表测出来的。

从用 UTF-8 字节开头,到数相邻的对、合并一对、学习合并规则、按学到的顺序编码、准确解码。然后改变词表大小,测量同一篇文章的令牌数减少的曲线。

最后一步是本实验的要点。拿内容相同的韩语段落和英语段落,学出两套词表——一套是同时看两篇文章学出来的词表,另一套是只看英语学出来的词表。大小相同。然后用两套词表对同一个段落编码,把令牌数并排放在一起。只看英语学出来的词表里,一条合并谚文字节的规则都没有,所以韩语在字节数上几乎没有减少。尽管如此,解码依然是准确的。你会用数字看到,损失和稳健出自同一个结构。评分器会真正导入你的模块,每次用不同的输入直接检验函数,并与它另外计算的值对照。