在英語中,諸如“helps”、“helped”和“helping”之類的詞是同一個詞“help”的變形形式。“dog”與“dogs”的關系與“cat”與“cats”的關系相同,“boy”與“boyfriend”的關系與“girl”與“girlfriend”的關系相同。在法語和西班牙語等其他語言中,許多動詞有超過 40 種變形形式,而在芬蘭語中,一個名詞可能有多達 15 種格。在語言學中,形態學研究詞的形成和詞的關系。然而,word2vec 和 GloVe 都沒有探索單詞的內部結構。
15.6.1。fastText 模型
回想一下單詞在 word2vec 中是如何表示的。在skip-gram模型和連續詞袋模型中,同一個詞的不同變形形式直接由不同的向量表示,沒有共享參數。為了使用形態信息,fastText 模型提出了一種子詞嵌入方法,其中一個子詞是一個字符n-gram (Bojanowski等人,2017 年)。與學習詞級向量表示不同,fastText 可以被視為子詞級 skip-gram,其中每個中心詞由其子詞向量的總和表示。
讓我們舉例說明如何使用單詞“where”為 fastText 中的每個中心詞獲取子詞。首先,在單詞的首尾添加特殊字符“<”和“>”,以區別于其他子詞的前綴和后綴。然后,提取字符n-克從字。例如,當n=3,我們得到所有長度為 3 的子詞:“”,以及特殊子詞“”。
在 fastText 中,對于任何單詞w, 表示為Gw其所有長度在 3 到 6 之間的子字及其特殊子字的并集。詞匯表是所有詞的子詞的并集。出租zg是子詞的向量g在字典中,向量vw為詞w作為 skip-gram 模型中的中心詞的是其子詞向量的總和:
(15.6.1)vw=∑g∈Gwzg.
fastText 的其余部分與 skip-gram 模型相同。與skip-gram模型相比,fastText中的詞匯量更大,導致模型參數更多。此外,為了計算一個詞的表示,必須將其所有子詞向量相加,從而導致更高的計算復雜度。然而,由于具有相似結構的詞之間的子詞共享參數,稀有詞甚至詞匯表外的詞可能會在 fastText 中獲得更好的向量表示。
15.6.2。字節對編碼
在 fastText 中,所有提取的子詞都必須具有指定的長度,例如3到6,因此無法預定義詞匯量大小。為了允許在固定大小的詞匯表中使用可變長度的子詞,我們可以應用一種稱為字節對編碼(BPE) 的壓縮算法來提取子詞 ( Sennrich et al. , 2015 )。
字節對編碼對訓練數據集進行統計分析,以發現單詞中的常見符號,例如任意長度的連續字符。從長度為 1 的符號開始,字節對編碼迭代地合并最頻繁的一對連續符號以產生新的更長的符號。請注意,為了提高效率,不考慮跨越單詞邊界的對。最后,我們可以使用子詞這樣的符號來分詞。字節對編碼及其變體已用于流行的自然語言處理預訓練模型中的輸入表示,例如 GPT-2 (Radford等人,2019 年)和 RoBERTa (Liu等人,2019 年). 下面,我們將說明字節對編碼的工作原理。
首先,我們將符號詞匯表初始化為所有英文小寫字符、一個特殊的詞尾符號'_'和一個特殊的未知符號'[UNK]'。
import collections symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]']
import collections symbols = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]']
由于我們不考慮跨越單詞邊界的符號對,我們只需要一個字典raw_token_freqs將單詞映射到它們在數據集中的頻率(出現次數)。請注意,特殊符號'_'附加到每個單詞,以便我們可以輕松地從輸出符號序列(例如,“a_ tall er_ man”)中恢復單詞序列(例如,“a taller man”)。由于我們從僅包含單個字符和特殊符號的詞匯表開始合并過程,因此在每個單詞中的每對連續字符之間插入空格(字典的鍵token_freqs)。換句話說,空格是單詞中符號之間的分隔符。
raw_token_freqs = {'fast_': 4, 'faster_': 3, 'tall_': 5, 'taller_': 4} token_freqs = {} for token, freq in raw_token_freqs.items(): token_freqs[' '.join(list(token))] = raw_token_freqs[token] token_freqs
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5, 't a l l e r _': 4}
raw_token_freqs = {'fast_': 4, 'faster_': 3, 'tall_': 5, 'taller_': 4} token_freqs = {} for token, freq in raw_token_freqs.items(): token_freqs[' '.join(list(token))] = raw_token_freqs[token] token_freqs
{'f a s t _': 4, 'f a s t e r _': 3, 't a l l _': 5, 't a l l e r _': 4}
我們定義了以下get_max_freq_pair函數,該函數返回單詞中出現頻率最高的一對連續符號,其中單詞來自輸入字典的鍵token_freqs。
def get_max_freq_pair(token_freqs): pairs = collections.defaultdict(int) for token, freq in token_freqs.items(): symbols = token.split() for i in range(len(symbols) - 1): # Key of `pairs` is a tuple of two consecutive symbols pairs[symbols[i], symbols[i + 1]] += freq return max(pairs, key=pairs.get) # Key of `pairs` with the max value
def get_max_freq_pair(token_freqs): pairs = collections.defaultdict(int) for token, freq in token_freqs.items(): symbols = token.split() for i in range(len(symbols) - 1): # Key of `pairs` is a tuple of two consecutive symbols pairs[symbols[i], symbols[i + 1]] += freq return max(pairs, key=pairs.get) # Key of `pairs` with the max value
作為一種基于連續符號頻率的貪婪方法,字節對編碼將使用以下merge_symbols函數合并最頻繁的一對連續符號以產生新的符號。
def merge_symbols(max_freq_pair, token_freqs, symbols): symbols.append(''.join(max_freq_pair)) new_token_freqs = dict() for token, freq in token_freqs.items(): new_token = token.replace(' '.join(max_freq_pair), ''.join(max_freq_pair)) new_token_freqs[new_token] = token_freqs[token] return new_token_freqs
def merge_symbols(max_freq_pair, token_freqs, symbols): symbols.append(''.join(max_freq_pair)) new_token_freqs = dict() for token, freq in token_freqs.items(): new_token = token.replace(' '.join(max_freq_pair), ''.join(max_freq_pair)) new_token_freqs[new_token] = token_freqs[token] return new_token_freqs
現在我們在字典的鍵上迭代執行字節對編碼算法token_freqs。在第一次迭代中,出現頻率最高的一對連續符號是't'和'a',因此字節對編碼將它們合并以產生新的符號'ta'。在第二次迭代中,字節對編碼繼續合并'ta'并 'l'產生另一個新符號'tal'。
num_merges = 10 for i in range(num_merges): max_freq_pair = get_max_freq_pair(token_freqs) token_freqs = merge_symbols(max_freq_pair, token_freqs, symbols) print(f'merge #{i + 1}:', max_freq_pair)
merge #1: ('t', 'a') merge #2: ('ta', 'l') merge #3: ('tal', 'l') merge #4: ('f', 'a') merge #5: ('fa', 's') merge #6: ('fas', 't') merge #7: ('e', 'r') merge #8: ('er', '_') merge #9: ('tall', '_') merge #10: ('fast', '_')
num_merges = 10 for i in range(num_merges): max_freq_pair = get_max_freq_pair(token_freqs) token_freqs = merge_symbols(max_freq_pair, token_freqs, symbols) print(f'merge #{i + 1}:', max_freq_pair)
merge #1: ('t', 'a') merge #2: ('ta', 'l') merge #3: ('tal', 'l') merge #4: ('f', 'a') merge #5: ('fa', 's') merge #6: ('fas', 't') merge #7: ('e', 'r') merge #8: ('er', '_') merge #9: ('tall', '_') merge #10: ('fast', '_')
在字節對編碼的 10 次迭代之后,我們可以看到該列表 symbols現在包含 10 個以上的符號,這些符號是從其他符號迭代合并而來的。
print(symbols)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]', 'ta', 'tal', 'tall', 'fa', 'fas', 'fast', 'er', 'er_', 'tall_', 'fast_']
print(symbols)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '_', '[UNK]', 'ta', 'tal', 'tall', 'fa', 'fas', 'fast', 'er', 'er_', 'tall_', 'fast_']
對于在字典的鍵中指定的相同數據集 raw_token_freqs,數據集中的每個單詞現在都被子詞“fast_”、“fast”、“er_”、“tall_”和“tall”分割為字節對編碼的結果算法。例如,單詞“faster_”和“taller_”分別被分割為“fast er_”和“tall er_”。
print(list(token_freqs.keys()))
['fast_', 'fast er_', 'tall_', 'tall er_']
print(list(token_freqs.keys()))
['fast_', 'fast er_', 'tall_', 'tall er_']
請注意,字節對編碼的結果取決于所使用的數據集。我們還可以使用從一個數據集學習的子詞來分割另一個數據集的詞。作為一種貪婪的方法,以下 segment_BPE函數嘗試將輸入參數中的單詞分解為最長可能的子詞symbols。
def segment_BPE(tokens, symbols): outputs = [] for token in tokens: start, end = 0, len(token) cur_output = [] # Segment token with the longest possible subwords from symbols while start < len(token) and start < end: if token[start: end] in symbols: cur_output.append(token[start: end]) start = end end = len(token) else: end -= 1 if start < len(token): cur_output.append('[UNK]') outputs.append(' '.join(cur_output)) return outputs
def segment_BPE(tokens, symbols): outputs = [] for token in tokens: start, end = 0, len(token) cur_output = [] # Segment token with the longest possible subwords from symbols while start < len(token) and start < end: if token[start: end] in symbols: cur_output.append(token[start: end]) start = end end = len(token) else: end -= 1 if start < len(token): cur_output.append('[UNK]') outputs.append(' '.join(cur_output)) return outputs
在下文中,我們使用symbols從上述數據集中學習的列表中的子詞來分割tokens代表另一個數據集。
tokens = ['tallest_', 'fatter_'] print(segment_BPE(tokens, symbols))
['tall e s t _', 'fa t t er_']
tokens = ['tallest_', 'fatter_'] print(segment_BPE(tokens, symbols))
['tall e s t _', 'fa t t er_']
15.6.3。概括
fastText 模型提出了一種子詞嵌入方法。基于 word2vec 中的 skip-gram 模型,它將中心詞表示為其子詞向量的總和。
字節對編碼對訓練數據集進行統計分析,以發現單詞中的常見符號。作為一種貪婪的方法,字節對編碼迭代地合并最頻繁的一對連續符號。
子詞嵌入可以提高罕見詞和詞典外詞的表示質量。
15.6.4。練習
例如,大約有3×108可能的 6-英語克。當子詞太多時會出現什么問題?如何解決這個問題?提示:參考 fastText 論文(Bojanowski et al. , 2017)第 3.2 節的結尾。
如何設計基于連續詞袋模型的子詞嵌入模型?
獲得尺寸詞匯表m, 當初始符號詞匯量為n?
如何擴展字節對編碼的思想來提取短語?
-
pytorch
+關注
關注
2文章
803瀏覽量
13150
發布評論請先 登錄
相關推薦
評論