如何用TensorFlow訓練聊天機器人(附github)

前言

原文:https://blog.csdn.net/wangyangzhizhou/article/details/78119339

實際工程中很少有直接用深度學習實現端對端的聊天機器人,但這里我們來看看怎么用深度學習的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 =''EOS =''PAD =''VOCAB_SIZE =1000

1

2

3

4

5

6

7

8

9

10

11

12

按照QA長度的限制進行篩選。

deffilter_data(sequences):filtered_q, filtered_a = [], []? ? raw_data_len = len(sequences) //2foriinrange(0, len(sequences),2):? ? ? ? qlen, alen = len(sequences[i].split(' ')), len(sequences[i +1].split(' '))ifqlen >= limit['minq']andqlen <= limit['maxq']:ifalen >= limit['mina']andalen <= 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')returnfiltered_q, filtered_a

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

我們還要得到整個語料庫所有單詞的頻率統計,還要根據頻率大小統計出排名前n個頻率的單詞作為整個詞匯,也就是前面對應的VOCAB_SIZE。另外我們還需要根據索引值得到單詞的索引,還有根據單詞得到對應索引值的索引。

defindex_(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]forxinvocab]? ? word2index = dict([(w, i)fori, winenumerate(index2word)])returnindex2word, word2index, freq_dist

1

2

3

4

5

6

前面也說到在我們的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”。

defzero_pad(qtokenized, atokenized, w2idx):data_len = len(qtokenized)# +2 dues to '' and ''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)foriinrange(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)returnidx_q, idx_a, idx_odefpad_seq(seq, lookup, maxlen, flag):ifflag ==1:? ? ? ? indices = []elifflag ==2:? ? ? ? indices = [lookup[GO]]elifflag ==3:? ? ? ? indices = []forwordinseq:ifwordinlookup:? ? ? ? ? ? indices.append(lookup[word])else:? ? ? ? ? ? indices.append(lookup[UNK])ifflag ==1:returnindices + [lookup[PAD]] * (maxlen - len(seq))elifflag ==2:returnindices + [lookup[EOS]] + [lookup[PAD]] * (maxlen - len(seq))elifflag ==3:returnindices + [lookup[EOS]] + [lookup[PAD]] * (maxlen - len(seq) +1)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

然后將上面處理后的結構都持久化起來,供訓練時使用。

構建圖

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])

1

2

3

4

創建四個占位符,分別為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)

1

2

創建循環神經網絡結構,這里使用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)

1

2

3

4

5

6

7

8

9

使用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)

1

2

3

4

接著使用sequence_loss來創建損失,這里根據embedding_rnn_seq2seq的輸出來計算損失,同時該輸出也可以用來做預測,最大的值對應的索引即為詞匯的單詞,優化器使用的事AdamOptimizer。

創建會話

withtf.Session() as sess:? ? ckpt = tf.train.get_checkpoint_state(model_dir)ifckptandckpt.model_checkpoint_path:? ? ? ? saver.restore(sess, ckpt.model_checkpoint_path)else:? ? ? ? sess.run(tf.global_variables_initializer())? ? epoch =0whileepoch <5000000:? ? ? ? epoch = epoch +1print("epoch:", epoch)forstepinrange(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+1ifepoch %100==0:? ? ? ? ? ? saver.save(sess, model_dir +'/model.ckpt', global_step=epoch + 1)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

創建會話開始執行,這里會用到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 +[3fori inrange(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)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

預測階段也同樣要創建相同的模型,然后將訓練時保存的模型加載進來,然后實現對問題的回答的預測。預測階段我們用cpu來執行就行了,避免使用GPU。創建圖的步驟和訓練時基本一致,參數也要保持一致,不同的地方在于我們要將embedding_rnn_seq2seq函數的feed_previous參數設為True,因為我們已經沒有decoder輸入了。另外我們也不需要損失函數和優化器,僅僅提供預測函數即可。

創建會話后開始執行,先加載model目錄下的模型,然后再將待測試的問題轉成向量形式,接著進行預測,得到輸出如下:?

[‘how’, ‘do’, ‘you’, ‘do’, ‘this’, ‘’, ‘’, ‘’, ‘’, ‘’]。

github

https://github.com/sea-boat/seq2seq_chatbot.git

以下是廣告相關閱讀

========廣告時間========

公眾號的菜單已分為“分布式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java并發核心”、“JDK源碼”、“Tomcat內核”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat內核設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什么寫《Tomcat內核設計剖析》

=========================

相關閱讀:

《LSTM神經網絡》

《循環神經網絡》

《深度學習的seq2seq模型》

《機器學習之神經網絡》

《GRU神經網絡》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容