简单的Seq2Seq实现作对联

作者: 文哥的学习日记 | 来源:发表于2018-02-23 14:40 被阅读6039次

    Seq2Seq全称Sequence to Sequence,在机器翻译、文章摘要等领域有着广泛的应用。其本身很简单,是一个如下图所示的Encoder-Decoder框架。

    本文不纠结于Seq2Seq的原理介绍,而是着重介绍代码实战。本文基于python3和tensorflow1.4 实现。

    本文代码参照github链接:https://github.com/NELSONZHAO/zhihu 以及 知乎专栏文章:https://zhuanlan.zhihu.com/p/27608348,感谢大神的引路!

    1、代码涉及Tensorflow函数介绍

    1.1 tf.contrib.layers.embed_sequence

    该函数的原型是:

    embed_sequence(
        ids,
        vocab_size=None,
        embed_dim=None,
        unique=False,
        initializer=None,
        regularizer=None,
        trainable=True,
        scope=None,
        reuse=None
    )
    

    该函数将输入的ids,转换成embeddings,输入的id是整型的维数为[batch_size, doc_length] 的张量,返回维数为[batch_size, doc_length, embed_dim]的张量。

    1.2 tf.strided_slice

    该函数的原型是:

    strided_slice(
        input_,
        begin,
        end,
        strides=None,
        begin_mask=0,
        end_mask=0,
        ellipsis_mask=0,
        new_axis_mask=0,
        shrink_axis_mask=0,
        var=None,
        name=None
    )
    

    我们主要有四个参数,input,begin,end和strides。begin和input以及strides和input的维数要一致。begin,end和strides决定了input的每一维要如何剪切。注意,这里end是闭区间。我们来看一个例子大家就明白啦。

    data = [[[1, 1, 1], [2, 2, 2]],
                [[3, 3, 3], [4, 4, 4]],
                [[5, 5, 5], [6, 6, 6]]]
    x = tf.strided_slice(data,[0,0,0],[1,1,1])
    y = tf.strided_slice(data,[0,0,0],[2,2,2],[1,1,1])
    z = tf.strided_slice(data,[0,0,0],[2,2,2],[1,2,1])
    
    with tf.Session() as sess:
        print(sess.run(x))
        print(sess.run(y))
        print(sess.run(z))
    

    输出为:

    # x
    [[[1]]]
    # y
    [[[1 1]
      [2 2]]
    
     [[3 3]
      [4 4]]]
    # z
    [[[1 1]]
    
     [[3 3]]]
    

    x的输出为[[[1]]],因为在每一维我们只截取[0,1),因此只保留了[0,0,0]这里的元素,即1。y在每一位截取[0,2),且步长为1,因此剩了8个元素。而z在第二维的步长是2,因此保留[0,0,0],[1,0,0],[1,0,1],[0,0,1]四个元素。

    1.3 tf.nn.embedding_lookup

    该函数的原型是:

    embedding_lookup(
        params,
        ids,
        partition_strategy='mod',
        name=None,
        validate_indices=True,
        max_norm=None
    )
    

    tf.nn.embedding_lookup函数的用法主要是选取一个张量里面索引对应的元素。tf.nn.embedding_lookup(params, id):params就是输入张量,id就是张量对应的索引,其他的参数不介绍。看个例子吧:

    c = np.random.random([10, 1])
    b = tf.nn.embedding_lookup(c, [1, 3])
    
    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        print(sess.run(b))
        print(c)
    

    输出为:

    [[ 0.94285588]
     [ 0.75749925]]
    [[ 0.69653103]
     [ 0.94285588]
     [ 0.23237639]
     [ 0.75749925]
     [ 0.53966384]
     [ 0.05784376]
     [ 0.80573055]
     [ 0.90221424]
     [ 0.34374387]
     [ 0.51868458]]
    

    可以看到,embedding_lookup选择了c中的索引为1和3的元素返回。

    1.4 tensorflow.ayers.Dense

    Dense是一个构建全链接层的类,如下面的例子:

    output_layer = Dense(target_vocab_size,kernel_initializer=tf.truncated_normal_initializer(mean=0.1,stddev=0.1))
    

    target_vocab_size定义了神经元的个数。

    1.5 tf.contrib.seq2seq.TrainingHelper

    这是用于seq2seq中帮助建立Decoder的一个类,只能在训练时使用,示例代码如下:

    helper = tf.contrib.seq2seq.TrainingHelper(
        input=input_vectors,
        sequence_length=input_lengths)
    

    1.6 tf.contrib.seq2seq.GreedyEmbeddingHelper

    这是用于seq2seq中帮助建立Decoder的一个类,在预测时使用,示例代码如下:

    helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
          embedding=embedding,
          start_tokens=tf.tile([GO_SYMBOL], [batch_size]),
          end_token=END_SYMBOL)
    

    start_tokens是预测时每个输入的开头的一个标志。

    1.7 tf.contrib.seq2seq.BasicDecoder

    用于构造一个decoder,示例代码如下

    decoder = tf.contrib.seq2seq.BasicDecoder(
        cell=cell,
        helper=helper,
        initial_state=cell.zero_state(batch_size, tf.float32))
    

    1.8 tf.contrib.seq2seq.dynamic_decode

    用于构造一个动态的decoder,返回的内容是:
    (final_outputs, final_state, final_sequence_lengths).
    其中,final_outputs是一个namedtuple,里面包含两项(rnn_outputs, sample_id)
    rnn_output: [batch_size, decoder_targets_length, vocab_size],保存decode每个时刻每个单词的概率,可以用来计算loss
    sample_id: [batch_size], tf.int32,保存最终的编码结果。可以表示最后的答案
    示例代码如下

    outputs, _ = tf.contrib.seq2seq.dynamic_decode(
       decoder=decoder,
       output_time_major=False,
       impute_finished=True,
       maximum_iterations=20)
    

    上面的1.5-1.8结合使用,可以构造一个完整的Decoder:

    cell = # instance of RNNCell
    
    if mode == "train":
      helper = tf.contrib.seq2seq.TrainingHelper(
        input=input_vectors,
        sequence_length=input_lengths)
    elif mode == "infer":
      helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(
          embedding=embedding,
          start_tokens=tf.tile([GO_SYMBOL], [batch_size]),
          end_token=END_SYMBOL)
    
    decoder = tf.contrib.seq2seq.BasicDecoder(
        cell=cell,
        helper=helper,
        initial_state=cell.zero_state(batch_size, tf.float32))
    outputs, _ = tf.contrib.seq2seq.dynamic_decode(
       decoder=decoder,
       output_time_major=False,
       impute_finished=True,
       maximum_iterations=20)
    

    1.9 tf.tile

    该函数的原型是:

    tile(
        input,
        multiples,
        name=None
    )
    

    tf.tile主要的功能就是在tensorflow中对矩阵进行自身进行复制的功能,比如按行进行复制,或是按列进行复制,如下面的例子:

    a = tf.constant([[1, 2],[2, 3],[3, 4]], dtype=tf.float32)
    tile_a_1 = tf.tile(a, [1,2])
    tile_a_2 = tf.tile(a,[2,1])
    tile_a_3 = tf.tile(a,[2,2])
    
    with tf.Session() as sess:
        print(sess.run(tile_a_1))
        print(sess.run(tile_a_2))
        print(sess.run(tile_a_3))
    

    输出为:

    [[ 1.  2.  1.  2.]
     [ 2.  3.  2.  3.]
     [ 3.  4.  3.  4.]]
    [[ 1.  2.]
     [ 2.  3.]
     [ 3.  4.]
     [ 1.  2.]
     [ 2.  3.]
     [ 3.  4.]]
    [[ 1.  2.  1.  2.]
     [ 2.  3.  2.  3.]
     [ 3.  4.  3.  4.]
     [ 1.  2.  1.  2.]
     [ 2.  3.  2.  3.]
     [ 3.  4.  3.  4.]]
    

    第一次我们按第二维复制了两倍,即列数变成了两倍,第二次是第一维复制了两倍,所以行数变成了两倍。第三次是两个维度都复制两倍,因此横向纵向都变成了两倍长。

    1.10 tf.identity

    该函数的原型是:

    identity(
        input,
        name=None
    )
    

    该函数用于返回一个跟input一样维度和内容的张量。

    1.11 tf.sequence_mask

    该函数的原型是:

    sequence_mask(
        lengths,
        maxlen=None,
        dtype=tf.bool,
        name=None
    )
    

    lengths代表的是一个一维数组,代表每一个sequence的长度,那么该函数返回的是一个mask的张量,张量的维数是:(lengths.shape,maxlen):
    例如:

    tf.sequence_mask([1, 3, 2], 5)
    

    输出为:

    [[True, False, False, False, False],
    [True, True, True, False, False],
    [True, True, False, False, False]]
    

    1.12 tf.contrib.seq2seq.sequence_loss

    该函数的原型是:

    sequence_loss(
        logits,
        targets,
        weights,
        average_across_timesteps=True,
        average_across_batch=True,
        softmax_loss_function=None,
        name=None
    )
    

    用于计算seq2seq中的loss。当我们的输入是不定长的时候,weights参数常常使用我们1.11中得到的mask。

    1.13 tf.train.AdamOptimizer

    我们都知道,我们进行训练需要使用一个优化器,这里我并不是想讲AdamOptimizer,你当然可以使用其他的优化器。这里我们想要介绍的是在使用优化器之后,我们想要对什么进行优化,在之前的代码中,我们可能用tf.train.Optimizer.minimize更多,这个函数用于最小化loss,并更新var_list。这个函数其实可以拆解成两个函数来实现同样的功能:

    tf.train.Optimizer.compute_gradients(loss,var_list=None, gate_gradients=1,
    aggregation_method=None,
    colocate_gradients_with_ops=False, grad_loss=None)
    ,该函数对var_list中的变量计算loss的梯度
    该函数为函数minimize()的第一部分,返回一个以元组(gradient, variable)组成的列表。

    tf.train.Optimizer.apply_gradients(grads_and_vars, global_step=None, name=None) ,该函数将计算出的梯度应用到变量上,是函数minimize()的第二部分,返回一个应用指定的梯度的操作Operation,对global_step做自增操作。

    本文中,将使用以上两个函数来对loss进行优化。

    1.14 tf.contrib.rnn.LSTMCell

    用于构建一个LSTMCell的类,示例如下:

    lstm_cell = tf.contrib.rnn.LSTMCell(rnn_size,initializer=tf.random_uniform_initializer(-0.1,0.1,seed=2))
    

    rnn_size就是我们隐藏层的神经元的数量。

    1.15 tf.contrib.rnn.MultiRNNCell

    用于构建多层RNN的类,需要传入一个cell的list,示例如下:

    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)])
    

    1.16 tf.nn.dynamic_rnn

    该函数的原型是:

    dynamic_rnn(
        cell,
        inputs,
        sequence_length=None,
        initial_state=None,
        dtype=None,
        parallel_iterations=None,
        swap_memory=False,
        time_major=False,
        scope=None
    )
    

    用于构造一个动态的rnn模型,返回模型的输出,以及state的值,如果是lstm,那么state是一个tuple,有短时记忆c和长时记忆h。示例如下:

    rnn_layers = [tf.nn.rnn_cell.LSTMCell(size) for size in [128, 256]]
    
    # create a RNN cell composed sequentially of a number of RNNCells
    multi_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layers)
    
    # 'outputs' is a tensor of shape [batch_size, max_time, 256]
    # 'state' is a N-tuple where N is the number of LSTMCells containing a
    # tf.contrib.rnn.LSTMStateTuple for each cell
    outputs, state = tf.nn.dynamic_rnn(cell=multi_rnn_cell,
                                       inputs=data,
                                       dtype=tf.float32)
    

    2、代码实现思路

    这里的代码并不是所有的代码,完整的代码参照github(https://github.com/princewen/tensorflow_practice/blob/master/basic_seq2seq.py)。

    2.1 数据处理

    首先,我们将我们的对联分为上联和下联,上联用于encoder的输入,下联用于decoder的输入以及损失计算:

    source = open("data/source.txt",'w')
    target = open("data/target.txt",'w')
    
    with open("data/对联.txt",'r') as f:
        lines = f.readlines()
        for line in lines:
            line = line.strip().split(" ")
            print(line)
            source.write(line[0]+'\n')
            target.write(line[1]+'\n')
    

    读入我们的数据,并进行打印:

    with open('data/source.txt','r',encoding='utf-8') as f:
        source_data = f.read()
    
    with open('data/target.txt','r',encoding='utf-8') as f:
        target_data = f.read()
    
    print(source_data.split('\n')[:10])
    print(target_data.split('\n')[:10])
    

    输出为:

    ['承上下求索志', '除旧岁破旧俗', '处处春光济美', '处处欢歌遍地', '处处明山秀水', '窗外红梅最艳', '创造万千气象', '春到碧桃树上', '为江山添秀色', '爆竹一声除旧']
    ['绘春秋振兴图', '迎新年树新风', '年年人物风流', '家家喜笑连天', '家家笑语欢歌', '心头美景尤佳', '建设两个文明', '莺歌绿柳楼前', '与日月争光辉', '桃符万户更新']
    

    在seq2seq中,我们输入的不能是中文字符,必须是整数数字,因此我们要建立一个中文到整数数字的双向转换的字典,同时需要添加以下的几个特殊字符:<PAD>主要用来进行字符补全,<EOS>和<GO>都是用在Decoder端的序列中,告诉解码器句子的起始与结束,<UNK>则用来替代一些未出现过的词或者低频词。。

    def extract_character_vocab(data):
        """
        :param data:
        :return: 字符映射表
        """
        special_words = ['<PAD>','<UNK>','<GO>','<EOS>']
        set_words = list(set([character for line in data.split('\n') for character in line]))
        int_to_vocab = {idx:word for idx,word in enumerate(special_words + set_words)}
        vocab_to_int = {word:idx for idx,word in int_to_vocab.items()}
    
        return int_to_vocab,vocab_to_int
    
    # 得到输入和输出的字符映射表
    source_int_to_letter,source_letter_to_int = extract_character_vocab(source_data+target_data)
    target_int_to_letter,target_letter_to_int = extract_character_vocab(source_data+target_data)
    
    # 将每一行转换成字符id的list
    source_int = [[source_letter_to_int.get(letter,source_letter_to_int['<UNK>'])
                   for letter in line] for line in source_data.split('\n')]
    
    target_int = [[target_letter_to_int.get(letter,target_letter_to_int['<UNK>'])
                   for letter in line] for line in target_data.split('\n')]
    
    

    2.2 模型构建

    这里,我们构建模型按以下三步,构建Encoder,构建Decoder,二者进行连接建立Seq2Seq模型。

    2.2.1Encoder

    在Encoder层,我们首先需要对定义输入的tensor,同时要对字母进行Embedding,再输入到LSTM层,这里构建Embedding我们使用的是 embed_sequence函数,有关该函数的解释我们前文已经介绍过了。

    def get_encoder_layer(input_data,rnn_size,num_layers,source_sequence_length,source_vocab_size,encoding_embedding_size):
        """
        构造Encoder层
    
        参数说明:
        - input_data: 输入tensor
        - rnn_size: rnn隐层结点数量
        - num_layers: 堆叠的rnn cell数量
        - source_sequence_length: 源数据的序列长度
        - source_vocab_size: 源数据的词典大小
        - encoding_embedding_size: embedding的大小
        """
        # https://www.tensorflow.org/versions/r1.4/api_docs/python/tf/contrib/layers/embed_sequence
       
        encoder_embed_input = tf.contrib.layers.embed_sequence(input_data,source_vocab_size,encoding_embedding_size)
    
        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
    
        cell = tf.contrib.rnn.MultiRNNCell([get_lstm_cell(rnn_size) for _ in range(num_layers)])
    
        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
    

    2.2.2 Decoder

    在Decoder端,我们主要要完成以下几件事情:
    1、对target数据进行处理
    2、构造Decoder
    2.1Embedding
    2.2构造Decoder层
    2.3构造输出层,输出层会告诉我们每个时间序列的RNN输出结果
    2.4Training Decoder
    2.5Predicting Decoder

    我们这里将decoder分为了training和predicting,这两个encoder实际上是共享参数的,也就是通过training decoder学得的参数,predicting会拿来进行预测。那么为什么我们要分两个呢,这里主要考虑模型的robust。
    在training阶段,为了能够让模型更加准确,我们并不会把t-1的预测输出作为t阶段的输入,而是直接使用target data中序列的元素输入到Encoder中。而在predict阶段,我们没有target data,有的只是t-1阶段的输出和隐层状态。

    而在predict阶段我们没有target data,这个时候前一阶段的预测结果就会作为下一阶段的输入。

    下面我们会对这每个部分进行一一介绍。

    对target数据进行处理
    我们的target数据有两个作用:
    1)在训练过程中,我们需要将我们的target序列作为输入传给Decoder端RNN的每个阶段,而不是使用前一阶段预测输出,这样会使得模型更加准确。
    2)需要用target数据来计算模型的loss。

    我们首先需要对target端的数据进行一步预处理。在我们将target中的序列作为输入给Decoder端的RNN时,序列中的最后一个字母(或单词)其实是没有用的。看下图:

    我们此时只看右边的Decoder端,可以看到我们的target序列是[<go>, W, X, Y, Z, <eos>],其中<go>,W,X,Y,Z是每个时间序列上输入给RNN的内容,我们发现,<eos>并没有作为输入传递给RNN。因此我们需要将target中的最后一个字符去掉,同时还需要在前面添加<go>标识,告诉模型这代表一个句子的开始。

    代码如下:

    def process_decoder_input(data,vocab_to_int,batch_size):
    
        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
    

    我们使用strided_slice进行裁剪,由于是闭区间的缘故,我们在第二维使用-1,即可裁剪掉每一个序列的最后一个输入。随后,使用tail复制了batch_size个'<GO>'的标记,使用concat在axis=1拼接,从而给每一个序列加入了开始标记。

    对target数据进行embedding
    我们使用了与Encoder不同的embedding方式,通过变量以及embedding_lookup相结合的方式对对target数据进行embedding。代码如下:

    target_vocab_size = len(target_letter_to_int)
    decoder_embeddings = tf.Variable(tf.random_uniform([target_vocab_size,decoding_embedding_size]))
    decoder_embed_input = tf.nn.embedding_lookup(decoder_embeddings,decoder_input)
    

    构造Decoder层
    我们构建一个多层的LSTM单元:

    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)])
    

    构造全链接的输出层

    output_layer = Dense(target_vocab_size,kernel_initializer=tf.truncated_normal_initializer(mean=0.1,stddev=0.1))
    

    构造training decoder

        with tf.variable_scope("decode"):
            training_helper = tf.contrib.seq2seq.TrainingHelper(inputs = decoder_embed_input,
                                                                sequence_length = target_sequence_length,
                                                                time_major = False)
    
    
            training_decoder = tf.contrib.seq2seq.BasicDecoder(cell,training_helper,encoder_state,output_layer)
            training_decoder_output,_,_ = tf.contrib.seq2seq.dynamic_decode(training_decoder,impute_finished=True,
                                                                            maximum_iterations = max_target_sequence_length)
    

    构造predicting decoder

        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_token')
            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)
    

    完整的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单元数量
        - rnn_size: RNN单元的隐层结点数量
        - 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)
        decoder_embeddings = tf.Variable(tf.random_uniform([target_vocab_size,decoding_embedding_size]))
        decoder_embed_input = tf.nn.embedding_lookup(decoder_embeddings,decoder_input)
    
        # 构造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)])
    
        # Output全连接层
        # target_vocab_size定义了输出层的大小
        output_layer = Dense(target_vocab_size,kernel_initializer=tf.truncated_normal_initializer(mean=0.1,stddev=0.1))
    
    
        # 4. Training decoder
        with tf.variable_scope("decode"):
            training_helper = tf.contrib.seq2seq.TrainingHelper(inputs = decoder_embed_input,
                                                                sequence_length = target_sequence_length,
                                                                time_major = False)
    
    
            training_decoder = tf.contrib.seq2seq.BasicDecoder(cell,training_helper,encoder_state,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_token')
            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
    

    2.2.3 Seq2Seq模型

    上面已经构建完成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_state = get_encoder_layer(input_data,
                                            rnn_size,
                                            num_layers,
                                            source_sequence_length,
                                            source_vocab_size,
                                            encoding_embedding_size)
    
        decoder_input = process_decoder_input(targets,target_letter_to_int,batch_size)
    
        training_decoder_output,predicting_decoder_output = decoding_layer(target_letter_to_int,
                                                                           decoding_embedding_size,
                                                                           num_layers,
                                                                           rnn_size,
                                                                           target_sequence_length,
                                                                           max_target_sequence_length,
                                                                           encoder_state,
                                                                           decoder_input)
    
        return training_decoder_output,predicting_decoder_output
    

    2.3 定义loss以及optimizer

    接下来,我们定义我们的loss和optimizer。loss的计算使用sequence_loss函数,同时使用AdamOptimizer去最小化这个loss。我们这里没有直接使用minimize方法,而是使用了两步compute_gradients和apply_gradients。详细的过程我们之前已经介绍过了。

    train_graph = tf.Graph()
    
    with train_graph.as_default():
        input_data, targets, lr, target_sequence_length, max_target_sequence_length, source_sequence_length = get_inputs()
    
        training_decoder_output, predicting_decoder_output = seq2seq_model(input_data,
                                                                           targets,
                                                                           lr,
                                                                           target_sequence_length,
                                                                           max_target_sequence_length,
                                                                           source_sequence_length,
                                                                           len(source_letter_to_int),
                                                                           len(target_letter_to_int),
                                                                           encoding_embedding_size,
                                                                           decoding_embedding_size,
                                                                           rnn_size,
                                                                           num_layers)
    
        training_logits = tf.identity(training_decoder_output.rnn_output,'logits')
        predicting_logits = tf.identity(predicting_decoder_output.sample_id,name='predictions')
    
        #mask是权重的意思
        #tf.sequence_mask([1, 3, 2], 5)  # [[True, False, False, False, False],
                                    #  [True, True, True, False, False],
                                    #  [True, True, False, False, False]]
        masks = tf.sequence_mask(target_sequence_length,max_target_sequence_length,dtype=tf.float32,name="masks")
    
        with tf.name_scope("optimization"):
            cost = tf.contrib.seq2seq.sequence_loss(
                training_logits,
                targets,
                masks
            )
    
            optimizer = tf.train.AdamOptimizer(lr)
    
            # minimize函数用于添加操作节点,用于最小化loss,并更新var_list.
            # 该函数是简单的合并了compute_gradients()与apply_gradients()函数返回为一个优化更新后的var_list,
            # 如果global_step非None,该操作还会为global_step做自增操作
    
            #这里将minimize拆解为了以下两个部分:
    
            # 对var_list中的变量计算loss的梯度 该函数为函数minimize()的第一部分,返回一个以元组(gradient, variable)组成的列表
            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]
            # 将计算出的梯度应用到变量上,是函数minimize()的第二部分,返回一个应用指定的梯度的操作Operation,对global_step做自增操作
            train_op = optimizer.apply_gradients(capped_gradients)
    

    2.4 训练模型

    我们拿出一个batch的数据做为验证集,其余的数据做训练,每次得到一个batch的数据,同时,在训练完成时,对模型进行了保存。
    获取batch 的代码如下:

    def pad_sentence_batch(sentence_batch,pad_int):
        '''
        对batch中的序列进行补全,保证batch中的每行都有相同的sequence_length
    
        参数:
        - sentence batch
        - pad_int: <PAD>对应索引号
        '''
        max_sentence = max([len(sentence) for sentence in sentence_batch])
        return [sentence + [pad_int] * (max_sentence - len(sentence)) for sentence in sentence_batch]
    
    
    def get_batches(targets,sources,batch_size,source_pad_int,target_pad_int):
    
        for batch_i in range(0,len(sources)//batch_size):
            start_i = batch_i * batch_size
            sources_batch = sources[start_i : start_i + batch_size]
            targets_batch = targets[start_i : start_i + batch_size]
    
            pad_sources_batch = np.array(pad_sentence_batch(sources_batch,source_pad_int))
            pad_targets_batch = np.array(pad_sentence_batch(targets_batch,target_pad_int))
    
            targets_lengths = []
            for target in targets_batch:
                targets_lengths.append(len(target))
    
            source_lengths = []
            for source in sources_batch:
                source_lengths.append(len(source))
    
            yield pad_targets_batch,pad_sources_batch,targets_lengths,source_lengths
    

    训练过程代码如下:

    # Train
    train_source = source_int[batch_size:]
    train_target = source_int[batch_size:]
    
    # 留出一个batch进行验证
    valid_source = source_int[:batch_size]
    valid_target = target_int[:batch_size]
    
    (valid_targets_batch, valid_sources_batch, valid_targets_lengths, valid_sources_lengths) = next(get_batches(valid_target, valid_source, batch_size,
                               source_letter_to_int['<PAD>'],
                               target_letter_to_int['<PAD>']))
    
    display_step = 50
    
    checkpoint = "data/trained_model.ckpt"
    
    with tf.Session(graph=train_graph) as sess:
        sess.run(tf.global_variables_initializer())
    
        for epoch_i in range(1,epochs+1):
            for batch_i,(targets_batch, sources_batch, targets_lengths, sources_lengths) in enumerate(get_batches(
                train_target,train_source,batch_size,source_letter_to_int['<PAD>'],
                               target_letter_to_int['<PAD>']
            )):
                _,loss = sess.run([train_op,cost],feed_dict={
                    input_data:sources_batch,
                    targets:targets_batch,
                    lr:learning_rate,
                    target_sequence_length:targets_lengths,
                    source_sequence_length:sources_lengths
                })
    
                if batch_i % display_step == 0:
                    # 计算validation loss
                    validation_loss = sess.run(
                        [cost],
                        {input_data: valid_sources_batch,
                         targets: valid_targets_batch,
                         lr: learning_rate,
                         target_sequence_length: valid_targets_lengths,
                         source_sequence_length: valid_sources_lengths})
    
                    print('Epoch {:>3}/{} Batch {:>4}/{} - Training Loss: {:>6.3f}  - Validation loss: {:>6.3f}'
                          .format(epoch_i,
                                  epochs,
                                  batch_i,
                                  len(train_source) // batch_size,
                                  loss,
                                  validation_loss[0]))
    
        saver = tf.train.Saver()
        saver.save(sess, checkpoint)
        print('Model Trained and Saved')
    

    训练过程:


    2.5 模型测试

    在训练阶段,我们将模型进行了保存,那么在测试阶段,我们首先将保存的模型进行恢复,再进行预测:

    def source_to_seq(text):
        sequence_length = 7
        return [source_letter_to_int.get(word,source_letter_to_int['<UNK>']) for word in text] + [source_letter_to_int['<PAD>']] * (sequence_length - len(text))
    
    
    input_word = '日丽风和人乐'
    text = source_to_seq(input_word)
    
    checkpoint = "data/trained_model.ckpt"
    loaded_graph = tf.Graph()
    
    with tf.Session(graph=loaded_graph) as sess:
        loader = tf.train.import_meta_graph(checkpoint+'.meta')
        loader.restore(sess,checkpoint)
    
        input_data = loaded_graph.get_tensor_by_name('inputs:0')
        logits = loaded_graph.get_tensor_by_name('predictions:0')
        source_sequence_length = loaded_graph.get_tensor_by_name('source_sequence_length:0')
        target_sequence_length = loaded_graph.get_tensor_by_name('target_sequence_length:0')
    
        answer_logits = sess.run(logits, {input_data: [text] * batch_size,
                                          target_sequence_length: [len(input_word)] * batch_size,
                                          source_sequence_length: [len(input_word)] * batch_size})[0]
    
        pad = source_letter_to_int["<PAD>"]
    
        print('原始输入:', input_word)
    
        print('\nSource')
        print('  Word 编号:    {}'.format([i for i in text]))
        print('  Input Words: {}'.format(" ".join([source_int_to_letter[i] for i in text])))
    
        print('\nTarget')
        print('  Word 编号:       {}'.format([i for i in answer_logits if i != pad]))
        print('  Response Words: {}'.format(" ".join([target_int_to_letter[i] for i in answer_logits if i != pad])))
    

    我们预测一个五字对联:

    我们再预测一个七字对联:

    哈哈,感觉对的还不错呀,起码字数能够统一起来,嘻嘻!

    3、参考文献

    参考文献
    1、seq2seq学习笔记:http://blog.csdn.net/jerr__y/article/details/53749693
    2、从Encoder到Decoder实现Seq2Seq模型:https://zhuanlan.zhihu.com/p/27608348
    3、Tensorflow一些常用基本概念与函数(4):
    http://blog.csdn.net/lenbow/article/details/52218551
    4、tf.strided_slice使用简介:https://www.jianshu.com/p/a1a9e44708f6
    5、tf.nn.embedding_lookup函数的用法“
    http://blog.csdn.net/uestc_c2_403/article/details/72779417
    6、http://blog.csdn.net/zj360202/article/details/78872076
    7、tf.identity的意义以及用例:http://blog.csdn.net/zj360202/article/details/78872076

    github连接
    https://github.com/princewen/tensorflow_practice/blob/master/basic_seq2seq.py

    微信公众号

    相关文章

      网友评论

      • 5302b1eb2c39:LZ很棒。所以我想问个问题,能不能直接将训练好的embedding作为输入,送入encoder和decoder呢?那样的话encoder,decoder的inputs的shape会有什么问题么
      • 言若_6d2e:您好,看了您写的收获颇多,但是有个疑问就是,有没有单独的测试代码?这样每次测试的时候必须重新训练,我把最后那点的测试代码单独拿出来就报错,说是维度不匹配。。但是你的整个代码跑下来测试时没有问题的?
      • 寒寒_21b7:代码的第332行是不是应该是target_int
        文哥的学习日记:@寒寒_21b7 对的,谢谢指正!
        寒寒_21b7:@石晓文的学习日记 https://github.com/princewen/tensorflow_practice/blob/master/basic_seq2seq.py
        第332行
        文哥的学习日记:您好,具体是哪一行呢?可能会出现大小写的问题。。
      • 八神苍月:您好大神,请教一下,可以基于seq2seq实现基于中文的句子复述吗?

      本文标题:简单的Seq2Seq实现作对联

      本文链接:https://www.haomeiwen.com/subject/faftxftx.html