WordPiece字面理解是把word拆成piece一片一片,其實就是這個意思。
WordPiece的一種主要的實現方式叫做BPE(Byte-Pair Encoding)雙位元組編碼。
「loved」,「loving」,「loves"這三個單詞。其實本身的語意都是「愛」的意思,但是如果我們以單詞爲單位,那它們就算不一樣的詞,在英語中不同後綴的詞非常的多,就會使得詞表變的很大,訓練速度變慢,訓練的效果也不是太好。
BPE演算法通過訓練,能夠把上面的3個單詞拆分成"lov」,「ed」,「ing」,"es"幾部分,這樣可以把詞的本身的意思和時態分開,有效的減少了詞表的數量。
1.傳統詞表示方法無法很好的處理未知或罕見的詞彙(OOV問題:out of vocabulary)
2.傳統詞tokenization方法不利於模型學習詞綴之前的關係
3.Character embedding作爲OOV的解決方法粒度太細
4.Subword粒度在詞與字元之間,能夠較好的平衡OOV問題
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個字詞同時連續出現)
實際上,隨着合併的次數增加,詞表大小通常先增加後減小。
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 >"是爲了在解碼階段隔離開不同的單詞。
BPE一般適用在歐美語言,因爲歐美語言大多是字元形式,涉及字首、後綴的單詞比較多。而中文的漢字一般不用BPE進行編碼,因爲中文是字無法進行拆分。對中文的處理通常只有分詞和分字兩種。理論上分詞效果更好,更好的區別語意。分字效率高、簡潔,因爲常用的字不過3000字,詞表更加簡短。
參考鏈接:
一文讀懂BERT中的WordPiece
NLP Subword三大演算法原理:BPE、WordPiece、ULM