自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

LLM實踐系列-詳談Tokenizer訓練細節(jié)

發(fā)布于 2024-10-15 13:42
瀏覽
0收藏

經(jīng)過了數(shù)據(jù)收集、篩選、去重,馬上就可以開始訓練實驗了。但是在實驗之前,我們還需要先獲取一個語言模型的基石:分詞器(Tokenizer)。Tokenizer 的作用是對一條文本數(shù)據(jù)進行切分、詞表映射,得到這條文本的token序列。

用開源 Tokenizer 還是自己訓練

Tokenizer可以自己訓練,也可以從目前開源的模型中扒一個來用,用開源Tokenizer有幾個點需要著重關(guān)注:

  • 壓縮率:壓縮率決定了文本向量化后的長度,壓縮率越高,向量后數(shù)據(jù)越短,訓練和推理效率越高,但是對訓練數(shù)據(jù)的數(shù)量要求也越大,主流的tokenizer對漢字的壓縮率都在1.5-1.6之間,也就是1.5-1.6個漢字劃分為一個token。
  • token覆蓋率:token覆蓋率不用糾結(jié)細節(jié),只需要關(guān)注是否有你的目標語種的token,比如llama的tokenizer中文就很少,相應地中文上壓縮率就比較低,token向字節(jié)流的退化率比較高,也一定程度的反應了中文訓練數(shù)據(jù)不多。
  • 預留token數(shù)量:預留token也叫特殊token,一般寫作reserved_token、unused_token,paded_token,都是一個意思。這些token是指不會出現(xiàn)在自然語料中,僅保留為后續(xù)post train階段的一些特殊用途使用。比如任務隔離、角色隔離、function call的特殊指令、agent特殊指令等等。預留token最好足夠,100-1000為佳。如果下載的tokenizer預留token不夠,可以手動添加。
  • 詞表大?。耗壳伴_源的tokenizer詞表大小一般在8萬或15萬左右。詞表越大,壓縮率一般也越高,同時模型的embedding層和logits層也會更大,對顯存資源敏感的需要注意一下。

有開源tokenizer,訓練自己的tokenizer意義何在?用自己的數(shù)據(jù)訓練的 tokenizer,在同詞表大小的情況下,會比開源tokenizer有更高的壓縮率(也不會高太多),可以一定程度降低訓練和推理成本。另外更主觀一點的原因是,訓tokenizer是個很基礎(chǔ)的工作,訓不訓一定程度上反映了團隊的技術(shù)棧是否全面。還有一點需要注意的是,不同壓縮率的tokenizer訓練的模型loss可能有差別,但是性能不會有太大差別。

經(jīng)典的分詞器有WordPiece 、subword-nmt、Unigram等等,但是這些并不是本文的重點,本文主要說目前最流行的兩種tokenizer:BPE和BBPE。

BPE(Byte Pair Encoding)

BPE是目前大模型主流的兩種分詞法之一,訓練過程總結(jié)成一句話就是:迭代合并當前最高頻的token對,直到到達預設(shè)詞表大小上限。舉個具體的例子來說,假設(shè)我要訓練一個詞表大小是12的tokenizer,訓練語料就是下面這句話:

“海水潮潮潮潮潮潮落,浮云長長長長長長長消”

1、 這句話首先會按字拆分成:海 水 潮 潮 潮 潮 潮 潮 落 , 浮 云 長 長 長 長 長 長 長 消。

我沒數(shù)錯的話算上逗號應該是總共有9個不重復的字。這些字會被當作初始token,加入詞表?,F(xiàn)在我們離目標詞表大小還差12-9=3個token,下面開始迭代合并。

2、 統(tǒng)計當前已經(jīng)加入詞表的所有token兩兩組合在訓練語料中出現(xiàn)的次數(shù)。

這里有兩點注意,首先是“當前”,也就是說每次迭代加入新token后,下一次統(tǒng)計兩兩組合要算上這個新token。其次是“所有token兩兩組合”,也就是既要統(tǒng)計A token與B token的組合,也要統(tǒng)計A token與自身的組合。比如上面這個句子,我們要統(tǒng)計“海水”、“水潮”、“潮潮”、”潮落“....這些所有兩兩組合出現(xiàn)的次數(shù)。

3、 取兩兩組合中出現(xiàn)次數(shù)最高的那一個,作為新token加入詞表,同時記錄下這個token的合成路徑。

比如上面“潮潮”和“長長”是出現(xiàn)次數(shù)最高的組合,都出現(xiàn)了5次,那么我們?nèi)「绯霈F(xiàn)的“潮潮”作為本次要加入詞表的新token,同時記錄下”潮“+”潮“=”潮潮“?,F(xiàn)在詞表大小為10。

4、 如果詞表沒有達到設(shè)定的上限12,那么就迭代執(zhí)行2-3步。

再一次統(tǒng)計兩兩組合出現(xiàn)次數(shù),這一次最多的就是剛才并列第一的“長長”。當然也不要忘記上一步剛加入的“潮潮”這個token,他可以和前面的token組成“水潮潮”,也可以和后面的token組成“潮潮潮”,不過次數(shù)都不如“長長”。所以這一次加入詞表的是“長長”。

再迭代一次,再統(tǒng)計組合,此時次數(shù)最多的是“潮潮”+“潮” 組成的“潮潮潮”,以及“長長”+“長”組成的“長長長”,分別出現(xiàn)4次。按照之前的原則,取次數(shù)最多且更靠前的“潮潮潮”加入詞表,此時詞表大小為12,訓練停止,我們已經(jīng)得到了大小為12,在“海水潮潮潮潮潮潮落,浮云長長長長長長長消”上訓練的分詞器。

BPE的訓練過程還是很簡單很好理解的,但是還是有一些需要注意的地方。上面的解釋中有兩個我刻意嚴謹表達的點,一個是「取次數(shù)最多且更靠前的“潮潮潮”加入詞表」,另一個是「“潮潮”+“潮” 組成的“潮潮潮”,以及“長長”+“長”組成的“長長長”」。前者想說明token加入詞表的順序是有先后的,是有優(yōu)先級的,后者說明token的合成路徑方式需要嚴格遵循,改變詞表可能導致錯誤的合成路徑。

特意強調(diào)這個是因為我看到一些改詞表的開源工作其實是有問題的。舉個我看到的實際的例子,有一個地名”烏魯木齊“,假設(shè)詞表中包含”烏魯“和”魯木“兩個token。首先說可不可能出現(xiàn)這倆token?完全可能,如果我的訓練語料是下面這樣的就會出現(xiàn)這兩個token:

烏魯
烏魯
魯木
魯木
齊

這個語料訓出來的tokenizer詞表大概率是這樣的:「烏」「魯」「木」「齊」「烏魯」「魯木」

如果詞表里烏魯這個token在前,分詞結(jié)果就是 「烏魯」「 木」「 齊」,如果是魯木這個token在前,分詞結(jié)果就是「烏」「魯木」「齊」。分詞結(jié)果是不一樣的。如果拿到一個訓練過的模型,改一改詞表順序,肯能會導致分詞結(jié)果的不一致,模型可能完全沒有見過這樣的token,導致一些無法理解的怪異生成結(jié)果。

再說合成路徑,如果我的詞表是 「烏」「魯」「木」「齊」「烏魯」「木齊」,后來擴增了詞表,增加了「烏魯」+「木」=「烏魯木」,和「烏魯木」+「齊」=「烏魯木齊」兩個token,「烏魯木齊」這個token是合成不出來的,因為「木齊」在「烏魯木」之前,所以優(yōu)先合成「木齊」,而不是「烏魯木」,那么沒有「烏魯木」,自然無法合成「烏魯木齊」。如果要進行詞表刪減、擴增,或者兩個tokenizer進行合并,尤其要注意這個問題。

BBPE(Byte-Level Byte Pair Encoding)

BBPE和BPE大體上是一樣的,區(qū)別在于BPE把文本看作字符集合,第一步是按照字符切分獲得初始token,BBPE把文本看作是二進制編碼,按照8bit切分獲得原始token。比如還是上面那句話,會先轉(zhuǎn)成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進制數(shù)作為初始token,也就是「\xe6」「\xb5」「\xb7」...這些。剩下的統(tǒng)計兩兩組合、合成路徑都和BPE是一樣的,不過都是在二進制層面去合并。我們知道utf8是變長編碼,ascii字符在utf8中的編碼長度是1,也就是剛好一個2位16進制數(shù)。比如我上面句子里的逗號對應的utf8編碼是“\x2c”。所以ascii字符一定會作為一個基礎(chǔ)字符加入詞表,而且也不會被拆分,所以英文單詞、數(shù)字這種ascii字符組成的詞,一定是整數(shù)個token表示的。但是漢字的編碼長度大部分是3,比如“海”的編碼是“\xe6\xb5\xb7”,這就導致漢字在bbpe的詞表中并不一定是1個、2個字這種整數(shù)個token組成。可能是3/2個token表示一個漢字。

BBPE與BPE的對比

從流行度來說,BPE是去年、前年大家普遍使用的方法,而BBPE是去年底到今年的模型主要使用的方法。GPT2使用的tokenizer也是BBPE。

從編碼的角度,有一些文章說BBPE對比BPE的優(yōu)勢是不存在OOV問題,字符只要能轉(zhuǎn)utf8,就一定能被BBPE表示,但是實際執(zhí)行起來并不是,因為大部分BPE的庫也支持bytes退化,遇到超出詞表范圍的字符,也會退化到二進制表示。這么看下來BBPE剩下的優(yōu)點就是多語種下、token切分更均勻。畢竟一個中文能占3/2個token了。

從實現(xiàn)的角度,BPE的tokenizer用sentencepice庫的居多,BBPE用huggingface的tokenizers庫的居多,但是sentencepice庫產(chǎn)出的tokenizer.model本質(zhì)是一個protobuf文件,可以用protobuf庫讀出這個tokenizer原始的訓練參數(shù),甚至帶著訓練語料的磁盤路徑,不太安全。

訓練參數(shù)

除了最基本的詞表大小外,實際訓練的tokenizer還有一些可配置關(guān)鍵參數(shù)。我比較喜歡讀google的文檔,就拿sentencepice的訓練參數(shù)來介紹了,兩個庫的可配置參數(shù)其實差不多,可以類比。

--vocab_size

tokenizer預設(shè)的詞表大小。最終模型訓練的時候,我們一般會確保embedding層的shape可以被128整除,這個一方面是為了量化考慮,一方面是為了序列并行考慮。所以可以在這一步直接設(shè)置一個能被128整除的詞表大小,也可以這里不設(shè)置,等tokenizer訓練完了加一些特殊token,補到128的倍數(shù)。另外詞表大小也決定了壓縮率。

--character_coverage

字符覆蓋率。這個表示在最一開始,初始單字token要覆蓋訓練語料中全部token的百分之多少。如果是1,表示所有token只要出現(xiàn)就加入初始詞表,這會導致詞表的單字token過多。一般可以設(shè)置0.9998、0.9999,表示初始單字token要覆蓋訓練集字符的99.99%

--max_sentencepiece_length

單一token最多多少個字符組成,一般設(shè)為2、4、6,8或以上就比較大了,不太推薦,可能會出現(xiàn)低頻超長token。

--split_digits

是否做數(shù)字的一致性切分,說白了就是數(shù)字和其他token是否可以組合成新token,還是數(shù)字必須一個字符一個token不做合并。早期一些工作認為開了對數(shù)學任務有好處,但是從我的實驗上來看,是可以打開,但不是必須。數(shù)字在自然界是均勻分布的,也就是說1、2、3還是111、222、132、863數(shù)量其實是差不多的,所以就算不開一致性切分,這些token也都應該在詞表里。只要模型訓練充分,模型是能分辨111和1 1 1是一個東西的。我一直認為不應該對語言模型的泛化抱有太樂觀的態(tài)度,它就只能說出見過的話。所以我不指望在欠訓練的模型上,依賴一致性切分提高數(shù)學能力。當然這是理想情況,如果數(shù)據(jù)集準備的不充分,不能保證所有這些數(shù)字都出現(xiàn)在詞表里,可以手動把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> ...,如果要手動添加數(shù)字的一致切分和連續(xù)空格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,這里面現(xiàn)在我們一般只用pad和eos。在訓練的時候文檔或者一個turn的末尾增加一個eos token。需要做padding補齊的時候拼pad token,也可以直接用eos token當補齊token。不過建議四個都設(shè)置上,也算是致敬一下之前的NLPer

番外篇1:tokenizer與loss

不同tokenizer的壓縮率,每個token的信息量是不同的,這就導致不同tokenizer在同一份數(shù)據(jù)下訓練出來的模型loss不一樣。假設(shè)不考慮訓練tokenizer的語料質(zhì)量差異,那么tokenizer的壓縮率越高,同一個文本分詞后產(chǎn)生的token越少,整句話的平均loss就越高。相反壓縮率越低,一句話的loss通常也越低。但是實際推理起來,兩種tokenizer訓出來的模型效果差異不大,所以更多的還是從效率和成本的角度考慮壓縮率的取舍吧。

再引申一下,從這一點上不禁讓我們升起一個疑惑,loss能代表模型性能嗎?答案是不行。其實同loss不同tokenizer、同loss不同參數(shù)量的模型性能都不一樣。這個具體可以等寫到scaling law擬合、模型性能預測或者continue pretraining的時候再討論。

番外篇2:看一看真實的tokenizer

再拿qwen2的tokenizer舉例,qwen2的tokenizer時BBPE,重要文件有這么幾個:

LLM實踐系列-詳談Tokenizer訓練細節(jié)-AI.x社區(qū)圖片

merges.txt就是保存合成路徑的文件,里面看上去會有一些像亂碼的東西:

LLM實踐系列-詳談Tokenizer訓練細節(jié)-AI.x社區(qū)圖片

這些其實是轉(zhuǎn)義后的控制字符。前面也說了BBPE的初始token是2位16進制數(shù)。如果直接存這些二進制字符,可能被翻譯成ascii碼中的控制字符,比如換行制表符之類的,所以這里坐了下轉(zhuǎn)義。

vocab.json是每次token的映射id,tokenizer_config.json里面可以配置一下控制字符。tokenizer訓練完之后如果想加特殊字符,也可以在這里配置。

番外篇3:詞表增減問題

詞表的修改最好發(fā)生在模型訓練之前,包括tokenizer合并、添加特殊token、自定義token等等,這其中還尤其要注意增加詞表。語言模型訓練的時候,計算logits時是hidden_states和所有token的logits_weight的乘積,softmax也是所有token的歸一,刪除詞表相當于減小歸一化的分母,會導致最后sample時的概率發(fā)生變化。添加token則跟要小心,如果添加的token未經(jīng)訓練,可能導致歸一化后亂掉。如果添加的token初始化的不好,比如正常token是1e-2,新增加的token是1e-23量級,rms norm會回傳給embedding層一個大概在1e21量級的梯度。這個時候如果你開了梯度裁切,在求梯度的模長的時候1e21求平方直接變成inf,再歸一化其他token,所有token梯度都會變成0,這樣你不太看得出來報錯,但是embedding實際一點沒訓。

再有就是前文強調(diào)過的,注意處理合成路徑和token順序的沖突問題。

本文轉(zhuǎn)載自 ??NLP工作站??,作者: 真中合歡????

收藏
回復
舉報
回復
相關(guān)推薦