官术网_书友最值得收藏!

2.4 基于計數的方法的改進

上一節我們創建了單詞的共現矩陣,并使用它成功地將單詞表示為了向量。但是,這個共現矩陣還有許多可以改進的地方。本節我們將對其進行改進,并使用更實用的語料庫,獲得單詞的“真實的”分布式表示。

2.4.1 點互信息

上一節的共現矩陣的元素表示兩個單詞同時出現的次數。但是,這種“原始”的次數并不具備好的性質。如果我們看一下高頻詞匯(出現次數很多的單詞),就能明白其原因了。

比如,我們來考慮某個語料庫中the和car共現的情況。在這種情況下,我們會看到很多“...the car...”這樣的短語。因此,它們的共現次數將會很大。另外,car和drive也明顯有很強的相關性。但是,如果只看單詞的出現次數,那么與drive相比,the和car的相關性更強。這意味著,僅僅因為the是個常用詞,它就被認為與car有很強的相關性。

為了解決這一問題,可以使用點互信息(Pointwise Mutual Information, PMI)這一指標。對于隨機變量xy,它們的PMI定義如下(關于概率,將在3.5.1節詳細說明):

其中,Px)表示x發生的概率,Py)表示y發生的概率,Px, y)表示xy同時發生的概率。PMI的值越高,表明相關性越強。

在自然語言的例子中,Px)就是指單詞x在語料庫中出現的概率。假設某個語料庫中有10000個單詞,其中單詞the出現了100次,則P("the")==0.01。另外,Px, y)表示單詞xy同時出現的概率。假設the和car一起出現了10次,則。

現在,我們使用共現矩陣(其元素表示單詞共現的次數)來重寫式(2.2)。這里,將共現矩陣表示為C,將單詞xy的共現次數表示為Cx, y),將單詞xy的出現次數分別表示為Cx)、Cy),將語料庫的單詞數量記為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矩陣而進行了簡單的實現。具體來說,當單詞xy的共現次數為Cx, 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個矩陣的乘積,其中UV是列向量彼此正交的正交矩陣,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所示根據操作系統的種類或Matplotlib版本的不同,輸出的圖可能和圖2-11所有不同。。

圖2-11 對共現矩陣執行SVD,并在圖上繪制各個單詞的二維向量(i和goodbye重疊)

觀察該圖可以發現,goodbye和hello、you和i位置接近,這是比較符合我們的直覺的。但是,因為我們使用的語料庫很小,有些結果就比較微妙。下面,我們將使用更大的PTB數據集進行相同的實驗。首先,我們簡單介紹一下PTB數據集。

如果矩陣大小是N,SVD的計算的復雜度將達到ON3)。這意味著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降維獲得好的單詞向量。這就是單詞的分布式表示,每個單詞表示為固定長度的密集向量。

在本章的實驗中,我們只看了一部分單詞的近義詞,但是可以確認許多其他的單詞也有這樣的性質。期待使用更大的語料庫可以獲得更好的單詞的分布式表示!

主站蜘蛛池模板: 河曲县| 元阳县| 横山县| 福泉市| 岚皋县| 灌南县| 连州市| 邵东县| 阿克陶县| 古田县| 安岳县| 富川| 桓仁| 和林格尔县| 湟源县| 方山县| 蒙阴县| 微山县| 嘉峪关市| 乐至县| 宾阳县| SHOW| 肇庆市| 象州县| 永善县| 临澧县| 泸定县| 吴旗县| 蛟河市| 富源县| 洛南县| 三都| 卢氏县| 岳池县| 仙居县| 重庆市| 沙湾县| 东宁县| 丁青县| 绥滨县| 舒兰市|