騰訊DeepOcean原創文章:dopro.io/nlp_seq2seq…
智慧機器人在生活中隨處可見:iPhone裡會說話的siri、會下棋的阿法狗、調皮可愛的微軟小冰……她們都具有一定的智慧,能夠和人類進行互動。這些智慧機器人非常神奇,看上去離我們也十分遙遠,但其實只要我們動動手,便可以造一個屬於自己的智慧機器人。
本文將教你從零開始造出一個智障,不對是“智慧聊天機器人"。
要造一個聊天機器人,首先你需要了解一些相關概念——自然語言處理(NLP),它是一門融語言學、電腦科學、數學於一體的科學,研究讓電腦“懂”人類語言的方法。當然,它也包含很多分支:文字朗讀、語音識別、句法分析、自然語言生成、人機對話、資訊檢索、資訊抽取、文字校對、文字分類、自動文摘、機器翻譯、文字蘊含等等等。
看到這裡的朋友,千萬別被這些嚇跑。既然本文叫《從零開始造一個“智障”聊天機器人》那麼各位看官老爺不懂這些也沒有關係!跟著我的腳步一步一步做吧。
0x1 基本概念
這裡涉及到的原理基礎,沒興趣的看官老爺略過即可,不影響後續程式碼實現。
01|神經網路
人工智慧的底層是”神經網路“,許多複雜的應用(比如模式識別、自動控制)和高階模型(比如深度學習)都基於它。學習人工智慧,一定是從它開始。
那麼問題來了,什麼是神經網路呢?簡單來說,神經網路就是模擬人腦神經元網路,從而讓計算機懂得”思考“。具體概念在這裡不再贅述,網路上有很多簡單易懂的解釋。
本文使用的的是迴圈神經網路(RNN),我們來看一個最簡單的基本迴圈神經網路:
雖然影象看起來很抽象,但是實際很好理解。x、o、s是一個向量,x代表輸入層的值,o代表輸出層的值,s是隱藏層的值(這裡其實有很多節點);U、V是權重矩陣,U代表輸入層到隱藏層的權重矩陣,而V則代表隱藏層到輸出層的權重矩陣。那麼W是什麼呢?其實迴圈神經網路的隱藏層的值s不僅僅由x、U決定,還會由上一次隱藏層的值s,而W就是上一次到隱藏層到這一次的權重矩陣,將其展開就是這樣:
這樣邏輯就清晰很多了,這便是一個簡單的迴圈神經網路。而我們的智障,不對是“智慧聊天機器人"便是使用迴圈神經網路,基於自然語言的詞法分析、句法分析不斷的訓練語料,並把語義分析都融入進來做的補充和改進。
02|深度學習框架
適合RNN的深度學習框架有很多,本文的聊天機器人基於Google開源的Tensorflow,從GayhubGithub的starts數便可以看出,Tensorflow是一個極其火爆的深度學習框架,並且可以輕鬆地在cpu / gpu 上進行分散式計算,下面羅列了一些目前主流深度學習框架的特性,大家可以憑興趣選擇框架進行研究:
03|seq2seq模型
顧名思義,seq2seq 模型就像一個翻譯模型,輸入是一個序列(比如一個英文句子),輸出也是一個序列(比如該英文句子所對應的法文翻譯)。這種結構最重要的地方在於輸入序列和輸出序列的長度是可變的。
舉個例子:
在對話機器中:輸入(hello) -> 輸出 (你好)。
輸入是1個英文單詞,輸出為2個漢字。我們提(輸入)一個問題,機器會自動生成(輸出)回答。這裡的輸入和輸出顯然是長度沒有確定的序列(sequences)
我們再舉一個長一點的例子:
我教小黃雞說“大白天的做什麼美夢啊?”回答是“哦哈哈哈不用你管”。
Step1:應用雙向最大匹配演算法分詞:雙向分詞結果,正向《大白天,的,做什麼,美夢,啊》;反向《大白天,的,做什麼,美夢,啊》。正向反向都是一樣的,所以不需要處理歧義問題。長詞優先選擇,“大白天”和“做什麼”。
Step2:以“大白天”舉例,假設hash函式為f(),並設f(大白天)指向首字hash表項[大,11,P]。於是由該表項指向“3字索引”,再指向對應“詞表”。
Step3:將結構體<大白天,…>插入隊尾。體中有一個Ans域,域中某一指標指向“哦哈哈哈不用你管”。
這便是seq2seq的基本原理,原理和技術我們都有了,下一步就是將它實現出來!
0x2 語料準備
瞭解完一些前置基礎,我們話不多說,直接進入造智慧聊天機器人的階段。首先我們需要準備相關訓練的語料。
01|語料整理
本次訓練的語料庫是從Github上下載的(Github用於對話系統的中英文語料:https://github.com/candlewill/Dialog_Corpus)。我們下載其中的xiaohuangji50w_fenciA.conv(小黃雞語料)進行我們的訓練。
當我們下載完後開啟發現,它這個語料庫是這樣的:
雖然這裡面的文字、對話我們都能看懂,但是這些E、M、/都是些什麼鬼?其實從圖來看很容易理解,M即代表這句話,而E則代表一段對話的開始與結束。
我們拿到這些語料後,用程式碼將其按照問/答分為兩類"Question.txt"、"Answer.txt":
1import re
2import sys
3def prepare(num_dialogs=50000):
4 with open("xhj.conv") as fopen:
5 # 替換E、M等
6 reg = re.compile("EnM (.*?)nM (.*?)n")
7 match_dialogs = re.findall(reg, fopen.read())
8 # 使用5W條對話作為訓練語料
9 if num_dialogs >= len(match_dialogs):
10 dialogs = match_dialogs
11 else:
12 dialogs = match_dialogs[:num_dialogs]
13 questions = []
14 answers = []
15 for que, ans in dialogs:
16 questions.append(que)
17 answers.append(ans)
18 # 儲存到data/資料夾目錄下
19 save(questions, "data/Question.txt")
20 save(answers, "data/Answer.txt")
21def save(dialogs, file):
22 with open(file, "w") as fopen:
23 fopen.write("n".join(dialogs))
最終我們得到5W條問題與回答資料:
02|向量表對映建立
到這裡,大家可能會問,那麼這個"智慧"聊天機器人是不是就是將我們輸入的問題匹配Question.txt裡面的問題,然後再從Answer.txt找到相應回答進行輸出?
當然不會是這麼簡單,本質上聊天機器人是基於問句的上下文環境產生一個新的回答,而非是從資料庫中拿出一條對應好的回答資料。
那麼機器怎麼知道該回答什麼呢?此處借用一下谷歌的seq2seq原理圖:
簡單來說就是:我們輸入的每一句話,都會被機器拆成詞並向量化;這些詞作為輸入層的向量,與權重矩陣進行計算後到隱藏層,隱藏層輸出的向量再與權重矩陣進行計算,得到最終向量。我們再將此向量對映到詞向量庫時,便可得到我們想要的結果。
在程式碼上實現比較簡單,因為複雜底層邏輯的都由Tensorflow幫我們完成了,我們將詞彙表進行最終的梳理:
1def gen_vocabulary_file(input_file, output_file):
2 vocabulary = {}
3 with open(input_file) as f:
4 counter = 0
5 for line in f:
6 counter += 1
7 tokens = [word for word in line.strip()]
8 for word in tokens:
9 # 過濾非中文 文字
10 if u'u4e00' <= word <= u'u9fff':
11 if word in vocabulary:
12 vocabulary[word] += 1
13 else:
14 vocabulary[word] = 1
15 vocabulary_list = START_VOCABULART + sorted(vocabulary, key=vocabulary.get, reverse=True)
16 # 取前3500個常用漢字,vocabulary_size = 3500
17 if len(vocabulary_list) > vocabulary_size:
18 vocabulary_list = vocabulary_list[:vocabulary_size]
19 print(input_file + " 詞彙表大小:", len(vocabulary_list))
20 with open(output_file, "w") as ff:
21 for word in vocabulary_list:
22 ff.write(word + "n")
23 ff.close
複製程式碼
0x3 開始訓練
01|訓練
在我們的語料準備好之後,便可以開始我訓練,其實訓練本身是很簡單的,其核心是呼叫Tensorflow的Seq2SeqModel,不斷的進行迴圈訓練。下面是訓練的核心程式碼與引數設定:
1# 源輸入詞表的大小
2vocabulary_encode_size = 3500
3# 目標輸出詞表的大小
4vocabulary_decode_size = 3500
5#一種有效處理不同長度的句子的方法
6buckets = [(5, 10), (10, 15), (20, 25), (40, 50)]
7# 每層單元數目
8layer_size = 256
9# 網路的層數。
10num_layers = 3
11# 訓練時的批處理大小
12batch_size = 64
13# max_gradient_norm:表示梯度將被最大限度地削減到這個規範
14# learning_rate: 初始的學習率
15# learning_rate_decay_factor: 學習率衰減因子
16# forward_only: false意味著在解碼器端,使用decoder_inputs作為輸入。例如decoder_inputs 是‘GO, W, X, Y, Z ’,正確的輸出應該是’W, X, Y, Z, EOS’。假設第一個時刻的輸出不是’W’,在第二個時刻也要使用’W’作為輸入。當設為true時,只使用decoder_inputs的第一個時刻的輸入,即’GO’,以及解碼器的在每一時刻的真實輸出作為下一時刻的輸入。
17model = seq2seq_model.Seq2SeqModel(source_vocab_size=vocabulary_encode_size, target_vocab_size=vocabulary_decode_size,buckets=buckets, size=layer_size, num_layers=num_layers, max_gradient_norm= 5.0,batch_size=batch_size, learning_rate=0.5, learning_rate_decay_factor=0.97, forward_only=False)
18
19config = tf.ConfigProto()
20config.gpu_options.allocator_type = 'BFC' # 防止 out of memory
21
22with tf.Session(config=config) as sess:
23 # 恢復前一次訓練
24 ckpt = tf.train.get_checkpoint_state('.')
25 if ckpt != None:
26 print(ckpt.model_checkpoint_path)
27 model.saver.restore(sess, ckpt.model_checkpoint_path)
28 else:
29 sess.run(tf.global_variables_initializer())
30
31 train_set = read_data(train_encode_vec, train_decode_vec)
32 test_set = read_data(test_encode_vec, test_decode_vec)
33
34 train_bucket_sizes = [len(train_set[b]) for b in range(len(buckets))]
35 train_total_size = float(sum(train_bucket_sizes))
36 train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size for i in range(len(train_bucket_sizes))]
37
38 loss = 0.0
39 total_step = 0
40 previous_losses = []
41 # 一直訓練,每過一段時間儲存一次模型
42 while True:
43 random_number_01 = np.random.random_sample()
44 bucket_id = min([i for i in range(len(train_buckets_scale)) if train_buckets_scale[i] > random_number_01])
45
46 encoder_inputs, decoder_inputs, target_weights = model.get_batch(train_set, bucket_id)
47 _, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, False)
48
49 loss += step_loss / 500
50 total_step += 1
51
52 print(total_step)
53 if total_step % 500 == 0:
54 print(model.global_step.eval(), model.learning_rate.eval(), loss)
55
56 # 如果模型沒有得到提升,減小learning rate
57 if len(previous_losses) > 2 and loss > max(previous_losses[-3:]):
58 sess.run(model.learning_rate_decay_op)
59 previous_losses.append(loss)
60 # 儲存模型
61 checkpoint_path = "chatbot_seq2seq.ckpt"
62 model.saver.save(sess, checkpoint_path, global_step=model.global_step)
63 loss = 0.0
64 # 使用測試資料評估模型
65 for bucket_id in range(len(buckets)):
66 if len(test_set[bucket_id]) == 0:
67 continue
68 encoder_inputs, decoder_inputs, target_weights = model.get_batch(test_set, bucket_id)
69 _, eval_loss, _ = model.step(sess, encoder_inputs, decoder_inputs, target_weights, bucket_id, True)
70 eval_ppx = math.exp(eval_loss) if eval_loss < 300 else float('inf')
71 print(bucket_id, eval_ppx)
複製程式碼
02|實際問答效果
如果我們的模型一直在訓練,那麼機器怎麼知道在什麼時候停止訓練呢?這個停止訓練的閥值又靠什麼去衡量?在這裡我們引入一個語言模型評價指標——Perplexity。
① Perplexity是什麼:
PPL是用在自然語言處理領域(NLP)中,衡量語言模型好壞的指標。它主要是根據每個詞來估計一句話出現的概率,並用句子長度作normalize,公式為 :
S代表sentence,N是句子長度,p(wi)是第i個詞的概率。第一個詞就是 p(w1|w0),而w0是START,表示句子的起始,是個佔位符。
這個式子可以這樣理解,PPL越小,p(wi)則越大,一句我們期望的sentence出現的概率就越高。
還有人說,Perplexity可以認為是average branch factor(平均分支系數),即預測下一個詞時可以有多少種選擇。別人在作報告時說模型的PPL下降到90,可以直觀地理解為,在模型生成一句話時下一個詞有90個合理選擇,可選詞數越少,我們大致認為模型越準確。這樣也能解釋,為什麼PPL越小,模型越好。
對於我們的訓練,其最近幾次的Perplexity如下:
截止發文時,此模型已經訓練了27h,其Perplexity仍然比較難收斂,所以模型的訓練真的需要一些耐心。如果有條件使用GPU進行訓練,那麼此速度將會大大提高。
我們使用現階段的模型進行一些對話,發現已經初具雛形:
至此,我們的“智慧聊天機器人”已經大功告成!但不難看出,這個機器人還是在不斷的犯傻,很多問題牛頭不對馬嘴,所以我們又稱其為“智障機器人”。
0x4 結語
至此我們就從無到有訓練了一個問答機器人,雖然它還有點”智障“不太理解更多的詞彙,但是整體流程已經跑通,並且具有一定的效果。後面的工作就是不斷的完善其中的演算法、引數與語料了。其中語料是特別關鍵的部分,大概會佔用到50%-70%的工作量,因為本文使用的是網際網路上已經處理好的語料,省去了不少時間。事實上大部分開發人員的時間都在進行語料預處理:資料清洗、分詞、詞性標註、去停用詞等方面。
後續有機會再和大家分享語料預處理這一塊。這裡有一個可愛的二維碼,大家記得關注喲~
相關文獻與參考資料:
從機器學習談起 (http://www.cnblogs.com/subconscious/p/4107357.html)
使用python實現神經網路 (http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/)
迴圈神經網路 (https://zybuluo.com/hanbingtao/note/541458)
語言模型評價指標 (https://blog.csdn.net/index20001/article/details/78884646)
Tensorflow(https://github.com/google/seq2seq)
始發於微信公眾號: 騰訊DeepOcean