全文共字,预计学习时长34分钟
谷歌翻译大家想必都不陌生,但你有没有想过,它究竟是如何将几乎所有的已知语言翻译成我们所选择的语言?本文将解开这个谜团,并且向各位展示如何用长短期记忆网络(LSTM)构建语言翻译程序。
本文分为两部分。第一部分简单介绍神经网络机器翻译(NMT)和编码器-解码器(Encoder-Decoder)结构。第二部分提供了使用Python创建语言翻译程序的详细步骤。
图源:谷歌什么是机器翻译?
机器翻译是计算语言学的一个分支,主要研究如何将一种语言的源文本自动转换为另一种语言的文本。在机器翻译领域,输入已经由某种语言的一系列符号组成,而计算机必须将其转换为另一种语言的一系列符号。
神经网络机器翻译是针对机器翻译领域所提出的主张。它使用人工神经网络来预测某个单词序列的概率,通常在单个集成模型中对整个句子进行建模。
凭借神经网络的强大功能,神经网络机器翻译已经成为翻译领域最强大的算法。这种最先进的算法是深度学习的一项应用,其中大量已翻译句子的数据集用于训练能够在任意语言对之间的翻译模型。
谷歌语言翻译程序理解Seq2Seq架构
顾名思义,Seq2Seq将单词序列(一个或多个句子)作为输入,并生成单词的输出序列。这是通过递归神经网络(RNN)实现的。具体来说,就是让两个将与某个特殊令牌一起运行的递归神经网络尝试根据前一个序列来预测后一个状态序列。
一种简单的编码器-解码器架构它主要由编码器和解码器两部分构成,因此有时候被称为编码器-解码器网络。
·编码器:使用多个深度神经网络层,将输入单词转换为相应的隐藏向量。每个向量代表当前单词及其语境。
·解码器:与编码器类似。它将编码器生成的隐藏向量、自身的隐藏状态和当前单词作为输入,从而生成下一个隐藏向量,最终预测下一个单词。
任何神经网络机器翻译的最终目标都是接收以某种语言输入的句子,然后将该句子翻译为另一种语言作为输出结果。下图是一个汉译英翻译算法的简单展示:
将“Knowledgeispower”翻译成汉语。它如何运行?
第一步,通过某种方式将文本数据转换为数字形式。为了在机器翻译中实现这一点,需要将每个单词转换为可输入到模型中的独热编码(OneHotEncoding)向量。独热编码向量是在每个索引处都为0(仅在与该特定单词相对应的单个索引处为1)的向量。
独热编码为输入语言中的每个唯一单词设置索引来创建这些向量,输出语言也是如此。为每个唯一单词分配唯一索引时,也就创建了针对每种语言的所谓的“词汇表”。理想情况下,每种语言的词汇表将仅包含该语言的每个唯一单词。
如上图所示,每个单词都变成了一个长度为9(这是词汇表的大小)的向量,索引中除去一个1以外,其余全部都是0。
通过为输入和输出语言创建词汇表,人们可以将该技术应用于任何语言中的任何句子,从而将语料库中所有已翻译的句子彻底转换为适用于机器翻译任务的格式。
现在来一起感受一下编码器-解码器算法背后的魔力。在最基本的层次上,模型的编码器部分选择输入语言中的某个句子,并从该句中创建一个语义向量(thoughtvector)。该语义向量存储句子的含义,然后将其传递给解码器,解码器将句子译为输出语言。
编码器-解码器结构将英文句子“Iamastudent”译为德语就编码器来说,输入句子的每个单词会以多个连续的时间步分别输入模型。在每个时间步(t)中,模型都会使用该时间步输入到模型单词中的信息来更新隐藏向量(h)。
该隐藏向量用来存储输入句子的信息。这样,因为在时间步t=0时尚未有任何单词输入编码器,所以编码器在该时间步的隐藏状态从空向量开始。下图以蓝色框表示隐藏状态,其中下标t=0表示时间步,上标E表示它是编码器(Encoder)的隐藏状态[D则用来表示解码器(Decoder)的隐藏状态]。
在每个时间步中,该隐藏向量都会从该时间步的输入单词中获取信息,同时保留从先前时间步中存储的信息。因此,在最后一个时间步中,整个输入句子的含义都会储存在隐藏向量中。最后一个时间步中的隐藏向量就是上文中提到的语义向量,它之后会被输入解码器。
另外,请注意编码器中的最终隐藏向量如何成为语义向量并在t=0时用上标D重新标记。这是因为编码器的最终隐藏向量变成了解码器的初始隐藏向量。通过这种方式,句子的编码含义就传递给了解码器,从而将其翻译成输出语言。但是,与编码器不同,解码器需要输出长度可变的译文。因此,解码器将在每个时间步中输出一个预测词,直到输出一个完整的句子。
开始翻译之前,需要输入SOS标签作为解码器第一个时间步的输入。与编码器一样,解码器将在时间步t=1处使用SOS输入来更新其隐藏状态。但是,解码器不仅会继续进行到下一个时间步,它还将使用附加权重矩阵为输出词汇表中的所有单词创建概率。这样,输出词汇表中概率最高的单词将成为预测输出句子中的第一个单词。
解码器必须输出长度可变的预测语句,它将以该方式继续预测单词,直到其预测语句中的下一个单词为EOS标签。一旦该标签预测完成,解码过程就结束了,呈现出的是输入句子的完整预测翻译。
通过Keras和Python实现神经网络机器翻译
了解了编码器-解码器架构之后,创建一个模型,该模型将通过Keras和python把英语句子翻译成法语。第一步,导入需要的库,为将在代码中使用的不同参数配置值。
#ImportLibraries
importos,sys
fromkeras.modelsimportModel
fromkeras.layersimportInput,LSTM,GRU,Dense,Embedding
fromkeras.preprocessing.textimportTokenizer
fromkeras.preprocessing.sequenceimportpad_sequences
fromkeras.utilsimportto_categorical
importnumpyasnp
importpandasaspd
importpickle
importmatplotlib.pyplotasplt
#Valuesfordifferentparameters:
BATCH_SIZE=64
EPOCHS=20
LSTM_NODES=
NUM_SENTENCES=
MAX_SENTENCE_LENGTH=50
MAX_NUM_WORDS=
EMBEDDING_SIZE=
数据集
我们需要一个包含英语句子及其法语译文的数据集,下载fra-eng.zip文件并将其解压。每一行的文本文件都包含一个英语句子及其法语译文,通过制表符分隔。继续将每一行分为输入文本和目标文本。
input_sentences=[]
output_sentences=[]
output_sentences_inputs=[]
count=0
forlineinopen(./drive/MyDrive/fra.txt,encoding=utf-8):
count+=1
ifcountNUM_SENTENCES:
break
if\tnotinline:
continue
input_sentence=line.rstrip().split(\t)[0]
output=line.rstrip().split(\t)[1]
output_sentence=output+eos
output_sentence_input=sos+output
input_sentences.append(input_sentence)
output_sentences.append(output_sentence)
output_sentences_inputs.append(output_sentence_input)
print(Numberofsampleinput:,len(input_sentences))
print(Numberofsampleoutput:,len(output_sentences))
print(Numberofsampleoutputinput:,len(output_sentences_inputs))
Output:
Numberofsampleinput:
Numberofsampleoutput:
Numberofsampleoutputinput:
在上面的脚本中创建input_sentences[]、output_sentences[]和output_sentences_inputs[]这三个列表。接下来,在for循环中,逐个读取每行fra.txt文件。每一行都在制表符出现的位置被分为两个子字符串。左边的子字符串(英语句子)插入到input_sentences[]列表中。制表符右边的子字符串是相应的法语译文。
此处表示句子结束的eos标记被添加到已翻译句子的前面。同理,表示“句子开始”的sos标记和已翻译句子的开头相连接。还是从列表中随机打印一个句子:
print(Englishsentence:,input_sentences[])
print(Frenchtranslation:,output_sentences[])
Output:Englishsentence:Joinus.
Frenchtranslation:Joignez-vousànous.eos
标记和填充
下一步是标记原句和译文,并填充长度大于或小于某一特定长度的句子。对于输入而言,该长度将是输入句子的最大长度。对于输出而言,它也是输出句子的最大长度。在此之前,先设想一下句子的长度。将分别在两个单独的英语和法语列表中获取所有句子的长度。
eng_len=[]
fren_len=[]
#populatethelistswithsentencelengths
foriininput_sentences:
eng_len.append(len(i.split()))
foriinoutput_sentences:
fren_len.append(len(i.split()))
length_df=pd.DataFrame({english:eng_len,french:fren_len})
length_df.hist(bins=20)
plt.show()
上面的直方图显示,法语句子的最大长度为12,英语句子的最大长度为6。接下来,用Keras的Tokenizer()类矢量化文本数据。句子将因此变为整数序列。然后,用零填充这些序列,使它们长度相等。
标记器类的word_index属性返回一个单词索引词典,其中键表示单词,值表示对应的整数。最后,上述脚本打印出词典中唯一单词的数量和输入的最长英文句子的长度。
#tokenizetheinputsentences(inputlanguage)
input_tokenizer=Tokenizer(num_words=MAX_NUM_WORDS)
input_tokenizer.fit_on_texts(input_sentences)
input_integer_seq=input_tokenizer.texts_to_sequences(input_sentences)
print(input_integer_seq)
word2idx_inputs=input_tokenizer.word_index
print(Totaluniquewordsintheinput:%s%len(word2idx_inputs))
max_input_len=max(len(sen)forsenininput_integer_seq)
print(Lengthoflongestsentenceininput:%g%max_input_len)
Output:
Totaluniquewordsintheinput:
Lengthoflongestsentenceininput:6
同样,输出语句也可以用相同的方式标记:
#tokenizetheoutputsentences(Outputlanguage)
output_tokenizer=Tokenizer(num_words=MAX_NUM_WORDS,filters=)
output_tokenizer.fit_on_texts(output_sentences+output_sentences_inputs)
output_integer_seq=output_tokenizer.texts_to_sequences(output_sentences)
output_input_integer_seq=output_tokenizer.texts_to_sequences(output_sentences_inputs)
print(output_input_integer_seq)
word2idx_outputs=output_tokenizer.word_index
print(Totaluniquewordsintheoutput:%s%len(word2idx_outputs))
num_words_output=len(word2idx_outputs)+1
max_out_len=max(len(sen)forseninoutput_integer_seq)
print(Lengthoflongestsentenceintheoutput:%g%max_out_len)
Output:
Totaluniquewordsintheoutput:
Lengthoflongestsentenceintheoutput:12
现在,可以通过上面的直方图来验证两种语言中最长句子的长度。还可以得出这样的结论:英语句子通常较短,平均单词量比法语译文句子的单词量要少。
接下来需要填充输入。填充输入和输出的原因是文本的句子长度不固定,但长短期记忆网络希望输入的例句长度都相等。因此需要将句子转换为长度固定的向量。为此,一种可行的方法就是填充。
#Paddingtheencoderinput
encoder_input_sequences=pad_sequences(input_integer_seq,maxlen=max_input_len)
print(encoder_input_sequences.shape:,encoder_input_sequences.shape)
#Paddingthedecoderinputs
decoder_input_sequences=pad_sequences(output_input_integer_seq,maxlen=max_out_len,padding=post)
print(decoder_input_sequences.shape:,decoder_input_sequences.shape)
#Paddingthedecoderoutputs
decoder_output_sequences=pad_sequences(output_integer_seq,maxlen=max_out_len,padding=post)
print(decoder_output_sequences.shape:,decoder_output_sequences.shape)
encoder_input_sequences.shape:(,6)
decoder_input_sequences.shape:(,12)
decoder_output_sequences.shape:(,12)
输入中有个句子(英语),每个输入句子的长度都为6,所以现在输入的形式为(,6)。同理,输出中有个句子(法语),每个输出句子的长度都为12,所以现在输出的形式为(,12),被翻译的语言也是如此。
大家可能还记得,索引处的原句为joinus。标记生成器将该句拆分为join和us两个单词,将它们转换为整数,然后通过对输入列表中索引处的句子所对应的整数序列的开头添加四个零来实现前填充(pre-padding)。
print(encoder_input_sequences[]:,encoder_input_sequences[])Output:
encoder_input_sequences[]:[]
要验证join和us的整数值是否分别为和59,可将单词传递给word2index_inputs词典,如下图所示:
print(word2idx_inputs[join])
print(word2idx_inputs[us])Output:
59
更值得一提的是,解码器则会采取后填充(post-padding)的方法,即在句子末尾添加零。而在编码器中,零被填充在开头位置。该方法背后的原因是编码器输出基于出现在句末的单词,因此原始单词被保留在句末,零则被填充在开头位置。而解码器是从开头处理句子,因此对解码器的输入和输出执行后填充。
词嵌入向量(WordEmbeddings)
图源:unsplash我们要先将单词转换为对应的数字向量表示,再将向量输入给深度学习模型。我们也已经将单词转化成了数字。那么整数/数字表示和词嵌入向量之间有什么区别呢?
单个整数表示和词嵌入向量之间有两个主要区别。在整数表示中,一个单词仅用单个整数表示。而在向量表示中,一个单词可以用50、、或任何你喜欢的维数表示。因此词嵌入向量可以获取更多与单词有关的信息。其次,单个整数表示无法获取不同单词之间的关系。而词嵌入向量却能做到这一点。
对于英语句子(即输入),我们将使用GloVe词嵌入模型。对于输出的法语译文,我们将使用自定义词嵌入模型。点击此处可下载GloVe词嵌入模型。
首先,为输入内容创建词嵌入向量。在此之前需要将GloVe词向量加载到内存中。然后创建一个词典,其中单词为键,其对应的向量为值:
fromnumpyimportarray
fromnumpyimportasarray
fromnumpyimportzeros
embeddings_dictionary=dict()
glove_file=open(r./drive/MyDrive/glove.twitter.27B.d.txt,encoding=utf8)
forlineinglove_file:
rec=line.split()
word=rec[0]
vector_dimensions=asarray(rec[1:],dtype=float32)
embeddings_dictionary[word]=vector_dimensions
glove_file.close()
回想一下,输入中包含个唯一单词。我们将创建一个矩阵,其中行数代表单词的整数值,而列数将对应单词的维数。该矩阵将包含输入句子中单词的词嵌入向量。
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
创建模型
第一步,为神经网络创建一个嵌入层。嵌入层被认为是网络的第一隐藏层。它必须指定3个参数:
·input_dim:表示文本数据中词汇表的容量。比如,如果数据被整数编码为0-10之间的值,那么词汇表的容量为11个单词。
·output_dim:表示将嵌入单词的向量空间大小。它决定该层每个单词的输出向量大小。比如,它可以是32或,甚至还可以更大。如果大家对此有疑问,可以用不同的值测试。
·input_length:表示输入序列的长度,正如大家为Keras模型的输入层所定义的那样。比如,如果所有的输入文档都由0个单词组成,那么该值也为0。
embedding_layer=Embedding(num_words,EMBEDDING_SIZE,weights=[embedding_matrix],input_length=max_input_len)
接下来需要做的是定义输出,大家都知道输出将是一个单词序列。回想一下,输出中唯一单词的总数为。因此,输出中的每个单词都可以是这个单词中的一个。输出句子的长度为12。每个输入句子都需要一个对应的输出句子。因此,输出的最终形式将是:(输入量、输出句子的长度、输出的单词数)
#shapeoftheoutput
decoder_targets_one_hot=np.zeros((len(input_sentences),max_out_len,num_words_output),
dtype=float32
)decoder_targets_one_hot.shapeShape:(,12,)
为了进行预测,该模型的最后一层将是一个稠密层(denselayer),因此需要以独热编码向量的形式输出,因为我们将在稠密层使用softmax激活函数。为创建独热编码输出,下一步是将1分配给与该单词整数表示对应的列数。
fori,dinenumerate(decoder_output_sequences):
fort,wordinenumerate(d):
decoder_targets_one_hot[i,t,word]=1
下一步是定义编码器和解码器网络。编码器将输入英语句子,并输出长短期记忆网络的隐藏状态和单元状态。
encoder_inputs=Input(shape=(max_input_len,))
x=embedding_layer(encoder_inputs)
encoder=LSTM(LSTM_NODES,return_state=True)
encoder_outputs,h,c=encoder(x)
encoder_states=[h,c]
下一步是定义解码器。解码器将有两个输入:编码器的隐藏状态和单元状态,它们实际上是开头添加了令牌后的输出语句。
decoder_inputs=Input(shape=(max_out_len,))
decoder_embedding=Embedding(num_words_output,LSTM_NODES)
decoder_inputs_x=decoder_embedding(decoder_inputs)
decoder_lstm=LSTM(LSTM_NODES,return_sequences=True,return_state=True)
decoder_outputs,_,_=decoder_lstm(decoder_inputs_x,initial_state=encoder_states)
#Finally,theoutputfromthedecoderLSTMispassedthroughadenselayertopredictdecoderoutputs.
decoder_dense=Dense(num_words_output,activation=softmax)
decoder_outputs=decoder_dense(decoder_outputs)
训练模型
编译定义了优化器和交叉熵损失的模型。
#Compile
model=Model([encoder_inputs,decoder_inputs],decoder_outputs)
model.