智能機器人在生活中隨處可見:iPhone裏會說話的siri、會下棋的阿法狗、調皮可愛的微軟小冰……她們都具備必定的智能,可以和人類進行交互。這些智能機器人很是神奇,看上去離咱們也十分遙遠,但其實只要咱們動動手,即可以造一個屬於本身的智能機器人。
python本文將教你從零開始造出一個智障,不對是「智能聊天機器人"。
要造一個聊天機器人,首先你須要瞭解一些相關概念——天然語言處理(NLP),它是一門融語言學、計算機科學、數學於一體的科學,研究讓電腦「懂」人類語言的方法。固然,它也包含不少分支:文本朗讀、語音識別、句法分析、天然語言生成、人機對話、信息檢索、信息抽取、文字校對、文本分類、自動文摘、機器翻譯、文字蘊含等等等。 git
看到這裏的朋友,千萬別被這些嚇跑。既然本文叫《從零開始造一個「智障」聊天機器人》那麼各位看官老爺不懂這些也沒有關係!跟着個人腳步一步一步作吧。github
0x1 基本概念
這裏涉及到的原理基礎,沒興趣的看官老爺略過便可,不影響後續代碼實現。算法
01|神經網絡數據庫
人工智能的底層是」神經網絡「,許多複雜的應用(好比模式識別、自動控制)和高級模型(好比深度學習)都基於它。學習人工智能,必定是從它開始。 微信
那麼問題來了,什麼是神經網絡呢?簡單來講,神經網絡就是模擬人腦神經元網絡,從而讓計算機懂得」思考「。具體概念在這裏再也不贅述,網絡上有不少簡單易懂的解釋。 網絡
本文使用的的是循環神經網絡(RNN),咱們來看一個最簡單的基本循環神經網絡: app
雖然圖像看起來很抽象,可是實際很好理解。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