從零開始造一個“智障”聊天機器人

騰訊DeepOcean發表於2018-11-01

騰訊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 = [(510), (1015), (2025), (4050)]
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

相關文章