python用於NLP的seq2seq模型實例:用Keras實現神經機器翻譯

原文連接:http://tecdat.cn/?p=8438git

在本文中,咱們將看到如何建立語言翻譯模型,這也是神經機器翻譯的很是著名的應用。咱們將使用seq2seq體系結構經過Python的Keras庫建立咱們的語言翻譯模型。github

假定您對循環神經網絡(尤爲是LSTM)有很好的瞭解。本文中的代碼是使用Keras庫用Python編寫的。 算法

庫和配置設置

 首先導入所需的庫:數組

importos, sysfromkeras.modelsimportModelfromkeras.layersimportInput, LSTM, GRU, Dense, Embeddingfromkeras.preprocessing.textimportTokenizerfromkeras.preprocessing.sequenceimportpad_sequencesfromkeras.utilsimportto_categoricalimportnumpyasnpimportmatplotlib.pyplotasplt網絡

執行如下腳原本設置不一樣參數的值:架構

BATCH_SIZE =64EPOCHS =20LSTM_NODES =256NUM_SENTENCES =20000MAX_SENTENCE_LENGTH =50MAX_NUM_WORDS =20000EMBEDDING_SIZE =100app

數據集

咱們將在本文中開發的語言翻譯模型會將英語句子翻譯成法語。要開發這樣的模型,咱們須要一個包含英語句子及其法語翻譯的數據集。 在每一行上,文本文件包含一個英語句子及其法語翻譯,並用製表符分隔。文件的前20行fra.txt以下所示:機器學習

Go. Va ! Hi. Salut ! Hi. Salut. Run! Cours! Run! Courez! Who? Qui ? Wow! Ça alors! Fire! Au feu ! Help! À l'aide! Jump. Saute. Stop! Ça suffit! Stop! Stop! Stop! Arrête-toi ! Wait! Attends ! Wait! Attendez ! Go on. Poursuis. Go on. Continuez. Go on. Poursuivez. Hello! Bonjour ! Hello! Salut !ide

該模型包含超過170,000條記錄,可是咱們將僅使用前20,000條記錄來訓練咱們的模型。您能夠根據須要使用更多記錄。函數

數據預處理

神經機器翻譯模型一般基於seq2seq架構。seq2seq體系結構是一種編碼器-解碼器體系結構,由兩個LSTM網絡組成:編碼器LSTM和解碼器LSTM。 

在咱們的數據集中,咱們不須要處理輸入,可是,咱們須要生成翻譯後的句子的兩個副本:一個帶有句子開始標記,另外一個帶有句子結束標記。這是執行此操做的腳本:

input_sentences = [] output_sentences = [] output_sentences_inputs = [] count =0forlineinopen(r'/content/drive/My Drive/datasets/fra.txt', encoding="utf-8"): count +=1ifcount > NUM_SENTENCES:breakif'\t'notinline:continueinput_sentence, output = line.rstrip().split('\t') output_sentence = output +' 'output_sentence_input =' '+ output input_sentences.append(input_sentence) output_sentences.append(output_sentence) output_sentences_inputs.append(output_sentence_input) print("num samples input:", len(input_sentences)) print("num samples output:", len(output_sentences)) print("num samples output input:", len(output_sentences_inputs))

注意:您可能須要更改fra.txt計算機上文件的文件路徑,才能起做用。

 

最後,輸出中將顯示三個列表中的樣本數量:

numsamples input: 20000numsamples output: 20000numsamples output input: 20000

如今讓咱們來隨機打印一個句子從input_sentences[]output_sentences[]output_sentences_inputs[]列表:

print(input_sentences[172]) print(output_sentences[172]) print(output_sentences_inputs[172])

這是輸出:

I'm ill. Je suis malade. <<span >eos> <<span >sos> Je suis malade.

您能夠看到原始句子,即I'm ill;在輸出中對應的翻譯,即Je suis malade.。 

標記化和填充

下一步是標記原始句子和翻譯後的句子,並對大於或小於特定長度的句子應用填充,在輸入的狀況下,這將是最長輸入句子的長度。對於輸出,這將是輸出中最長句子的長度。

對於標記化,可使用庫中的Tokenizerkeras.preprocessing.text。本tokenizer類執行兩個任務:

  • 它將句子分爲相應的單詞列表
  • 而後將單詞轉換爲整數

這是很是重要的,由於深度學習和機器學習算法能夠處理數字。如下腳本用於標記輸入句子:

除了標記化和整數轉換外,該類的word_index屬性還Tokenizer返回一個單詞索引字典,其中單詞是鍵,而相應的整數是值。上面的腳本還輸出字典中惟一詞的數量和輸入中最長句子的長度:

Total unique wordsinthe input:3523Lengthoflongest sentenceininput:6

一樣,輸出語句也能夠用如下所示的相同方式進行標記:

這是輸出:

Total unique wordsinthe output:9561Lengthoflongest sentenceinthe output:13

經過比較輸入和輸出中惟一詞的數量,能夠得出結論,與翻譯後的法語句子相比,英語句子一般較短,平均包含較少的單詞。

接下來,咱們須要填充輸入。對輸入和輸出進行填充的緣由是文本句子的長度能夠變化,可是LSTM(咱們將要訓練模型的算法)指望輸入實例具備相同的長度。所以,咱們須要將句子轉換爲固定長度的向量。一種方法是經過填充。

在填充中,爲句子定義了必定的長度。在咱們的狀況下,輸入和輸出中最長句子的長度將分別用於填充輸入和輸出句子。輸入中最長的句子包含6個單詞。對於少於6個單詞的句子,將在空索引中添加零。如下腳本將填充應用於輸入句子。

上面的腳本顯示了填充的輸入句子的形狀。還打印了索引爲172的句子的填充整數序列。這是輸出:

encoder_input_sequences.shape: (20000, 6)encoder_input_sequences[172]:[ 0 0 0 0 6 539]

因爲輸入中有20,000個句子,而且每一個輸入句子的長度爲6,因此輸入的形狀如今爲(20000,6)。若是查看輸入句子索引172處句子的整數序列,能夠看到存在三個零,後跟值爲6和539。您可能還記得索引172處的原始句子是I'm ill。標記生成器分割的句子翻譯成兩個詞I'mill,將它們轉換爲整數,而後經過在輸入列表的索引172在用於句子對應的整數序列的開始添加三個零施加預填充。

要驗證的整數值i'mill是6和539分別能夠傳遞話到word2index_inputs詞典,以下圖所示:

print(word2idx_inputs["i'm"]) print(word2idx_inputs["ill"])

輸出:

6 539

以相同的方式,解碼器輸出和解碼器輸入的填充以下:

print("decoder_input_sequences.shape:", decoder_input_sequences.shape) print("decoder_input_sequences[172]:", decoder_input_sequences[172])

輸出:

decoder_input_sequences.shape: (20000, 13)decoder_input_sequences[172]:[ 2 3 6 188 0 0 0 0 0 0 0 0 0]

解碼器輸入的索引172處的句子爲je suis malade.。若是從word2idx_outputs字典中打印相應的整數,則應該在控制檯上看到二、三、6和188,以下所示:

print(word2idx_outputs[""]) print(word2idx_outputs["je"]) print(word2idx_outputs["suis"]) print(word2idx_outputs["malade."])

輸出:

2 3 6 188

進一步重要的是要提到,在解碼器的狀況下,應用後填充,這意味着在句子的末尾添加了零。在編碼器中,_開始時_填充零。這種方法背後的緣由是,編碼器輸出基於句子末尾出現的單詞,所以原始單詞保留在句子末尾,而且在開頭填充零。另外一方面,在解碼器的狀況下,處理從句子的開頭開始,所以對解碼器的輸入和輸出執行後填充。

詞嵌入

 

因爲咱們使用的是深度學習模型,而且深度學習模型使用數字,所以咱們須要將單詞轉換爲相應的數字矢量表示形式。可是咱們已經將單詞轉換爲整數。 

在本文中,對於英文句子(即輸入),咱們將使用GloVe詞嵌入。對於輸出中的法語翻譯句子,咱們將使用自定義單詞嵌入。

讓咱們首先爲輸入建立單詞嵌入。爲此,咱們須要將GloVe字向量加載到內存中。而後,咱們將建立一個字典,其中單詞是鍵,而相應的向量是值,以下所示:

回想一下,咱們在輸入中包含3523個惟一詞。咱們將建立一個矩陣,其中行號將表示單詞的整數值,而列將對應於單詞的尺寸。此矩陣將包含輸入句子中單詞的單詞嵌入。

num_words = min(MAX_NUM_WORDS, len(word2idx_inputs) +1) embedding_matrix = zeros((num_words, EMBEDDING_SIZE))forword, indexinword2idx_inputs.items(): embedding_vector = embeddings_dictionary.get(word)ifembedding_vectorisnotNone: embedding_matrix[index] = embedding_vector

首先,ill使用GloVe詞嵌入詞典爲該詞打印詞嵌入。

print(embeddings_dictionary["ill"])

輸出:

[0.126480.13660.22192-0.025204-0.71970.661470.485090.0572230.13829-0.26375-0.236470.743490.46737-0.4620.20031-0.263020.093948-0.61756-0.282130.13530.282130.218130.164180.22547-0.989450.29624-0.62476-0.295350.215340.922740.383880.55744-0.14628-0.15674-0.519410.25629-0.00796780.12998-0.0291920.20868-0.551270.0753530.44746-0.710460.755620.0103780.0952290.166730.22073-0.46562-0.10199-0.803860.451620.451830.19869-1.65710.7584-0.402980.82426-0.3860.00395460.613180.02701-0.3308-0.095652-0.0821640.78580.13394-0.32715-0.31371-0.20247-0.73001-0.493430.564450.610380.36777-0.0701820.44859-0.61774-0.188490.655920.44797-0.104690.62512-1.9474-0.606220.0738740.50013-1.1278-0.42066-0.37322-0.505380.591710.46534-0.424820.832650.081548-0.44147-0.084311-1.2304]

在上一節中,咱們看到了單詞的整數表示形式爲ill539。如今讓咱們檢查單詞嵌入矩陣的第539個索引。

print(embedding_matrix[539])

輸出:

[0.126480.13660.22192-0.025204-0.71970.661470.485090.0572230.13829-0.26375-0.236470.743490.46737-0.4620.20031-0.263020.093948-0.61756-0.282130.13530.282130.218130.164180.22547-0.989450.29624-0.62476-0.295350.215340.922740.383880.55744-0.14628-0.15674-0.519410.25629-0.00796780.12998-0.0291920.20868-0.551270.0753530.44746-0.710460.755620.0103780.0952290.166730.22073-0.46562-0.10199-0.803860.451620.451830.19869-1.65710.7584-0.402980.82426-0.3860.00395460.613180.02701-0.3308-0.095652-0.0821640.78580.13394-0.32715-0.31371-0.20247-0.73001-0.493430.564450.610380.36777-0.0701820.44859-0.61774-0.188490.655920.44797-0.104690.62512-1.9474-0.606220.0738740.50013-1.1278-0.42066-0.37322-0.505380.591710.46534-0.424820.832650.081548-0.44147-0.084311-1.2304]

能夠看到,嵌入矩陣中第539行的值相似於GloVe ill詞典中單詞的向量表示,這證明了嵌入矩陣中的行表明了GloVe單詞嵌入詞典中的相應單詞嵌入。這個詞嵌入矩陣將用於爲咱們的LSTM模型建立嵌入層。

如下腳本爲輸入建立嵌入層:

建立模型

如今是時候開發咱們的模型了。咱們須要作的第一件事是定義輸出,由於咱們知道輸出將是一個單詞序列。回想一下,輸出中的惟一單詞總數爲9562。所以,輸出中的每一個單詞能夠是9562個單詞中的任何一個。輸出句子的長度爲13。對於每一個輸入句子,咱們須要一個對應的輸出句子。所以,輸出的最終形狀將是:

如下腳本建立空的輸出數組:

decoder_targets_one_hot = np.zeros(( len(input_sentences), max_out_len, num_words_output ), dtype='float32')

如下腳本打印解碼器的形狀:

decoder_targets_one_hot.shape

輸出:

(20000, 13, 9562)

爲了進行預測,模型的最後一層將是一個密集層,所以咱們須要以一熱編碼矢量的形式進行輸出,由於咱們將在密集層使用softmax激活函數。要建立這樣的單編碼輸出,下一步是將1分配給與該單詞的整數表示形式對應的列號。例如,的整數表示形式je suis malade[ 2 3 6 188 0 0 0 0 0 0 0 ]。在decoder_targets_one_hot輸出數組的第一行的第二列中,將插入1。一樣,在第二行的第三個索引處,將插入另外一個1,依此類推。

看下面的腳本:

fori, dinenumerate(decoder_output_sequences):fort, wordinenumerate(d): decoder_targets_one_hot[i, t, word] =1

接下來,咱們須要建立編碼器和解碼器。編碼器的輸入將是英文句子,輸出將是LSTM的隱藏狀態和單元狀態。

如下腳本定義了編碼器:

下一步是定義解碼器。解碼器將有兩個輸入:編碼器和輸入語句的隱藏狀態和單元狀態,它們實際上將是輸出語句,並在開頭添加了令牌。

如下腳本建立解碼器LSTM:

最後,來自解碼器LSTM的輸出將經過密集層以預測解碼器輸出,以下所示:

decoder_dense = Dense(num_words_output, activation='softmax')下一步是編譯模型:

model = Model([encoder_inputs_placeholder, decoder_inputs_placeholder], decoder_outputs)

讓咱們繪製模型以查看:

plot_model(model, to_file='model_plot4a.png', show_shapes=True, show_layer_names=True)

輸出:

從輸出中,能夠看到咱們有兩種輸入。input_1是編碼器的輸入佔位符,它被嵌入並經過lstm_1層,該層基本上是編碼器LSTM。該lstm_1層有三個輸出:輸出,隱藏層和單元狀態。可是,只有單元狀態和隱藏狀態才傳遞給解碼器。

這裏的lstm_2層是解碼器LSTM。該input_2包含輸出句子令牌在開始追加。在input_2還經過一個嵌入層傳遞,而且被用做輸入到解碼器LSTM, lstm_2。最後,來自解碼器LSTM的輸出將經過密集層進行預測。

下一步是使用如下fit()方法訓練模型:

r = model.fit( ... )

該模型通過18,000條記錄的訓練,並針對其他2,000條記錄進行了測試。 通過20個時間段後,我獲得了90.99%的訓練精度和79.11%的驗證精度,這代表該模型是過分擬合的。 

修改預測模型

在訓練時,咱們知道序列中全部輸出字的實際輸入解碼器。訓練期間發生的狀況的示例以下。假設咱們有一句話i'm ill。句子翻譯以下:

//Inputs on the left of Encoder/Decoder, outputs on the right.Step1:I'mill -> Encoder -> enc(h1,c1)enc(h1,c1)+ -> Decoder -> je + dec(h1,c1)step2:enc(h1,c1)+ je -> Decoder -> suis + dec(h2,c2)step3:enc(h2,c2)+ suis -> Decoder -> malade. + dec(h3,c3)step3:enc(h3,c3)+ malade. -> Decoder -> + dec(h4,c4)

您能夠看到解碼器的輸入和解碼器的輸出是已知的,而且基於這些輸入和輸出對模型進行了訓練。

可是,在預測期間,將根據前一個單詞預測下一個單詞,而該單詞又會在前一個時間步長中進行預測。如今,您將瞭解和令牌的用途。在進行實際預測時,沒法得到完整的輸出序列,實際上這是咱們必須預測的。在預測期間,因爲全部輸出句子均以開頭,所以惟一可用的單詞是。

預測期間發生的狀況的示例以下。咱們將再次翻譯句子i'm ill

//Inputs on the left of Encoder/Decoder, outputs on the right.Step1:I'mill -> Encoder -> enc(h1,c1)enc(h1,c1)+ -> Decoder -> y1(je) + dec(h1,c1)step2:enc(h1,c1)+ y1 -> Decoder -> y2(suis) + dec(h2,c2)step3:enc(h2,c2)+ y2 -> Decoder -> y3(malade.) + dec(h3,c3)step3:enc(h3,c3)+ y3 -> Decoder -> y4() + dec(h4,c4)

 能夠看到編碼器的功能保持不變。原始語言的句子經過編碼器和隱藏狀態傳遞,而單元格狀態是編碼器的輸出。

在步驟1中,將編碼器的隱藏狀態和單元狀態以及用做解碼器的輸入。解碼器預測一個單詞y1可能爲真或不爲真。可是,根據咱們的模型,正確預測的機率爲0.7911。在步驟2,未來自步驟1的解碼器隱藏狀態和單元狀態與一塊兒y1用做預測的解碼器的輸入y2。該過程一直持續到遇到令牌爲止。而後,未來自解碼器的全部預測輸出進行級聯以造成最終輸出語句。讓咱們修改模型以實現此邏輯。

編碼器型號保持不變:

encoder_model = Model(encoder_inputs_placeholder, encoder_states)

由於如今在每一步咱們都須要解碼器的隱藏狀態和單元狀態,因此咱們將修改模型以接受隱藏狀態和單元狀態,以下所示:

decoder_state_input_h = Input(shape=(LSTM_NODES,)) ...

如今,在每一個時間步長,解碼器輸入中只有一個字,咱們須要按以下所示修改解碼器嵌入層:

decoder_inputs_single = Input(shape=(1,))...接下來,咱們須要爲解碼器輸出建立佔位符:

decoder_outputs, h, c = decoder_lstm(...)

爲了進行預測,解碼器的輸出將經過密集層:

decoder_states = [h, c] decoder_outputs = decoder_dense(decoder_outputs)

最後一步是定義更新的解碼器模型,以下所示:

decoder_model = Model( ... )

如今,讓咱們繪製通過修改的解碼器LSTM來進行預測:

plot_model(decoder_model, to_file='model_plot_dec.png', show_shapes=True, show_layer_names=True)

輸出:

上圖中lstm_2是修改後的解碼器LSTM。您會看到它接受帶有一個單詞的句子(如所示)input_5,以及上一個輸出(input_3input_4)的隱藏狀態和單元格狀態。您能夠看到輸入句子的形狀如今是這樣的,(none,1)由於在解碼器輸入中將只有一個單詞。相反,在訓練期間,輸入句子的形狀是(None,6)由於輸入包含完整的句子,最大長度爲6。

作出預測

在這一步中,您將看到如何使用英語句子做爲輸入進行預測。

在標記化步驟中,咱們將單詞轉換爲整數。解碼器的輸出也將是整數。可是,咱們但願輸出是法語中的單詞序列。爲此,咱們須要將整數轉換回單詞。咱們將爲輸入和輸出建立新的字典,其中的鍵將是整數,而相應的值將是單詞。

idx2word_input = {v:kfork, vinword2idx_inputs.items()} idx2word_target = {v:kfork, vinword2idx_outputs.items()}

接下來,咱們將建立一個方法,即translate_sentence()。該方法將接受帶有輸入填充序列的英語句子(以整數形式),並將返回翻譯後的法語句子。看一下translate_sentence()方法:

deftranslate_sentence(input_seq): states_value = encoder_model.predict(input_seq) target_seq = np.zeros((1,1)) target_seq[0,0] = word2idx_outputs[''] eos = word2idx_outputs[''] output_sentence = []for_inrange(max_out_len): ...return' '.join(output_sentence)

在上面的腳本中,咱們將輸入序列傳遞給encoder_model,以預測隱藏狀態和單元格狀態,這些狀態存儲在states_value變量中。

接下來,咱們定義一個變量target_seq,它是一個1 x 1全零的矩陣。的target_seq變量包含所述第一字給解碼器模型,這是。

以後,將eos初始化變量,該變量存儲令牌的整數值。在下一行中,將output_sentence定義列表,其中將包含預測的翻譯。

接下來,咱們執行一個for循環。循環的執行週期數for等於輸出中最長句子的長度。在循環內部,在第一次迭代中,decoder_model預測器使用編碼器的隱藏狀態和單元格狀態以及輸入令牌(即)來預測輸出狀態,隱藏狀態和單元格狀態。預測單詞的索引存儲在idx變量中。若是預測索引的值等於令牌,則循環終止。不然,若是預測的索引大於零,則從idx2word詞典中檢索相應的單詞並將其存儲在word變量中,而後將其附加到output_sentence列表中。的states_value使用解碼器的新隱藏狀態和單元狀態更新變量,並將預測字的索引存儲在target_seq變量中。在下一個循環週期中,更新的隱藏狀態和單元狀態以及先前預測的單詞的索引將用於進行新的預測。循環繼續進行,直到達到最大輸出序列長度或遇到令牌爲止。

最後,output_sentence使用空格將列表中的單詞鏈接起來,並將結果字符串返回給調用函數。

測試模型

爲了測試代碼,咱們將從input_sentences列表中隨機選擇一個句子,檢索該句子的相應填充序列,並將其傳遞給該translate_sentence()方法。該方法將返回翻譯後的句子,以下所示。

這是測試模型功能的腳本:

print('-') print('Input:', input_sentences[i]) print('Response:', translation)

這是輸出:

- Input: You're not fired. Response: vous n'êtes pas viré.

 

再次執行上述腳本,以查看其餘一些翻譯成法語的英語句子。我獲得如下結果:

-Input: I'm not a lawyer.Response: je ne suis pas avocat.

該模型已成功將另外一個英語句子翻譯爲法語。

結論與展望

神經機器翻譯是天然語言處理的至關先進的應用,涉及很是複雜的體系結構。

本文介紹瞭如何經過seq2seq體系結構執行神經機器翻譯,該體系結構又基於編碼器-解碼器模型。編碼器是一種LSTM,用於對輸入語句進行編碼,而解碼器則對輸入進行解碼並生成相應的輸出。本文中介紹的技術能夠用於建立任何機器翻譯模型,只要數據集的格式相似於本文中使用的格式便可。

相關文章
相關標籤/搜索