- 深度學習進階:自然語言處理
- (日)齋藤康毅
- 5070字
- 2021-02-07 09:25:59
2.4 基于計數的方法的改進
上一節我們創建了單詞的共現矩陣,并使用它成功地將單詞表示為了向量。但是,這個共現矩陣還有許多可以改進的地方。本節我們將對其進行改進,并使用更實用的語料庫,獲得單詞的“真實的”分布式表示。
2.4.1 點互信息
上一節的共現矩陣的元素表示兩個單詞同時出現的次數。但是,這種“原始”的次數并不具備好的性質。如果我們看一下高頻詞匯(出現次數很多的單詞),就能明白其原因了。
比如,我們來考慮某個語料庫中the和car共現的情況。在這種情況下,我們會看到很多“...the car...”這樣的短語。因此,它們的共現次數將會很大。另外,car和drive也明顯有很強的相關性。但是,如果只看單詞的出現次數,那么與drive相比,the和car的相關性更強。這意味著,僅僅因為the是個常用詞,它就被認為與car有很強的相關性。
為了解決這一問題,可以使用點互信息(Pointwise Mutual Information, PMI)這一指標。對于隨機變量x和y,它們的PMI定義如下(關于概率,將在3.5.1節詳細說明):

其中,P(x)表示x發生的概率,P(y)表示y發生的概率,P(x, y)表示x和y同時發生的概率。PMI的值越高,表明相關性越強。
在自然語言的例子中,P(x)就是指單詞x在語料庫中出現的概率。假設某個語料庫中有10000個單詞,其中單詞the出現了100次,則P("the")==0.01。另外,P(x, y)表示單詞x和y同時出現的概率。假設the和car一起出現了10次,則
。
現在,我們使用共現矩陣(其元素表示單詞共現的次數)來重寫式(2.2)。這里,將共現矩陣表示為C,將單詞x和y的共現次數表示為C(x, y),將單詞x和y的出現次數分別表示為C(x)、C(y),將語料庫的單詞數量記為N,則式(2.2)可以重寫為:

根據式(2.3),可以由共現矩陣求PMI。下面我們來具體地算一下。這里假設語料庫的單詞數量(N)為10000,the出現100次,car出現20次, drive出現10次,the和car共現10次,car和drive共現5次。這時,如果從共現次數的角度來看,則與drive相比,the和car的相關性更強。而如果從PMI的角度來看,結果是怎樣的呢?我們來計算一下。

結果表明,在使用PMI的情況下,與the相比,drive和car具有更強的相關性。這是我們想要的結果。之所以出現這個結果,是因為我們考慮了單詞單獨出現的次數。在這個例子中,因為the本身出現得多,所以PMI的得分被拉低了。式中的“≈”(near equal)表示近似相等的意思。
雖然我們已經獲得了PMI這樣一個好的指標,但是PMI也有一個問題。那就是當兩個單詞的共現次數為0時,log20=-∞。為了解決這個問題,實踐上我們會使用下述正的點互信息(Positive PMI,PPMI)。

根據式(2.6),當PMI是負數時,將其視為0,這樣就可以將單詞間的相關性表示為大于等于0的實數。下面,我們來實現將共現矩陣轉化為PPMI矩陣的函數。我們把這個函數稱為ppmi(C, verbose=False, eps=1e-8) (common/util.py)。
def ppmi(C, verbose=False, eps=1e-8): M = np.zeros_like(C, dtype=np.float32) N = np.sum(C) S = np.sum(C, axis=0) total = C.shape[0] * C.shape[1] cnt = 0 for i in range(C.shape[0]): for j in range(C.shape[1]): pmi = np.log2(C[i, j] * N / (S[j]*S[i]) + eps) M[i, j] = max(0, pmi) if verbose: cnt += 1 if cnt % (total//100+1) == 0: print ('%.1f%% done' % (100*cnt/total)) return M
這里,參數C表示共現矩陣,verbose是決定是否輸出運行情況的標志。當處理大語料庫時,設置verbose=True,可以用于確認運行情況。在這段代碼中,為了僅從共現矩陣求PPMI矩陣而進行了簡單的實現。具體來說,當單詞x和y的共現次數為C(x, y)時,,
,
,進行這樣近似并實現。另外,在上述代碼中,為了防止np.log2(0)=-inf而使用了微小值eps。
在2.3.5節中,為了防止“除數為0”的錯誤,我們給分母添加了一個微小值。這里也一樣,通過將np.log(x)改為np.log(x + eps),可以防止對數運算發散到負無窮大。
現在將共現矩陣轉化為PPMI矩陣,可以像下面這樣進行實現(ch02/ppmi.py)。
import sys sys.path.append('..') import numpy as np from common.util import preprocess, create_co_matrix, cos_similarity, ? ppmi text = 'You say goodbye and I say hello.' corpus, word_to_id, id_to_word = preprocess(text) vocab_size = len(word_to_id) C = create_co_matrix(corpus, vocab_size) W = ppmi(C) np.set_printoptions(precision=3) # 有效位數為3位 print ('covariance matrix') print (C) print ('-'*50) print ('PPMI') print (W)
運行該文件,可以得到下述結果。
covariance matrix [[0 1 0 0 0 0 0] [1 0 1 0 1 1 0] [0 1 0 1 0 0 0] [0 0 1 0 1 0 0] [0 1 0 1 0 0 0] [0 1 0 0 0 0 1] [0 0 0 0 0 1 0]] -------------------------------------------------- PPMI [[ 0. 1.807 0. 0. 0. 0. 0. ] [ 1.807 0. 0.807 0. 0.807 0.807 0. ] [ 0. 0.807 0. 1.807 0. 0. 0. ] [ 0. 0. 1.807 0. 1.807 0. 0. ] [ 0. 0.807 0. 1.807 0. 0. 0. ] [ 0. 0.807 0. 0. 0. 0. 2.807] [ 0. 0. 0. 0. 0. 2.807 0. ]]
這樣一來,我們就將共現矩陣轉化為了PPMI矩陣。此時,PPMI矩陣的各個元素均為大于等于0的實數。我們得到了一個由更好的指標形成的矩陣,這相當于獲取了一個更好的單詞向量。
但是,這個PPMI矩陣還是存在一個很大的問題,那就是隨著語料庫的詞匯量增加,各個單詞向量的維數也會增加。如果語料庫的詞匯量達到10萬,則單詞向量的維數也同樣會達到10萬。實際上,處理10萬維向量是不現實的。
另外,如果我們看一下這個矩陣,就會發現其中很多元素都是0。這表明向量中的絕大多數元素并不重要,也就是說,每個元素擁有的“重要性”很低。另外,這樣的向量也容易受到噪聲影響,穩健性差。對于這些問題,一個常見的方法是向量降維。
2.4.2 降維
所謂降維(dimensionality reduction),顧名思義,就是減少向量維度。但是,并不是簡單地減少,而是在盡量保留“重要信息”的基礎上減少。如圖2-8所示,我們要觀察數據的分布,并發現重要的“軸”。

圖2-8 降維示意圖:發現重要的軸(數據分布廣的軸),將二維數據表示為一維數據
在圖2-8中,考慮到數據的廣度,導入了一根新軸,以將原來用二維坐標表示的點表示在一個坐標軸上。此時,用新軸上的投影值來表示各個數據點的值。這里非常重要的一點是,選擇新軸時要考慮數據的廣度。如此,僅使用一維的值也能捕獲數據的本質差異。在多維數據中,也可以進行同樣的處理。
向量中的大多數元素為0的矩陣(或向量)稱為稀疏矩陣(或稀疏向量)。這里的重點是,從稀疏向量中找出重要的軸,用更少的維度對其進行重新表示。結果,稀疏矩陣就會被轉化為大多數元素均不為0的密集矩陣。這個密集矩陣就是我們想要的單詞的分布式表示。
降維的方法有很多,這里我們使用奇異值分解(Singular Value Decomposition,SVD)。SVD將任意矩陣分解為3個矩陣的乘積,如下式所示:

如式(2.7)所示,SVD將任意的矩陣X分解為U、S、V這3個矩陣的乘積,其中U和V是列向量彼此正交的正交矩陣,S是除了對角線元素以外其余元素均為0的對角矩陣。圖2-9中直觀地表示出了這些矩陣。

圖2-9 基于SVD的矩陣變換(白色部分表示元素為0)
在式(2.7)中,U是正交矩陣。這個正交矩陣構成了一些空間中的基軸(基向量),我們可以將矩陣U作為“單詞空間”。S是對角矩陣,奇異值在對角線上降序排列。簡單地說,我們可以將奇異值視為“對應的基軸”的重要性。這樣一來,如圖2-10所示,減少非重要元素就成為可能。

圖2-10 基于SVD的降維示意圖
如圖2-10所示,矩陣S的奇異值小,對應的基軸的重要性低,因此,可以通過去除矩陣U中的多余的列向量來近似原始矩陣。用我們正在處理的“單詞的PPMI矩陣”來說明的話,矩陣X的各行包含對應的單詞ID的單詞向量,這些單詞向量使用降維后的矩陣U′表示。
單詞的共現矩陣是正方形矩陣,但在圖2-10中,為了和之前的圖一致,畫的是長方形。另外,這里對SVD的介紹僅限于最直觀的概要性的說明。想從數學角度仔細理解的讀者,請參考文獻[20]等。
2.4.3 基于SVD的降維
接下來,我們使用Python來實現SVD,這里可以使用NumPy的linalg模塊中的svd方法。linalg是linear algebra(線性代數)的簡稱。下面,我們創建一個共現矩陣,將其轉化為PPMI矩陣,然后對其進行SVD (ch02/count_method_small.py)。
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_co_matrix, ppmi
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(id_to_word)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)
# SVD
U, S, V = np.linalg.svd(W)
SVD執行完畢。上面的變量U包含經過SVD轉化的密集向量表示?,F在,我們來看一下它的內容。單詞ID為0的單詞向量如下。
print (C[0]) # 共現矩陣 # [0 1 0 0 0 0 0] print (W[0]) # PPMI矩陣 # [ 0. 1.807 0. 0. 0. 0. 0. ] print (U[0]) # SVD # [ 3.409e-01 -1.110e-16 -1.205e-01 -4.441e-16 0.000e+00 -9.323e-01 # 2.226e-16]
如上所示,原先的稀疏向量W[0]經過SVD被轉化成了密集向量U[0]。如果要對這個密集向量降維,比如把它降維到二維向量,取出前兩個元素即可。
print (U[0, :2])
# [ 3.409e-01 -1.110e-16]
這樣我們就完成了降維?,F在,我們用二維向量表示各個單詞,并把它們畫在圖上,代碼如下。
for word, word_id in word_to_id.items(): plt.annotate(word, (U[word_id, 0], U[word_id, 1])) plt.scatter(U[:,0], U[:,1], alpha=0.5) plt.show()
plt.annotate(word, x, y)函數在2D圖形中坐標為(x, y)的地方繪制單詞的文本。執行上述代碼,結果如圖2-11所示。

圖2-11 對共現矩陣執行SVD,并在圖上繪制各個單詞的二維向量(i和goodbye重疊)
觀察該圖可以發現,goodbye和hello、you和i位置接近,這是比較符合我們的直覺的。但是,因為我們使用的語料庫很小,有些結果就比較微妙。下面,我們將使用更大的PTB數據集進行相同的實驗。首先,我們簡單介紹一下PTB數據集。
如果矩陣大小是N,SVD的計算的復雜度將達到O(N3)。這意味著SVD需要與N的立方成比例的計算量。因為現實中這樣的計算量是做不到的,所以往往會使用Truncated SVD[21]等更快的方法。Truncated SVD通過截去(truncated)奇異值較小的部分,從而實現高速化。下一節,作為另一個選擇,我們將使用sklearn庫的Truncated SVD。
2.4.4 PTB數據集
到目前為止,我們使用了非常小的文本數據作為語料庫。這里,我們將使用一個大小合適的“真正的”語料庫——Penn Treebank語料庫(以下簡稱為PTB)。
PTB語料庫經常被用作評價提案方法的基準。本書中我們將使用PTB語料庫進行各種實驗。
我們使用的PTB語料庫在word2vec的發明者托馬斯·米科洛夫(Tomas Mikolov)的網頁上有提供。這個PTB語料庫是以文本文件的形式提供的,與原始的PTB的文章相比,多了若干預處理,包括將稀有單詞替換成特殊字符<unk>(unk是unknown的簡稱),將具體的數字替換成“N”等。下面,我們將經過這些預處理之后的文本數據作為PTB語料庫使用。作為參考,圖2-12給出了PTB語料庫的部分內容。
如圖2-12所示,在PTB語料庫中,一行保存一個句子。在本書中,我們將所有句子連接起來,并將其視為一個大的時序數據。此時,在每個句子的結尾處插入一個特殊字符<eos>(eos是end of sentence的簡稱)。

圖2-12 PTB語料庫(文本文件)的例子
本書不考慮句子的分割,將多個句子連接起來得到的內容視為一個大的時序數據。當然,也可以以句子為單位進行處理,比如,以句子為單位計算詞頻。不過,考慮到簡單性,本書不進行以句子為單位的處理。
在本書中,為了方便使用Penn Treebank數據集,我們準備了專門的Python代碼。這個文件在dataset/ptb.py中,并假定從章節目錄(ch01、ch02、...)使用。比如,我們將當前目錄移到ch02目錄,并在這個目錄中調用pythonshow_ptb.py。使用ptb.py的例子如下所示(ch02/show_ptb.py)。
import sys sys.path.append('..') from dataset import ptb corpus, word_to_id, id_to_word = ptb.load_data('train') print ('corpus size:', len(corpus)) print ('corpus[:30]:', corpus[:30]) print () print ('id_to_word[0]:', id_to_word[0]) print ('id_to_word[1]:', id_to_word[1]) print ('id_to_word[2]:', id_to_word[2]) print () print ("word_to_id['car']:", word_to_id['car']) print ("word_to_id['happy']:", word_to_id['happy']) print ("word_to_id['lexus']:", word_to_id['lexus'])
后面再具體解釋這段代碼,我們先來看一下它的執行結果。
corpus size: 929589 corpus[:30]: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29] id_to_word[0]: aer id_to_word[1]: banknote id_to_word[2]: berlitz word_to_id['car']: 3856 word_to_id['happy']: 4428 word_to_id['lexus']: 7426
語料庫的用法和之前一樣。corpus中保存了單詞ID列表,id_to_word是將單詞ID轉化為單詞的字典,word_to_id是將單詞轉化為單詞ID的字典。
如上面的代碼所示,使用ptb.load_data()加載數據。此時,指定參數’train'、'test’和’valid’中的一個,它們分別對應訓練用數據、測試用數據和驗證用數據中的一個。以上就是ptb.py文件的使用方法。
2.4.5 基于PTB數據集的評價
下面,我們將基于計數的方法應用于PTB數據集。這里建議使用更快速的SVD對大矩陣執行SVD,為此我們需要安裝sklearn模塊。當然,雖然仍可以使用基本版的SVD(np.linalg.svd()),但是這需要更多的時間和內存。我們把源代碼一并給出,如下所示(ch02/count_method_big.py)。
import sys sys.path.append('..') import numpy as np from common.util import most_similar, create_co_matrix, ppmi from dataset import ptb window_size = 2 wordvec_size = 100 corpus, word_to_id, id_to_word = ptb.load_data('train') vocab_size = len(word_to_id) print ('counting co-occurrence ...') C = create_co_matrix(corpus, vocab_size, window_size) print ('calculating PPMI ...') W = ppmi(C, verbose=True) print ('calculating SVD ...') try: # truncated SVD (fast!) from sklearn.utils.extmath import randomized_svd U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5, random_state=None) except ImportError: # SVD (slow) U, S, V = np.linalg.svd(W) word_vecs = U[:, :wordvec_size] querys = ['you', 'year', 'car', 'toyota'] for query in querys: most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
這里,為了執行SVD,我們使用了sklearn的randomized_svd()方法。該方法通過使用了隨機數的Truncated SVD,僅對奇異值較大的部分進行計算,計算速度比常規的SVD快。剩余的代碼和之前使用小語料庫時的代碼差不太多。執行代碼,可以得以下結果(因為使用了隨機數,所以在使用Truncated SVD的情況下,每次的結果都不一樣)。
[query] you i: 0.702039909619 we: 0.699448543998 've: 0.554828709147 do: 0.534370693098 else: 0.512044146526 [query] year month: 0.731561990308 quarter: 0.658233992457 last: 0.622425716735 earlier: 0.607752074689 next: 0.601592506413 [query] car luxury: 0.620933665528 auto: 0.615559874277 cars: 0.569818364381 vehicle: 0.498166879744 corsica: 0.472616831915 [query] toyota motor: 0.738666107068 nissan: 0.677577542584 motors: 0.647163210589 honda: 0.628862370943 lexus: 0.604740429865
觀察結果可知,首先,對于查詢詞you,可以看到i、we等人稱代詞排在前面,這些都是在語法上具有相同用法的詞。再者,查詢詞year有month、quarter等近義詞,查詢詞car有auto、vehicle等近義詞。此外,將toyota作為查詢詞時,出現了nissan、honda和lexus等汽車制造商名或者品牌名。像這樣,在含義或語法上相似的單詞表示為相近的向量,這符合我們的直覺。
我們終于成功地將單詞含義編碼成了向量,真是可喜可賀!使用語料庫,計算上下文中的單詞數量,將它們轉化PPMI矩陣,再基于SVD降維獲得好的單詞向量。這就是單詞的分布式表示,每個單詞表示為固定長度的密集向量。
在本章的實驗中,我們只看了一部分單詞的近義詞,但是可以確認許多其他的單詞也有這樣的性質。期待使用更大的語料庫可以獲得更好的單詞的分布式表示!