深度學習對話系統實戰篇 -- 簡單 chatbot 程式碼實現
本文的程式碼都可以到我的 github 中下載:https://github.com/lc222/seq2seq_chatbot 前面幾篇文章我們已經介紹了 seq2seq 模型的理論知識,並且從 tensorflow 原始碼層面解析了其實現原理,本篇文章我們會聚焦於如何呼叫 tf 提供的 seq2seq 的 API,實現一個簡單的 chatbot 對話系統。這裡先給出幾個參考的部落格和程式碼:
- tensorflow 官網 API 指導(http://t.cn/R8MiZcR )
- Chatbots with Seq2Seq Learn to build a chatbot using TensorFlow(http://t.cn/R8MiykP )
- DeepQA(http://t.cn/R8MiVld )
- 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,並去掉了一些重複的回答: