實際工程中不多有直接用深度學習實現端對端的聊天機器人,但這裏咱們來看看怎麼用深度學習的seq2seq模型來實現一個簡易的聊天機器人。這篇文章將嘗試使用TensorFlow來訓練一個基於seq2seq的聊天機器人,實現根據語料庫的訓練讓機器人回答問題。html
關於seq2seq的機制原理可看以前的文章《深度學習的seq2seq模型》。git
在seq2seq模型中會使用到循環神經網絡,目前流行的幾種循環神經網絡包括RNN、LSTM和GRU。這三種循環神經網絡的機制原理可看以前的文章《循環神經網絡》 《LSTM神經網絡》 《GRU神經網絡》。github
主要是一些QA對,開放數據也不少能夠下載,這裏只是隨便選用一小部分問題和回答,存放的格式是第一行爲問題,第二行爲回答,第三行又是問題,第四行爲回答,以此類推。bash
要訓練就確定要將數據轉成數字,能夠用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長度的限制進行篩選。app
def filter_data(sequences):
filtered_q, filtered_a = [], []
raw_data_len = len(sequences) // 2
for 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_o
def 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 = 0
while epoch < 5000000:
epoch = epoch + 1
print("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 + 1
if 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 = 1
sequence_length = 10
num_encoder_symbols = 1004
num_decoder_symbols = 1004
embedding_size = 256
hidden_size = 256
num_layers = 2
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])
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', '', '', '', '', '']。
如下是廣告和相關閱讀
========廣告時間========
鄙人的新書《Tomcat內核設計剖析》已經在京東銷售了,有須要的朋友能夠到 item.jd.com/12185360.ht… 進行預約。感謝各位朋友。
=========================
相關閱讀:
歡迎關注: