seq2seq裡在chatbot的一些用法

dunne21發表於2021-09-09

轉處:https://blog.csdn.net/liuchonge/article/details/79021938

上篇文章我們使用tf.contrib.legacy_seq2seq下的API構建了一個簡單的chatbot對話系統,但是我們已經說過,這部分程式碼是1.0版本之前所提供的API,將來會被棄用,而且API介面並不靈活,在實際使用過程中還會存在版本不同導致的各種個樣的錯誤。所以我們有必要學習一下新版本的API,這裡先來說一下二者的不同:

  • 新版本都是用dynamic_rnn來構造RNN模型,這樣就避免了資料長度不同所帶來的困擾,不需要再使用model_with_buckets這種方法來構建模型,使得我們資料處理和模型程式碼都簡潔很多

  • 新版本將Attention、Decoder等幾個主要的功能都分別進行封裝,直接呼叫相應的Wapper函式進行封裝即可,呼叫起來更加靈活方便,而且只需要寫幾個簡單的函式既可以自定義的各個模組以滿足我們個性化的需求。

  • 實現了beam_search功能,可直接呼叫

這次我們先來看如何直接使用新版本API構造對話系統,然後等下一篇文章在分析一些主要檔案和函式的原始碼實現。本文程式碼可以再我的github中找到:。歡迎fork和star~~

):
  • inference圖往往與train和eval結構存在較大差異(沒有decoder輸入和目標,需要使用貪婪或者beam_search進行decode,batch_size也不同等等),所以往往需要單獨進行構建

  • eval圖也會得到簡化,因為其不需要進行反向傳播,只需要得到一個loss和acc值

  • 資料可以分別進行feed,簡化資料操作

  • 變數重用變得簡單,因為train、eval存在一些公用變數和程式碼塊,就不需要我們重複定義,使程式碼簡化

  • 只需要在train時不斷儲存模型引數,然後在eval和infer的時候restore引數即可

以上,所以我們構建了train、eval、infer三個函式來實現上面的功能。在看程式碼之前我們先來簡單說一下新版API幾個主要的模組以及相互之間的呼叫關係。tf.contrib.seq2seq資料夾下面主要有下面6個檔案,除了loss檔案和之前的sequence_loss函式沒有很大區別,這裡不介紹之外,其他幾個檔案都會簡單的說一下,這裡主要介紹函式和類的功能,原始碼會放在下篇文章中介紹。

  • decoder

  • basic_decoder

  • helper

  • attention_wrapper

  • beam_search_decoder

  • loss

BasicDecoder類和dynamic_decode

decoder和basic_decoder檔案可以放在一起看,decoder檔案中定義了Decoder抽象類和dynamic_decode函式,dynamic_decode可以視為整個解碼過程的入口,需要傳入的引數就是Decoder的一個例項,他會動態的呼叫Decoder的step函式按步執行decode,可以理解為Decoder類定義了單步解碼(根據輸入求出輸出,並將該輸出當做下一時刻輸入),而dynamic_decode則會呼叫control_flow_ops.while_loop這個函式來迴圈執行直到輸出結束編碼過程。而basic_decoder檔案定義了一個基本的Decoder類例項BasicDecoder,看一下其初始化函式:

def __init__(self, cell, helper, initial_state, output_layer=None):
  • 1

  • 2

需要傳入的引數就是cell型別、helper型別、初始化狀態(encoder的最後一個隱層狀態)、輸出層(輸出對映層,將rnn_size轉化為vocab_size維),需要注意的就是前面兩個,下面分別介紹:

cell型別(Attention型別)

cell型別就是RNNCell,也就是decode階段的神經元,可以使簡單的RNN、GRU、LSTM(也可以加上dropout、並使用MultiRNNCell進行堆疊成多層),也可以是加上了Attention功能之後的RNNcell。這就引入了attention_wrapper檔案中定義的幾種attention機制(BahdanauAttention、 LuongAttention、 BahdanauMonotonicAttention、 LuongMonotonicAttention)和將attention機制封裝到RNNCell上面的方法AttentionWrapper。其實很簡單,就跟dropoutwrapper、outputwrapper一樣,我們只需要在原本RNNCell的基礎上在封裝一層attention即可。程式碼如下所示:

    # 分為三步,第一步是定義attention機制,第二步是定義要是用的基礎的RNNCell,第三步是使用AttentionWrapper進行封裝
    #定義要使用的attention機制。
    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')

helper型別

helper其實就是decode階段如何根據預測結果得到下一時刻的輸入,比如訓練過程中應該直接使用上一時刻的真實值作為下一時刻輸入,預測過程中可以使用貪婪的方法選擇機率最大的那個值作為下一時刻等等。所以Helper也就可以大致分為訓練時helper和預測時helper兩種。官網給出了下面幾種Helper類:

  • “Helper”:最基本的抽象類

  • “TrainingHelper”:訓練過程中最常使用的Helper,下一時刻輸入就是上一時刻target的真實值

  • “GreedyEmbeddingHelper”:預測階段最常使用的Helper,下一時刻輸入是上一時刻機率最大的單詞透過embedding之後的向量

  • “SampleEmbeddingHelper”:預測時helper,繼承自GreedyEmbeddingHelper,下一時刻輸入是上一時刻透過某種機率分佈取樣而來在經過embedding之後的向量

  • “CustomHelper”:最簡單的helper,一般使用者自定義helper時會基於此,需要使用者自己定義如何根據輸出得到下一時刻輸入

  • “ScheduledEmbeddingTrainingHelper”:訓練時Helper,繼承自TrainingHelper,新增了廣義伯努利分佈,對id的embedding向量進行sampling

  • “ScheduledOutputTrainingHelper”:訓練時Helper,繼承自TrainingHelper,直接對輸出進行取樣

  • “InferenceHelper”:CustomHelper的特例,只用於預測的helper,也需要使用者自定義如何得到下一時刻輸入

所以瞭解cell和helper類之後我們就可以很輕鬆的構建decode階段的模型,以train階段為例:

    #分為四步,第一步是定義cell型別,第二步是定義helper型別,第三步是定義BasicDecoder類例項,第四步是呼叫dynamic_decode函式進行解碼
    cell = ***(上面的程式碼)
    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)
    decoder_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder=training_decoder, impute_finished=True,
                                                        maximum_iterations=self.max_target_sequence_length)

beam search decoder類

到這,基本上就可以構建一個完整的seq2seq模型了,但是上面的檔案中還有beam_search_decoder.py檔案沒有介紹,也就是我們常用的beam_search方法,下面也簡單說一下。該檔案定義了BeamSearchDecoder類,其實是一個Decoder的例項,跟BasicDecoder在一個等級上,但是二者又存在著不同,因為BasicDecoder需要指定helper引數,也就是指定decode階段如何根據上一時刻輸出獲得下一時刻輸入。但是BeamSearchDecoder不需要,因為其在內部實現了beam_search的功能,也就包含了helper的效果,不需要再額外定義。所以BeamSearchDecoder的呼叫方法如下所示:

    #分為三步,第一步是定義cell,第二步是定義BeamSearchDecoder,第三步是呼叫dynamic_decode函式進行解碼
    cell = ***(上面程式碼)
    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)
    decoder_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder=inference_decoder,
                                                    maximum_iterations=self.max_target_sequence_length)

OK,接下來切入正題,看一下model部分程式碼:

    class Seq2SeqModel():
        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()        def _create_rnn_cell(self, single=False):
            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        def build_model(self):
            print('building model... ...')            #=================================1, 定義模型的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')            # 根據目標序列長度,選出其中最大值,然後使用該值構建序列長度的mask標誌。用一個sequence_mask的例子來說明起作用
            #  tf.sequence_mask([1, 3, 2], 5)
            #  [[True, False, False, False, False],
            #  [True, True, True, False, False],
            #  [True, True, False, False, False]]
            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')            #=================================2, 定義模型的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)            # =================================3, 定義模型的decoder部分
            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機制。
                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                #定義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開始處新增一個,並刪除結尾處的,並進行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['']), 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['']
                    end_token = self.word_to_idx['']                    # 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)            # =================================4, 儲存模型
            self.saver = tf.train.Saver(tf.all_variables())        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        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        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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1600/viewspace-2801344/,如需轉載,請註明出處,否則將追究法律責任。

相關文章