WordPiece、BPE詳解及程式碼

2020-08-13 16:40:35

1.BPE是幹什麼用的?

WordPiece字面理解是把word拆成piece一片一片,其實就是這個意思。
WordPiece的一種主要的實現方式叫做BPE(Byte-Pair Encoding)雙位元組編碼。
「loved」,「loving」,「loves"這三個單詞。其實本身的語意都是「愛」的意思,但是如果我們以單詞爲單位,那它們就算不一樣的詞,在英語中不同後綴的詞非常的多,就會使得詞表變的很大,訓練速度變慢,訓練的效果也不是太好。
BPE演算法通過訓練,能夠把上面的3個單詞拆分成"lov」,「ed」,「ing」,"es"幾部分,這樣可以把詞的本身的意思和時態分開,有效的減少了詞表的數量。

BPE的作用如下:

1.傳統詞表示方法無法很好的處理未知或罕見的詞彙(OOV問題:out of vocabulary)

2.傳統詞tokenization方法不利於模型學習詞綴之前的關係

3.Character embedding作爲OOV的解決方法粒度太細

4.Subword粒度在詞與字元之間,能夠較好的平衡OOV問題

2.BPE演算法

1.準備足夠大的訓練語料

2.確定期望的subword詞表大小

3.將單詞拆分爲字元序列並在末尾新增後綴「 </ w>」,統計單詞頻率。本階段的subword的粒度是字元。例如,「 low」的頻率爲5,那麼我們將其改寫爲「 l o w </ w>」:5
(備註:爲什麼加入"< /w >"在解碼階段有說明)

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

5.重複第4步直到達到第2步設定的subword詞表大小或下一個最高頻的位元組對出現頻率爲1

例子

{'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}

Iter 1, 最高頻連續位元組對"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}

Iter 2, 最高頻連續位元組對"es""t"出現了6+3=9, 合併成"est"。輸出:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est </w>': 6, 'w i d est </w>': 3}

Iter 3, 以此類推,最高頻連續位元組對爲"est""</w>" 輸出:
{'l o w </w>': 5, 'l o w e r </w>': 2, 'n e w est</w>': 6, 'w i d est</w>': 3}

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

說明
每次合併後詞表可能出現3種變化:

+1,表明加入合併後的新字詞,同時原來在2個子詞還保留(2個字詞不是完全同時連續出現)

+0,表明加入合併後的新字詞,同時原來2個子詞中一個保留,一個被消解(一個字詞完全隨着另一個字詞的出現而緊跟着出現)

-1,表明加入合併後的新字詞,同時原來2個子詞都被消解(2個字詞同時連續出現)

實際上,隨着合併的次數增加,詞表大小通常先增加後減小。

3.BPE程式碼實現


import re, collections
 
 
def get_stats(vocab):
    pairs = collections.defaultdict(int)
    for word, freq in vocab.items():
        symbols = word.split()
        for i in range(len(symbols)-1):
            pairs[symbols[i],symbols[i+1]] += freq
    return pairs
 
 
def merge_vocab(pair, v_in):
    v_out = {}
    bigram = re.escape(' '.join(pair))
    p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')
    for word in v_in:
        w_out = p.sub(''.join(pair), word)
        v_out[w_out] = v_in[word]
    return v_out
 
 
vocab = {'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}
num_merges = 1000
for i in range(num_merges):
    pairs = get_stats(vocab)
    if not pairs:
        break
    best = max(pairs, key=pairs.get)
    vocab = merge_vocab(best, vocab)
    print(best)
 
 
# print output
# ('e', 's')
# ('es', 't')
# ('est', '</w>')
# ('l', 'o')
# ('lo', 'w')
# ('n', 'e')
# ('ne', 'w')
# ('new', 'est</w>')
# ('low', '</w>')
# ('w', 'i')
# ('wi', 'd')
# ('wid', 'est</w>')
# ('low', 'e')
# ('lowe', 'r')
# ('lower', '</w>')

編碼:構建完詞表之後,對詞表按照長度進行排序。對於要預訓練的text,先將其按照詞表的順序進行分解(即編碼)。
如下例子:


# 給定單詞序列
[「the</w>, 「highest</w>, 「mountain</w>]
 
# 假設已有排好序的subword詞表
[「errrr</w>, 「tain</w>, 「moun」, 「est</w>, 「high」, 「the</w>, 「a</w>]
 
# 迭代結果
"the</w>" -> ["the</w>"]
"highest</w>" -> ["high", "est</w>"]
"mountain</w>" -> ["moun", "tain</w>"]

解碼

# 編碼序列
[「the</w>, 「high」, 「est</w>, 「moun」, 「tain</w>]
 

# 解碼序列
「the</w> highest</w> mountain</w>

直接拼接起來,"< /w >"就可以隔離開不同的單詞。所以,加入"< /w >"是爲了在解碼階段隔離開不同的單詞。

4.適用範圍

BPE一般適用在歐美語言,因爲歐美語言大多是字元形式,涉及字首、後綴的單詞比較多。而中文的漢字一般不用BPE進行編碼,因爲中文是字無法進行拆分。對中文的處理通常只有分詞分字兩種。理論上分詞效果更好,更好的區別語意。分字效率高、簡潔,因爲常用的字不過3000字,詞表更加簡短。
參考鏈接:
一文讀懂BERT中的WordPiece
NLP Subword三大演算法原理:BPE、WordPiece、ULM