LLM通常系列
通过了数据搜集、挑选、去重,马上就可以开局训练试验了。然而在试验之前,咱们还须要先失掉一个言语模型的基石:分词器(Tokenizer)。Tokenizer 的作用是对一条文本数据启动切分、词表映射,失掉这条文本的token序列。
用开源 Tokenizer 还是自己训练
Tokenizer可以自己训练,也可以从目前开源的模型中扒一个来用,用开源Tokenizer有几个点须要着重关注:
有开源tokenizer,训练自己的tokenizer意义何在?用自己的数据训练的 tokenizer,在同词表大小的状况下,会比开源tokenizer有更高的紧缩率(也不会高太多),可以必定水平降落训练和推理老本。另外更客观一点的要素是,训tokenizer是个很基础的上班,训不训必定水平上反映了团队的技术栈能否片面。还有一点须要留意的是,不同紧缩率的tokenizer训练的模型loss或许有差异,然而性能不会有太大差异。
经典的分词器有WordPiece 、subword-nmt、Unigram等等,然而这些并不是本文的重点,本文关键说目前最盛行的两种tokenizer:BPE和BBPE。
BPE(Byte Pair Encoding)
BPE是目前大模型干流的两种分词法之一,训练环节总结成一句话就是:迭代兼并最高频的token对,直到抵达预设词表大小下限。举个详细的例子来说,假定我要训练一个词表大小是12的tokenizer,训练语料就是上方这句话:
“淡水潮潮潮潮潮潮落,浮云长长长长长长长消”
1、 这句话首先会按字拆分红:海 水 潮 潮 潮 潮 潮 潮 落 , 浮 云 长 长 长 长 长 长 长 消。
我没数错的话算上逗号应该是总共有9个不重复的字。这些字会被当作初始token,参与词表。如今咱们离指标词表大小还差12-9=3个token,上方开局迭代兼并。
2、 统计曾经参与词表的一切token两两组合在训练语料中出现的次数。
这里有两点留意,首先是“”,也就是说每次迭代参与新token后,下一次性统计两两组合要算上这个新token。其次是“一切token两两组合”,也就是既要统计A token与B token的组合,也要统计A token与自身的组合。比如上方这个句子,咱们要统计“淡水”、“水潮”、“潮潮”、”潮落“....这些一切两两组合出现的次数。
3、 取两两组合中出现次数最高的那一个,作为新token参与词表,同时记载下这个token的分解门路。
比如上方“潮潮”和“长长”是出现次数最高的组合,都出现了5次,那么咱们取更早出现的“潮潮”作为本无所谓参与词表的新token,同时记载下”潮“+”潮“=”潮潮“。如今词表大小为10。
4、 假设词表没有到达设定的下限12,那么就迭代口头2-3步。
再一次性统计两两组合出现次数,这一次性最多的就是刚才并列第一的“长长”。当然也不要遗记上一步刚参与的“潮潮”这个token,他可以和前面的token组成“水潮潮”,也可以和前面的token组成“潮潮潮”,不过次数都不如“长长”。所以这一次性参与词表的是“长长”。
再迭代一次性,再统计组合,此时次数最多的是“潮潮”+“潮” 组成的“潮潮潮”,以及“长长”+“长”组成的“长长长”,区分出现4次。依照之前的准则,取次数最多且更靠前的“潮潮潮”参与词表,此时词表大小为12,训练中止,咱们曾经失掉了大小为12,在“淡水潮潮潮潮潮潮落,浮云长长长长长长长消”上训练的分词器。
BPE的训练环节还是很便捷很好了解的,然而还是有一些须要留意的中央。 上方的解释中有两个我刻意谨严表白的点,一个是「取次数最多且更靠前的“潮潮潮”参与词表」,另一个是「“潮潮”+“潮” 组成的“潮潮潮”,以及“长长”+“长”组成的“长长长”」。前者想说明token参与词表的顺序是有先后的,是有优先级的,后者说明token的分解门路模式须要严厉遵照,扭转词表或许造成失误的分解门路。
特地强调这个是由于我看到一些改词表的开源上班其实是有疑问的。举个我看到的实践的例子,有一个地名”乌鲁木齐“,假定词表中蕴含”乌鲁“和”鲁木“两个token。首先说可无法能出现这俩token?齐全或许,假设我的训练语料是上方这样的就会出现这两个token:
乌鲁乌鲁鲁木鲁木齐
这个语料训进去的tokenizer词表大略率是这样的:「乌」「鲁」「木」「齐」「乌鲁」「鲁木」
假设词表里乌鲁这个token在前,分词结果就是 「乌鲁」「 木」「 齐」,假设是鲁木这个token在前,分词结果就是「乌」「鲁木」「齐」。分词结果是不一样的。假设拿到一个训练过的模型,改一改词表顺序,肯能会造成分词结果的不分歧,模型或许齐全没有见过这样的token,造成一些无法了解的怪异生成结果。
再说分解门路,假设我的词表是 「乌」「鲁」「木」「齐」「乌鲁」「木齐」,起初扩增了词表,参与了「乌鲁」+「木」=「乌鲁木」,和「乌鲁木」+「齐」=「乌鲁木齐」两个token,「乌鲁木齐」这个token是分解不进去的,由于「木齐」在「乌鲁木」之前,所以优先分解「木齐」,而不是「乌鲁木」,那么没有「乌鲁木」,人造无法分解「乌鲁木齐」。假设要启动词表删减、扩增,或许两个tokenizer启动兼并,尤其要留意这个疑问。
BBPE(Byte-Level Byte Pair Encoding)
BBPE和BPE大体上是一样的,区别在于BPE把文本看作字符汇合,第一步是依照字符切分取得初始token,BBPE把文本看作是二进制编码,依照8bit切分取得原始token。比如还是上方那句话,会先转成utf8编码:
“淡水潮潮潮潮潮潮落,浮云长长长长长长长消” =>
"\xe6\xb5\xb7\xe6\xb0\xb4\xe6\xbd\xae\xe6\xbd\xae\xe6\xbd\xae\xe6\xbd\xae\xe6\xbd\xae\xe6\xbd\xae\xe8\x90\xbd\x2c..."
而后取每一个2位16进制数作为初始token,也就是「\xe6」「\xb5」「\xb7」...这些。剩下的统计两两组合、分解门路都和BPE是一样的,不过都是在二进制层面去兼并。咱们知道utf8是变长编码,ascii字符在utf8中的编码长度是1,也就是刚好一个2位16进制数。比如我上方句子里的逗号对应的utf8编码是“\x2c”。所以ascii字符必定会作为一个基础字符参与词表,而且也不会被拆分,所以英文单词、数字这种ascii字符组成的词,必定是整数个token示意的。然而汉字的编码长度大局部是3,比如“海”的编码是“\xe6\xb5\xb7”,这就造成汉字在bbpe的词表中并不必定是1个、2个字这种整数个token组成。或许是3/2个token示意一个汉字。
BBPE与BPE的对比
从盛行度来说,BPE是去年、前年大家广泛经常使用的方法,而BBPE是去年底到往年的模型关键经常使用的方法。GPT2经常使用的tokenizer也是BBPE。
从编码的角度,有一些文章说BBPE对比BPE的优势是不存在OOV疑问,字符只需能转utf8,就必定能被BBPE示意,然而实践口头起来并不是,由于大局部BPE的库也支持bytes退步,遇到超出词表范畴的字符,也会退步到二进制示意。这么看上去BBPE剩下的优势就是多语种下、token切分更平均。毕竟一个中文能占3/2个token了。
从成功的角度,BPE的tokenizer用sentencepice库的居多,BBPE用huggingface的tokenizers库的居多,然而sentencepice库产出的tokenizer.model实质是一个protobuf文件,可以用protobuf库读出这个tokenizer原始的训练参数,甚至带着训练语料的磁盘门路,不太安保。
训练参数
除了最基本的词表大小外,实践训练的tokenizer还有一些可性能关键参数。我比拟青睐读google的文档,就拿sentencepice的训练参数来引见了,两个库的可性能参数其实差不多,可以类比。
--vocab_size
tokenizer预设的词表大小。最终模型训练的时刻,咱们普通会确保embedding层的shape可以被128整除,这个一方面是为了量化思考,一方面是为了序列并行思考。所以可以在这一步间接设置一个能被128整除的词表大小,也可以这里不设置,等tokenizer训练完了加一些不凡token,补到128的倍数。另外词表大小也选择了紧缩率。
--character_coverage
字符笼罩率。这个示意在最一开局,初始单字token要笼罩训练语料中所有token的百分之多少。假设是1,示意一切token只需出现就参与初始词表,这会造成词表的单字token过多。普通可以设置0.9998、0.9999,示意初始单字token要笼罩训练集字符的99.99%
--max_sentencepiece_length
繁多token最多多少个字符组成,普通设为2、4、6,8或以上就比拟大了,不太介绍,或许会出现低频超长token。
--split_digits
能否做数字的分歧性切分,说白了就是数字和其余token能否可以组分解新token,还是数字必定一个字符一个token不做兼并。早期一些上班以为开了对数学义务无好处,然而从我的试验过去看,是可以关上,但不是必定。数字在人造界是平均散布的,也就是说1、2、3还是111、222、132、863数量其实是差不多的,所以就算不开分歧性切分,这些token也都应该在词表里。只需模型训练充沛,模型是能分辨111和1 1 1是一个物品的。我不时以为不应该对言语模型的泛化抱有太失望的态度,它就只能说出见过的话。所以我不指望在欠训练的模型上,依赖分歧性切分提高数学才干。当然这是现实状况,假设数据集预备的不充沛,不能保障一切这些数字都出如今词表里,可以手动把0-9999参与到词表里,这样既坚持分歧性,又能提高紧缩率。说个题外话,前段期间知乎高端行讨论为什么9.11>9.8,我以为就是token欠训练,又刚好没有泛化过去。为什么没有泛化过去呢?由于真的有9.11大于9.8的时刻。python3.11就大于python3.8
--allow_whitespace_only_pieces
准许多个空格作为一个token,普通是准许,关键是为了排版,比如python的代码排版。当然也可以不开,手动把1-20个空格组成的token参与到词表里。
-user_defined_symbols
这个关键就是为了性能之前说的不凡token,可以预留几百比如<reserved_0> <reserved_1> ...,假设要手动参与数字的分歧切分和延续空格token,也可以在这里加
--byte_fallback
之前说的二进制退步,让BPE当半个BBPE用
--remove_extra_whitespaces
能否删除多余空格,这个普通改为False,不要让tokenizer随意动咱们的空格。
--unk_id 、--bos_id、--eos_id、--pad_id 、 --unk_piece、--bos_piece、--eos_piece、--pad_piece
指定控制字符和ID,这外面如今咱们普通只用pad和eos。在训练的时刻文档或许一个turn的末尾参与一个eos token。须要做padding补齐的时刻拼pad token,也可以间接用eos token当补齐token。不过倡导四个都设置上,也算是致敬一下之前的NLPer
番外篇1:tokenizer与loss
不同tokenizer的紧缩率,每个token的消息量是不同的,这就造成不同tokenizer在同一份数据下训练进去的模型loss不一样。假定不思考训练tokenizer的语料品质差异,那么tokenizer的紧缩率越高,同一个文本分词后发生的token越少,整句话的平均loss就越高。同样紧缩率越低,一句话的loss通常也越低。然而实践推理起来,两种tokenizer训进去的模型成果差异不大,所以更多的还是从效率和老本的角度思考紧缩率的取舍吧。
再引申一下,从这一点上不由让咱们升起一个纳闷,loss能代表模型性能吗?答案是不行。其实同loss不同tokenizer、同loss不同参数量的模型性能都不一样。这个详细可以等写到scaling law拟合、模型性能预测或许continue pretraining的时刻再讨论。
番外篇2:看一看实在的tokenizer
再拿qwen2的tokenizer举例,qwen2的tokenizer时BBPE,关键文件有这么几个:
merges.txt就是保管分解门路的文件,外面看上去会有一些像乱码的物品:
这些其实是转义后的控制字符。前面也说了BBPE的初始token是2位16进制数。假设间接存这些二进制字符,或许被翻译成ascii码中的控制字符,比如换行制表符之类的,所以这里坐了下转义。
vocab.json是每次token的映射id,tokenizer_config.json外面可以性能一下控制字符。tokenizer训练完之后假构想加不凡字符,也可以在这里性能。
番外篇3:词表增减疑问
词表的修正最好出当初模型训练之前,包括tokenizer兼并、参与不凡token、自定义token等等,这其中还尤其要留意参与词表。言语模型训练的时刻,计算logits时是hidden_states和一切token的logits_weight的乘积,softmax也是一切token的归一,删除词表相当于减小归一化的分母,会造成最后sample时的概率出现变动。参与token则跟要小心,假设参与的token未经训练,或许造成归一化后乱掉。假设参与的token初始化的不好,比如反常token是1e-2,新参与的token是1e-23量级,rms norm会回传给embedding层一个大略在1e21量级的梯度。这个时刻假设你开了梯度裁切,在求梯度的模长的时刻1e21求平方间接变成inf,再归一化其余token,一切token梯度都会变成0,这样你不太看得进去报错,然而embedding实践一点没训。
再有就是前文强调过的,留意解决分解门路和token顺序的抵触疑问。
本文转载自,作者: