大模型系列:大模型tokenizer分詞編碼算法BPE理論簡述和實踐

關(guān)鍵詞:大語言模型分詞,BPE,BBPE

前言

token是大模型處理和生成語言文本的基本單位,在之前介紹的Bert和GPT-2中,都是簡單地將中文文本切分為單個漢字字符作為token,而目前LLaMA,ChatGLM等大模型采用的是基于分詞工具sentencepiece實現(xiàn)的BBPE(Byte-level BPE)分詞編碼算法,本節(jié)介紹BBPE分詞編碼作為大模型系列的開篇。


內(nèi)容摘要

  • 常用分詞算法簡述
  • 以中文LLaMA(Atom)為例快速開始BBPE
  • Byte-Pair Encoding (BPE) 原理簡述
  • Byte-level BPE(BBPE)原理簡述
  • 使用sentencepiece訓(xùn)練BPE,BBPE

常用分詞算法簡述

分詞編碼指的是將自然語言切割為最小的語義單元token,并且將token轉(zhuǎn)化為數(shù)值id供給計算機進(jìn)行模型學(xué)習(xí)的過程。常用的分詞算法根據(jù)切分文本的顆粒度大小分為word,char,subword三類,以英文文本I am disappointed in you為例,三種方法切分結(jié)果如下

顆粒度 切割方式 分詞結(jié)果
word 單詞級別分詞,英文天然可以根據(jù)空格分割出單詞 [I, am, disappointed, in, you]
character 字符級別分詞,以單個字符作為最小顆粒度 [I, , a, m, , d, i, s, ..., y, o, u]
subword 介于word和character之間,將word拆分為子串 [I, am, disappoint, ed, in, you]

word方式的優(yōu)點是保留住了完整的單詞作為有意義的整體,相比于character語義表達(dá)更加充分,但是缺點是導(dǎo)致詞表變大,因為羅列出單詞的所有組合明顯比窮舉出所有字符更加困難,并且對于極少出現(xiàn)單詞組合容易訓(xùn)練不充分,另外的word雖然區(qū)分出了單詞,但是對于單詞之間語義關(guān)系無法進(jìn)一步刻畫,比如英文中的cat和cats這種單復(fù)數(shù)情況。
character方法的優(yōu)勢在于詞表小,5000多個中文常用字基本能組合出所有文本序列,但是缺點也很明顯,缺乏單詞的語義信息,并且分詞的結(jié)果較長,增加了文本表征的成本。
subword方法平衡以上兩種方法, 它可以較好的平衡詞表大小和語義表達(dá)能力,本篇介紹的Byte-Pair Encoding (BPE) 和Byte-level BPE(BBPE)都屬于subword方法。


以中文LLaMA(Atom)為例快速開始BBPE

Atom是基于LLaMA架構(gòu)在中文文本上進(jìn)行訓(xùn)練得到的語言模型,它對中文采用BBPE分詞,整個詞表包含65000個token,在HuggingFace搜索Atom-7B進(jìn)行模型下載

HuggingFace Atom

Atom的分詞器使用的是LlamaTokenizer,使用Python簡單調(diào)用對文本進(jìn)行分詞如下

>>> from transformers import LlamaTokenizer
>>> tokenizer = LlamaTokenizer.from_pretrained("./Atom-7B")
>>> text = "我很開心我能和我們的團隊一起工作"
>>> tokenizer.tokenize(text)
['▁我', '很開心', '我能', '和我們', '的團隊', '一起', '工作']
>>> tokenizer.encode(text)
[32337, 43804, 42764, 53769, 49300, 32212, 32001]

從分詞結(jié)果來看,BBPE類似jieba分詞一樣將中文字符進(jìn)行了聚合成為一個一個的子串,而最終也是以子串整體映射到一個數(shù)值id,其中句子開頭,或者文本中存在空格符,分詞算法會將其替換為符號。
在LlamaTokenizer類中調(diào)用了sentencepiece來獲取模型分詞器,后續(xù)的分詞操作也是基于sentencepiece提供的API方法

import sentencepiece as spm
...
self.sp_model = spm.SentencePieceProcessor(**self.sp_model_kwargs)
self.sp_model.Load(vocab_file)

Atom-7B模型目錄下的tokenizer.model為BBPE分詞模型,使用sentencepiece載入該分詞模型可以實現(xiàn)LlamaTokenizer同樣的效果

# pip install sentencepiece
>>> import sentencepiece
>>> tokenizer = sentencepiece.SentencePieceProcessor()
>>> tokenizer.Load("./Atom-7B/tokenizer.model")
>>> tokenizer.encode_as_pieces(text)
['▁我', '很開心', '我能', '和我們', '的團隊', '一起', '工作']
>>> tokenizer.encode(text)
[32337, 43804, 42764, 53769, 49300, 32212, 32001]

tokenizer.model分詞模型可以通過手動安裝谷歌的項目源碼,使用命令行導(dǎo)出為tokenizer.vocab詞表,從而得到每個token和token id的對應(yīng)關(guān)系,sentencepiece命令工具安裝方式如下

# download sentencepiece項目源碼
$ unzip sentencepiece.zip
$ cd sentencepiece
$ mkdir build
$ cd build
$ cmake ..
$ make -j $(nproc)
$ make install
$ ldconfig -v

安裝完成在環(huán)境變量下出現(xiàn)命令spm_export_vocab,指定分詞模型地址和輸出詞表文本地址即可完成詞表導(dǎo)出

$ which spm_export_vocab
/usr/local/bin/spm_export_vocab

$ spm_export_vocab \
--model=./Atom-7B/tokenizer.model \
--output=./Atom-7B/tokenizer.vocab

完成后生成tokenizer.vocab詞表文件,打開詞表搜索下'很開心'這個子串處在43805行,和編碼結(jié)果43804一致(索引從0開始)

$ less -N tokenizer.vocab
...
43804 騎行    0
43805 很開心  0
43806 在里面  0
...

對于不在tokenizer.vocab中的生僻中文字符,BBPE會將他進(jìn)行UTF-8編碼用字節(jié)表示,使用字節(jié)去映射詞表的token id,而不是使用UNK位置填充,這也是BBPE中Byte-level的體現(xiàn)

# 以生僻字’龘‘為例,對’龘‘進(jìn)行UTF-8編碼為字節(jié)表示
>>> "龘".encode("utf-8")
b'\xe9\xbe\x98'

>>> tokenizer.encode_as_pieces("龘")
['▁', '<0xE9>', '<0xBE>', '<0x98>']
>>> tokenizer.tokenize("龘")
[29871, 236, 193, 155]

Byte-Pair Encoding (BPE) 原理簡述

BBPE是基于BPE在字節(jié)顆粒度上的拓展,兩者在分詞算法上沒有本質(zhì)區(qū)別,本節(jié)先介紹BPE分詞算法。
BPE的核心思想是事先給定一個最大分詞數(shù)量,針對語料文本中的每個字符token,逐步合并出現(xiàn)頻率最高的連續(xù)的兩個字符組合,形成一個新詞token,直到達(dá)到目標(biāo)分詞數(shù)量。BPE的計算流程圖如下


BPE計算流程圖
  • step 1:設(shè)定最大分詞詞典數(shù)量vocab size,初始化一個詞典
  • step 2:將語料中所有文本切成單個字符形式加入詞典,并且將<eos>,<bos>,<unk>,空格符等特殊字符也加入詞典
  • step 3:對已經(jīng)切為字符的語料,全局統(tǒng)計一輪連續(xù)兩個字符出現(xiàn)組合的頻率
  • step 4:取最大頻率的組合,將這兩個字符合并為一個整體,將這個整體添加到詞典,并且在語料中將這兩個字符也同步全部替換為這個新的整體,當(dāng)作一個詞
  • step 5:重復(fù)step 3和step 4直到達(dá)到vocab size或者無法再合并為止
  • step 6:將最終的詞典生成分詞編碼模型文件,比如tokenizer.model,后續(xù)的任務(wù)都以這個分詞詞典來切詞和編碼

以一個只有2行的小文本“我很開心我能和我們的團隊一起工作。我很欣賞我團隊”為例,來說明BPE的計算流程,事先設(shè)定vocab size為23。兩句話一共包含16個字符,但是還需要加上<eos>,<bos>,<unk>以及句子開頭▁四種特殊符號,將它們?nèi)刻砑拥皆~表已經(jīng)有20個token,下一步對單個token進(jìn)行聚合,統(tǒng)計出有三個組合的頻率最高,分別為'▁我','我很','團隊'各出現(xiàn)了2次,繼續(xù)添加到詞表,最終剛好形成23個詞,BPE算法停止

語料 我很開心我能和我們的團隊一起工作。我很欣賞我團隊
單字符 作, 們, 一, 工, 能, 隊, 欣, 的, 和, 我, 心, 起, 開, 賞, 團, 很
特殊字符 <eos>, <bos>, <unk>, ▁
組合字符 ▁我, 我很, 團隊

Byte-level BPE(BBPE)原理簡述

BBPE將BPE的聚合下推到字節(jié)級別的,先通過UTF-8的編碼方式將任意字符轉(zhuǎn)化為長度1到4個字節(jié),1個字節(jié)有256種表示,以字節(jié)為顆粒度進(jìn)行聚合,其他流程和BPE是一樣的。
在BBPE訓(xùn)練之前,256個字節(jié)表示作為token會全部加入詞典,觀察Atom的tokenizer.vocab,前三個位置分別為未登錄詞UNK,句子開頭符,句子結(jié)束符,從第四個位置開始插入了256個字節(jié)集

1 <unk>   0
2 <s>     0
3 </s>    0
4 <0x00>  0
5 <0x01>  0
...
258 <0xFE>  0
259 <0xFF>  0
...

隨著字節(jié)的聚合形成原始的字符,進(jìn)一步可以形成詞組,最終輸出到tokenizer.model的時候會轉(zhuǎn)化為聚合后的字符。
在模型使用的時候?qū)τ谳斎氲淖址?,如果直接存在則映射為token id,如果不存在則轉(zhuǎn)化為UTF-8編碼之后的字節(jié)作為單位做映射,例如前文中的'龘‘會被映射為3個token id。
BBPE的優(yōu)點:可以跨語言共用詞表,任意語種都可以被編碼到字節(jié)進(jìn)行表示,另外UTF-8編碼可以在不同語言之間具有一定互通性,底層字節(jié)層面的共享來實可能能夠帶來知識遷移。針對稀有字符,BBPE不會為其分配專門的token id,而是使用字節(jié)級別來編碼來解決OOV的問題,一定程度上控制了詞表大小和解決了稀疏字符難以訓(xùn)練的問題。
BBPE的缺點:會使得單個中文字符被切割為多個字節(jié)表示,導(dǎo)致表征的成本上升,可以通過擴大vocab size來促進(jìn)字節(jié)的聚合,使得更多的字符和詞組被挖掘出來作為單獨的token id。


使用sentencepiece訓(xùn)練BPE,BBPE

Python安裝的包sentencepiece和源碼安裝的spm_train命令工具都可以完成BPE和BBPE的訓(xùn)練,例如以小部分《狂飆》的劇本作為語料訓(xùn)練分詞模型,代碼如下

>>> import sentencepiece as spm

>>> spm.SentencePieceTrainer.train(
    input='./data/corpus.txt',
    model_type="bpe",
    model_prefix='tokenizer',   
    vocab_size=3000, 
    character_coverage=1,  
    max_sentencepiece_length=6, 
    byte_fallback=False
)

SentencePieceTrainer的訓(xùn)練模式支持BPE,unigram等多種模式,當(dāng)model_type為'bpe'且不開啟byte_fallback,該模式為BPE,如果開啟byte_fallback代表BBPE模式,byte_fallback代表是否將未知詞轉(zhuǎn)化為UTF-8字節(jié)表示進(jìn)行編碼,如果不開啟則對于OOV的詞會直接輸出<unk>。
訓(xùn)練完成后在目錄下會生成tokenizer.model和tokenizer.vocab兩個文件,查看BPE的分詞詞表tokenizer.vocab如下

1 <unk>   0
2 <s>     0
3 </s>    0
4 :“      -0
5 ▁”      -1
6 安欣    -2
7 ..      -3
8 高啟    -4
9 ?”      -5
10 高啟強  -6

詞表從上到下的順序也蘊含了詞頻從高到低的關(guān)系。針對未在語料中出現(xiàn)過的字符分別測試下BPE和BBPE的編碼結(jié)果

>>> # BPE
>>> token_model_1 = sentencepiece.SentencePieceProcessor()
>>> token_model_1.Load("./tokenizer.model")

>>> # BBPE
>>> token_model_2 = sentencepiece.SentencePieceProcessor()
>>> token_model_2.Load("./tokenizer2.model")

針對文本中未出現(xiàn)的'凰'字符分詞編碼結(jié)果如下

>>> token_model_1.encode("凰")
[882, 0]

>>> token_model_2.encode("凰")
[882, 232, 138, 179]

結(jié)論和前文一致,BPE方式對于未登陸詞輸出<unk>的token id為0,而BBPE如果映射不到該詞會轉(zhuǎn)化為3個字節(jié)表示,輸出三個token id,全文完畢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容