如何用TensorFlow训练聊天机器人(附github)
前言
實際工程中很少有直接用深度學習實現端對端的聊天機器人,但這里我們來看看怎么用深度學習的seq2seq模型來實現一個簡易的聊天機器人。這篇文章將嘗試使用TensorFlow來訓練一個基于seq2seq的聊天機器人,實現根據語料庫的訓練讓機器人回答問題。
seq2seq
關于seq2seq的機制原理可看之前的文章《深度學習的seq2seq模型》。
循環神經網絡
在seq2seq模型中會使用到循環神經網絡,目前流行的幾種循環神經網絡包括RNN、LSTM和GRU。這三種循環神經網絡的機制原理可看之前的文章《循環神經網絡》 《LSTM神經網絡》 《GRU神經網絡》。
訓練樣本集
主要是一些QA對,開放數據也很多可以下載,這里只是隨便選用一小部分問題和回答,存放的格式是第一行為問題,第二行為回答,第三行又是問題,第四行為回答,以此類推。
數據預處理
要訓練就肯定要將數據轉成數字,可以用0到n的值來表示整個詞匯,每個值表示一個單詞,這里用VOCAB_SIZE來定義。還有問題的最大最小長度,回答的最大最小長度。除此之外還要定義UNK、GO、EOS和PAD符號,分別表示未知單詞,比如你超過 VOCAB_SIZE范圍的則認為未知單詞,GO表示decoder開始的符號,EOS表示回答結束的符號,而PAD用于填充,因為所有QA對放到同個seq2seq模型中輸入和輸出都必須是相同的,于是就需要將較短長度的問題或回答用PAD進行填充。
limit = {'maxq': 10,'minq': 0,'maxa': 8,'mina': 3 }UNK = 'unk' GO = '<go>' EOS = '<eos>' PAD = '<pad>' VOCAB_SIZE = 1000復制代碼按照QA長度的限制進行篩選。
def filter_data(sequences):filtered_q, filtered_a = [], []raw_data_len = len(sequences) // 2for i in range(0, len(sequences), 2):qlen, alen = len(sequences[i].split(' ')), len(sequences[i + 1].split(' '))if qlen >= limit['minq'] and qlen <= limit['maxq']:if alen >= limit['mina'] and alen <= limit['maxa']:filtered_q.append(sequences[i])filtered_a.append(sequences[i + 1])filt_data_len = len(filtered_q)filtered = int((raw_data_len - filt_data_len) * 100 / raw_data_len)print(str(filtered) + '% filtered from original data')return filtered_q, filtered_a復制代碼我們還要得到整個語料庫所有單詞的頻率統計,還要根據頻率大小統計出排名前n個頻率的單詞作為整個詞匯,也就是前面對應的VOCAB_SIZE。另外我們還需要根據索引值得到單詞的索引,還有根據單詞得到對應索引值的索引。
def index_(tokenized_sentences, vocab_size):freq_dist = nltk.FreqDist(itertools.chain(*tokenized_sentences))vocab = freq_dist.most_common(vocab_size)index2word = [GO] + [EOS] + [UNK] + [PAD] + [x[0] for x in vocab]word2index = dict([(w, i) for i, w in enumerate(index2word)])return index2word, word2index, freq_dist復制代碼前面也說到在我們的seq2seq模型中,對于encoder來說,問題的長短是不同的,那么不夠長的要用PAD進行填充,比如問題為"how are you",假如長度定為10,則需要將其填充為"how are you pad pad pad pad pad pad pad"。對于decoder來說,要以GO開始,以EOS結尾,不夠長還得填充,比如"fine thank you",則要處理成"go fine thank you eos pad pad pad pad pad "。第三個要處理的則是我們的target,target其實和decoder的輸入是相同的,只不過它剛好有一個位置的偏移,比如上面要去掉go,變成"fine thank you eos pad pad pad pad pad pad"。
def zero_pad(qtokenized, atokenized, w2idx):data_len = len(qtokenized)# +2 dues to '<go>' and '<eos>'idx_q = np.zeros([data_len, limit['maxq']], dtype=np.int32)idx_a = np.zeros([data_len, limit['maxa'] + 2], dtype=np.int32)idx_o = np.zeros([data_len, limit['maxa'] + 2], dtype=np.int32)for i in range(data_len):q_indices = pad_seq(qtokenized[i], w2idx, limit['maxq'], 1)a_indices = pad_seq(atokenized[i], w2idx, limit['maxa'], 2)o_indices = pad_seq(atokenized[i], w2idx, limit['maxa'], 3)idx_q[i] = np.array(q_indices)idx_a[i] = np.array(a_indices)idx_o[i] = np.array(o_indices)return idx_q, idx_a, idx_odef pad_seq(seq, lookup, maxlen, flag):if flag == 1:indices = []elif flag == 2:indices = [lookup[GO]]elif flag == 3:indices = []for word in seq:if word in lookup:indices.append(lookup[word])else:indices.append(lookup[UNK])if flag == 1:return indices + [lookup[PAD]] * (maxlen - len(seq))elif flag == 2:return indices + [lookup[EOS]] + [lookup[PAD]] * (maxlen - len(seq))elif flag == 3:return indices + [lookup[EOS]] + [lookup[PAD]] * (maxlen - len(seq) + 1)復制代碼然后將上面處理后的結構都持久化起來,供訓練時使用。
構建圖
encoder_inputs = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length]) decoder_inputs = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length]) targets = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length]) weights = tf.placeholder(dtype=tf.float32, shape=[batch_size, sequence_length])復制代碼創建四個占位符,分別為encoder的輸入占位符、decoder的輸入占位符和decoder的target占位符,還有權重占位符。其中batch_size是輸入樣本一批的數量,sequence_length為我們定義的序列的長度。
cell = tf.nn.rnn_cell.BasicLSTMCell(hidden_size) cell = tf.nn.rnn_cell.MultiRNNCell([cell] * num_layers)復制代碼創建循環神經網絡結構,這里使用LSTM結構,hidden_size是隱含層數量,用MultiRNNCell是因為我們希望創建一個更復雜的網絡,num_layers為LSTM的層數。
results, states = tf.contrib.legacy_seq2seq.embedding_rnn_seq2seq(tf.unstack(encoder_inputs, axis=1),tf.unstack(decoder_inputs, axis=1),cell,num_encoder_symbols,num_decoder_symbols,embedding_size,feed_previous=False )復制代碼使用TensorFlow為我們準備好了的embedding_rnn_seq2seq函數搭建seq2seq結構,當然我們也可以自己從LSTM搭起,分別創建encoder和decoder,但為了方便直接使用embedding_rnn_seq2seq即可。使用tf.unstack函數是為了將encoder_inputs和decoder_inputs展開成一個列表,num_encoder_symbols和num_decoder_symbols對應到我們的詞匯數量。embedding_size則是我們的嵌入層的數量,feed_previous這個變量很重要,設為False表示這是訓練階段,訓練階段會使用decoder_inputs作為decoder的其中一個輸入,但feed_previous為True時則表示預測階段,而預測階段沒有decoder_inputs,所以只能依靠decoder上一時刻輸出作為當前時刻的輸入。
logits = tf.stack(results, axis=1) loss = tf.contrib.seq2seq.sequence_loss(logits, targets=targets, weights=weights) pred = tf.argmax(logits, axis=2) train_op = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)復制代碼接著使用sequence_loss來創建損失,這里根據embedding_rnn_seq2seq的輸出來計算損失,同時該輸出也可以用來做預測,最大的值對應的索引即為詞匯的單詞,優化器使用的事AdamOptimizer。
創建會話
with tf.Session() as sess:ckpt = tf.train.get_checkpoint_state(model_dir)if ckpt and ckpt.model_checkpoint_path:saver.restore(sess, ckpt.model_checkpoint_path)else:sess.run(tf.global_variables_initializer())epoch = 0while epoch < 5000000:epoch = epoch + 1print("epoch:", epoch)for step in range(0, 1):print("step:", step)train_x, train_y, train_target = loadQA()train_encoder_inputs = train_x[step * batch_size:step * batch_size + batch_size, :]train_decoder_inputs = train_y[step * batch_size:step * batch_size + batch_size, :]train_targets = train_target[step * batch_size:step * batch_size + batch_size, :]op = sess.run(train_op, feed_dict={encoder_inputs: train_encoder_inputs, targets: train_targets,weights: train_weights, decoder_inputs: train_decoder_inputs})cost = sess.run(loss, feed_dict={encoder_inputs: train_encoder_inputs, targets: train_targets,weights: train_weights, decoder_inputs: train_decoder_inputs})print(cost)step = step + 1if epoch % 100 == 0:saver.save(sess, model_dir + '/model.ckpt', global_step=epoch + 1)復制代碼創建會話開始執行,這里會用到tf.train.Saver對象來保存和讀取模型,保險起見可以每隔一定間隔保存一次模型,下次重啟會接著訓練而不用從頭重新來過,這里因為是一個例子,QA對數量不多,所以直接一次性當成一批送進去訓練,而并沒有分成多批。
預測
with tf.device('/cpu:0'):batch_size = 1sequence_length = 10num_encoder_symbols = 1004num_decoder_symbols = 1004embedding_size = 256hidden_size = 256num_layers = 2encoder_inputs = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length])decoder_inputs = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length])targets = tf.placeholder(dtype=tf.int32, shape=[batch_size, sequence_length])weights = tf.placeholder(dtype=tf.float32, shape=[batch_size, sequence_length])cell = tf.nn.rnn_cell.BasicLSTMCell(hidden_size)cell = tf.nn.rnn_cell.MultiRNNCell([cell] * num_layers)results, states = tf.contrib.legacy_seq2seq.embedding_rnn_seq2seq(tf.unstack(encoder_inputs, axis=1),tf.unstack(decoder_inputs, axis=1),cell,num_encoder_symbols,num_decoder_symbols,embedding_size,feed_previous=True,)logits = tf.stack(results, axis=1)pred = tf.argmax(logits, axis=2)saver = tf.train.Saver()with tf.Session() as sess:module_file = tf.train.latest_checkpoint('./model/')saver.restore(sess, module_file)map = Word_Id_Map()encoder_input = map.sentence2ids(['you', 'want', 'to', 'turn', 'twitter', 'followers', 'into', 'blog', 'readers'])encoder_input = encoder_input + [3 for i in range(0, 10 - len(encoder_input))]encoder_input = np.asarray([np.asarray(encoder_input)])decoder_input = np.zeros([1, 10])print('encoder_input : ', encoder_input)print('decoder_input : ', decoder_input)pred_value = sess.run(pred, feed_dict={encoder_inputs: encoder_input, decoder_inputs: decoder_input})print(pred_value)sentence = map.ids2sentence(pred_value[0])print(sentence)復制代碼預測階段也同樣要創建相同的模型,然后將訓練時保存的模型加載進來,然后實現對問題的回答的預測。預測階段我們用cpu來執行就行了,避免使用GPU。創建圖的步驟和訓練時基本一致,參數也要保持一致,不同的地方在于我們要將embedding_rnn_seq2seq函數的feed_previous參數設為True,因為我們已經沒有decoder輸入了。另外我們也不需要損失函數和優化器,僅僅提供預測函數即可。
創建會話后開始執行,先加載model目錄下的模型,然后再將待測試的問題轉成向量形式,接著進行預測,得到輸出如下:
['how', 'do', 'you', 'do', 'this', '', '', '', '', '']。
github
github.com/sea-boat/se…
?
作者:超人汪小建
鏈接:https://juejin.im/post/59cc4a9a6fb9a00a69750eeb
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的如何用TensorFlow训练聊天机器人(附github)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 时间序列分类算法简介及其在能耗数据分类上
- 下一篇: 2018汇总数据结构算法篇