seq2seq 模型常用于实现翻译,对话等生成任务。实际上是建立输入和输出的映射关系,翻译对话等任务的实现比价复杂,为了帮助我们更快的理解seq2seq,我们这里实现一个简单的反序任务。本文章重点关注seq2seq模型的代码实现,主要参考了这篇文章。本文的代码在Pycharm上实现,不同与原文使用Jupiter,在代码上有一些改动,并增加了详细的注释。本文的代码后面会放到我的Github上。
好了,下面开始。
任务目标
输入一个单词,输出实现反序。例如:
输入:hello
输出:olleh
数据集
TXT文本。
源数据source_data,每一行是一个单词。
目标数据target_data,每一行与源数据对应,是其的反序。
source_data: target_data:
bsaqq abqqs
npy npy
lbwuj bjluw
数据处理
数据处理部分主要分为下面几部:
1.字符转整数,建立字符到整数的映射表,和整数到字符的映射表
2.添加特殊字符
3.pad到相同长度
4.对每个字母进行embedding
数据处理这部分代码在项目的data_process.py文件中,需要的同学可以去看一下。
另外,还有一点对数据处理的部分在训练时调用,主要是为decoder训练的输入数据服务,需要添加<go>
和删除最后一个<eos>
标志符或者<pad>
。对数据处理的详细解释可以参考这一篇博客。代码在下面:
def process_decoder_input(data, vocab_to_int, batch_size):
'''
先移除最后一个字符,再在前面补充<GO>
'''
# cut掉最后一个字符
ending = tf.strided_slice(data, [0, 0], [batch_size, -1], [1, 1])
decoder_input = tf.concat([tf.fill([batch_size, 1], vocab_to_int['<GO>']), ending], 1)
return decoder_input
模型构建
对于模型的构建使用的是tensorflow提供的API接口,使用非常方便,如果对接口不是很熟悉的话可以看一看我的这篇博客,里面详细介绍了搭建encoder和decoder用到的接口的使用。
encoder部分
我们首先将输入通过embedding映射成词向量的形式,这里是将每一个字母映射成为了一个15维的向量。当然将26个字母每个映射成一个这么长的想来那个在实际中没有什么意义,这个项目也只是为了我们更好的额理解seq2seq,大家明白就好。然后我们创建想要的LSTM单元。最后将这些参数放入 tf.nn.dynamic_rnn
中进行编码。
def get_encoder_layer(input_data, rnn_size, num_layers,
source_sequence_length, source_vocab_size,
encoding_embedding_size):
'''
构造Encoder层
参数说明:
- input_data: 输入tensor [None, None]
- rnn_size: rnn隐层结点数量 50
- num_layers: 堆叠的rnn cell数量, RNN层数 2
- source_sequence_length: 源数据的序列长度
- source_vocab_size: 源数据的词典大小
- encoding_embedding_size: embedding的大小 15
'''
# Encoder embedding
encoder_embed_input = tf.contrib.layers.embed_sequence(input_data, source_vocab_size, encoding_embedding_size)
# RNN cell
def get_lstm_cell(rnn_size):
lstm_cell = tf.contrib.rnn.LSTMCell(rnn_size, initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return lstm_cell
# 堆叠的 rnn cell
cell = tf.contrib.rnn.MultiRNNCell([get_lstm_cell(rnn_size) for _ in range(num_layers)])
# 动态RNN 允许每个batch内有相同padding长度,不同batch间可以有不同的batch长度
encoder_output, encoder_state = tf.nn.dynamic_rnn(cell, encoder_embed_input,
sequence_length=source_sequence_length, dtype=tf.float32)
return encoder_output, encoder_state
decoder部分
decoder的构建需要两个部分,分别用于训练和预测。在训练时我们将target数据输入decoder中进行训练,上一时刻的输出不放入下一时刻的输入。再预测阶段,我们没有target,所以将上一时刻的输出作为下一时刻的输入放入decoder中。预测阶段的过程如下图所示:
def decoding_layer(target_letter_to_int, decoding_embedding_size, num_layers, rnn_size,
target_sequence_length, max_target_sequence_length, encoder_state, decoder_input):
'''
构造Decoder层
参数:
- target_letter_to_int: target数据的映射表
- decoding_embedding_size: embed向量大小
- num_layers: 堆叠的RNN单元数量 2
- rnn_size: RNN单元的隐层结点数量 50
- target_sequence_length: target数据序列长度
- max_target_sequence_length: target数据序列最大长度
- encoder_state: encoder端编码的状态向量
- decoder_input: decoder端输入
'''
# 1. Embedding
target_vocab_size = len(target_letter_to_int)
# 这里为甚么没有与encoder用相同的embedding,
decoder_embeddings = tf.Variable(tf.random_uniform([target_vocab_size, decoding_embedding_size]))
decoder_embed_input = tf.nn.embedding_lookup(decoder_embeddings, decoder_input)
# 2. 构造Decoder中的RNN单元
def get_decoder_cell(rnn_size):
decoder_cell = tf.contrib.rnn.LSTMCell(rnn_size,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return decoder_cell
cell = tf.contrib.rnn.MultiRNNCell([get_decoder_cell(rnn_size) for _ in range(num_layers)])
# 3. Output全连接层 输出的维度是target_vocab_size
output_layer = Dense(target_vocab_size,
kernel_initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))
# 4. Training decoder
with tf.variable_scope("decode"):
# 得到help对象
# A helper for use during training. Only reads inputs.
# Returned sample_ids are the argmax of the RNN output logits.
# Decoder端用来训练的函数。这个函数不会把t-1阶段的输出作为t阶段的输入,而是把target中的真实值直接输入给RNN
training_helper = tf.contrib.seq2seq.TrainingHelper(inputs=decoder_embed_input,
sequence_length=target_sequence_length,
time_major=False)
# 构造decoder
training_decoder = tf.contrib.seq2seq.BasicDecoder(cell=cell,
helper=training_helper,
initial_state=encoder_state,
output_layer=output_layer)
training_decoder_output, _, _ = tf.contrib.seq2seq.dynamic_decode(training_decoder,
impute_finished=True,
maximum_iterations=max_target_sequence_length)
# 5. Predicting decoder
# 与training共享参数
with tf.variable_scope("decode", reuse=True):
# 创建一个常量tensor并复制为batch_size的大小
start_tokens = tf.tile(tf.constant([target_letter_to_int['<GO>']], dtype=tf.int32), [batch_size],
name='start_tokens')
# GreedyEmbeddingHelper和TrainingHelper的区别在于它会把t-1下的输出进行embedding后再输入给RNN
# 注意:input 是 embeddings 而不是 decoder_embed_input
predicting_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(decoder_embeddings,
start_tokens,
target_letter_to_int['<EOS>'])
predicting_decoder = tf.contrib.seq2seq.BasicDecoder(cell,
predicting_helper,
encoder_state,
output_layer)
predicting_decoder_output, _, _ = tf.contrib.seq2seq.dynamic_decode(predicting_decoder,
impute_finished=True,
maximum_iterations=max_target_sequence_length)
return training_decoder_output, predicting_decoder_output
最终模型
建立好encoder和decoder后,我们就可以将一个完整的seq2seq模型搭建起来了。
def seq2seq_model(input_data, targets, lr, target_sequence_length,
max_target_sequence_length, source_sequence_length,
source_vocab_size, target_vocab_size,
encoder_embedding_size, decoder_embedding_size,
rnn_size, num_layers):
# 获取encoder的状态输出
_, encoder_state = get_encoder_layer(input_data,
rnn_size,
num_layers,
source_sequence_length,
source_vocab_size,
encoding_embedding_size)
# 预处理后的decoder输入
decoder_input = process_decoder_input(targets, data_process.target_letter_to_int, batch_size)
# 将状态向量与输入传递给decoder
training_decoder_output, predicting_decoder_output = decoding_layer(data_process.target_letter_to_int,
decoding_embedding_size, # 15
num_layers, # 2
rnn_size, # 50
target_sequence_length,
max_target_sequence_length,
encoder_state,
decoder_input)
return training_decoder_output, predicting_decoder_output
模型的训练
模型的训练我们使用mask减去padding的影响
masks = tf.sequence_mask(target_sequence_length, max_target_sequence_length, dtype=tf.float32, name='masks')
with tf.name_scope("optimization"):
# Loss function 对序列logits计算加权交叉熵
# training_logits是输出层的结果,targets是目标值,
# masks是我们使用tf.sequence_mask计算的结果,在这里作为权重,也就是说我们在计算交叉熵时不会把<PAD>计算进去
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, -5., 5.), var) for grad, var in gradients if grad is not None]
train_op = optimizer.apply_gradients(capped_gradients)
训练结果
原始输入: common
Source
Word 编号: [5, 7, 13, 13, 7, 21, 0]
Input Words: c o m m o n <PAD>
Target
Word 编号: [21, 5, 13, 13, 7, 7]
Response Words: n c m m o o
原始输入: result
Source
Word 编号: [7, 11, 13, 29, 22, 9, 0]
Input Words: r e s u l t <PAD>
Target
Word 编号: [11, 29, 9, 22, 13, 7]
Response Words: e u t l s r