bert深入分析以及bert家族總結

  • 2021 年 3 月 29 日
  • AI

本文主要對bert以及各類bert based model做一個總結,在總結的過程中,對bert的各種細節(分詞演算法、相對\絕對為止編碼、預訓練任務等)進行整理,主要是因為在研究bert家族的過程中發現bert的各種變體基本都是從這些細節層面入手進行的魔改。所以其實bertology的models理解起來並不是非常複雜。

首先從input開始

tokenizer部分

Python/latest/” data-draft-node=”block” data-draft-type=”link-card” class=”LinkCard old LinkCard–noImage”>Tokenizers – tokenizers documentationhuggingface.co

特點:

1、rust實現,速度飛快,只需不到20秒即可用伺服器CPU進行1GB文本處理;

2、用起來很簡單,api平易近人;

3、截斷、補0padding、特殊的token添加等一站式解決

Summary of the tokenizershuggingface.co

目前huggingface實現了BPE、wordpeice和sentencepiece等分詞方法。

常見而直觀的英文或者中文分詞的方式,往往是以word為基礎的,例如:

"Don't you love   Transformers? We sure do."
分詞為
["Don", "'", "t", "you", "love", " ", "Transformers", "?", "We", "sure", "do", "."]

「我來自中國」
分詞為:
「我」,「來」,「 自 」,「中國」

word-level

這些分詞的方法都是將句子拆分為詞,即word-level,這麼做的優缺點是:

優點:能夠保存較為完整的語義資訊

缺點:

1、辭彙表會非常大,大的辭彙表對應模型需要使用很大的embedding層,這既增加了記憶體,又增加了時間複雜度。通常,transformer模型的辭彙量很少會超過50,000,特別是如果僅使用一種語言進行預訓練的話,而transformerxl使用了常規的分詞方式,辭彙表高達267735;

2、 word-level級別的分詞略顯粗糙,無法發現更加細節的語義資訊,例如模型學到的「old」, 「older」, and 「oldest」之間的關係無法泛化到「smart」, 「smarter」, and 「smartest」。

3、word-level級別的分詞對於拼寫錯誤等情況的魯棒性不好;

char-level

一個簡單的方法就是將word-level的分詞方法改成 char-level的分詞方法,對於英文來說,就是字母界別的,比如 “China”拆分為”C”,”h”,”i”,”n”,”a”,對於中文來說,”中國”拆分為”中”,”國”,

優點:

1、這可以大大降低embedding部分計算的記憶體和時間複雜度,以英文為例,英文字母總共就26個。。。。,中文常用字也就幾千個。

2、char-level的文本中蘊含了一些word-level的文本所難以描述的模式,因此一方面出現了可以學習到char-level特徵的詞向量FastText,另一方面在有監督任務中開始通過淺層CNN、HIghwayNet、RNN等網路引入char-level文本的表示;

缺點:

1、但是這樣使得任務的難度大大增加了,畢竟使用字元大大扭曲了詞的意義,一個字母或者一個單中文字實際上並沒有任何語義意義,單純使用char-level往往伴隨著模型性能的下降;

2、增加了輸入的計算壓力,原本」I love you「是3個embedding進入後面的cnn、rnn之類的網路結構,而進行char-level拆分之後則變成 8個embedding進入後米娜的cnn或者rnn之類的網路結構,這樣計算起來非常慢;

subword-level

為了兩全其美,transformer使用了混合了char-level和word-level的分詞方式,稱之為subword-level的分詞方式。

subword-level的分詞方式遵循的原則是:盡量不分解常用詞,而是將不常用此分解為常用的子詞,例如,"annoyingly"可能被認為是一個罕見的單詞,並且可以分解為"annoying""ly""annoying""ly"作為獨立的子詞會更頻繁地出現,同時,"annoyingly"是由”annoying”和”ly”這兩個子詞的複合含義構成的複雜含義,這在諸如土耳其語之類的凝集性語言中特別有用,在該語言中,可以通過將子詞串在一起來形成(幾乎)任意長的複雜詞。

subword-level的分詞方式使模型相對合理的辭彙量(不會太多也不會太少),同時能夠學習有意義的與上下文無關的表示形式(另外,subword-level的分詞方式通過將模型分解成已知的子詞,使模型能夠處理以前從未見過的詞(oov)。

subword-level又分為不同的切法,這裡就到huggingface的tokenizers的實現部分了,常規的char-level或者word-level的分詞用spacy,nltk之類的工具就可以勝任了。

1、BPE;

GPT2和Roberta使用了這種分詞的方法,思路也很簡單,對應的是:

tk=kenizers.CharBPETokenizer()

Luke:深入理解NLP Subword演算法:BPE、WordPiece、ULMzhuanlan.zhihu.com圖標

參考自上文:

以語料:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}

為例,假設我們對原始的文本進行分詞之後只有上面4個詞,則:

1、對每個詞進行詞頻統計,並且對每個詞末尾的字元轉化為 末尾字元和</w>兩個字元,停止符”</w>”的意義在於表示subword是詞後綴。舉例來說:”st”字詞不加”</w>”可以出現在詞首如”st ar”,加了”</w>”表明改字詞位於詞尾,如”wide st</w>”,二者意義截然不同。

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w e s t </w>': 6, 'w i d e s t </w>': 3}

此時我們的詞表為

{“l”,”o”,’w’,”e”,”r”,”</w>”,”n”,”s”,”t”,”i”}

2、統計每一個連續位元組對的出現頻率,選擇最高頻者合併成新的subword

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}

最高頻連續位元組對”e”和”s”出現了6+3=9次,合併成”es”

需要注意的是,”es”生成後會消除”s”,因為上述語料中 “s”總和”es”共同出現,但是s除了”es”外就沒有其它的字元組合出現了,所以”s”被消除,但是”e”在”lower”中出現過有”er”或”we”這樣的組合,所以”e”沒有被消除,此時詞表變化為:

{“l”,”o”,’w’,”e”,”r”,”</w>”,”n”,”es”,”t”,”i”}

(補充另外兩種情況:

如果es中的e和s都是和es一起出現,除此之外沒有單獨再出現其它的字元組合,則es一起雄消除;

如果es中的e和s都各自有和其它字元的組合,則e和s都不會被消除,但是多了個新詞es)

3、繼續上述過程:

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}

此時最高頻連續位元組對”es”和”t”出現了6+3=9次, 合併成”est”。輸出:

同理,上一步的詞表中”t”被消除,”est”加入此表

。。。。。

繼續迭代直到達到預設的subword詞表大小或下一個最高頻的位元組對出現頻率為1。

上述就完成了編碼過程,得到了詞表,然後要把原始的word-level的分詞結果進行相應轉化:

1、將此表按照其中token的長度,從長到短排列;

例如排序好之後的詞表為:

[「errrr</w>」, 「tain</w>」, 「moun」, 「est</w>」, 「high」, 「the</w>」, 「a</w>」]

2、對原始的word-level的分詞結果進行轉化,例如原始的語料為:

[「the</w>」, 「highest</w>」, 「mountain</w>」]

則轉化為:

"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]

這樣就完成了BPE的分詞了;

迭代的終止條件是subword詞表大小或下一個最高頻的位元組對出現頻率為1。

  • 優點
    • 可以有效地平衡辭彙表大小和步數(編碼句子所需的token數量)。
  • 缺點
    • 基於貪婪和確定的符號替換,不能提供帶概率的多個分片結果,簡單來說就是某個詞可以被不同的詞表中的token替換,也就是一個word可以用不同的token的替換方式,例如”apple”可以被替換為app和le/w,也可以被替換為ap,ple(這裡的例子不是很恰當,理解即可)。

2、ByteLevelBPE

tk=tokenizers.ByteLevelBPETokenizer()

前面提到bpe以詞頻top-k數量建立的詞典;但是針對字元相對雜亂的日文和字元較豐富的中文,往往他們的罕見詞難以表示,就中文來說,字元級別就是到單個中文字,例如「竊位素餐」(指高級官員飽食終日,無所用心,貶義詞)這樣生僻的詞語最後很可能都沒法納入詞典之中,但是,舉個例子,在情感分析中,這個詞對於句子的情感分析又可能是決定性作用的。

為了理解這個演算法,看了一下原論文:

//arxiv.org/pdf/1909.03341.pdfarxiv.org

BBPE整體和BPE的邏輯類似,不同的是,粒度更細緻,BPE最多做到字元級別,但是BBPE是做到byte級別:

日文看不懂,看英文吧,「ask」這個單詞變成字元是”a”,”s”,”k”,然後對”a”,”s”,”k”做utf8編碼得到 「41 73 6B E2 96 81 71 75 65 73 74 69 6F 6E 73」。。。。這樣得到了byte級別的初始的分詞結果,這樣,原來的一個」a「變成了」41 73 6B E2 96 「。。。然後以”41″這樣的位元組為單位,更加細緻的做BPE了。。。wtf

然後使用BPE演算法來處理,這樣下來encode截斷我們最終的詞表就是一大堆的utf8編碼的組合了。。。。encode部分到這裡結束

比較麻煩的是decode部分,因為位元組碼的組合可能會得到無意義的組合,也就是得到的utf8的編碼組合可能無法映射回一個有意義的字元,所以作者使用了:

下面的公式來進行矯正。

沒查到啥模型用這種分詞演算法。。。。wtf

具體可見:

CaesarEX:淺談Byte-Level BPEzhuanlan.zhihu.com圖標

不想研究。。。

3、wordpiece

這個比較多模型用這種分詞方式,包括bert,distilbert,electra

對應的介面為:

from tokenizers import BertWordPieceTokenizer

wordpiece是BPE演算法的變種,整體的計算思路和BPE類似,僅僅在生成subword的時候不同,見下面的例子

wordpice整體上和BPE的計算思路類似,只不過下面的這個階段不一樣

2、(統計每一個連續位元組對的出現頻率,選擇最高頻者合併成新的subword)BPE使用的方法是k括弧中的,而wordpiece則使用了另一種方法

最高頻連續位元組對”e”和”s”出現了6+3=9次,合併成”es”

{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w es t </w>': 6, 'w i d es t </w>': 3}

而wordpiece則是從整個句子的層面出發去確認subword的合併結果,假設有個句子是:

“see you next week”初始拆分為字元之後是

“s”,”e”,”e”……. “e”,”k”

則語言模型概率為:

n表示這個句子拆分成字元之後的長度(繼續迭代的話就是拆分成subword的長度了),P(ti)表示”ti”這個字元或者subword在詞表中佔比的概率值,不過我們只需要計算下面的式子就可以:

可以看到,這裡和決策樹的分裂過層非常類似,兩個兩個相鄰字元或subword之間進行分裂判斷分裂增益是否增大,增大則合併。

從上面的公式,很容易發現,似然值的變化就是兩個子詞之間的互資訊。簡而言之,WordPiece每次選擇合併的兩個子詞,他們具有最大的互資訊值,也就是兩子詞在語言模型上具有較強的關聯性,它們經常在語料中以相鄰方式同時出現。

from

阿北:NLP三大Subword模型詳解:BPE、WordPiece、ULMzhuanlan.zhihu.com圖標


感覺是個大工程。。算了先看看別的東西吧。。。