基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

飛槳PaddlePaddle發表於2019-04-22

詞向量是自然語言處理中常見的一個操作,是搜尋引擎、廣告系統、推薦系統等網際網路服務背後常見的基礎技術。

在這些網際網路服務裡,我們經常要比較兩個詞或者兩段文字之間的相關性。為了做這樣的比較,我們往往把詞表示成計算機適合處理的方式。最自然的方式莫過於向量空間模型(vector space model)。在這種方式裡,每個詞被表示成一個實數向量(one-hot vector),其長度為字典大小,每個維度對應一個字典裡的每個詞,除了這個詞對應維度上的值是1,其他元素都是0。One-hot vector雖然自然,但是用處有限。比如,在網際網路廣告系統裡,如果使用者輸入的query是“母親節”,而有一個廣告的關鍵詞是“康乃馨”。

按照常理,我們知道這兩個詞之間是有聯絡的——母親節通常應該送給母親一束康乃馨;但是這兩個詞對應的one-hot vectors之間的距離度量,無論是歐氏距離還是餘弦相似度(cosine similarity),由於其向量正交,都認為這兩個詞毫無相關性。得出這種與我們相悖的結論的根本原因是:每個詞本身的資訊量都太小。所以,僅僅給定兩個詞,不足以讓我們準確判別它們是否相關。要想精確計算相關性,我們還需要更多的資訊——從大量資料裡透過機器學習方法歸納出來的知識。

在機器學習領域,透過詞向量模型(word embedding model)可將一個one-hot vector對映到一個維度更低的實數向量(embedding vector),如:

embedding(母親節)=[0.3,4.2,−1.5,...];
embedding(康乃馨)=[0.2,5.6,−2.3,...];

在這個對映到的實數向量表示中,兩個語義(或用法)上相似的詞對應的詞向量“更像”,這樣如“母親節”和“康乃馨”的對應詞向量的餘弦相似度就不再為零了。

詞向量模型可以是機率模型、共生矩陣(co-occurrence matrix)模型或神經元網路模型。在用神經網路模型求詞向量之前,傳統的做法是統計一個詞語的共生矩陣X,在對X做矩陣分解,得到了所有詞的詞向量。

但是傳統的方法有三大問題:1)由於很多詞沒有出現,導致矩陣極其稀疏;2)矩陣非常大,維度太高;3)需要手動去掉停用詞(如although, a,...),不然這些頻繁出現的詞也會影響矩陣分解的效果。

而基於神經網路的模型不需要計算和儲存一個在全語料上統計產生的大表,是透過學習語義資訊得到詞向量,因此能很好地解決以上問題。本教程旨在展示神經網路訓練詞向量的細節,以及如何用PaddlePaddle訓練一個詞向量模型。

專案地址:

http://paddlepaddle.org/documentation/docs/zh/1.3/beginners_guide/basics/word2vec/index.html

基於PaddlePaddle訓練一個詞向量模型操作詳情請參照Github:

https://github.com/PaddlePaddle/book/blob/develop/04.word2vec/README.cn.md

效果展示

當詞向量訓練好後,我們可以用資料視覺化演算法t-SNE[4]畫出詞語特徵在二維上的投影(如下圖所示)。從圖中可以看出,語義相關的詞語(如a, the, these; big, huge)在投影上距離很近,語意無關的詞(如say, business; decision, japan)在投影上的距離很遠

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

另一方面,我們知道兩個向量的餘弦值在[−1,1][−1,1]的區間內:兩個完全相同的向量餘弦值為1, 兩個相互垂直的向量之間餘弦值為0,兩個方向完全相反的向量餘弦值為-1,即相關性和餘弦值大小成正比。因此我們還可以計算兩個詞向量的餘弦相似度:

please input two words: big huge
similarity: 0.899180685161
please input two words: from company
similarity: -0.0997506977351

以上結果可以透過執行calculate_dis.py載入字典裡的單詞和對應訓練特徵結果得到,我們將在模型應用中詳細描述用法。

模型概覽

在這裡我們介紹三個訓練詞向量的模型:N-gram模型,CBOW模型和Skip-gram模型,它們的中心思想都是透過上下文得到一個詞出現的機率。對於N-gram模型,我們會先介紹語言模型的概念,並在之後的訓練模型中,帶大家用PaddlePaddle實現它。而後兩個模型,是近年來最有名的神經元詞向量模型,由Tomas Mikolov 在Google 研發[3],雖然它們很淺很簡單,但訓練效果很好。

N-gram neural model

在計算語言學中,N-gram是一種重要的文字表示方法,表示一個文字中連續的n個項。基於具體的應用場景,每一項可以是一個字母、單詞或者音節。N-gram模型也是統計語言模型中的一種重要方法,用N-gram訓練語言模型時,一般用每個N-gram的歷史n-1個詞語組成的內容來預測第n個詞。

Yoshua Bengio等科學家就於2003年在著名論文Neural Probabilistic Language Models [1]中介紹如何學習一個神經元網路表示的詞向量模型。文中的神經機率語言模型(Neural Network Language Model,NNLM)透過一個線性對映和一個非線性隱層連線,同時學習了語言模型和詞向量,即透過學習大量語料得到詞語的向量表達,透過這些向量得到整個句子的機率。

因所有的詞語都用一個低維向量來表示,用這種方法學習語言模型可以克服維度災難(curse of dimensionality)。一句話中第t個詞的機率和該句話的前t−1個詞相關。可實際上越遠的詞語其實對該詞的影響越小,那麼如果考慮一個n-gram, 每個詞都只受其前面n-1個詞的影響,則有:

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

語料中都是有意義的句子,N-gram模型的最佳化目標則是最大化目標函式:

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)其中f(Wt,Wt-1,...,Wt-n+1)表示根據歷史n-1個詞得到當前詞Wt的條件機率,R(θ)表示引數正則項。

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)圖2. N-gram神經網路模型

圖2展示了N-gram神經網路模型,從下往上看,該模型分為以下幾個部分:-對於每個樣本,模型輸入Wt-n+1,...,Wt-1,輸出句子第t個詞在字典中|V|個詞上的機率分佈。每個輸入詞Wt-n+1,...,Wt-1首先透過對映矩陣對映到詞C(Wt-n-1),...,C(Wt-1)。然後所有詞語的詞向量拼接成一個大向量,並經過一個非線性對映得到歷史詞語的隱層表示

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

其中,x為所有詞語的詞向量拼接成的大向量,表示文字歷史特徵;θ、U、b1、b2和W分別為詞向量層到隱層連線的引數。g表示未經歸一化的所有輸出單詞機率,gi表示未經歸一化的字典中第i個單詞的輸出機率。

根據softmax的定義,透過歸一化gi, 生成目標詞Wt的機率為

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

整個網路的損失值(cost)為多類分類交叉熵,用公式表示為

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

其中yik表示第i個樣本第k類的真實標籤(0或1),softmax(gik)表示第ii個樣本第k類softmax輸出的機率。

Continuous Bag-of-Words model(CBOW)

CBOW模型透過一個詞的上下文(各N個詞)預測當前詞。當N=2時,模型如下圖所示:

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

圖3. CBOW模型

具體來說,不考慮上下文的詞語輸入順序,CBOW是用上下文詞語的詞向量的均值來預測當前詞。即:

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

其中Xt為第t個詞的詞向量,分類分數(score)向量z=U*context,最終的分類y採用softmax,損失函式採用多類分類交叉熵。

Skip-gram model

CBOW的好處是對上下文詞語的分佈在詞向量上進行了平滑,去掉了噪聲,因此在小資料集上很有效。而Skip-gram的方法中,用一個詞預測其上下文,得到了當前詞上下文的很多樣本,因此可用於更大的資料集。

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

圖4. Skip-gram模型

如上圖所示,Skip-gram模型的具體做法是,將一個詞的詞向量對映到2n個詞的詞向量(2n表示當前輸入詞的前後各n個詞),然後分別透過softmax得到這2n個詞的分類損失值之和。

資料準備

1、資料介紹

本教程使用Penn Treebank (PTB)(經Tomas Mikolov預處理過的版本)資料集。PTB資料集較小,訓練速度快,應用於Mikolov的公開語言模型訓練工具[2]中。其統計情況如下:

訓練資料

驗證資料

測試資料

ptb.train.txt

ptb.valid.txt

ptb.test.txt

42068句

3370句

3761句

2、資料預處理

本教程訓練的是5-gram模型,表示在PaddlePaddle訓練時,每條資料的前4個詞用來預測第5個詞。PaddlePaddle提供了對應PTB資料集的python包paddle.dataset.imikolov,自動完成資料的下載與預處理,方便大家使用。

預處理會把資料集中的每一句話前後加上開始符號<s>以及結束符號<e>。然後依據視窗大小(本教程中為5),從頭到尾每次向右滑動視窗並生成一條資料。

如"I have a dream that one day" 一句提供了5條資料:

<s> I have a dream
I have a dream that
have a dream that one
a dream that one day
dream that one day <e>

最後,每個輸入會按其單詞次在字典裡的位置,轉化成整數的索引序列,作為PaddlePaddle的輸入。

模型結構

本配置的模型結構如下圖所示:

基於PaddlePaddle的詞向量實戰 | 深度學習基礎任務教程系列(二)

圖5. 模型配置中的N-gram神經網路模型

首先我們先載入所需的包

from__future__importprint_function
importpaddleaspaddle
importpaddle.fluidasfluid
importsix
importnumpy
import sys
importmath

然後,定義引數

EMBED_SIZE = 32      # embedding維度
HIDDEN_SIZE = 256    # 隱層大小
N = 5                # ngram大小,這裡固定取5
BATCH_SIZE = 100     # batch大小
PASS_NUM = 100       # 訓練輪數
 use_cuda = False  # 如果用GPU訓練,則設定為True
word_dict = paddle.dataset.imikolov.build_dict()
dict_size = len(word_dict)

更大的BATCH_SIZE將使得訓練更快收斂,但也會消耗更多記憶體。由於詞向量計算規模較大,如果環境允許,請開啟使用GPU進行訓練,能更快得到結果。在新的Fluid版本里,我們不必再手動計算詞向量。PaddlePaddle提供了一個內建的方法fluid.layers.embedding,我們就可以直接用它來構造N-gram 神經網路。

現在,我們來定義我們的N-gram 神經網路結構。這個結構在訓練和預測中都會使用到。因為詞向量比較稀疏,我們傳入引數 is_sparse == True, 可以加速稀疏矩陣的更新。      

definference_program(words,is_sparse): 
    embed_first=fluid.layers.embedding(
        input=words[0],
        size=[dict_size,EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_second=fluid.layers.embedding(
        input=words[1],
        size=[dict_size,EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_third=fluid.layers.embedding(
        input=words[2],
        size=[dict_size,EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
    embed_fourth=fluid.layers.embedding(
        input=words[3],
        size=[dict_size,EMBED_SIZE],
        dtype='float32',
        is_sparse=is_sparse,
        param_attr='shared_w')
 
    concat_embed=fluid.layers.concat(
        input=[embed_first,embed_second,embed_third,embed_fourth],axis=1)
    hidden1=fluid.layers.fc(input=concat_embed,
                              size=HIDDEN_SIZE,
                              act='sigmoid')
    predict_word=fluid.layers.fc(input=hidden1,size=dict_size,act='softmax')
    returnpredict_word


基於以上的神經網路結構,我們可以如下定義我們的訓練方法
deftrain_program(predict_word):
    # 'next_word'的定義必須要在inference_program的宣告之後,
    否則train program輸入資料的順序就變成了[next_word, firstw, secondw,
    # thirdw, fourthw], 這是不正確的.
    next_word=fluid.layers.data(name='nextw',shape=[1],dtype='int64')
    cost=fluid.layers.cross_entropy(input=predict_word,label=next_word)
    avg_cost=fluid.layers.mean(cost)
    returnavg_cost
 
defoptimizer_func():
    returnfluid.optimizer.AdagradOptimizer(
        learning_rate=3e-3,
        regularization=fluid.regularizer.L2DecayRegularizer(8e-4))


現在我們可以開始訓練啦。我們有現成的訓練和測試集:paddle.dataset.imikolov.train()和paddle.dataset.imikolov.test()。兩者都會返回一個讀取器。paddle.batch 會讀入一個讀取器,然後輸出一個批次化了的讀取器。我們還可以在訓練過程中輸出每個步驟,批次的訓練情況。

deftrain(if_use_cuda,params_dirname,is_sparse=True):
    place=fluid.CUDAPlace(0)ifif_use_cudaelsefluid.CPUPlace()
 
    train_reader=paddle.batch(
        paddle.dataset.imikolov.train(word_dict,N),BATCH_SIZE)
    test_reader=paddle.batch(
        paddle.dataset.imikolov.test(word_dict,N),BATCH_SIZE)
 
    first_word=fluid.layers.data(name='firstw',shape=[1],dtype='int64')
    second_word=fluid.layers.data(name='secondw',shape=[1],dtype='int64')
    third_word=fluid.layers.data(name='thirdw',shape=[1],dtype='int64')
    forth_word=fluid.layers.data(name='fourthw',shape=[1],dtype='int64')
    next_word=fluid.layers.data(name='nextw',shape=[1],dtype='int64')
 
    word_list=[first_word,second_word,third_word,forth_word,next_word]
    feed_order=['firstw','secondw','thirdw','fourthw','nextw']
 
    main_program=fluid.default_main_program()
    star_program=fluid.default_startup_program()
 
    predict_word=inference_program(word_list,is_sparse)
    avg_cost=train_program(predict_word)
    test_program=main_program.clone(for_test=True)
 
    sgd_optimizer=optimizer_func()
    sgd_optimizer.minimize(avg_cost)
 
    exe=fluid.Executor(place)
 
    deftrain_test(program,reader):
        count=0
        feed_var_list=[
            program.global_block().var(var_name)forvar_nameinfeed_order
]
        feeder_test=fluid.DataFeeder(feed_list=feed_var_list,place=place)
        test_exe=fluid.Executor(place)
        accumulated=len([avg_cost])*[0]
        fortest_datainreader():
            avg_cost_np=test_exe.run(
                program=program,
                feed=feeder_test.feed(test_data),
                fetch_list=[avg_cost])
            accumulated=[
                x[0]+x[1][0]forxinzip(accumulated,avg_cost_np)
]
            count+=1
        return[x/countforxinaccumulated]
 
    deftrain_loop():
        step=0
        feed_var_list_loop=[
            main_program.global_block().var(var_name)forvar_nameinfeed_order
]
        feeder=fluid.DataFeeder(feed_list=feed_var_list_loop,place=place)
        exe.run(star_program)
        forpass_idinrange(PASS_NUM):
            fordataintrain_reader():
                avg_cost_np=exe.run(
                    main_program,feed=feeder.feed(data),fetch_list=[avg_cost])
                ifstep%10==0:
                    outs=train_test(test_program,test_reader)
                    print("Step %d: Average Cost %f"%(step,outs[0]))
                    整個訓練過程要花費幾個小時,如果平均損失低於5.8
                    我們就認為模型已經達到很好的效果可以停止訓練了。
                    注意5.8是一個相對較高的值,為了獲取更好的模型,可以將
                    這裡的閾值設為3.5,但訓練時間也會更長。
                    ifouts[0]<5.8:
                        ifparams_dirnameisnotNone:
                            fluid.io.save_inference_model(params_dirname,[
                                'firstw','secondw','thirdw','fourthw'
                            ], [predict_word],exe)
                        return
                step+=1
                ifmath.isnan(float(avg_cost_np[0])):
                    sys.exit("got NaN loss, training failed.")
        raiseAssertionError("Cost is too large {0:2.2}".format(avg_cost_np[0]))
    tain_loop()

  train_loop將會開始訓練。期間列印訓練過程的日誌如下:

Step 0: Average Cost 7.337213
Step 10: Average Cost 6.136128
Step 20: Average Cost 5.766995
...

預測下一個詞的配置

我們可以用我們訓練過的模型,在得知之前的N-gram 後,預測下一個詞。

definfer(use_cuda,params_dirname=None):
    place=fluid.CUDAPlace(0)ifuse_cudaelsefluid.CPUPlace()
 
    exe=fluid.Executor(place)
 
    inference_scope=fluid.core.Scope()
    withfluid.scope_guard(inference_scope):
        使用fluid.io.load_inference_model獲取inference program
        # feed變數的名稱feed_target_names和從scopefetch的物件fetch_targets
[inferencer,feed_target_names,
         fetch_targets]=fluid.io.load_inference_model(params_dirname,exe)
 
        設定輸入,用四個LoDTensor來表示4個詞語。這裡每個詞都是一個id
        用來查詢embedding表獲取對應的詞向量,因此其形狀大小是[1]
        # recursive_sequence_lengths設定的是基於長度的LoD,因此都應該設為[[1]]
        注意recursive_sequence_lengths是列表的列表
        data1=[[211]]  # 'among'
        data2=[[6]]  # 'a'
        data3=[[96]]  # 'group'
        data4=[[4]]  # 'of'
        lod=[[1]]
 
        first_word=fluid.create_lod_tensor(data1,lod,place)
        second_word=fluid.create_lod_tensor(data2,lod,place)
        third_word=fluid.create_lod_tensor(data3,lod,place)
        fourth_word=fluid.create_lod_tensor(data4,lod,place)
 
        assertfeed_target_names[0]=='firstw'
        assertfeed_target_names[1]=='secondw'
        assertfeed_target_names[2]=='thirdw'
        assertfeed_target_names[3]=='fourthw'
 
        構造feed詞典 {feed_target_name: feed_target_data}
        預測結果包含在results之中
        results=exe.run(
            inferencer,
            feed={
                feed_target_names[0]:first_word,
                feed_target_names[1]:second_word,
                feed_target_names[2]:third_word,
                feed_target_names[3]:fourth_word
},
            fetch_list=fetch_targets,
            return_numpy=False)
        print(numpy.array(results[0]))
        most_possible_word_index=numpy.argmax(results[0])
        print(most_possible_word_index)
        print([
            keyforkey,valueinsix.iteritems(word_dict)
            ifvalue==most_possible_word_index
][0])

由於詞向量矩陣本身比較稀疏,訓練的過程如果要達到一定的精度耗時會比較長。為了能簡單看到效果,教程只設定了經過很少的訓練就結束並得到如下的預測。我們的模型預測 among a group of 的下一個詞是the。這比較符合文法規律。如果我們訓練時間更長,比如幾個小時,那麼我們會得到的下一個預測是 workers。預測輸出的格式如下所示:

[[0.03768077 0.03463154 0.00018074 ... 0.00022283 0.00029888 0.02967956]]0the

其中第一行表示預測詞在詞典上的機率分佈,第二行表示機率最大的詞對應的id,第三行表示機率最大的詞。

整個程式的入口很簡單:

defmain(use_cuda,is_sparse):
    ifuse_cudaandnotfluid.core.is_compiled_with_cuda():
        return


    params_dirname="word2vec.inference.model" 
    train(
        if_use_cuda=use_cuda,
        params_dirname=params_dirname,
        is_sparse=is_sparse)
infer(use_cuda=use_cuda,params_dirname=params_dirname)
main(use_cuda=use_cuda,is_sparse=True)

總結

在本教程中,我們最開始先介紹了詞向量、語言模型和詞向量的關係、以及如何透過訓練神經網路模型獲得詞向量。在資訊檢索中,我們可以根據向量間的餘弦夾角,來判斷query和文件關鍵詞這二者間的相關性。在句法分析和語義分析中,訓練好的詞向量可以用來初始化模型,以得到更好的效果。在文件分類中,有了詞向量之後,可以用聚類的方法將文件中同義詞進行分組,也可以用N-gram 來預測下一個詞。希望大家在閱讀完本教程能夠自行運用詞向量進行相關領域的研究。

參考文獻

[1]Bengio Y, Ducharme R, Vincent P, et al. A neural probabilistic language model[J]. journal of machine learning research, 2003, 3(Feb): 1137-1155.
[2]Mikolov T, Kombrink S, Deoras A, et al. Rnnlm-recurrent neural network language modeling toolkit[C]//Proc. of the 2011 ASRU Workshop. 2011: 196-201.
[3]Mikolov T, Chen K, Corrado G, et al. Efficient estimation of word representations in vector space[J]. arXiv preprint arXiv:1301.3781, 2013.
[4]Maaten L, Hinton G. Visualizing data using t-SNE[J]. Journal of Machine Learning Research, 2008, 9(Nov): 2579-2605.

相關文章