GPT-2 Tokenizer 效率观察

对基于 Transformer 结构的 LLM (大语言模型)来说,模型的输入输出都是 Token(词元)。一段输入文本,首先要经过 Tokenizer(分词器)切分成 Token 再输入给模型。

不同的 Tokenizer 会把文本按不同的边界切分,那一段文本到底会被切成几个 Token 就体现了 Tokenizer 本身的效率,这本身也是信息论的讨论范畴。不过今天不做理论分析,也不介绍 Tokenizer 的训练算法,就看下 GPT-2 和 GPT-3 Tokenizer 的实际分词效率(GPT-2 和 GPT-3 使用的是同样的 Tokenizer)。

用 GPT-2 Tokenizer 编码一段中文

from transformers import GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
input_str = '不同的 Tokenizer 会把文本按不同的边界切分,那一段文本到底会被切成几个 Token 就体现了 Tokenizer 本身的效率,这本身也是信息论的讨论范畴。'
tokenids = tokenizer(input_str)['input_ids']
raw_tokens = tokenizer.convert_ids_to_tokens(tokenids)
token_strs = [tokenizer.convert_tokens_to_string([token]) for token in raw_tokens]
print(len(input_str), len(tokenids), tokenids, token_strs)
82 121 [38834, 28938, 234, 21410, 29130, 7509, 220, 27670, 248, 162, 232, 232, 23877, 229, 17312, 105, 162, 234, 231, 38834, 28938, 234, 21410, 164, 122, 117, 45911, 234, 26344, 229, 26344, 228, 171, 120, 234, 165, 224, 96, 31660, 162, 106, 113, 23877, 229, 17312, 105, 26344, 108, 41753, 243, 27670, 248, 164, 95, 104, 26344, 229, 22755, 238, 49035, 254, 10310, 103, 29130, 10263, 108, 109, 19526, 241, 163, 236, 108, 12859, 228, 29130, 7509, 42164, 105, 164, 118, 104, 21410, 46763, 230, 163, 236, 229, 171, 120, 234, 32573, 247, 17312, 105, 164, 118, 104, 20046, 253, 42468, 46479, 94, 162, 223, 107, 164, 106, 118, 21410, 164, 106, 101, 164, 106, 118, 164, 234, 225, 45911, 112, 16764] ['不', '�', '�', '的', ' Token', 'izer', ' ', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '不', '�', '�', '的', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '一', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', ' Token', ' �', '�', '�', '�', '�', '�', '�', '�', '�', '�', ' Token', 'izer', ' �', '�', '�', '�', '�', '的', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '是', '�', '�', '�', '�', '�', '�', '�', '�', '的', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '�', '。']

输入的中英文夹杂的字符串长度为 82 个字符,其中有 6 个空格,23 个英文字符(3个单词),3个标点符号,50 个汉字字符。最终编码出来的 tokenids 长度是 121 个 Token,仔细检查可以看到,大部分汉字被编码成两个 Token,英文则是按照 subword 级别进行了编码。由此可见,GPT-2 Tokenizer 编码中文的效率是很低的

GPT-2 Tokenizer 词典中包含中文的 Token 数量

import re
tokens = [tokenizer.convert_tokens_to_string([key]) for key in tokenizer.get_vocab()]
cn_tokens = [t for t in tokens if re.search(u'[\u4e00-\u9fff]', t)]
cn_tokens.sort(key=len, reverse=True)
print(len(tokens), len(cn_tokens), cn_tokens)
50257 51 [' 裏覚醒', ' 裏�', '��士', '龍喚士', ' 裏�', '龍契士', '��極', ' 裏', '龍�', '�醒', '覚醒', 'の魔', '�士', '龍�', ' 神', '龍', '神', '士', '魔', '的', '人', '天', '王', '一', '大', '装', '田', '子', '戦', '生', '者', '不', '姫', '中', '上', '闘', '是', '女', '方', '作', '黒', '之', '使', '光', '代', '版', '三', '五', '武', '将', '機']

对 GPT-2 Tokenizer 词典中的 Token 进行分析,可以看到:在数量为 50257 大小的词典中,只有 51 个 Token 包含常见的中文字符。上一节中出现的“是”、“的”、“不”也在其中,但是大家随便一想就能想到的,常见的“你”、“我”、“他”并不在内。这些数据印证了它对中文分词的效率低下。

GPT-2 Tokenizer 词典中包含英文的 Token 数量

en_tokens = [t for t in tokens if re.search(u'[A-Za-z]', t)]
en_tokens.sort(key=len, reverse=True)
print(len(en_tokens), en_tokens[:10])
46949 ['rawdownloadcloneembedreportprint', 'BuyableInstoreAndOnline', 'cloneembedreportprint', ' RandomRedditorWithNo', ' telecommunications', ' disproportionately', ' guiActiveUnfocused', 'channelAvailability', ' Telecommunications', ' externalToEVAOnly']

但是针对英文字符进行统计,可以看到在数量为 50257 大小的词典中,有 46949 个 Token 包含 26 个英文字母。由于数量太多,这里仅输出了长度排前 10 的英文 Token。

从 Tokenizer 词典中不同语言字符的分布,我们可以验证 GPT-3 论文 “Language Models are Few-Shot Learners” 中提到的这句话:

... This could be a weakness due to reusing the byte-level BPE tokenizer of GPT-2 which was developed for an almost entirely English training dataset.

GPT-2 Tokenizer 词典中最长的 Token 是什么

tokens.sort(key=len, reverse=True)
print(len(tokens), tokens[:10])
50257 [' =================================================================', ' ----------------------------------------------------------------', '----------------------------------------------------------------', '................................................................', '================================================================', '________________________________________________________________', 'ÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂÃÂ', '--------------------------------------------------------', '------------------------------------------------', ' =================================']

可以看到,最长的 Token 居然是一些连续的标记,看起来像是论坛上的分割线。在“Natural Language Processing with Transformers” 一书中说,这可能是由于 GPT-2 的词典是用以 Reddit 为核心的语料训练的。

Codex Tokenizer 提升代码处理效率的 Trick

上面讲到 Tokenizer 效率对最终生成 Token 序列大小的影响,如果只关注最终的模型效果,可能并不在意中间过程的效率问题。

但从一个小点可以看到 OpenAI 还是很重视 Tokenizer 效率的。在训练 Codex 时,它在 50257 的词典之外新增了 23 个 Token,分别编码了:[2个连续空格, 3个连续空格, ..., 23个连续空格]。

大家都知道,代码中有很多锁进和对齐,如果每个空格都编码成一个 Token,训练和推理的效率就会极低;如果像常规语言模型那样忽视更多连续的空格,又会导致生成的代码不可读,甚至对 Python 这样的语言来说代码完全不可理解。所以 OpenAI 专门修改了 Tokenizer 的词典,提升了对连续空格的编码效率。

在中文数据集上训练的 Tokenizer 表现如何?

同样使用第一节的代码,我们换一个在中文数据集上训练的 Tokenizer,效果会如何呢?我随便在 HuggingFace 上找了一个模型 HuiHuang/gpt3-base-zh,使用它的 Tokenizer 对那段话进行重新分词。

from transformers import BertTokenizerFast

tokenizer = BertTokenizerFast.from_pretrained("HuiHuang/gpt3-base-zh")
input_str = '不同的 Tokenizer 会把文本按不同的边界切分,那一段文本到底会被切成几个 Token 就体现了 Tokenizer 本身的效率,这本身也是信息论的讨论范畴。'
tokenids = tokenizer(input_str)['input_ids']
raw_tokens = tokenizer.convert_ids_to_tokens(tokenids)
token_strs = [tokenizer.convert_tokens_to_string([token]) for token in raw_tokens]
print(len(input_str), len(tokenids), tokenids, token_strs)
82 65 [2, 215, 1750, 10574, 22090, 24330, 23635, 21748, 484, 5460, 6225, 6646, 5587, 215, 1750, 10574, 17027, 10262, 1233, 1232, 21124, 17261, 202, 7807, 6225, 6646, 1274, 4447, 484, 15221, 1233, 5338, 1194, 244, 22090, 24330, 3835, 541, 9850, 336, 22090, 24330, 23635, 21748, 6646, 16757, 10574, 6162, 9809, 21124, 17059, 6646, 16757, 297, 6393, 683, 4921, 16004, 10574, 15986, 16004, 13773, 10302, 160, 3] ['[CLS]', '不', '同', '的', 'to', '##ken', '##ize', '##r', '会', '把', '文', '本', '按', '不', '同', '的', '边', '界', '切', '分', ',', '那', '一', '段', '文', '本', '到', '底', '会', '被', '切', '成', '几', '个', 'to', '##ken', '就', '体', '现', '了', 'to', '##ken', '##ize', '##r', '本', '身', '的', '效', '率', ',', '这', '本', '身', '也', '是', '信', '息', '论', '的', '讨', '论', '范', '畴', '。', '[SEP]']

可以看到,82 个字符的输入被分成了 63 个 Token(除去[CLS]和[SEP]),比 GPT-2 Tokenizer 的分词数量 121 几乎少了一半。这就是 Tokenizer 训练数据集对分词效率的影响。

Tokenizer 的效率意味着什么

在以前,Tokenizer 的效率只是算法工程师们关注的话题。但是现在,LLM 逐渐成为云服务,Token 成为 API 调用的基本收费单元。例如:OpenAI gpt-3.5-turbo 的收费标准是 $0.002/1K tokens。

在这种情况下,Tokenizer 的效率就意味着成本。这意味着使用中文调用 OpenAI 的 chatGPT API,假设单 Token 成本一样的话,每次调用的实际成本可能是调用国内类似 chatGPT 云服务的 2 倍以上。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注