1. 程式人生 > 其它 >深度學習對話系統實戰篇 -- 簡單 chatbot 程式碼實現

深度學習對話系統實戰篇 -- 簡單 chatbot 程式碼實現

本文的程式碼都可以到我的 github 中下載:https://github.com/lc222/seq2seq_chatbot 前面幾篇文章我們已經介紹了 seq2seq 模型的理論知識,並且從 tensorflow 原始碼層面解析了其實現原理,本篇文章我們會聚焦於如何呼叫 tf 提供的 seq2seq 的 API,實現一個簡單的 chatbot 對話系統。這裡先給出幾個參考的部落格和程式碼:

  1. tensorflow 官網 API 指導(http://t.cn/R8MiZcR )
  2. Chatbots with Seq2Seq Learn to build a chatbot using TensorFlow(http://t.cn/R8MiykP )
  3. DeepQA(http://t.cn/R8MiVld )
  4. Neural_Conversation_Models(http://t.cn/RtthjXn )

經過一番調查發現網上現在大部分 chatbot 的程式碼都是基於 1.0 版本之前的 tf 實現的,而且都是從 tf 官方指導文件 nmt 上進行遷移和改進,所以基本上大同小異,但是在實際使用過程中會發現一個問題,由於 tf 版本之間的相容問題導致這些程式碼在新版本的 tf 中無法正常執行,常見的幾個問題主要是:

  • seq2seq API 從 tf.nn 遷移到了 tf.contrib.legacy_seq2seq;
  • rnn 目前也大都使用 tf.contrib.rnn 下面的 RNNCell;
  • embedding_attention_seq2seq 函式中呼叫 deepcopy(cell) 這個函式經常會爆出(TypeError: can't pickle _thread.lock objects)的錯誤

關於上面第三個錯誤這裡多說幾句,因為確實困擾了我很久,基本上我在網上找到的每一份程式碼都會有這個錯(DeepQA 除外)。首先來講一種最簡單的方法是將 tf 版本換成 1.0.0,這樣問題就解決了。

然後說下不想改 tf 版本的辦法,我在網上找了很久,自己也嘗試著去找 bug 所在,錯誤定位在 embedding_attention_seq2seq 函式中呼叫 deepcopy 函式,於是就有人嘗試著把 deepcopy 改成 copy,或者乾脆不進行 copy 直接讓 encoder 和 decoder 使用相同引數的 RNNcell,但這明顯是不正確的做法。我先想出了一種解決方案就是將 embedding_attention_seq2seq 的傳入引數中的 cell 改成兩個,分別是 encoder_cell 和 decoder_cell,然後這兩個 cell 分別使用下面程式碼進行初始化:

encoCell = tf.contrib.rnn.MultiRNNCell([create_rnn_cell() for _ in range(num_layers)],)
   decoCell = tf.contrib.rnn.MultiRNNCell([create_rnn_cell() for _ in range(num_layers)],)

這樣做不需要呼叫 deepcopy 函式對 cell 進行復制了,問題也就解決了,但是在模型構建的時候速度會比較慢,我猜測是因為需要構造兩份 RNN 模型,但是最後訓練的時候發現速度也很慢,就先放棄了這種做法。

然後我又分析了一下程式碼,發現問題並不是單純的出現在 embedding_attention_seq2seq 這個函式,而是在呼叫 module_with_buckets 的時候會構建很多個不同 bucket 的 seq2seq 模型,這就導致了 embedding_attention_seq2seq 會被重複呼叫很多次,後來經過測試發現確實是這裡出現的問題,因為即便不使用 model_with_buckets 函式,我們自己為每個 bucket 構建模型時同樣也會報錯,但是如果只有一個 bucket 也就是隻呼叫一次 embedding_attention_seq2seq 函式時就不會報錯,其具體的內部原理我現在還沒有搞清楚,就看兩個最簡單的例子:

  import tensorflow as tf
   import copy

   cell = tf.contrib.rnn.BasicLSTMCell(10)
   cell1 = copy.deepcopy(cell)#這句程式碼不會報錯,可以正常執行

   a = tf.constant([1,2,3,4,5])
   b = copy.deepcopy(a)#這句程式碼會報錯,就是can't pickle _thread.lock objects。

可以理解為a已經有值了,而且是tf內部型別,導致執行時出錯???
還是不太理解tf內部執行機制,為什麼cell沒有執行緒鎖,但是a有呢==

所以先忽視原因,只看解決方案的話就是,不適用 buckets 構建模型,而是簡單的將所有序列都 padding 到統一長度,然後直接呼叫一次 embedding_attention_seq2seq 函式構建模型即可,這樣是不會抱錯的。(希望看到這的同學如果對這裡比較理解可以指點一二,或者互相探討一下)

最後我也是採用的這種方案,綜合了別人的程式碼實現了一個 embedding+attention+beam_search 等多種功能的 seq2seq 模型,訓練一個基礎版本的 chatbot 對話機器人,tf 的版本是 1.4。寫這份程式碼的目的一方面是為了讓自己對 tf 的 API 介面的使用方法更熟悉,另一方面是因為網上的一些程式碼都很繁雜,想 DeepQA 這種,裡面會有很多個檔案還實現了前端,然後各種封裝,顯得很複雜,不適合新手入門,所以就想寫一個跟 textcnn 相似風格的程式碼,只包含四個檔案,程式碼讀起來也比較友好。接下來就讓我們看一下具體的程式碼實現吧。最終的程式碼我會放在 github 上

資料處理

這裡我們借用 [DeepQA](https://github.com/Conchylicultor/DeepQA#chatbot) 裡面資料處理部分的程式碼,省去從原始本文檔案構造對話的過程直接使用其生成的 dataset-cornell-length10-filter1-vocabSize40000.pkl 檔案。有了該檔案之後資料處理的程式碼就精簡了很多,主要包括:

1. 讀取資料的函式 loadDataset()

2. 根據資料建立 batches 的函式 getBatches() 和 createBatch()

3. 預測時將使用者輸入的句子轉化成 batch 的函式 sentence2enco()

具體的程式碼含義在註釋中都有詳細的介紹,這裡就不贅述了,見下面的程式碼:

padToken, goToken, eosToken, unknownToken = 0, 1, 2, 3

   class Batch:
       #batch類,裡面包含了encoder輸入,decoder輸入,decoder標籤,decoder樣本長度mask
       def __init__(self):
           self.encoderSeqs = []
           self.decoderSeqs = []
           self.targetSeqs = []
           self.weights = []

   def loadDataset(filename):
       '''       讀取樣本資料       :param filename: 檔案路徑,是一個字典,包含word2id、id2word分別是單詞與索引對應的字典和反序字典,                       trainingSamples樣本資料,每一條都是QA對       :return: word2id, id2word, trainingSamples       '''
       dataset_path = os.path.join(filename)
       print('Loading dataset from {}'.format(dataset_path))
       with open(dataset_path, 'rb') as handle:
           data = pickle.load(handle)  # Warning: If adding something here, also modifying saveDataset
           word2id = data['word2id']
           id2word = data['id2word']
           trainingSamples = data['trainingSamples']
       return word2id, id2word, trainingSamples

   def createBatch(samples, en_de_seq_len):
       '''       根據給出的samples(就是一個batch的資料),進行padding並構造成placeholder所需要的資料形式       :param samples: 一個batch的樣本資料,列表,每個元素都是[question, answer]的形式,id       :param en_de_seq_len: 列表,第一個元素表示source端序列的最大長度,第二個元素表示target端序列的最大長度       :return: 處理完之後可以直接傳入feed_dict的資料格式       '''
       batch = Batch()
       #根據樣本長度獲得batch size大小
       batchSize = len(samples)
       #將每條資料的問題和答案分開傳入到相應的變數中
       for i in range(batchSize):
           sample = samples[i]
           batch.encoderSeqs.append(list(reversed(sample[0])))  # 將輸入反序,可提高模型效果
           batch.decoderSeqs.append([goToken] + sample[1] + [eosToken])  # Add the <go> and <eos> tokens
           batch.targetSeqs.append(batch.decoderSeqs[-1][1:])  # Same as decoder, but shifted to the left (ignore the <go>)
           # 將每個元素PAD到指定長度,並構造weights序列長度mask標誌
           batch.encoderSeqs[i] = [padToken] * (en_de_seq_len[0] - len(batch.encoderSeqs[i])) + batch.encoderSeqs[i]
           batch.weights.append([1.0] * len(batch.targetSeqs[i]) + [0.0] * (en_de_seq_len[1] - len(batch.targetSeqs[i])))
           batch.decoderSeqs[i] = batch.decoderSeqs[i] + [padToken] * (en_de_seq_len[1] - len(batch.decoderSeqs[i]))
           batch.targetSeqs[i] = batch.targetSeqs[i] + [padToken] * (en_de_seq_len[1] - len(batch.targetSeqs[i]))

       #--------------------接下來就是將資料進行reshape操作,變成序列長度*batch_size格式的資料------------------------
       encoderSeqsT = []  # Corrected orientation
       for i in range(en_de_seq_len[0]):
           encoderSeqT = []
           for j in range(batchSize):
               encoderSeqT.append(batch.encoderSeqs[j][i])
           encoderSeqsT.append(encoderSeqT)
       batch.encoderSeqs = encoderSeqsT

       decoderSeqsT = []
       targetSeqsT = []
       weightsT = []
       for i in range(en_de_seq_len[1]):
           decoderSeqT = []
           targetSeqT = []
           weightT = []
           for j in range(batchSize):
               decoderSeqT.append(batch.decoderSeqs[j][i])
               targetSeqT.append(batch.targetSeqs[j][i])
               weightT.append(batch.weights[j][i])
           decoderSeqsT.append(decoderSeqT)
           targetSeqsT.append(targetSeqT)
           weightsT.append(weightT)
       batch.decoderSeqs = decoderSeqsT
       batch.targetSeqs = targetSeqsT
       batch.weights = weightsT

       return batch

   def getBatches(data, batch_size, en_de_seq_len):
       '''       根據讀取出來的所有資料和batch_size將原始資料分成不同的小batch。對每個batch索引的樣本呼叫createBatch函式進行處理       :param data: loadDataset函式讀取之後的trainingSamples,就是QA對的列表       :param batch_size: batch大小       :param en_de_seq_len: 列表,第一個元素表示source端序列的最大長度,第二個元素表示target端序列的最大長度       :return: 列表,每個元素都是一個batch的樣本資料,可直接傳入feed_dict進行訓練       '''
       #每個epoch之前都要進行樣本的shuffle
       random.shuffle(data)
       batches = []
       data_len = len(data)
       def genNextSamples():
           for i in range(0, data_len, batch_size):
               yield data[i:min(i + batch_size, data_len)]

       for samples in genNextSamples():
           batch = createBatch(samples, en_de_seq_len)
           batches.append(batch)
       return batches

   def sentence2enco(sentence, word2id, en_de_seq_len):
       '''       測試的時候將使用者輸入的句子轉化為可以直接feed進模型的資料,現將句子轉化成id,然後呼叫createBatch處理       :param sentence: 使用者輸入的句子       :param word2id: 單詞與id之間的對應關係字典       :param en_de_seq_len: 列表,第一個元素表示source端序列的最大長度,第二個元素表示target端序列的最大長度       :return: 處理之後的資料,可直接feed進模型進行預測       '''
       if sentence == '':
           return None
       #分詞
       tokens = nltk.word_tokenize(sentence)
       if len(tokens) > en_de_seq_len[0]:
           return None
       #將每個單詞轉化為id
       wordIds = []
       for token in tokens:
           wordIds.append(word2id.get(token, unknownToken))
       #呼叫createBatch構造batch
       batch = createBatch([[wordIds, []]], en_de_seq_len)
       return batch

模型構建

有了資料之後看一下模型構建的程式碼,其實主體程式碼還是跟前面說到的 tf 官方指導文件差不多,主要分為以下幾個功能模組:

1. 一些變數的傳入和定義

2. OutputProjection 層和 sampled_softmax_loss 函式的定義

3. RNNCell 的定義和建立

4. 根據訓練或者測試呼叫相應的 embedding_attention_seq2seq 函式構建模型

5. step 函式定義,主要用於給定一個 batch 的資料,構造相應的 feed_dict 和 run_opt

程式碼如下所示:

import tensorflow as tf
   from my_seq2seq_chatbot.seq2seq import embedding_attention_seq2seq
   class Seq2SeqModel():

       def __init__(self, source_vocab_size, target_vocab_size, en_de_seq_len, hidden_size, num_layers,
                    batch_size, learning_rate, num_samples=1024,
                    forward_only=False, beam_search=True, beam_size=10):
           '''           初始化並建立模型           :param source_vocab_size:encoder輸入的vocab size           :param target_vocab_size: decoder輸入的vocab size,這裡跟上面一樣           :param en_de_seq_len: 源和目的序列最大長度           :param hidden_size: RNN模型的隱藏層單元個數           :param num_layers: RNN堆疊的層數           :param batch_size: batch大小           :param learning_rate: 學習率           :param num_samples: 計算loss時做sampled softmax時的取樣數           :param forward_only: 預測時指定為真           :param beam_search: 預測時是採用greedy search還是beam search           :param beam_size: beam search的大小           '''
           self.source_vocab_size = source_vocab_size
           self.target_vocab_size = target_vocab_size
           self.en_de_seq_len = en_de_seq_len
           self.hidden_size = hidden_size
           self.num_layers = num_layers
           self.batch_size = batch_size
           self.learning_rate = tf.Variable(float(learning_rate), trainable=False)
           self.num_samples = num_samples
           self.forward_only = forward_only
           self.beam_search = beam_search
           self.beam_size = beam_size
           self.global_step = tf.Variable(0, trainable=False)

           output_projection = None
           softmax_loss_function = None
           # 定義取樣loss函式,傳入後面的sequence_loss_by_example函式
           if num_samples > 0 and num_samples < self.target_vocab_size:
               w = tf.get_variable('proj_w', [hidden_size, self.target_vocab_size])
               w_t = tf.transpose(w)
               b = tf.get_variable('proj_b', [self.target_vocab_size])
               output_projection = (w, b)
               #呼叫sampled_softmax_loss函式計算sample loss,這樣可以節省計算時間
               def sample_loss(logits, labels):
                   labels = tf.reshape(labels, [-1, 1])
                   return tf.nn.sampled_softmax_loss(w_t, b, labels=labels, inputs=logits, num_sampled=num_samples, num_classes=self.target_vocab_size)
               softmax_loss_function = sample_loss

           self.keep_drop = tf.placeholder(tf.float32)
           # 定義encoder和decoder階段的多層dropout RNNCell
           def create_rnn_cell():
               encoDecoCell = tf.contrib.rnn.BasicLSTMCell(hidden_size)
               encoDecoCell = tf.contrib.rnn.DropoutWrapper(encoDecoCell, input_keep_prob=1.0, output_keep_prob=self.keep_drop)
               return encoDecoCell
           encoCell = tf.contrib.rnn.MultiRNNCell([create_rnn_cell() for _ in range(num_layers)])

           # 定義輸入的placeholder,採用了列表的形式
           self.encoder_inputs = []
           self.decoder_inputs = []
           self.decoder_targets = []
           self.target_weights = []
           for i in range(en_de_seq_len[0]):
               self.encoder_inputs.append(tf.placeholder(tf.int32, shape=[None, ], name="encoder{0}".format(i)))
           for i in range(en_de_seq_len[1]):
               self.decoder_inputs.append(tf.placeholder(tf.int32, shape=[None, ], name="decoder{0}".format(i)))
               self.decoder_targets.append(tf.placeholder(tf.int32, shape=[None, ], name="target{0}".format(i)))
               self.target_weights.append(tf.placeholder(tf.float32, shape=[None, ], name="weight{0}".format(i)))

           # test模式,將上一時刻輸出當做下一時刻輸入傳入
           if forward_only:
               if beam_search:#如果是beam_search的話,則呼叫自己寫的embedding_attention_seq2seq函式,而不是legacy_seq2seq下面的
                   self.beam_outputs, _, self.beam_path, self.beam_symbol = embedding_attention_seq2seq(
                       self.encoder_inputs, self.decoder_inputs, encoCell, num_encoder_symbols=source_vocab_size,
                       num_decoder_symbols=target_vocab_size, embedding_size=hidden_size,
                       output_projection=output_projection, feed_previous=True)
               else:
                   decoder_outputs, _ = tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
                       self.encoder_inputs, self.decoder_inputs, encoCell, num_encoder_symbols=source_vocab_size,
                       num_decoder_symbols=target_vocab_size, embedding_size=hidden_size,
                       output_projection=output_projection, feed_previous=True)
                   # 因為seq2seq模型中未指定output_projection,所以需要在輸出之後自己進行output_projection
                   if output_projection is not None:
                       self.outputs = tf.matmul(decoder_outputs, output_projection[0]) + output_projection[1]
           else:
               # 因為不需要將output作為下一時刻的輸入,所以不用output_projection
               decoder_outputs, _ = tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
                   self.encoder_inputs, self.decoder_inputs, encoCell, num_encoder_symbols=source_vocab_size,
                   num_decoder_symbols=target_vocab_size, embedding_size=hidden_size, output_projection=output_projection,
                   feed_previous=False)
               self.loss = tf.contrib.legacy_seq2seq.sequence_loss(
                   decoder_outputs, self.decoder_targets, self.target_weights, softmax_loss_function=softmax_loss_function)

               # Initialize the optimizer
               opt = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999, epsilon=1e-08)
               self.optOp = opt.minimize(self.loss)
               self.saver = tf.train.Saver(tf.all_variables())

       def step(self, session, encoder_inputs, decoder_inputs, decoder_targets, target_weights, go_token_id):
           #傳入一個batch的資料,並訓練性對應的模型
           # 構建sess.run時的feed_inpits
           feed_dict = {}
           if not self.forward_only:
               feed_dict[self.keep_drop] = 0.5
               for i in range(self.en_de_seq_len[0]):
                   feed_dict[self.encoder_inputs[i].name] = encoder_inputs[i]
               for i in range(self.en_de_seq_len[1]):
                   feed_dict[self.decoder_inputs[i].name] = decoder_inputs[i]
                   feed_dict[self.decoder_targets[i].name] = decoder_targets[i]
                   feed_dict[self.target_weights[i].name] = target_weights[i]
               run_ops = [self.optOp, self.loss]
           else:
               feed_dict[self.keep_drop] = 1.0
               for i in range(self.en_de_seq_len[0]):
                   feed_dict[self.encoder_inputs[i].name] = encoder_inputs[i]
               feed_dict[self.decoder_inputs[0].name] = [go_token_id]
               if self.beam_search:
                   run_ops = [self.beam_path, self.beam_symbol]
               else:
                   run_ops = [self.outputs]

           outputs = session.run(run_ops, feed_dict)
           if not self.forward_only:
               return None, outputs[1]
           else:
               if self.beam_search:
                   return outputs[0], outputs[1]

接下來我們主要說一下我做的主要工作,就是 beam_search 這部分,其原理想必大家看過前面的文章應該已經很清楚了,那麼如何程式設計實現呢,首先我們要考慮的是在哪裡進行 beam search,因為 beam search 是在預測時需要用到,代替 greedy 的一種搜尋策略,所以第一種方案是在 tf 之外,用 python 實現,這樣做的缺點是 decode 速度會很慢。第二種方案是在 tf 內模型構建時進行,這樣做的好處是速度快但是比較麻煩。

在網上找了很久在 tensorflow 的一個 issue(http://t.cn/R8M6mDo ) 裡面發現了一個方案,他的思路是修改 loop_function 函式,也就是之前根據上一時刻輸出得到下一時刻輸入的函式,在 loop function 裡面實現 top_k 取出概率最大的幾個序列,並把相應的路徑和單詞對應關係儲存下來。但是存在一個問題就是一開始 decode 的時候傳入的是一句話,也就是 batch_size 為 1,但是經過 loop_function 之後返回的是 beam_size 句話,但是再將其傳入 RNNCell 的時候就會報錯,如何解決這個問題呢,想了很久決定直接從 decode 開始的時候就把輸入擴充套件為 beam_size 個,把 encoder 階段的輸出和 attention 向量都變成 beam_size 維的 tensor,就說把 decoder 階段的 RNN 輸入的 batch_size 當做為 beam_size。

但是這樣做仍然會出現一個問題,就是你會發現最後的輸出全部都相同,原因就在於 decoder 開始的時候樣本是 beam_szie 個完全相同的輸入,所以經過 loop_function 得到的 beam_size 個最大序列也是完全相同的,為了解決這個問題我們需要在第一次編碼的時候不取整體最大的前 beam_size 個序列,而是取第一個元素編碼結果的前 beam_size 個值作為結果。這部分程式碼比較多就只貼出來 loop_function 的函式,有興趣的同學可以去看我 github 上面的程式碼,就在 seq2seq 檔案中。

def loop_function(prev, i, log_beam_probs, beam_path, beam_symbols):
       if output_projection is not None:
           prev = nn_ops.xw_plus_b(prev, output_projection[0], output_projection[1])
       # 對輸出概率進行歸一化和取log,這樣序列概率相乘就可以變成概率相加
       probs = tf.log(tf.nn.softmax(prev))
       if i == 1:
           probs = tf.reshape(probs[0, :], [-1, num_symbols])
       if i > 1:
           # 將當前序列的概率與之前序列概率相加得到結果之前有beam_szie個序列,本次產生num_symbols個結果,
           # 所以reshape成這樣的tensor
           probs = tf.reshape(probs + log_beam_probs[-1], [-1, beam_size * num_symbols])
       # 選出概率最大的前beam_size個序列,從beam_size * num_symbols個元素中選出beam_size個
       best_probs, indices = tf.nn.top_k(probs, beam_size)
       indices = tf.stop_gradient(tf.squeeze(tf.reshape(indices, [-1, 1])))
       best_probs = tf.stop_gradient(tf.reshape(best_probs, [-1, 1]))

       # beam_size * num_symbols,看對應的是哪個序列和單詞
       symbols = indices % num_symbols  # Which word in vocabulary.
       beam_parent = indices // num_symbols  # Which hypothesis it came from.
       beam_symbols.append(symbols)
       beam_path.append(beam_parent)
       log_beam_probs.append(best_probs)

       # 對beam-search選出的beam size個單詞進行embedding,得到相應的詞向量
       emb_prev = embedding_ops.embedding_lookup(embedding, symbols)
       emb_prev = tf.reshape(emb_prev, [-1, embedding_size])
       return emb_prev

模型訓練

其實模型訓練部分的程式碼很簡單,就是每個 epoch 都對樣本進行 shuffle 然後分 batches,接下來將每個 batch 的資料分別傳入 model.step() 進行模型的訓練,這裡比較好的一點是,DeepQA 用的是 embedding_rnn_seq2seq 函式,訓練過程中 loss 經過 30 個人 epoch 大概可以降到 3 點多,但是我這裡改成了 embedding_attention_seq2seq 函式,最後 loss 可以降到 2.0 以下,可以說效果還是很顯著的,而且模型的訓練速度並沒有降低,仍然是 20 個小時左右就可以完成訓練。

for e in range(FLAGS.numEpochs):
       print("----- Epoch {}/{} -----".format(e + 1, FLAGS.numEpochs))
       batches = getBatches(trainingSamples, FLAGS.batch_size, model.en_de_seq_len)
       for nextBatch in tqdm(batches, desc="Training"):
           _, step_loss = model.step(sess, nextBatch.encoderSeqs, nextBatch.decoderSeqs, nextBatch.targetSeqs,
                                     nextBatch.weights, goToken)
           current_step += 1
           if current_step % FLAGS.steps_per_checkpoint == 0:
               perplexity = math.exp(float(step_loss)) if step_loss < 300 else float('inf')
               tqdm.write("----- Step %d -- Loss %.2f -- Perplexity %.2f" % (current_step, step_loss, perplexity))
               checkpoint_path = os.path.join(FLAGS.train_dir, "chat_bot.ckpt")
               model.saver.save(sess, checkpoint_path, global_step=model.global_step)

貼上兩張圖看一下訓練的效果,這裡用的是 deepQA 的截圖,因為我的程式碼訓練的時候忘了加 tensorboard 的東西:

模型預測

預測好模型之後,接下來需要做的就是對模型效果進行測試,這裡也比較簡單,主要是如何根據 beam_search 都所處的結果找到對應的句子進行輸出。程式碼如下所示:

      if beam_search:
           sys.stdout.write("> ")
           sys.stdout.flush()
           sentence = sys.stdin.readline()
           while sentence:
               #將使用者輸入的句子轉化為id並處理成feed_dict的格式
               batch = sentence2enco(sentence, word2id, model.en_de_seq_len)
               beam_path, beam_symbol = model.step(sess, batch.encoderSeqs, batch.decoderSeqs, batch.targetSeqs,
                                                   batch.weights, goToken)
               paths = [[] for _ in range(beam_size)]
               curr = [i for i in range(beam_size)]
               num_steps = len(beam_path)
               #根據beam_path和beam_symbol得到真正的輸出語句,存在paths中
               for i in range(num_steps-1, -1, -1):
                   for kk in range(beam_size):
                       paths[kk].append(beam_symbol[i][curr[kk]])
                       curr[kk] = beam_path[i][curr[kk]]
               recos = set()
               print("Replies --------------------------------------->")
               #轉換成句子並輸出
               for kk in range(beam_size):
                   foutputs = [int(logit) for logit in paths[kk][::-1]]
                   if eosToken in foutputs:
                       foutputs = foutputs[:foutputs.index(eosToken)]
                   rec = " ".join([tf.compat.as_str(id2word[output]) for output in foutputs if output in id2word])
                   if rec not in recos:
                       recos.add(rec)
                       print(rec)
               print("> ", "")
               sys.stdout.flush()
               sentence = sys.stdin.readline()

接下來我們看一下幾個例子,這裡 beam_size=5,並去掉了一些重複的回答: