本文做者:在線實驗室javascript
文章結構:html
詞向量java
模型概覽python
在這裏咱們介紹三個訓練詞向量的模型:N-gram模型,CBOW模型和Skip-gram模型,它們的中心思想都是經過上下文獲得一個詞出現的機率。對於N-gram模型,咱們會先介紹語言模型的概念,並在以後的訓練模型中,帶你們用PaddlePaddle實現它。然後兩個模型,是近年來最有名的神經元詞向量模型,由 Tomas Mikolov 在Google 研發[3],雖然它們很淺很簡單,但訓練效果很好。git
語言模型github
在介紹詞向量模型以前,咱們先來引入一個概念:語言模型。 語言模型旨在爲語句的聯合機率函數P(w1,...,wT)P(w1,...,wT)建模, 其中wiwi表示句子中的第i個詞。語言模型的目標是,但願模型對有意義的句子賦予大機率,對沒意義的句子賦予小几率。 這樣的模型能夠應用於不少領域,如機器翻譯、語音識別、信息檢索、詞性標註、手寫識別等,它們都但願能獲得一個連續序列的機率。 以信息檢索爲例,當你在搜索「how long is a football bame」時(bame是一個醫學名詞),搜索引擎會提示你是否但願搜索"how long is a football game", 這是由於根據語言模型計算出「how long is a football bame」的機率很低,而與bame近似的,可能引發錯誤的詞中,game會使該句生成的機率最大。編程
對語言模型的目標機率P(w1,...,wT)P(w1,...,wT),若是假設文本中每一個詞都是相互獨立的,則整句話的聯合機率能夠表示爲其中全部詞語條件機率的乘積,即:網絡
然而咱們知道語句中的每一個詞出現的機率都與其前面的詞緊密相關, 因此實際上一般用條件機率表示語言模型:ide
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)。注意:因爲「神經機率語言模型」說法較爲泛泛,咱們在這裏不用其NNLM的本名,考慮到其具體作法,本文中稱該模型爲N-gram neural model。
咱們在上文中已經講到用條件機率建模語言模型,即一句話中第tt個詞的機率和該句話的前t−1t−1個詞相關。可實際上越遠的詞語其實對該詞的影響越小,那麼若是考慮一個n-gram, 每一個詞都只受其前面n-1
個詞的影響,則有:
給定一些真實語料,這些語料中都是有意義的句子,N-gram模型的優化目標則是最大化目標函數:
其中f(wt,wt−1,...,wt−n+1)f(wt,wt−1,...,wt−n+1)表示根據歷史n-1個詞獲得當前詞wtwt的條件機率,R(θ)R(θ)表示參數正則項。
圖2. N-gram神經網絡模型
圖2展現了N-gram神經網絡模型,從下往上看,該模型分爲如下幾個部分: - 對於每一個樣本,模型輸入wt−n+1,...wt−1wt−n+1,...wt−1, 輸出句子第t個詞在字典中|V|
個詞上的機率分佈。
每一個輸入詞wt−n+1,...wt−1wt−n+1,...wt−1首先經過映射矩陣映射到詞向量C(wt−n+1),...C(wt−1)C(wt−n+1),...C(wt−1)。
其中yikyki表示第ii個樣本第kk類的真實標籤(0或1),softmax(gik)softmax(gki)表示第i個樣本第k類softmax輸出的機率。
Continuous Bag-of-Words model(CBOW)
CBOW模型經過一個詞的上下文(各N個詞)預測當前詞。當N=2時,模型以下圖所示:
圖3. CBOW模型
具體來講,不考慮上下文的詞語輸入順序,CBOW是用上下文詞語的詞向量的均值來預測當前詞。即:
其中xtxt爲第tt個詞的詞向量,分類分數(score)向量 z=U∗contextz=U∗context,最終的分類yy採用softmax,損失函數採用多類分類交叉熵。
Skip-gram model
CBOW的好處是對上下文詞語的分佈在詞向量上進行了平滑,去掉了噪聲,所以在小數據集上頗有效。而Skip-gram的方法中,用一個詞預測其上下文,獲得了當前詞上下文的不少樣本,所以可用於更大的數據集。
圖4. Skip-gram模型
如上圖所示,Skip-gram模型的具體作法是,將一個詞的詞向量映射到2n2n個詞的詞向量(2n2n表示當前輸入詞的先後各nn個詞),而後分別經過softmax獲得這2n2n個詞的分類損失值之和。
數據準備
數據介紹
本教程使用Penn Treebank (PTB)(經Tomas Mikolov預處理過的版本)數據集。PTB數據集較小,訓練速度快,應用於Mikolov的公開語言模型訓練工具[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的輸入。
編程實現
本配置的模型結構以下圖所示:
圖5. 模型配置中的N-gram神經網絡模型
首先,加載所須要的包:
import paddle as paddle import paddle.fluid as fluid import six import numpy import math from __future__ import print_function
而後,定義參數:
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進行訓練,能更快獲得結果。 不一樣於以前的PaddlePaddle v2版本,在新的Fluid版本里,咱們沒必要再手動計算詞向量。PaddlePaddle提供了一個內置的方法fluid.layers.embedding
,咱們就能夠直接用它來構造 N-gram 神經網絡。
is_sparse == True
, 能夠加速稀疏矩陣的更新。def inference_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') return predict_word
def train_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) return avg_cost def optimizer_func(): return fluid.optimizer.AdagradOptimizer( learning_rate=3e-3, regularization=fluid.regularizer.L2DecayRegularizer(8e-4))
如今咱們能夠開始訓練啦。現在的版本較之之前就簡單了許多。咱們有現成的訓練和測試集:paddle.dataset.imikolov.train()
和paddle.dataset.imikolov.test()
。二者都會返回一個讀取器。在PaddlePaddle中,讀取器是一個Python的函數,每次調用,會讀取下一條數據。它是一個Python的generator。
paddle.batch
會讀入一個讀取器,而後輸出一個批次化了的讀取器。咱們還能夠在訓練過程當中輸出每一個步驟,批次的訓練狀況。
def train(if_use_cuda, params_dirname, is_sparse=True): place = fluid.CUDAPlace(0) if if_use_cuda else fluid.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) def train_test(program, reader): count = 0 feed_var_list = [ program.global_block().var(var_name) for var_name in feed_order ] feeder_test = fluid.DataFeeder(feed_list=feed_var_list, place=place) test_exe = fluid.Executor(place) accumulated = len([avg_cost]) * [0] for test_data in reader(): avg_cost_np = test_exe.run( program=program, feed=feeder_test.feed(test_data), fetch_list=[avg_cost]) accumulated = [ x[0] + x[1][0] for x in zip(accumulated, avg_cost_np) ] count += 1 return [x / count for x in accumulated] def train_loop(): step = 0 feed_var_list_loop = [ main_program.global_block().var(var_name) for var_name in feed_order ] feeder = fluid.DataFeeder(feed_list=feed_var_list_loop, place=place) exe.run(star_program) for pass_id in range(PASS_NUM): for data in train_reader(): avg_cost_np = exe.run( main_program, feed=feeder.feed(data), fetch_list=[avg_cost]) if step % 10 == 0: outs = train_test(test_program, test_reader) print("Step %d: Average Cost %f" % (step, outs[0])) # 整個訓練過程要花費幾個小時,若是平均損失低於5.8, # 咱們就認爲模型已經達到很好的效果能夠中止訓練了。 # 注意5.8是一個相對較高的值,爲了獲取更好的模型,能夠將 # 這裏的閾值設爲3.5,但訓練時間也會更長。 if outs[0] < 5.8: if params_dirname is not None: fluid.io.save_inference_model(params_dirname, [ 'firstw', 'secondw', 'thirdw', 'fourthw' ], [predict_word], exe) return step += 1 if math.isnan(float(avg_cost_np[0])): sys.exit("got NaN loss, training failed.") raise AssertionError("Cost is too large {0:2.2}".format(avg_cost_np[0])) train_loop()
train_loop
將會開始訓練。期間打印訓練過程的日誌以下:Step 0: Average Cost 7.337213 Step 10: Average Cost 6.136128 Step 20: Average Cost 5.766995 ...
參考文獻
本教程 由 PaddlePaddle 創做,採用 知識共享 署名-相同方式共享 4.0 國際 許可協議進行許可。