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,用于对输入语句进行编码,而解码器则对输入进行解码并生成相应的输出。本文中介绍的技术能够用于建立任何机器翻译模型,只要数据集的格式相似于本文中使用的格式便可。

相关文章
相关标签/搜索