手把手教你寫一個中文聊天機器人

註:這邊文章是我在Gitchat上的一次分享,覺得應該讓更多人看到,所以就搬到知乎上,也是自己開始在知乎上寫AI相關技術的開始。

前言:

發布這篇 Chat 的初衷是想和各位一起分享一下動手來做聊天機器人的樂趣,因此本篇文章適合用於深度機器學習的研究和興趣發展,因為從工業應用的角度來看使用百度、科大訊飛的 API 介面會更加的適合。在這篇文章中,希望和大家一起共同交流和探索動手實踐的樂趣,當然也歡迎大神來做深度的探討以及吐槽。這篇 Chat 的基礎源代碼來自互聯網,我進行了綜合優化和部分代碼的重寫,我也會在這邊文章發布的同時將所有源代碼上傳到 Git 分享出來,這樣在文章中我就不佔用篇幅貼出全部的源代碼,大家可以從 Git 上 pull 下來對照著文章來看。

源碼地址:zhaoyingjun/chatbot

系統設計思路和框架

本次系統全部使用 Python 編寫,在系統設計上遵循著配置靈活、代碼模塊化的思路,分為數據預處理器、數據處理器、執行器、深度學習模型、可視化展示五個模塊。模塊間的邏輯關係大致為:數據預處理是將原始語料進行初步的處理以滿足於數據處理模塊的要求;執行器是整個系統引擎分別在運轉的時候調用數據處理器、深度學習模型進行數據處理、模型訓練、模型運作等工作;深度學習模型是一個基於TF的seq2seq模型,用於定義神經網路並進行模型計算;可視化展示是一個用Flask前端框架寫的簡單的人機交互程序,在運行時調用執行器進行人機對話。整體的框架圖如下:

本系統利用seq2seq模型的特點,結合word2vec的思路(當然這樣做有點簡單粗暴,沒有進行分詞),將訓練語料分為Ask語料集和Replay語料集,並根據一定的比例分為訓練語料集和驗證語料集,然後就是word2vec。這些步驟都是在數據預處理器和數據處理器中完成的,關於訓練語料集與驗證語料集的數量都是做成可外部配置的,這也是這個系統我認為設計的最合理的地方(可惜不是我設計的)。在完成數據的處理後,執行器就會根據訓練模式(這也是外部可配置的)來調用seq2seq進行神經網路的創建,神經網路的超參數同樣也是可以外部進行配置的。在進行訓練過程中,使用perprelixy來計算模型的loss,通過自動的調整learning rate來逐步的取得最優值,當learning rate減少為0達到最優值。最後,就是可視化展示模塊啟動一個進程調用執行器來實時在線提供聊天服務,在語句輸入和輸出利用seq2seq的特點,直接將輸入seq轉換成vec作為已經訓練好的神經網路,然後神經網路會生成一個seq向量,然後通過查詢詞典的方式將生成的向量替換成中文句子。在神經網路模型這裡,TF有GRU和LSTM模型可供選擇,我比較了GRU和LSTM的訓練效果,發現還是GRU比較適合對話語句場景的訓練,這點在後面的代碼分析環節會詳細解釋。

源碼結構

  • datautls.py(數據預處理器)包含的函數convertseq2seq_files,將主程序分好的ask語料集和response語料集轉換成seq2seq文件,以便數據處理器進行進一步的數據處理。對於整個系統,一般該數據預處理器只需要運行一次即可。
  • prepareData.py(數據處理器)包含的函數:createvocabulary、converttovector、preparecustomdata、 basictokenizer、initialize_vocabulary,這些函數的作用分別是創建字典、句子轉向量、根據超參定製化訓練數據、基礎數據標記、初始化詞典。
  • execute.py(執行器)包含的函數:getconfig、readdata、createmodel、train、 selftest、initsession、decodeline,這些函數的作用分別是獲取配置參數和超參、讀取數據、創建模型、訓練模型、模式測試、初始化會話、在線對話。
  • seq2seqmodel.py(深度機器學習模型)包含的函數:init、sampledloss、seq2seqf、step、getbatch,這些函數的作用分別是程序初始化、loss函數、seq2seq函數、擬合模型、獲取批量數據。
  • app.py(可視化展示模塊)包含的函數: heartbeat、reply、index,這些函數的作用分別是心跳、在線對話、主頁入口。

源碼講解

數據預處理器(data_utls.py)

首先在代碼的頭部聲明代碼編碼類型,#!/usr/bin/env Python #coding=utf-8,然後導入所需的依賴:

import osimport randomimport getConfig

os是提供對python進行一些操作系統層面的工具,比如read、open等,主要是對文件的一些操作。 random是一個隨機函數,提供對數據的隨機分布操作。 getConfig是外部定義的一個函數,用來獲取seq2seq.ini里的參數配置。 接下來是對原始語料的處理,主要是通過語句的奇偶行數來區分問答語句(由於語料是電影的台詞,所以直接默認是一問一答的方式),然後把區分後的語句分別存儲下來,保存為ask文件和response文件。

gConfig = {}gConfig=getConfig.get_config()conv_path = gConfig[resource_data]if not os.path.exists(conv_path): exit()convs = [] # 用於存儲對話集合with open(conv_path) as f: one_conv = [] # 存儲一次完整對話 for line in f: line = line.strip(
).replace(/, ) if line == : continue if line[0] == gConfig[e]: if one_conv: convs.append(one_conv) one_conv = [] elif line[0] == gConfig[m]: one_conv.append(line.split( )[1])ask = [] # 用來存儲問的語句response = [] # 用來存儲回答的語句for conv in convs: if len(conv) == 1: continue if len(conv) % 2 != 0: # 保持對話是一問一答 conv = conv[:-1] for i in range(len(conv)): if i % 2 == 0: ask.append(conv[i]) #因為這裡的i是從0開始的,因此偶數為問的語句,奇數為回答的語句 else: response.append(conv[i])

然後調用convertseq2seqfiles函數,將區分後的問答語句分別保存成訓練集和測試機,保存文件為train.enc、train.dec、test.enc、test.dec。convertseq2seqfiles函數就不一一貼出來,可以對照源碼來看。

數據處理器(prepareData.py)

編碼聲明和導出依賴部分不再重複,在真正的數據處理前,需要對訓練集和測試集中的一些特殊字元進行標記,以便能夠進行統一的數據轉換。特殊標記如下:

PAD = "__PAD__"#空白補位GO = "__GO__"#對話開始EOS = "__EOS__" # 對話結束UNK = "__UNK__" # 標記未出現在辭彙表中的字元START_VOCABULART = [PAD, GO, EOS, UNK]PAD_ID = 0#特殊標記對應的向量值GO_ID = 1#特殊標記對應的向量值EOS_ID = 2#特殊標記對應的向量值UNK_ID = 3#特殊標記對應的向量值

接下來定義生成詞典函數,這裡及後續的函數只貼出函數名和參數部分,詳細的代碼參見Git上的源碼:

def create_vocabulary(input_file,output_file):

這個函數演算法思路會將input_file中的字元出現的次數進行統計,並按照從小到大的順序排列,每個字元對應的排序序號就是它在詞典中的編碼,這樣就形成了一個key-vlaue的字典查詢表。當然函數里可以根據實際情況設置字典的大小。

def convert_to_vector(input_file, vocabulary_file, output_file):

這個函數從參數中就可以看出是直接將輸入文件的內容按照詞典的對應關係,將語句替換成向量,這也是所有seq2seq處理的步驟,因為完成這一步之後,不管原訓練語料是什麼語言都沒有區別了,因為對於訓練模型來說都是數字化的向量。

def prepare_custom_data(working_directory, train_enc, train_dec, test_enc, test_dec, enc_vocabulary_size, dec_vocabulary_size, tokenizer=None):

這個函數是數據處理器的集成函數,執行器調用的數據處理器的函數也主要是調用這個函數,這個函數是將預處理的數據從生成字典到轉換成向量一次性搞定,將數據處理器對於執行器來說實現透明化。working_directory這個參數是存放訓練數據、訓練模型的文件夾路徑,其他參數不一一介紹。

seq2seq 模型

seq2seq模型是直接參照TF官方的源碼來做的,只是對其中的一些tf參數針對tf的版本進行了修正。如Chat簡介中說的,考慮到大家的接受程度不一樣,本次不對代碼、演算法進行太過深入的分析,後面我會開一個達人課專門詳細的進行分析和相關知識的講解。

class Seq2SeqModel(object):

前面的導入依賴不贅述了,這裡在開頭需要先定義一個Seq2SeqModel對象,這個對象實現了一個多層的RNN神經網路以及具有attention-based解碼器,其實就是一個白盒的神經網路對象,我們只需拿來用即可,詳細的可以參閱arxiv.org/abs/1412.7449這個paper。關於這個paper這次不做解讀。

def __init__(self, source_vocab_size, target_vocab_size, buckets, size, num_layers, max_gradient_norm, batch_size, learning_rate, learning_rate_decay_factor, use_lstm=False, num_samples=512, forward_only=False):

這個函數是整個模型的初始化函數(父函數),這個函數裡面會定義多個子函數,簡單來講這個函數執行完之後就完成了訓練模型的創建。這個函數中的大部分參數都是通過seq2seq.ini文件進行朝參數配置的,其中uselstm這個參數是決定是使用gru cell還是lstm cell來構建神經網路,gru其實是lstm的變種,我兩個cell都測試了,發現在進行語句對話訓練時使用gru cell效果會更好,而且好的不是一點。由於時間的緣故,只對超參size、numlayers、learningrate、learningratedecayfactor、use_lstm進行簡單對比調試,大家有興趣的話可以自己進行調參,看看最優的結果值preprlexity會不會小於10。

sampledloss、seq2seqf、step、get_batch這些子函數不一一的講了,大家可以百度一下,都有很詳細的解釋和講解。如果需要,我會在達人課里對這些子函數進行講解。

執行器

_buckets = [(1, 10), (10, 15), (20, 25), (40, 50)]

這個bukets的設置特別關鍵,也算是一個超參數,因為這個關係到模型訓練的效率。具體設置的時候,有兩個大原則:盡量覆蓋到所有的語句長度、每個bucket覆蓋的語句數量盡量均衡。

def read_data(source_path, target_path, max_size=None):

這個函數是讀取數據函數,參數也比較簡單,其中max_size這個參數默認是空或者是None的時候表示無限制,同時這個參數也是可以通過seq2seq.ini進行設置。

def create_model(session, forward_only):

這個函數是用來生成模型,參數簡單。model的定義也只有一句:

model = seq2seq_model.Seq2SeqModel( gConfig[enc_vocab_size], gConfig[dec_vocab_size], _buckets, gConfig[layer_size], gConfig[num_layers], gConfig[max_gradient_norm], gConfig[batch_size], gConfig[learning_rate], gConfig[learning_rate_decay_factor], forward_only=forward_only)

這裡可以看到,模型的生成直接調用了seq2seq_model中的對象Seq2SeqModel,將相應的參數按照要求傳進去就可以,具體這個對象的作用以及詳細的細節如前面所說可以參照具體的paper來研究,但是作為入門的興趣愛好者來說可以先不管這些細節,先拿來用就可以了,主要關注點建議還是在調參上。

def train():

train函數沒有參數傳遞,因為所有的參數都是通過gconfig來讀取的,這裡面有一個特殊的設計,就是將prepareData函數調用放在train()函數里,這樣做的話就是每次進行訓練時都會對數據進行處理一次,我認為這是非常好的設計,大家可以參考,因為這個可以保證數據的最新以及可以對增長的數據進行訓練。具體代碼如下:

enc_train, dec_train, enc_dev, dec_dev, _, _ = prepareData.prepare_custom_data(gConfig[working_directory],gConfig[train_enc],gConfig[train_dec],gConfig[test_enc],gConfig[test_dec],gConfig[enc_vocab_size],gConfig[dec_vocab_size],tokenizer=None)

def selftest():和def initsession(sess, conf=seq2seq.ini): 這兩個函數分別是進行測試以及初始會話用的。由於TF的特殊機制,其每次圖運算都是要在session下進行的,因此需要在進行圖運算之前進行會話初始化。

def decode_line(sess, model, enc_vocab, rev_dec_vocab, sentence):

這個函數就是我們整個對話機器人的最終出效果的函數,這個函數會載入訓練好的模型,將輸入的sentence轉換為向量輸入模型,然後得到模型的生成向量,最終通過字典轉換後返回生成的語句。

由於執行器包含多種模式,因此我們在最後加上一個主函數入口並對執行模式判斷,

if __name__ == __main__:if gConfig[mode] == train: # start training train()elif gConfig[mode] == test: # interactive decode decode() else: print(Serve Usage : >> python3 webui/app.py) #當我們使用可視化模塊調用執行器時,需要在可視化模塊所在的目錄下進行調用,而是可視化模塊由於包含很多靜態文件,所以統一放在webui目錄下,因此需要將執行器與可視化模塊放在同一個目錄下。 print(# uses seq2seq_serve.ini as conf file)#seq2seq_serve.ini與seq2seq.ini除了執行器的模式外所有配置需要保持一致。

可視化展示模塊

def heartbeat():

由於可視化展示模塊需要賬期在線運行,為了了解運行狀態加了一個心跳函數,定時的輸出信息來check程序運行情況。

def reply():

這個函數是人機對話交互模塊,主要是從頁面上獲取提交的信息,然後調用執行器獲得生成的回答語句,然後返回給前端頁面。

其中有一點設計可以注意一下,就是在進行語句轉向量的過程中,為了保證準確識別向量值,需要在向量值中間加空格,比如123,這個默認的會識別成一個字元,但是1 2 3就是三個字元。因此在獲取到前端的語句後,在傳給執行器之前需要對語句進行字元間加空格處理,如下:

req_msg=.join([f+ for fh in req_msg for f in fh])def index():

這個函數是可視化展示模塊的首頁載入,默然返回一個首頁html文件。

另外,由於TF的特殊機制,需要在可視化模塊中初始化會話,這樣執行器才能運行,如下:

import tensorflow as tf import execute sess = tf.Session() sess, model, enc_vocab, rev_dec_vocab = execute.init_session(sess, conf=seq2seq_serve.ini)

最後和執行器一樣,需要加一個主函數入口啟動可視化模塊,並配置服務地址和埠號,如下:

if (__name__ == "__main__"): app.run(host = 0.0.0.0, port = 8808)

訓練過程和最優值

最優效果展示

展示地址:115.231.97.140:8999/


推薦閱讀:

基於Seq2Seq模型深度學習Github工單

TAG:人工智能 | bot聊天机器人 | seq2seq |