TensorFlow中的Seq2Seq模型

Kindle君 2018-05-02

TensorFlow中的Seq2Seq模型

在这个项目中,我将在TensorFlow中构建名为seq2seq model or encoder-decoder model的语言翻译模型。该模型的目标是将英语句子翻译成法语句子。我要展示的详细步骤,他们将回答诸如如何定义编码器模型,如何定义解码器模型,如何构建整个seq2seq模型,如何计算损失和剪辑梯度的问题。

构建Seq2Seq模型的步骤

您可以将整个模型分成2个小型子模型。第一个子模型称为[E] 编码器,第二个子模型称为[D] 解码器。[E]与其他RNN架构一样,采用原始输入文本数据。最后,[E]输出一个神经表示。这是一个非常典型的工作,但你需要注意这个输出的真实性。[E]的输出将成为[D]的输入数据。

这就是为什么我们称[E]为编码器,[D]为解码器的原因。[E]以神经表示形式编码输出,我们不知道它到底是什么。它有点加密。[D]能够查看[E]的输出内容,并且它会创建完全不同的输出数据(在这种情况下用法语翻译)。

为了建立这样一个模型,总共有6个步骤。我注意到要实施的功能与每个步骤有关。

(1)为编码器模型定义输入参数

enc_dec_model_inputs

(2)建立编码器模型

encoding_layer

(3)为解码器模型定义输入参数

enc_dec_model_inputs,process_decoder_input,decoding_layer

(4)构建用于训练的解码器模型

decoding_layer_train

(5)建立解码器模型进行推理

decoding_layer_infer

(6)将(4)和(5)放在一起

decoding_layer

(7)连接编码器和解码器型号

seq2seq_model

(8)定义损失函数,优化器和应用梯度裁剪

TensorFlow中的Seq2Seq模型

图1.神经机器翻译/训练阶段

编码器输入(1),(3)

enc_dec_model_inputs 函数创建并返回与架构模型相关的参数(TF占位符)。

def enc_dec_model_inputs():

inputs = tf.placeholder(tf.int32, [None, None], name='input')

targets = tf.placeholder(tf.int32, [None, None], name='targets')

target_sequence_length = tf.placeholder(tf.int32, [None], name='target_sequence_length')

max_target_len = tf.reduce_max(target_sequence_length)

return inputs, targets, target_sequence_length, max_target_len

输入占位符将用英语句子数据输入,其形状为[None, None]。第一个不意味着批大小,而且批大小是未知的,因为用户可以设置它。第二句没有意思是句子的长度。setence的最大长度不同于批处理,所以不能用确切的数字来设置。

  • 一种选择是将每个句子的长度设置为每个批次中所有句子的最大长度。无论选择哪种方法,都需要<PAD>在空位添加特殊字符。但是,对于后一种选择,可能会有不必要的更多<PAD>字符。

目标占位符与输入占位符相似,只不过它会使用法语句子数据。

target_sequence_length占位符表示每个句子的长度,所以形状是None一个列张量,它与批量大小相同。作为TrainerHelper构建训练解码器模型的参数,需要此特定值。我们将在(4)中看到。

max_target_len从所有目标语句(序列)的长度中获取最大值。如您所知,我们有target_sequence_length参数中所有句子的长度。从中获得最大值的方法是使用tf.reduce_max。

过程解码器输入(3)

在解码器方面,我们需要两种不同的输入来分别进行训练和推理。在训练阶段,输入是作为目标标签提供的,但仍需嵌入。然而,在推理阶段,每个时间步的输出将是下一个时间步的输入。它们也需要嵌入并且嵌入向量应该在两个不同阶段之间共享。

TensorFlow中的Seq2Seq模型

在本节中,我将对训练阶段的目标标签数据进行预处理。这不是什么特别的任务。您需要做的是<GO>在所有目标数据前添加特殊令牌。<GO>令牌是一种类似“这是翻译的开始”的引导标记。对于这个过程,你需要知道TensorFlow的三个库。

def process_decoder_input(target_data, target_vocab_to_int, batch_size):

# get '<GO>' id

go_id = target_vocab_to_int['<GO>']

after_slice = tf.strided_slice(target_data, [0, 0], [batch_size, -1], [1, 1])

after_concat = tf.concat( [tf.fill([batch_size, 1], go_id), after_slice], 1)

return after_concat

TF strided_slice

  • 提取张量的一个分段切片(广义python数组索引)。

  • 可以被认为是从开始到结束的跨越窗口大小分裂成多个张量

  • 参数:TF Tensor, Begin, End, Strides

TF fill

  • 创建一个充满标量值的张量。

  • 参数:TF Tensor(必须是int32 / int64),填充的值

TF concat

  • 沿一维连接张量。

  • 参数:TF Tensor的列表(在这种情况下为tf.fill和after_slice),axis = 1

在预处理目标标签数据之后,我们稍后在实现decode_layer函数时嵌入它

编码(2)

TensorFlow中的Seq2Seq模型

Fig 3. Encoding model highlighted — Embedding/RNN

如图3所示,编码模型由两个不同的部分组成。第一部分是嵌入层。句子中的每个单词都将用指定的特征数表示encoding_embedding_size。这一层为有用的解释提供了更丰富的代表权力。第二部分是RNN层。您可以使用任何种类的RNN相关技术或算法。例如,在这个项目中,多个LSTM单元在应用丢弃技术后堆叠在一起。您可以使用不同种类的RNN单元,如GRU。

def encoding_layer(rnn_inputs, rnn_size, num_layers, keep_prob,

source_vocab_size,

encoding_embedding_size):

"""

:return: tuple (RNN output, RNN state)

"""

embed = tf.contrib.layers.embed_sequence(rnn_inputs,

vocab_size=source_vocab_size,

embed_dim=encoding_embedding_size)

stacked_cells = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.DropoutWrapper(tf.contrib.rnn.LSTMCell(rnn_size), keep_prob) for _ in range(num_layers)])

outputs, state = tf.nn.dynamic_rnn(stacked_cells,

embed,

dtype=tf.float32)

return outputs, state

嵌入图层

TF contrib.layers.embed_sequence

RNN层

  • TF contrib.rnn.LSTMCell

  • :只是指定它有多少内部单位

  • TF contrib.rnn.DropoutWrapper

  • :用保持概率值包装单元格

  • TF contrib.rnn.MultiRNNCell

  • :堆栈多个RNN(类型)单元格

  • :该API如何在操作中使用?

编码模型

TF nn.dynamic_rnn

:将嵌入层和RNN层全部放在一起

解码 - 训练过程(4)

解码模型可以被认为是两个独立的过程,即训练和推理。这不是他们有不同的架构,但他们共享相同的架构和参数。这是他们有不同的策略来喂养共享模型。对于这个(训练)和下一个(推理)部分,图4清楚地显示了它们是什么。

TensorFlow中的Seq2Seq模型

图4.解码器移位输入

虽然编码器使用TF contrib.layers.embed_sequence,但它不适用于解码器,即使它可能需要嵌入输入。这是因为应该通过训练和推断阶段来共享相同的嵌入向量。TF contrib.layers.embed_sequence只能在运行之前嵌入准备好的数据集。推理过程需要的是动态嵌入功能。在运行模型之前嵌入推理过程的输出是不可能的,因为当前时间步的输出将是下一个时间步的输入。

我们如何嵌入?我们很快就会看到。但是,现在,您需要记住的是训练和推理过程共享相同的嵌入参数。对于训练部分,应该交付嵌入式输入。在推理部分,只能传递训练部分中使用的嵌入参数。

我们先看看训练部分。

  • tf.contrib.seq2seq.TrainingHelper :TrainingHelper是我们传递嵌入式输入的地方。正如名称所示,这只是一个帮手实例。这个实例应该传递给BasicDecoder,这是构建解码器模型的实际过程。

  • tf.contrib.seq2seq.BasicDecoder :BasicDecoder构建解码器模型。这意味着它连接解码器侧的RNN层和TrainingHelper准备的输入。

  • tf.contrib.seq2seq.dynamic_decode :dynamic_decode展开解码器模型,以便每个时间步可以通过BasicDecoder检索实际预测。

def decoding_layer_train(encoder_state, dec_cell, dec_embed_input,

target_sequence_length, max_summary_length,

output_layer, keep_prob):

"""

Create a training process in decoding layer

:return: BasicDecoderOutput containing training logits and sample_id

"""

dec_cell = tf.contrib.rnn.DropoutWrapper(dec_cell,

output_keep_prob=keep_prob)

# for only input layer

helper = tf.contrib.seq2seq.TrainingHelper(dec_embed_input,

target_sequence_length)

decoder = tf.contrib.seq2seq.BasicDecoder(dec_cell,

helper,

encoder_state,

output_layer)

# unrolling the decoder layer

outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder,

impute_finished=True,

maximum_iterations=max_summary_length)

return outputs

解码 - 推理过程(5)

  • tf.contrib.seq2seq.GreedyEmbeddingHelper :GreedyEmbeddingHelper动态获取当前步骤的输出并将其输入到下一个时间步骤的输入。为了动态嵌入每个输入结果,应该提供嵌入参数(只是一堆权重值)。与此同时,GreedyEmbeddingHelper要求提供start_of_sequence_id与批量大小相同的数量end_of_sequence_id。

  • tf.contrib.seq2seq.BasicDecoder :与训练过程部分所述相同

  • tf.contrib.seq2seq.dynamic_decode :与训练过程部分所述相同

def decoding_layer_infer(encoder_state, dec_cell, dec_embeddings, start_of_sequence_id,

end_of_sequence_id, max_target_sequence_length,

vocab_size, output_layer, batch_size, keep_prob):

"""

Create a inference process in decoding layer

:return: BasicDecoderOutput containing inference logits and sample_id

"""

dec_cell = tf.contrib.rnn.DropoutWrapper(dec_cell,

output_keep_prob=keep_prob)

helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(dec_embeddings,

tf.fill([batch_size], start_of_sequence_id),

end_of_sequence_id)

decoder = tf.contrib.seq2seq.BasicDecoder(dec_cell,

helper,

encoder_state,

output_layer)

outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder,

impute_finished=True,

maximum_iterations=max_target_sequence_length)

return outputs

构建解码层(3),(6)

嵌入目标序列

  • TF contrib.layers.embed_sequence创建嵌入参数的内部表示,因此我们无法查看或检索它。相反,您需要通过TF Variable手动创建嵌入参数。

  • 手动创建的嵌入参数用于训练阶段,以便在训练运行之前通过TF nn.embedding_lookup转换提供的目标数据(句子序列)。使用手动创建的嵌入参数的TF nn.embedding_lookup将相似的结果返回给TF contrib.layers.embed_sequence。对于推理过程,无论何时通过解码器计算当前时间步的输出,它都将嵌入共享嵌入参数并成为下一个时间步的输入。您只需要将嵌入参数提供给GreedyEmbeddingHelper,然后它将帮助该过程。

  • embedding_lookup如何工作? :简而言之,它选择指定的行

  • 注意:请注意设置变量范围。如前所述,参数/变量在训练和推理过程之间共享。共享可以通过tf.variable_scope指定。

构建解码器RNN层(s)

  • 如图3和图4所示,解码器模型中的RNN层的数量必须等于编码器模型中的RNN层的数量。

创建一个输出层,将解码器的输出映射到我们词汇表的元素

  • 这只是一个完全连接的层面,以最终获得每个单词的概率。

def decoding_layer(dec_input, encoder_state,

target_sequence_length, max_target_sequence_length,

rnn_size,

num_layers, target_vocab_to_int, target_vocab_size,

batch_size, keep_prob, decoding_embedding_size):

"""

Create decoding layer

:return: Tuple of (Training BasicDecoderOutput, Inference BasicDecoderOutput)

"""

target_vocab_size = len(target_vocab_to_int)

dec_embeddings = tf.Variable(tf.random_uniform([target_vocab_size, decoding_embedding_size]))

dec_embed_input = tf.nn.embedding_lookup(dec_embeddings, dec_input)

cells = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.LSTMCell(rnn_size) for _ in range(num_layers)])

with tf.variable_scope("decode"):

output_layer = tf.layers.Dense(target_vocab_size)

train_output = decoding_layer_train(encoder_state,

cells,

dec_embed_input,

target_sequence_length,

max_target_sequence_length,

output_layer,

keep_prob)

with tf.variable_scope("decode", reuse=True):

infer_output = decoding_layer_infer(encoder_state,

cells,

dec_embeddings,

target_vocab_to_int['<GO>'],

target_vocab_to_int['<EOS>'],

max_target_sequence_length,

target_vocab_size,

output_layer,

batch_size,

keep_prob)

return (train_output, infer_output)

构建Seq2Seq模型(7)

在本节中,先前定义的功能,encoding_layer,process_decoder_input,和decoding_layer放在一起打造大图,序列到序列模型。

def seq2seq_model(input_data, target_data, keep_prob, batch_size,

target_sequence_length,

max_target_sentence_length,

source_vocab_size, target_vocab_size,

enc_embedding_size, dec_embedding_size,

rnn_size, num_layers, target_vocab_to_int):

"""

Build the Sequence-to-Sequence model

:return: Tuple of (Training BasicDecoderOutput, Inference BasicDecoderOutput)

"""

enc_outputs, enc_states = encoding_layer(input_data,

rnn_size,

num_layers,

keep_prob,

source_vocab_size,

enc_embedding_size)

dec_input = process_decoder_input(target_data,

target_vocab_to_int,

batch_size)

train_output, infer_output = decoding_layer(dec_input,

enc_states,

target_sequence_length,

max_target_sentence_length,

rnn_size,

num_layers,

target_vocab_to_int,

target_vocab_size,

batch_size,

keep_prob,

dec_embedding_size)

return train_output, infer_output

构建图形+定义损失,使用渐变裁剪优化器

seq2seq_model函数创建模型。它定义了前馈和后向传播应该如何流动。这个模型的最后一步是可以训练的是决定和应用哪些优化算法来使用。在本节中,使用TF contrib.seq2seq.sequence_loss计算损失,然后应用TF train.AdamOptimizer计算损失的梯度下降。我们来看看下面代码单元格中的eatch步骤。

从检查点加载数据

  • (source_int_text,target_int_text)是输入数据,(source_vocab_to_int,target_vocab_to_int)是用于查找每个单词的索引号的字典。

  • max_target_sentence_length是来自源输入数据的最长句子的长度。当在解码器模式下构建推理过程时,这将用于GreedyEmbeddingHelper。

create inputs

  • 来自enc_dec_model_inputs函数的输入(input_data,targets,target_sequence_length,max_target_sequence_length)

  • 从hyperparam_inputs函数输入(lr,keep_prob)

构建seq2seq模型

  • 通过seq2seq_model函数构建模型。它将返回train_logits(logits来计算损失)和inference_logits(来自预测的logits)。

成本函数

  • 使用TF contrib.seq2seq.sequence_loss。这种损失函数只是一个加权的softmax交叉熵损失函数,但它特别设计用于时间序列模型(RNN)。权重应作为参数明确提供,并可由TF sequence_mask创建。在这个项目中,TF sequence_mask创建变量的[batch_size,max_target_sequence_length]大小,然后仅将第一个target_sequence_length元素的数目设为1,这意味着部件的权重将小于其他部件。

优化

  • 使用TF train.AdamOptimizer,这是应该指定学习速率的地方。你也可以选择其他算法,这只是一个选择。

Gradient Clipping

TensorFlow中的Seq2Seq模型

Fig 5. Gradient Clipping

  • 由于递归神经网络对消失/爆发梯度是臭名昭着的,所以梯度裁剪技术被认为可以改善问题。

  • 这个概念非常简单。您决定使梯度保持在某个边界的阈值。在这个项目中,阈值的范围在-1和1之间。

  • 现在,您需要将此概念性知识应用于TensorFlow代码。幸运的是,这个TF渐变剪辑的官方指南如何?。简而言之,您可以通过调用compute_gradients手动从优化器手动获取梯度值,然后使用clip_by_value操作梯度值。最后,您需要通过调用apply_gradients将修改的渐变放回优化器

save_path = 'checkpoints/dev'

(source_int_text, target_int_text), (source_vocab_to_int, target_vocab_to_int), _ = load_preprocess()

max_target_sentence_length = max([len(sentence) for sentence in source_int_text])

train_graph = tf.Graph()

with train_graph.as_default():

input_data, targets, target_sequence_length, max_target_sequence_length = enc_dec_model_inputs()

lr, keep_prob = hyperparam_inputs()

train_logits, inference_logits = seq2seq_model(tf.reverse(input_data, [-1]),

targets,

keep_prob,

batch_size,

target_sequence_length,

max_target_sequence_length,

len(source_vocab_to_int),

len(target_vocab_to_int),

encoding_embedding_size,

decoding_embedding_size,

rnn_size,

num_layers,

target_vocab_to_int)

training_logits = tf.identity(train_logits.rnn_output, name='logits')

inference_logits = tf.identity(inference_logits.sample_id, name='predictions')

# https://www.tensorflow.org/api_docs/python/tf/sequence_mask

# - Returns a mask tensor representing the first N positions of each cell.

masks = tf.sequence_mask(target_sequence_length, max_target_sequence_length, dtype=tf.float32, name='masks')

with tf.name_scope("optimization"):

# Loss function - weighted softmax cross entropy

cost = tf.contrib.seq2seq.sequence_loss(

training_logits,

targets,

masks)

# Optimizer

optimizer = tf.train.AdamOptimizer(lr)

# Gradient Clipping

gradients = optimizer.compute_gradients(cost)

capped_gradients = [(tf.clip_by_value(grad, -1., 1.), var) for grad, var in gradients if grad is not None]

train_op = optimizer.apply_gradients(capped_gradients)

相关推荐