采用带注意机制的序列序列结构进行英印地语神经机器翻译
Seq2seq模型构成了机器翻译、图像和视频字幕、文本摘要、聊天机器人以及任何你可能想到的包括从一个数据序列到另一个数据序列转换的任务的基础。如果您曾使用过谷歌Translate,或与Siri、Alexa或谷歌Assistant进行过互动,那么你就是序列对序列(seq2seq)神经结构的受益者。
我们这里的重点是机器翻译,基本上就是把一个句子x从一种语言翻译成另一种语言的句子y。机器翻译是seq2seq模型的主要用例,注意机制对机器翻译进行了改进。关于这类主题的文章通常涉及用于实现的大代码段和来自多个库的大量API调用,对概念本身没有直观的理解。在这方面,我们既要讲求理论,也要讲求执行。除了实现之外,我们还将详细了解seq2seq体系结构和注意力的每个组件表示什么。本文中使用的代码可以在最后的资源列表中找到。
目标
在Tensorflow中实现、训练和测试一个英语到印地语机器翻译模型。
对编码器、解码器、注意机制的作用形成直观透彻的理解。
讨论如何进一步改进现有的模型。
读数据集
首先,导入所有需要的库。在这个实现中使用的英语到印地语语料库可以在Kaggle找到。一个名为“Hindi_English_Truncated_Corpus”的文件。将下载csv "。请确保在pd.read_csv()函数中放置了正确的文件路径,该路径对应于文件系统中的路径。
import numpy as np import pandas as pd from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences import tensorflow as tf from sklearn.model_selection import train_test_split import time from matplotlib import pyplot as plt import os import re data = pd.read_csv("./Hindi_English_Truncated_Corpus.csv") english_sentences = data["english_sentence"] hindi_sentences = data["hindi_sentence"]
让我们快速看看我们正在处理的数据集的类型。这是相当简单的。
数据预处理
在我们继续我们的编码器,解码器和注意力实现之前,我们需要预处理我们的数据。请注意,预处理步骤也依赖于我们处理的数据类型。例如,在这里考虑的数据集中,也有带有空字符串的句子。我们需要相应地处理这类案例。如果使用其他数据集,也可能需要一些额外或更少的步骤。预处理步骤如下:
在单词和标点符号之间插入空格
如果手头上的句子是英语,我们就用空格替换除了(a-z, A-Z, “.”, “?”, “!”, “,”)
句子中去掉多余的空格,关键字“sentencestart”和“sentenceend”分别添加到句子的前面和后面,让我们的模型明确地知道句子开始和结束。
每个句子的以上三个任务都是使用preprocess_sentence()函数实现的。我们还在开始时初始化了所有的超参数和全局变量。请阅读下面的超参数和全局变量。我们将在需要时使用它们。
# Global variables and Hyperparameters num_words = 10000 oov_token = '<UNK>' english_vocab_size = num_words + 1 hindi_vocab_size = num_words + 1 MAX_WORDS_IN_A_SENTENCE = 16 test_ratio = 0.2 BATCH_SIZE = 512 embedding_dim = 64 hidden_units = 1024 learning_rate = 0.006 epochs = 100 def preprocess_sentence(sen, is_english): if (type(sen) != str): return '' sen = sen.strip('.') # insert space between words and punctuations sen = re.sub(r"([?.!,¿;।])", r" \1 ", sen) sen = re.sub(r'[" "]+', " ", sen) # For english, replacing everything with space except (a-z, A-Z, ".", "?", "!", ",", "'") if(is_english == True): sen = re.sub(r"[^a-zA-Z?.!,¿']+", " ", sen) sen = sen.lower() sen = sen.strip() sen = 'sentencestart ' + sen + ' sentenceend' sen = ' '.join(sen.split()) return sen
对包含英语句子和印地语句子的每个数据点进行循环,确保不考虑带有空字符串的句子,并且句子中的最大单词数不大于MAX_WORDS_IN_A_SENTENCE的值。这一步是为了避免我们的矩阵是稀疏的。
下一步是对文本语料库进行向量化。具体来说,fit_on_texts()为每个单词分配一个唯一的索引。texts_to_sequences()将一个文本句子转换为一个数字列表或一个向量,其中数字对应于单词的唯一索引。pad_sequences()通过添加刚好足够数量的oov_token(从vocab token中提取)来确保所有这些向量最后都具有相同的长度,使每个向量具有相同的长度。tokenize_statements()封装了上面这些功能。
接下来,我们从完整的数据集中得到训练集,然后对训练集进行批处理。我们训练模型所用的句子对总数为51712。
# Loop through each datapoint having english and hindi sentence processed_e_sentences = [] processed_h_sentences = [] for (e_sen, h_sen) in zip(english_sentences, hindi_sentences): processed_e_sen = preprocess_sentence(e_sen, True) processed_h_sen = preprocess_sentence(h_sen, False) if(processed_e_sen == '' or processed_h_sen == '' or processed_e_sen.count(' ') > (MAX_WORDS_IN_A_SENTENCE-1) or processed_h_sen.count(' ') > (MAX_WORDS_IN_A_SENTENCE-1)): continue processed_e_sentences.append(processed_e_sen) processed_h_sentences.append(processed_h_sen) print("Sentence examples: ") print(processed_e_sentences[0]) print(processed_h_sentences[0]) print("Length of English processed sentences: " + str(len(processed_e_sentences))) print("Length of Hindi processed sentences: " + str(len(processed_h_sentences))) def tokenize_sentences(processed_sentences, num_words, oov_token): tokenizer = Tokenizer(num_words = num_words, oov_token = oov_token) tokenizer.fit_on_texts(processed_sentences) word_index = tokenizer.word_index sequences = tokenizer.texts_to_sequences(processed_sentences) sequences = pad_sequences(sequences, padding = 'post') return word_index, sequences, tokenizer english_word_index, english_sequences, english_tokenizer = tokenize_sentences(processed_e_sentences, num_words, oov_token) hindi_word_index, hindi_sequences, hindi_tokenizer = tokenize_sentences(processed_h_sentences, num_words, oov_token) # split into traning and validation set english_train_sequences, english_val_sequences, hindi_train_sequences, hindi_val_sequences = train_test_split(english_sequences, hindi_sequences, test_size = test_ratio) BUFFER_SIZE = len(english_train_sequences) # Batching the training set dataset = tf.data.Dataset.from_tensor_slices((english_train_sequences, hindi_train_sequences)).shuffle(BUFFER_SIZE) dataset = dataset.batch(BATCH_SIZE, drop_remainder = True) print("No. of batches: " + str(len(list(dataset.as_numpy_iterator()))))
编码器Encoder
Seq2seq架构在原论文中涉及到两个长短期内存(LSTM)。一个用于编码器,另一个用于解码器。请注意,在编码器和解码器中,我们将使用GRU(门控周期性单元)来代替LSTM,因为GRU的计算能力更少,但结果与LSTM几乎相同。Encoder涉及的步骤:
输入句子中的每个单词都被嵌入并表示在具有embedding_dim(超参数)维数的不同空间中。换句话说,您可以说,在具有embedding_dim维数的空间中,词汇表中的单词的数量被投影到其中。这一步确保类似的单词(例如。boat & ship, man & boy, run & walk等)都位于这个空间附近。这意味着“男人”这个词和“男孩”这个词被预测的几率几乎一样(不是完全一样),而且这两个词的意思也差不多。
接下来,嵌入的句子被输入GRU。编码器GRU的最终隐藏状态成为解码器GRU的初始隐藏状态。编码器中最后的GRU隐藏状态包含源句的编码或信息。源句的编码也可以通过所有编码器隐藏状态的组合来提供[我们很快就会发现,这一事实对于注意力的概念的存在至关重要]。
class Encoder(tf.keras.Model): def __init__(self, english_vocab_size, embedding_dim, hidden_units): super(Encoder, self).__init__() self.embedding = tf.keras.layers.Embedding(english_vocab_size, embedding_dim) self.gru = tf.keras.layers.GRU(hidden_units, return_sequences = True, return_state = True) def call(self, input_sequence): x = self.embedding(input_sequence) encoder_sequence_output, final_encoder_state = self.gru(x) #Dimensions of encoder_sequence_output => (BATCH_SIZE, MAX_WORDS_IN_A_SENTENCE, hidden_units) #Dimensions of final_encoder_state => (BATCH_SIZE, hidden_units) return encoder_sequence_output, final_encoder_state # initialize our encoder encoder = Encoder(english_vocab_size, embedding_dim, hidden_units)
解码器Decoder ( 未使用注意力机制)
注意:在本节中,我们将了解解码器的情况下,不涉及注意力机制。这对于理解稍后与解码器一起使用的注意力的作用非常重要。
解码器GRU网络是生成目标句的语言模型。最终的编码器隐藏状态作为解码器GRU的初始隐藏状态。第一个给解码器GRU单元来预测下一个的单词是一个像“sentencestart”这样的开始标记。这个标记用于预测所有num_words数量的单词出现的概率。训练时使用预测的概率张量和实际单词的一热编码来计算损失。这种损失被反向传播以优化编码器和解码器的参数。同时,概率最大的单词成为下一个GRU单元的输入。重复上述步骤,直到出现像“sentenceend”这样的结束标记。
这种方法的问题是:
信息瓶颈:如上所述,编码器的最终隐藏状态成为解码器的初始隐藏状态。这就造成了信息瓶颈,因为源句的所有信息都需要压缩到最后的状态,这也可能会偏向于句子末尾的信息,而不是句子中很久以前看到的信息。
解决方案:我们解决了上述问题,不仅依靠编码器的最终状态来获取源句的信息,还使用了编码器所有输出的加权和。那么,哪个编码器的输出比另一个更重要?注意力机制就是为了解决这个问题。