赞
踩
对于GPT Tokenizer,论文《Language Models are Unsupervised Multitask Learners》中介绍了一种字节级编码作为LLM的标记化机制:
The vocabulary is expanded to 50,257. We also increase the context size from 512 to 1024 tokens and a larger batchsize of 512 is used.
最终结论是LLM的词汇扩展到50257,上下文可以看到1024个tokens,也就是在transformer的注意力层中,每个token都可以关注序列前最多1024个token。
标记化是将字符串或文本转换为标记序列的过程。字节对编码算法不算很复杂,下面我们可以从头开始构建它。
在进行构建之前,我们先简要了解一下tokenization的复杂性,许多看起来只是神经网络架构或大型语言模型本身的问题,实际上都是标记化的问题,可以从源头追溯到问题所在,当LLM出现以下问题时,通常是由分词引起的:
所以分词是很多问题的根源,我们将在文章末尾再回顾这些问题,现在我们先跳过它,进入下面这个网络应用(https://tiktokenizer.vercel.app/)
在这个网站中分词结果会用JavaScript很直观地展现出来,在左边框中随意输入一些内容(注意右上角我们选择的是GPT-2):
Tokenization is at the heart of much weirdness of LLMs. Do not brush it off. 127 + 677 = 804 1275 + 6773 = 8041 Egg. I have an Egg. egg. EGG. 很高兴见到你。我是OpenAI开发的大规模语言模型ChatGPT。如果有任何疑问,请随时问我。 for i in range(1, 101): if i % 3 == 0 and i % 5 == 0: print("FizzBuzz") elif i % 3 == 0: print("Fizz") elif i % 5 == 0: print("Buzz") else: print(i)
这些标记被不同的颜色区分开,例如第一个词Tokenization被标记成了30642,和1634
注意,每个token前面的空格也是token的一部分,GPT-2对英语句子的分词似乎没什么问题,但我们看下面的数学运算,677实际上应该划分为一个token但却分成了两个,其他数字也有这种情况。
同样,字符串Egg
在句子开头时被划分成了两个标记,但前面有空格时又可以准确划分成一个标记,大写变小写也可以划分成一个标记…以上这些情况都有可能产生不同的划分
语言模型必须从将要训练的所有互联网文本的原始数据中学习,它必须在神经网络的参数中对它们进行分组,并且仅仅根据数据模式来理解,这些都是非常相似的。
接着我们还测试了中文的分词结果,分词器将这个句子分成了很多的Token,这比同样的英文句子会多很多,这就意味着我们对完全相同的句子在处理中文时需要使用更多的Token,这样的话就增加了文本的序列长度因此,然后在transfomer的注意力中。当这些标记尝试捕捉信息时,很容易耗尽最大上下文长度而无法捕捉更多有效信息。
基本上所有的非英语文本在transformer的角度看序列都变长了,这也就是LLM在非英语问题上表现得不如英语好的原因,这与用于分词器和分词本身的训练有关。
最后的例子是一个执行FizzBuzz的Python代码片段,在这里,所有的单独的空格都有单独的标记,这无疑也增加了token序列的长度
而对于GPT-4,它的分词情况就好很多,对Python中空白字符的处理有了很大改进
所以从CPT-2到GPT-4的Python编码能力的提高不仅仅是语言模型、架构和优化细节的问题,而且也来源于分词器的设计以及它如何将字符组合成标记。
下面我们来构建分词器,记住我们的目的是什么,我们想要将字符串输入到语言模型中,所以我们需要以某种方式将字符串标记为机器能够看懂的整数,然后,,我们将使用这些整数来查找向量查找表,并将这些向量作为输入送到转换器中。这其中的难点是,我们不只是想支持的英文字母,而是适应不同种类的语言。
例如对于下面这个句子:
你好!
在Python中,这些字符串是不可变的Unicode(统一码)序列,我们可以通过在Python中使用ORD函数来访问给定单个字符的Unicode
例如传入单个字符"h"所得到的Unicode代码点是104
Input:ord("h")
Output:104
那么句子"你好!"
的统一码为:
[ord(x) for x in "你好!"]
#output: [20320, 22909, 65281]
那么为什么我们不能简单地使用这些整数而不需要任何标记化呢?主要原因有几点:
因此Unicode定义了三种类型的编码,UTF-8、UTF-6和UTF-32
我们将"你好!"
编码成UTF-8
list("你好!".encode("utf-8"))
#output: [228, 189, 160, 229, 165, 189, 239, 188, 129]
然而,如果我们只是简单地使用UTF-8这些字节流,这就意味着词汇长度只有256个可能的标记,那么所有的文本都会被拉伸成很长的字节序列,尽管嵌入表会很小,因此我们必须使用字节对编码算法()进行压缩。
下面是一个字节对编码算法的例子:
假设我们要编码如下数据
aaabdaaabac
- 1
字节对“aa”出现次数最多,所以我们用数据中没有出现的字节“Z”替换“aa”得到替换表
Z <- aa
- 1
数据转变为
ZabdZabac
- 1
在这个数据中,字节对“Za”出现的次数最多,我们用另外一个字节“Y”来替换它(这种情况下由于所有的“Z”都将被替换,所以也可以用“Z”来替换“Za”),得到替换表以及数据
Z <- aa Y <- Za YbdYbac
- 1
- 2
- 3
我们再次替换最常出现的字节对得到:
Z <- aa Y <- Za X <- Yb XdXac
- 1
- 2
- 3
- 4
由于不再有重复出现的字节对,所以这个数据不能再被进一步压缩。
经过压缩,原来的token长度11变成了5,词汇长度由5变成了7
通过这种方式,我们可以迭代地压缩我们的序列,同时创造新的token。
以相同的方式,对于文本UTF-8编码的字节序列,我们可以找出最常出现的字节对,用新的token替换它,通过这种方式我们将得到一个压缩的训练数据集,以及一个用于将任意序列编码的算法,并使用这个词汇表进行解码,将其解码回字符串。
如下例子所示,文本从一篇博客中截取的
# text from https://www.reedbeta.com/blog/programmers-intro-to-unicode/
text = "Unicode! 声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。