我们清楚了 ChatGPT 模型的输入和输出,实际上就是将文字输入 ChatGPT 模型当中,然后再让模型预测出文字,本质上就是一个“文字接龙”式的语言模型。
而文字在进入 ChatGPT 模型之前,需要先经过一个转换,形成另外一种数据形式。在 ChatGPT 计算处理完之后,也需要将结果再做逆转换,形成文字形式,反馈给用户。这种转换包括两个步骤,Tokenizer 和 Embedding。本文主要介绍这两个模块。
Tokenizer#
ChatGPT 官方目前已经开始对服务收费了,收费方式主要是计算用户使用的 token 数,数量越多,收费越高。
例如,用户提问了一条文本,文字(带标点和各种特殊符号)共有 50 个字符,但耗费了 30 个 token,ChatGPT 根据输入生成一条回答,总计 200 个 token,逆转换为文字总共 300 个字,那么用户一共消费的 token 数就是 30+200=230 个。那什么是 token 呢?
token 是任何 NLP 神经网络 模型接收用户输入的最小粒度。 token 本身就是一些字符的组合,如英文单词#cat、中文词汇鞋子、英文词缀ly、中文汉字珂等,都可以看作是一个 token。
将用户输入的文本转换为 token 序列的过程就叫做 Tokenizer。它包含两部分,一部分是从文字转换为 token(设置在进入 ChatGPT 之前),另一部分是将 token 转换为文字,也就是逆转换(设置在 ChatGPT 模型输出之后)。
Tokenizer 算法 BPE 执行流程#
Tokenizer 目前最流行的实现方法是 字符对编码 BPE(Byte Pair Encoding) 算法,它也是 ChatGPT 采用的算法。BPE 算法是根据一份 token 词表(Vocabulary),将输入的文本拆解成若干个 token。其中,每一个 token 都存在于词表。
具体以如下一条输入模型的文本为例:
The newest car has a lower price and the lowest fuel.
这条文本中,包含了 53 个字符(包含字母、空格和标点,以及任何键盘可以打出的特殊符号,均输入 ChatGPT 中)。
一般地,模型训练所使用的词表中 token 数量大约从几万~几十万不等。假设 BPE 算法已经生成一个 Token 词表(Vocabulary) ,其部分词表 token 内容如下:
| #low | est | #new | er | #the | #car |
|---|---|---|---|---|---|
| #and | #fuel | #a | #has | #have | and |
| #thailand | #price | #dog | #old | #most | … |
BPE 算法就是根据上述 token 词表对文本进行匹配,从文本中拆分出存在于词表中的 token,将文本转换成如下的形式:
The newest car has a lower price and the lowest fuel.
==>
#The,#new,est,#car,#has,#a,#low,er,#price,#and,#the,#low,est,#fuel.
在这条例子中,文本被拆分成 15 个 token。由于英文单词是以空格形式进行分割的,因此,每一个单词的首字母都添加#为单词起始的标识,它可以理解为一个空格,不加#的token表示无法独立成词。一些单词被拆分成若干部分,如newest被拆分成两部分,#new 和 est。然后,模型就接收这样的 token 数据做进一步处理计算。
从上面的例子中,我们可以看出,token 中一般都是以非常高频的字符组合构成的,而且这些 token 往往具备一定的语义。例如,newest被拆解为#new 和 est,前半部分是单词词根,后半部分是英文形容词最高级。
同样地,ChatGPT 模型在回答用户问题,输出答案时,也是首先输出 token 序列,再将 token 序列反转为正常的自然语言文本,这个操作叫做 De-tokenization。它与 Tokenization 是完全互逆的操作。读者可以尝试把上面的 token 序列合并成完整的文本句子。
对于中文而言,常用汉字大约为 7000 个左右,而且文本之间不存在空格。因此,也可以采用上述的算法来完成,唯一的区别就是中文的 token 的开头不添加
#符号。一些极为常见的中文单词可能合并为一个 token,如我们,而考虑到词频,绝大多数中文依然以单字独立成 token。
Byte-level BPE 算法#
之前介绍的 BPE 算法是基于字符的,除此之外,还有一种基于字节的 BPE 算法(Byte-level BPE)。这种方法,主要是为了克服基于字符的 token 词表,由于各种特殊字符数量太庞大导致效果变差。
除了我们常用的中文外,ChatGPT 可以随意操作英文、日文、韩文、法文等至少二十多种文字。这些语言的文字和符号更是多种多样,有英文拉丁字母ABCDabcd,中文汉字千百花鸟风月,西里尔字母БГД,日语假名ピンイ,当然也包括很多 emoji 特殊符号💁👌🎍😍。
所有的字符在计算机中都是以 Unicode 编码格式实现的。
Unicode 编码是一种用于计算机表示全球范围内各种语言文字字符的标准编码方式,它为世界上所有的字符都分配了一个唯一的数字编号,解决不同国家和地区使用不同语言文字、字符集的问题。 Unicode 编码采用 16 进制表示,每个字符都有一个唯一的码点,例如汉字“中”在 Unicode 编码中的码点是U+4E2D,其中 U+ 表示 Unicode 编码,4E2D 是该字符的 16 进制码点。若以 UTF-8 编码为例,汉字“中”被转换为 3 个字节(byte) 的二进制数据:11100100 10111000 10101101。
Unicode 常用字符目前总量大约有十多万,如果直接基于字符形式,构造 token 词表的话,那么词表可能会变得非常庞大,达到几十万。过于庞大的词表会对 ChatGPT 模型产生很强的不确定性因素,让模型难以训练。
因此,Byte-level BPE 算法应运而生。这种算法的执行步骤和上述的 BPE 算法完全一致,唯一的区别在于,BPE 算法直接操作 Unicode 字符,而 Byte-level BPE 算法把文本的字节作为直接操作的对象。
由于操作的基本单位是字节而不是字符,因此即使是复杂的、多样化的文字系统(如中文、日文等),其基础元素也仅仅是256个可能的字节值。这相较于直接基于字符的BPE算法,其可能需要包含成千上万个不同的字符,词表大小显著减少。小型词表意味着模型训练和推理时的内存和存储需求大大降低。
例如,在 BPE 算法中,中 字被当作一个字符进行 token 匹配。而在 Byte-level 算法中,它被当作 3 个字符进行匹配(因其 Unicode 占用 3 个字节)。而英文字母如 p 则在两种算法中,都被当作一个字符处理,因为字母的 Unicode 编码只占用一个字节。所有的字节个数全部加起来不过 256 (即一个字节所表示的符号个数 )个,这对模型训练是一个巨大的利好。
Byte-level BPE 算法的代码链接:Byte-level BPE ↗,感兴趣的可以阅读一下。
BPE 的词表是如何训练得到的?#
BPE 的词表主要是根据训练文本语料统计得到的,训练的语料数量越大,得到的 BPE 词表越准确,越具有词根语义。
假设根据一份语料数据,我们可以统计得到如下词汇和其对应出现的次数。
| #lowest | 7 | #lower | 4 | #newest | 5 | #older | 5 | #newer | 4 |
|---|---|---|---|---|---|---|---|---|---|
| #and | 10 | #fuel | 4 | #a | 14 | #has | 4 | #oldest | 5 |
| #thailand | 3 | #price | 6 | #new | 7 | #old | 6 | … | … |
以上均为文本中存在的完整的词汇。
- 接下来,我们可以按字母进行统计,得到频率最高的字符对为标红的 “es”,共计出现 17 次。我们单独把 “es” 提出来,并把语料中的所有 “es” 看作一个整体。
- 再重复上面的过程,可以发现“est”可以看作是“es” 和 “t” 的结合体,总计也出现 17 次。因此,可以把“est” 看作一个整体,放入词表,并把语料中所有的 “est” 看作一个整体。
- 再重复上面的过程,可以发现,“#a” 的频率仅次于 “est”,为 14 次。因此,把 “#a” 放入词表中。
- 再重复,可以把 “er” 这个字符对提取出来。
- 以此类推,我们可以逐渐将高频的字符对提取出来,不断放入词表中。
- 当放入词表中的 token 数达到了预定的最大数 N 时(一般从几万到几十万不等),得到最终的词表,即可用于 BPE 算法的执行流程,拆分每一条文本为若干 token。
Tokenizer 的好处#
克服长尾效应 OOV#
在英文单词中,最常出现的 5000 个单词占据了实际使用量的 90%。而那些极低频的单词数量极多,但总共加起来的实际使用量也不超过 2%。这就是自然语言的长尾效应 , 这种现象也出现在其它语言中。
直接把极低频的单词和字符当作 token,本身意味着数据量的缺乏,会导致它有可能不在词表中(Out Of Vocabulary,OOV),对 NLP 模型的性能产生很大的影响。因此,引入 Tokenizer,采用 BPE 算法可以避免低频词作为 token。
例如,根据上述训练例子得到的词表,#strangest 这个词在训练语料中词频较低,可能不出现在 token 词表中,但 “#strang” 和 “est” 一定以较高的频率出现在 token 词表中。
多语言支持#
在早期,NLP 神经网络模型功能十分单一,且仅支持某一种语言。一个针对英文的文本分类模型,并不能支持中文的文本分类。而 BPE 算法,包括 Byte-level BPE 算法的设计,使得一份词表中包含了多种语言的字符,支持模型的多语言处理功能。
词嵌入|Embedding#
ChatGPT 的输入文字转换为 token 之后,还需要将 token 再转换为张量,这个过程叫做词嵌入( Embedding),同时 embedding 也指被转换后得到的张量本身。
在神经网络中,张量(Tensor)是指多维数组,它可以存储和处理大量的数据,是神经网络中最基本的数据结构。张量一般都以浮点数(小数的一种计算机表示形式)作为元素进行填充。
例如, 就是一个 形状的张量,是一个多维数组。
而向量(vector),就是高中数学中的概念,一般就可以看作是一维张量。
ChatGPT 从功能上看,是一个语言模型,但从结构上看,它是一个多层的、复杂的神经网络模型,每一层的神经网络都在进行浮点数张量(Tensor)的数字计算,而 ChatGPT 的输入是文字符号,token 也是文字符号。因此,token 需要先转换为浮点数字,再进入模型中进行计算。将用户输入的 token 转换为浮点数张量的过程,就叫做词嵌入(Embedding) 。当模型将结果计算完,也要将最终的浮点数转换为具体的 token,作为输出。
#The,#new,est,#car,#has,#a,#low,er,#price,#and,#the,#low,est,#fuel.
仍以上述句子为例,假设 token 词表(Vocabulary)的数量总共为 N,每一个 token 都用一个 M 维的浮点数张量表示,其中每一个 token 都对应了一行张量,即该 token 的 embedding 表示。
例如,#price 这个 token 对应的 embedding 是一个 M 维向量:
这组数据就可以传入 ChatGPT 模型中,做模型的训练和使用。所有的词表组成了一个 维度的张量,如下图左侧方阵。
根据例子中的前四个 token,我们可以将其对应的 embedding 抽取出来,按 token 的顺序排列成一组 的张量,这组张量即可输入 ChatGPT 进行操作,图中白色部分表示词表中的词汇未匹配到 token 序列。换句话说,它完成了由 token 到其对应张量的映射。
在实际模型当中,一次性输入给模型的 token 数量 并不是无限大的,例如,在 ChatGPT 的 gpt-3.5-turbo 版本中,最大的输入 token 数量为 4097 个,超出这个范围则会被模型自动截断。
在自然语言中,文字的顺序是非常重要的,“我喜欢你”,和 “你喜欢我” 表达的含义是完全不同的。所以,ChatGPT 考虑到模型的每个 token 相互之间的顺序不能改变,需要明确地在输入端标识出每个 token 的位置张量(Position Embedding),其大小和 token 的 embedding 是一致的。两者以如下形式融合起来:。
其中, 是 token embedding 矩阵, 是 position embedding 矩阵。而其中的 是一个上下文矩阵。根据第 3 节的语言模型原理,模型在建模时有上下文限制,针对当前的一个 token,模型只能关注该 token 之前的 个 token。因此,,它是一个单位矩阵。
假设 token 数量小于模型可接收的最大数量,那么,上述公式可以退化为:。
由此,即可输入 ChatGPT 模型进行计算。
第 1 节中提到,ChatGPT 是有多轮对话能力的。
在模型中,需要从输入端将输入1(Q1)、输出1(A1)、输入2(Q2)等部分信息区分出来。这几个部分信息分别叫做一个 segment,其中每一个 segment 都包含了多个 token,它们共享了同一个 segment embedding。具体方式如下:
上图中做了假设:Q1、A1、Q2 分别包含了 4 个 token。当然,在实际输入中,每个 segment 包含的 token 数都是可以灵活变化的;上面对话的轮数仅有两轮,而实际输入中,对话轮数可以非常多,形如 Q1、A1、Q2、A2、…、Qn, 只要所有 segment 对应的 token 总数加起来不超过模型允许的最大 token 数即可。
因此,输入给 ChatGPT 的 embedding可以表示为如下公式:
Embedding 的好处#
最早的时候,NLP 是直接处理文本字符串,没有 Embedding 这个操作的。Embedding 这个操作最早是由 word2vec 模型提出并实施的,GPT 系列模型,包括 ChatGPT 已将此操作作为了固定默认步骤。
Embedding 方便接入大规模神经网络#
我们在第 2 节中论述了,AI 想要有较高水平的智能,其模型规模必然比较大,参数量众多。在机器学习领域,神经网络模型是最容易扩展其模型规模的。我们会在第 8 节讲解神经网络相关的概念。
如果没有 Embedding 操作,那么 NLP 领域依然停留在直接处理字符的层面上,模型的规模扩展难度较大。embedding 将文字对应的 token 转换为抽象的固定维度的张量,标志着 NLP 迈入了深度神经网络的大门。
Embedding 抽象了 token 的语义#
当我们训练好 ChatGPT 这个模型之后,假设我们抽取出如下 token 对应的 embedding 向量:
#price(价格),#cost(开销),#trunk(卡车),#texi(出租车)
其对应的均为 M 维 embedding 向量。计算两个向量相似度的方式主要采用余弦距离,则一定有:
其含义为,price 和 cost 在 embedding 上的相似度,要大于 price 和 truck 的相似度,这符合人们的语言直觉。可以得出结论,在自然语言中,语义相近的两个词汇,其 embedding 向量之间的数学意义上的距离更相近。
换句话说,Embedding 建立了自然语言的语义与数学之间的关联关系。
总结#
- Tokenizer 将模型输入的文字转换为 token 序列。
- ChatGPT 使用了 BPE 算法实现 Tokenizer。
- Embedding 将 token 序列映射为张量矩阵,方便模型进行张量矩阵运算。