智能機器人在生活中隨處可見: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則代表一段對話的開始與結束。
如果對于人工智能有相同興趣,或者技術上的交流可以加我QQ進行討論:425851955
我們拿到這些語料后,用代碼將其按照問/答分為兩類"Question.txt"、"Answer.txt":
1importre
2importsys
3defprepare(num_dialogs=50000):
4withopen("xhj.conv")asfopen:
5#?替換E、M等
6reg?=?re.compile("EnM?(.*?)nM?(.*?)n")
7match_dialogs?=?re.findall(reg,?fopen.read())
8#?使用5W條對話作為訓練語料
9ifnum_dialogs?>=?len(match_dialogs):
10dialogs?=?match_dialogs
11else:
12dialogs?=?match_dialogs[:num_dialogs]
13questions?=?[]
14answers?=?[]
15forque,?ansindialogs:
16questions.append(que)
17answers.append(ans)
18#?保存到data/文件夾目錄下
19save(questions,"data/Question.txt")
20save(answers,"data/Answer.txt")
21defsave(dialogs,?file):
22withopen(file,"w")asfopen:
23fopen.write("n".join(dialogs))
最終我們得到5W條問題與回答數據:
02|向量表映射建立
到這里,大家可能會問,那么這個"智能"聊天機器人是不是就是將我們輸入的問題匹配Question.txt里面的問題,然后再從Answer.txt找到相應回答進行輸出??
當然不會是這么簡單,本質上聊天機器人是基于問句的上下文環境產生一個新的回答,而非是從數據庫中拿出一條對應好的回答數據。
那么機器怎么知道該回答什么呢?此處借用一下谷歌的seq2seq原理圖:
簡單來說就是:我們輸入的每一句話,都會被機器拆成詞并向量化;這些詞作為輸入層的向量,與權重矩陣進行計算后到隱藏層,隱藏層輸出的向量再與權重矩陣進行計算,得到最終向量。我們再將此向量映射到詞向量庫時,便可得到我們想要的結果。?
在代碼上實現比較簡單,因為復雜底層邏輯的都由Tensorflow幫我們完成了,我們將詞匯表進行最終的梳理:
1defgen_vocabulary_file(input_file,?output_file):
2vocabulary?=?{}
3withopen(input_file)asf:
4counter?=0
5forlineinf:
6counter?+=1
7tokens?=?[wordforwordinline.strip()]
8forwordintokens:
9#?過濾非中文?文字
10ifu'u4e00'<=?word?<=u'u9fff':
11ifwordinvocabulary:
12vocabulary[word]?+=1
13else:
14vocabulary[word]?=1
15vocabulary_list?=?START_VOCABULART?+?sorted(vocabulary,?key=vocabulary.get,?reverse=True)
16#?取前3500個常用漢字,vocabulary_size?=?3500
17iflen(vocabulary_list)?>?vocabulary_size:
18vocabulary_list?=?vocabulary_list[:vocabulary_size]
19print(input_file?+"?詞匯表大小:",?len(vocabulary_list))
20withopen(output_file,"w")asff:
21forwordinvocabulary_list:
22ff.write(word?+"n")
23ff.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
22withtf.Session(config=config)assess:
23#?恢復前一次訓練
24ckpt?=?tf.train.get_checkpoint_state('.')
25ifckpt?!=None:
26print(ckpt.model_checkpoint_path)
27model.saver.restore(sess,?ckpt.model_checkpoint_path)
28else:
29sess.run(tf.global_variables_initializer())
30
31train_set?=?read_data(train_encode_vec,?train_decode_vec)
32test_set?=?read_data(test_encode_vec,?test_decode_vec)
33
34train_bucket_sizes?=?[len(train_set[b])forbinrange(len(buckets))]
35train_total_size?=?float(sum(train_bucket_sizes))
36train_buckets_scale?=?[sum(train_bucket_sizes[:i?+1])?/?train_total_sizeforiinrange(len(train_bucket_sizes))]
37
38loss?=0.0
39total_step?=0
40previous_losses?=?[]
41#?一直訓練,每過一段時間保存一次模型
42whileTrue:
43random_number_01?=?np.random.random_sample()
44bucket_id?=?min([iforiinrange(len(train_buckets_scale))iftrain_buckets_scale[i]?>?random_number_01])
45
46encoder_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
49loss?+=?step_loss?/500
50total_step?+=1
51
52print(total_step)
53iftotal_step?%500==0:
54print(model.global_step.eval(),?model.learning_rate.eval(),?loss)
55
56#?如果模型沒有得到提升,減小learning?rate
57iflen(previous_losses)?>2andloss?>?max(previous_losses[-3:]):
58sess.run(model.learning_rate_decay_op)
59previous_losses.append(loss)
60#?保存模型
61checkpoint_path?="chatbot_seq2seq.ckpt"
62model.saver.save(sess,?checkpoint_path,?global_step=model.global_step)
63loss?=0.0
64#?使用測試數據評估模型
65forbucket_idinrange(len(buckets)):
66iflen(test_set[bucket_id])?==0:
67continue
68encoder_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)
70eval_ppx?=?math.exp(eval_loss)ifeval_loss?<300elsefloat('inf')
71print(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如下:
如果對于人工智能有相同興趣,或者技術上的交流可以加我QQ進行討論:425851955
截止發文時,此模型已經訓練了27h,其Perplexity仍然比較難收斂,所以模型的訓練真的需要一些耐心。如果有條件使用GPU進行訓練,那么此速度將會大大提高。
我們使用現階段的模型進行一些對話,發現已經初具雛形:
至此,我們的“智能聊天機器人”已經大功告成!但不難看出,這個機器人還是在不斷的犯傻,很多問題牛頭不對馬嘴,所以我們又稱其為“智障機器人”。
0x4 結語
至此我們就從無到有訓練了一個問答機器人,雖然它還有點”智障“不太理解更多的詞匯,但是整體流程已經跑通,并且具有一定的效果。后面的工作就是不斷的完善其中的算法、參數與語料了。其中語料是特別關鍵的部分,大概會占用到50%-70%的工作量,因為本文使用的是互聯網上已經處理好的語料,省去了不少時間。事實上大部分開發人員的時間都在進行語料預處理:數據清洗、分詞、詞性標注、去停用詞等方面。