使用Seq2Seq+attention实现简单的Chatbot

本文代码的github连接:https://github.com/princewen/tensorflow_practice/tree/master/chat_bot_seq2seq_attention

1、attention model原理介绍

1.1 attention model

为什么要有attention机制 原本的Seq2seq模型只有一个encoder和一个decoder,通常的做法是将一个输入的句子编码成一个固定大小的state,然后作为decoder的初始状态(当然也可以作为每一时刻的输入),但这样的一个状态对于decoder中的所有时刻都是一样的。 attention即为注意力,人脑在对于的不同部分的注意力是不同的。需要attention的原因是非常直观的,当我们看一张照片时,照片上有一个人,我们的注意力会集中在这个人身上,而它身边的花草蓝天,可能就不会得到太多的注意力。也就是说,普通的模型可以看成所有部分的attention都是一样的,而这里的attention-based model对于不同的部分,重要的程度则不同,decoder中每一个时刻的状态是不同的。

Attention-based Model是什么 Attention-based Model其实就是一个相似性的度量,当前的输入与目标状态越相似,那么在当前的输入的权重就会越大,说明当前的输出越依赖于当前的输入。严格来说,Attention并算不上是一种新的model,而仅仅是在以往的模型中加入attention的思想,所以Attention-based Model或者Attention Mechanism是比较合理的叫法,而非Attention Model。

没有attention机制的encoder-decoder结构通常把encoder的最后一个状态作为decoder的输入(可能作为初始化,也可能作为每一时刻的输入),但是encoder的state毕竟是有限的,存储不了太多的信息,对于decoder过程,每一个步骤都和之前的输入都没有关系了,只与这个传入的state有关。attention机制的引入之后,decoder根据时刻的不同,让每一时刻的输入都有所不同。

Attention原理

1.2 Beam Search介绍

在sequence2sequence模型中,beam search的方法只用在测试的情况,因为在训练过程中,每一个decoder的输出是有正确答案的,也就不需要beam search去加大输出的准确率。

假设现在我们用机器翻译作为例子来说明,

我们需要翻译中文“我是中国人”--->英文“I am Chinese”

假设我们的词表大小只有三个单词就是I am Chinese。那么如果我们的beam size为2的话,我们现在来解释,

如下图所示,我们在decoder的过程中,有了beam search方法后,在第一次的输出,我们选取概率最大的"I"和"am"两个单词,而不是只挑选一个概率最大的单词。

然后接下来我们要做的就是,把“I”单词作为下一个decoder的输入算一遍得到y2的输出概率分布,把“am”单词作为下一个decoder的输入算一遍也得到y2的输出概率分布。

比如将“I”单词作为下一个decoder的输入算一遍得到y2的输出概率分布如下:

比如将“am”单词作为下一个decoder的输入算一遍得到y2的输出概率分布如下:

那么此时我们由于我们的beam size为2,也就是我们只能保留概率最大的两个序列,此时我们可以计算所有的序列概率:

“I I” = 0.40.3 "I am" = 0.40.6 "I Chinese" = 0.4*0.1

"am I" = 0.50.3 "am am" = 0.50.3 "am Chinese" = 0.5*0.4

我们很容易得出俩个最大概率的序列为 “I am”和“am Chinese”,然后后面会不断重复这个过程,直到遇到结束符为止。

最终输出2个得分最高的序列。

这就是seq2seq中的beam search算法过程,

2、tensorflow相关api介绍

2.1 tf.app.flags

tf定义了tf.app.flags,用于支持接受命令行传递参数,相当于接受argv。看下面的例子:

import tensorflow as tf

#第一个是参数名称,第二个参数是默认值,第三个是参数描述
tf.app.flags.DEFINE_string('str_name', 'def_v_1',"descrip1")
tf.app.flags.DEFINE_integer('int_name', 10,"descript2")
tf.app.flags.DEFINE_boolean('bool_name', False, "descript3")

FLAGS = tf.app.flags.FLAGS

#必须带参数,否则:'TypeError: main() takes no arguments (1 given)';   main的参数名随意定义,无要求
def main(_):  
    print(FLAGS.str_name)
    print(FLAGS.int_name)
    print(FLAGS.bool_name)

if __name__ == '__main__':
    tf.app.run()  #执行main函数

使用命令行运行得到的输出为:

[root@AliHPC-G41-211 test]# python tt.py
def_v_1
10
False
[root@AliHPC-G41-211 test]# python tt.py --str_name test_str --int_name 99 --bool_name True
test_str
99
True

2.2 tf.clip_by_global_norm

Gradient Clipping的直观作用就是让权重的更新限制在一个合适的范围。tf.clip_by_global_norm函数的作用就是通过权重梯度的总和的比率来截取多个张量的值。 使用方式如下:

tf.clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None) 

t_list 是梯度张量, clip_norm 是截取的比率, 这个函数返回截取过的梯度张量和一个所有张量的全局范数。 t_list[i] 的更新公式如下:

t_list[i] * clip_norm / max(global_norm, clip_norm)

其中global_norm = sqrt(sum([l2norm(t)**2 for t in t_list])) global_norm 是所有梯度的平方和,如果 clip_norm > global_norm ,就不进行截取。

2.3 tf中注意力机制的实现

注意力机制只在decoder中出现,在之前作对联的文章中,我们的decoder实现分三步走:定义decoder阶段要是用的Cell -》TrainingHelper+BasicDecoder的组合定义解码器-》调用dynamic_decode进行解码。 添加注意力机制主要是在第一步,对Cell进行包裹,tf中实现了两种主要的注意力机制,我们前文中所讲的注意力机制我们成为Bahdanau注意力机制,还有一种注意力机制称为Luong注意力机制,二者最主要的区别是前者为加法注意力机制,后者为乘法注意力机制。二者的更详细的介绍参考播客:http://blog.csdn.net/amds123/article/details/65938986

那么我们就来详细介绍一下 tf中注意力机制的实现: 定义cell

 def _create_rnn_cell(self):
        def single_rnn_cell():
            # 创建单个cell,这里需要注意的是一定要使用一个single_rnn_cell的函数,不然直接把cell放在MultiRNNCell
            # 的列表中最终模型会发生错误
            single_cell = tf.contrib.rnn.LSTMCell(self.rnn_size)
            #添加dropout
            cell = tf.contrib.rnn.DropoutWrapper(single_cell, output_keep_prob=self.keep_prob_placeholder)
            return cell
        #列表中每个元素都是调用single_rnn_cell函数
        cell = tf.contrib.rnn.MultiRNNCell([single_rnn_cell() for _ in range(self.num_layers)])
        return cell

decoder_cell = self._create_rnn_cell()

封装attention wrapper

attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(num_units=self.rnn_size, memory=encoder_outputs,
                                                                     memory_sequence_length=encoder_inputs_length)
#attention_mechanism = tf.contrib.seq2seq.LuongAttention(num_units=self.rnn_size, memory=encoder_outputs, memory_sequence_length=encoder_inputs_length)

decoder_cell = tf.contrib.seq2seq.AttentionWrapper(cell=decoder_cell, attention_mechanism=attention_mechanism,
                                                               attention_layer_size=self.rnn_size, name='Attention_Wrapper')

训练阶段,使用TrainingHelper+BasicDecoder的组合

training_helper = tf.contrib.seq2seq.TrainingHelper(inputs=decoder_inputs_embedded,
                                                                    sequence_length=self.decoder_targets_length,
                                                                    time_major=False, name='training_helper')

training_decoder = tf.contrib.seq2seq.BasicDecoder(cell=decoder_cell, helper=training_helper,
                                                                   initial_state=decoder_initial_state, output_layer=output_layer)

调用dynamic_decode进行解码

decoder_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder=training_decoder,
                                                                          impute_finished=True,
                                                                    maximum_iterations=self.max_target_sequence_length)

decoder_outputs是一个namedtuple,里面包含两项(rnn_outputs, sample_id) rnn_output: [batch_size, decoder_targets_length, vocab_size],保存decode每个时刻每个单词的概率,可以用来计算loss sample_id: [batch_size], tf.int32,保存最终的编码结果。可以表示最后的答案

3、代码解释

代码目录如下图所示:

其中,data存放我们的数据,model存放我们保存的训练模型,data_loader是我们处理数据的代码,model是我们建立seq2seq模型的代码,train是我们训练模型的代码,predict是我们进行模型预测的部分。这里我们只介绍model部分,其它部分的代码大家可以参照github自己练习。

定义基本的输入输出

    def __init__(self, rnn_size, num_layers, embedding_size, learning_rate, word_to_idx, mode, use_attention,
                 beam_search, beam_size, max_gradient_norm=5.0):
        self.learing_rate = learning_rate
        self.embedding_size = embedding_size
        self.rnn_size = rnn_size
        self.num_layers = num_layers
        self.word_to_idx = word_to_idx
        self.vocab_size = len(self.word_to_idx)
        self.mode = mode
        self.use_attention = use_attention
        self.beam_search = beam_search
        self.beam_size = beam_size
        self.max_gradient_norm = max_gradient_norm
        #执行模型构建部分的代码
        self.build_model()

定义我们多层LSTM的网络结构 这里,不论是encoder还是decoder,我们都定义一个两层的LSTMCell,同时每一个cell都添加上DropoutWrapper。

    def _create_rnn_cell(self):
        def single_rnn_cell():
            # 创建单个cell,这里需要注意的是一定要使用一个single_rnn_cell的函数,不然直接把cell放在MultiRNNCell
            # 的列表中最终模型会发生错误
            single_cell = tf.contrib.rnn.LSTMCell(self.rnn_size)
            #添加dropout
            cell = tf.contrib.rnn.DropoutWrapper(single_cell, output_keep_prob=self.keep_prob_placeholder)
            return cell
        #列表中每个元素都是调用single_rnn_cell函数
        cell = tf.contrib.rnn.MultiRNNCell([single_rnn_cell() for _ in range(self.num_layers)])
        return cell

定义模型的placeholder

self.encoder_inputs = tf.placeholder(tf.int32, [None, None], name='encoder_inputs')
self.encoder_inputs_length = tf.placeholder(tf.int32, [None], name='encoder_inputs_length')

self.batch_size = tf.placeholder(tf.int32, [], name='batch_size')
self.keep_prob_placeholder = tf.placeholder(tf.float32, name='keep_prob_placeholder')

self.decoder_targets = tf.placeholder(tf.int32, [None, None], name='decoder_targets')
self.decoder_targets_length = tf.placeholder(tf.int32, [None], name='decoder_targets_length')
self.max_target_sequence_length = tf.reduce_max(self.decoder_targets_length, name='max_target_len')
self.mask = tf.sequence_mask(self.decoder_targets_length, self.max_target_sequence_length, dtype=tf.float32, name='masks')

定义encoder

with tf.variable_scope('encoder'):
    #创建LSTMCell,两层+dropout
    encoder_cell = self._create_rnn_cell()
    #构建embedding矩阵,encoder和decoder公用该词向量矩阵
    embedding = tf.get_variable('embedding', [self.vocab_size, self.embedding_size])
    encoder_inputs_embedded = tf.nn.embedding_lookup(embedding, self.encoder_inputs)
    # 使用dynamic_rnn构建LSTM模型,将输入编码成隐层向量。
    # encoder_outputs用于attention,batch_size*encoder_inputs_length*rnn_size,
    # encoder_state用于decoder的初始化状态,batch_size*rnn_szie
    encoder_outputs, encoder_state = tf.nn.dynamic_rnn(encoder_cell, encoder_inputs_embedded,
                                                       sequence_length=self.encoder_inputs_length,
                                                       dtype=tf.float32)

定义decoder 在decoder阶段,我们仍然是定义了两种模式,一种是训练,一种是预测,在训练模式下,decoder的输入是真实的target序列,而在预测时,我们可以使用贪心策略或者是beam_search策略。

        with tf.variable_scope('decoder'):
            encoder_inputs_length = self.encoder_inputs_length
            # if self.beam_search:
            #     # 如果使用beam_search,则需要将encoder的输出进行tile_batch,其实就是复制beam_size份。
            #     print("use beamsearch decoding..")
            #     encoder_outputs = tf.contrib.seq2seq.tile_batch(encoder_outputs, multiplier=self.beam_size)
            #     encoder_state = nest.map_structure(lambda s: tf.contrib.seq2seq.tile_batch(s, self.beam_size), encoder_state)
            #     encoder_inputs_length = tf.contrib.seq2seq.tile_batch(self.encoder_inputs_length, multiplier=self.beam_size)


            attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(num_units=self.rnn_size, memory=encoder_outputs,
                                                                     memory_sequence_length=encoder_inputs_length)
            #attention_mechanism = tf.contrib.seq2seq.LuongAttention(num_units=self.rnn_size, memory=encoder_outputs, memory_sequence_length=encoder_inputs_length)
            # 定义decoder阶段要是用的LSTMCell,然后为其封装attention wrapper
            decoder_cell = self._create_rnn_cell()
            decoder_cell = tf.contrib.seq2seq.AttentionWrapper(cell=decoder_cell, attention_mechanism=attention_mechanism,
                                                               attention_layer_size=self.rnn_size, name='Attention_Wrapper')
            #如果使用beam_seach则batch_size = self.batch_size * self.beam_size。因为之前已经复制过一次
            #batch_size = self.batch_size if not self.beam_search else self.batch_size * self.beam_size
            batch_size = self.batch_size
            #定义decoder阶段的初始化状态,直接使用encoder阶段的最后一个隐层状态进行赋值
            decoder_initial_state = decoder_cell.zero_state(batch_size=batch_size, dtype=tf.float32).clone(cell_state=encoder_state)
            output_layer = tf.layers.Dense(self.vocab_size, kernel_initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))

            if self.mode == 'train':
                # 定义decoder阶段的输入,其实就是在decoder的target开始处添加一个<go>,并删除结尾处的<end>,并进行embedding。
                # decoder_inputs_embedded的shape为[batch_size, decoder_targets_length, embedding_size]
                ending = tf.strided_slice(self.decoder_targets, [0, 0], [self.batch_size, -1], [1, 1])
                decoder_input = tf.concat([tf.fill([self.batch_size, 1], self.word_to_idx['<go>']), ending], 1)
                decoder_inputs_embedded = tf.nn.embedding_lookup(embedding, decoder_input)
                #训练阶段,使用TrainingHelper+BasicDecoder的组合,这一般是固定的,当然也可以自己定义Helper类,实现自己的功能
                training_helper = tf.contrib.seq2seq.TrainingHelper(inputs=decoder_inputs_embedded,
                                                                    sequence_length=self.decoder_targets_length,
                                                                    time_major=False, name='training_helper')
                training_decoder = tf.contrib.seq2seq.BasicDecoder(cell=decoder_cell, helper=training_helper,
                                                                   initial_state=decoder_initial_state, output_layer=output_layer)
                #调用dynamic_decode进行解码,decoder_outputs是一个namedtuple,里面包含两项(rnn_outputs, sample_id)
                # rnn_output: [batch_size, decoder_targets_length, vocab_size],保存decode每个时刻每个单词的概率,可以用来计算loss
                # sample_id: [batch_size], tf.int32,保存最终的编码结果。可以表示最后的答案
                decoder_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder=training_decoder,
                                                                          impute_finished=True,
                                                                    maximum_iterations=self.max_target_sequence_length)
                # 根据输出计算loss和梯度,并定义进行更新的AdamOptimizer和train_op
                self.decoder_logits_train = tf.identity(decoder_outputs.rnn_output)
                self.decoder_predict_train = tf.argmax(self.decoder_logits_train, axis=-1, name='decoder_pred_train')
                # 使用sequence_loss计算loss,这里需要传入之前定义的mask标志
                self.loss = tf.contrib.seq2seq.sequence_loss(logits=self.decoder_logits_train,
                                                             targets=self.decoder_targets, weights=self.mask)

                # Training summary for the current batch_loss
                tf.summary.scalar('loss', self.loss)
                self.summary_op = tf.summary.merge_all()

                optimizer = tf.train.AdamOptimizer(self.learing_rate)
                trainable_params = tf.trainable_variables()
                gradients = tf.gradients(self.loss, trainable_params)
                clip_gradients, _ = tf.clip_by_global_norm(gradients, self.max_gradient_norm)
                self.train_op = optimizer.apply_gradients(zip(clip_gradients, trainable_params))
            elif self.mode == 'decode':
                start_tokens = tf.ones([self.batch_size, ], tf.int32) * self.word_to_idx['<go>']
                end_token = self.word_to_idx['<eos>']
                # decoder阶段根据是否使用beam_search决定不同的组合,
                # 如果使用则直接调用BeamSearchDecoder(里面已经实现了helper类)
                # 如果不使用则调用GreedyEmbeddingHelper+BasicDecoder的组合进行贪婪式解码
                if self.beam_search:
                    inference_decoder = tf.contrib.seq2seq.BeamSearchDecoder(cell=decoder_cell, embedding=embedding,
                                                                             start_tokens=start_tokens, end_token=end_token,
                                                                             initial_state=decoder_initial_state,
                                                                             beam_width=self.beam_size,
                                                                             output_layer=output_layer)
                else:
                    decoding_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(embedding=embedding,
                                                                               start_tokens=start_tokens, end_token=end_token)
                    inference_decoder = tf.contrib.seq2seq.BasicDecoder(cell=decoder_cell, helper=decoding_helper,
                                                                        initial_state=decoder_initial_state,
                                                                        output_layer=output_layer)
                decoder_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder=inference_decoder,
                                                                maximum_iterations=10)
                # 调用dynamic_decode进行解码,decoder_outputs是一个namedtuple,
                # 对于不使用beam_search的时候,它里面包含两项(rnn_outputs, sample_id)
                # rnn_output: [batch_size, decoder_targets_length, vocab_size]
                # sample_id: [batch_size, decoder_targets_length], tf.int32

                # 对于使用beam_search的时候,它里面包含两项(predicted_ids, beam_search_decoder_output)
                # predicted_ids: [batch_size, decoder_targets_length, beam_size],保存输出结果
                # beam_search_decoder_output: BeamSearchDecoderOutput instance namedtuple(scores, predicted_ids, parent_ids)
                # 所以对应只需要返回predicted_ids或者sample_id即可翻译成最终的结果
                if self.beam_search:
                    self.decoder_predict_decode = decoder_outputs.predicted_ids
                else:
                    self.decoder_predict_decode = tf.expand_dims(decoder_outputs.sample_id, -1)

训练阶段 对于训练阶段,需要执行self.train_op, self.loss, self.summary_op三个op,并传入相应的数据

    def train(self, sess, batch):
        #对于训练阶段,需要执行self.train_op, self.loss, self.summary_op三个op,并传入相应的数据
        feed_dict = {self.encoder_inputs: batch.encoder_inputs,
                      self.encoder_inputs_length: batch.encoder_inputs_length,
                      self.decoder_targets: batch.decoder_targets,
                      self.decoder_targets_length: batch.decoder_targets_length,
                      self.keep_prob_placeholder: 0.5,
                      self.batch_size: len(batch.encoder_inputs)}
        _, loss, summary = sess.run([self.train_op, self.loss, self.summary_op], feed_dict=feed_dict)
        return loss, summary

评估阶段 对于eval阶段,不需要反向传播,所以只执行self.loss, self.summary_op两个op,并传入相应的数据

    def eval(self, sess, batch):
        # 对于eval阶段,不需要反向传播,所以只执行self.loss, self.summary_op两个op,并传入相应的数据
        feed_dict = {self.encoder_inputs: batch.encoder_inputs,
                      self.encoder_inputs_length: batch.encoder_inputs_length,
                      self.decoder_targets: batch.decoder_targets,
                      self.decoder_targets_length: batch.decoder_targets_length,
                      self.keep_prob_placeholder: 1.0,
                      self.batch_size: len(batch.encoder_inputs)}
        loss, summary = sess.run([self.loss, self.summary_op], feed_dict=feed_dict)
        return loss, summary

预测阶段 infer阶段只需要运行最后的结果,不需要计算loss,所以feed_dict只需要传入encoder_input相应的数据即可

    def infer(self, sess, batch):
        #infer阶段只需要运行最后的结果,不需要计算loss,所以feed_dict只需要传入encoder_input相应的数据即可
        feed_dict = {self.encoder_inputs: batch.encoder_inputs,
                      self.encoder_inputs_length: batch.encoder_inputs_length,
                      self.keep_prob_placeholder: 1.0,
                      self.batch_size: len(batch.encoder_inputs)}
        predict = sess.run([self.decoder_predict_decode], feed_dict=feed_dict)
        return predict

4、参考文献

1、tensorflow 学习(三)使用flags定义命令行参数:http://blog.csdn.net/leiting_imecas/article/details/72367937 2、tf.clip_by_global_norm理解:http://blog.csdn.net/u013713117/article/details/56281715 3、浅谈Attention-based Model【原理篇】:http://blog.csdn.net/wuzqchom/article/details/75792501 4、seq2seq中的beam search算法过程:https://zhuanlan.zhihu.com/p/28048246 5、常见的两种注意力机制:http://blog.csdn.net/amds123/article/details/65938986 6、从头实现深度学习的对话系统--新版本tf seq2seq API构建chatbot:http://blog.csdn.net/liuchonge/article/details/79021938

想了解更多? 那就赶紧来关注我们

原文发布于微信公众号 - 小小挖掘机(wAIsjwj)

原文发表时间:2018-02-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏深度学习与计算机视觉

理解ResNet结构与TensorFlow代码分析

该博客主要以TensorFlow提供的ResNet代码为主,但是我并不想把它称之为代码解析,因为代码和方法,实践和理论总是缺一不可。 github地址,其中...

8367
来自专栏Spark学习技巧

最大子序列和问题之算法优化

1393
来自专栏数说工作室

【SAS Says】扩展篇:IML(2)

上一篇“高级篇:IML(1)”发出来之后,有朋友反映东西东西太简单了,根本不能算“高级”。想想也是,暂时还没有介绍太复杂的SAS程序,于是决定将本篇定为“扩展篇...

3016
来自专栏闪电gogogo的专栏

压缩感知重构算法之正则化正交匹配追踪(ROMP)

  在看代码之前,先拜读了ROMP的经典文章:Needell D,VershyninR.Signal recovery from incompleteand i...

3656
来自专栏机器学习与自然语言处理

最大子序列和问题之算法优化

算法一:穷举式地尝试所有的可能 int maxSubsequenceSum(const int a[], int n) { int i, j, k; ...

2307
来自专栏漫漫深度学习路

tensorflow学习笔记(三十九):双向rnn

tensorflow 双向 rnn 如何在tensorflow中实现双向rnn 单层双向rnn ? 单层双向rnn (cs224d) tensorfl...

8485
来自专栏WeaponZhi

使用Octave来学习Machine Learning(二)

前言 上一篇我们介绍了 Octave 的一些基本情况,大家对 Octave 应该已经有了一个基本的了解,我相信看这篇文章的朋友已经在自己的电脑中安装好 Ocat...

3606
来自专栏计算机视觉与深度学习基础

Codeforces 472D

看官方题解提供的是最小生成树,怎么也想不明白,you can guess and prove it! 看了好几个人的代码,感觉实现思路全都不一样,不得不佩服cf...

21510
来自专栏小鹏的专栏

02 The TensorFlow Way(1)

The TensorFlow Way Introduction:          现在我们介绍了TensorFlow如何创建张量,使用变量和占位符,我们将介...

22010
来自专栏人工智能LeadAI

译文 | 与TensorFlow的第一次接触 第三章:聚类

前一章节中介绍的线性回归是一种监督学习算法,我们使用数据与输出值(标签)来建立模型拟合它们。但是我们并不总是有已经打标签的数据,却仍然想去分析它们。这种情况下,...

4466

扫码关注云+社区

领取腾讯云代金券