原來聊天機器人是這麼做出來的

isuccess88發表於2017-04-23

原來聊天機器人是這麼做出來的

原來聊天機器人是這麼做出來的

tensorflow自帶的seq2seq模型基於one-hot的詞嵌入,每個詞用一個數字代替不足以表示詞與詞之間的關係,word2vec通過多維向量來做詞嵌入,能夠表示出詞之間的關係,比如:男-女≈王子-公主。基於seq2seq的思想,利用多維詞向量來實現模型,預期會有更高的準確性。

seq2seq模型原理

主要參考《Sequence to Sequence Learning with Neural Networks》這篇論文,核心思想如下圖:

原來聊天機器人是這麼做出來的

ABC是輸入語句,WXYZ是輸出語句,EOS是標識一句話結束,圖中的訓練單元是lstm,lstm的特點是有長短時記憶,所以能夠根據輸入的多個字來確定後面的多個字,有關lstm的知識可以參考《http://deeplearning.net/tutorial/lstm.html》

上面的模型中編碼器和解碼器共用了同一個lstm層,也就是共享了引數,牛人們嘗試把他們分開像https://github.com/farizrahman4u/seq2seq中提到的樣子:

原來聊天機器人是這麼做出來的

其中綠色是編碼器,黃色是解碼器,橙色的箭頭傳遞的是lstm層的狀態資訊也就是記憶資訊,編碼器唯一傳給解碼器的就是這個狀態資訊

我們看到解碼器每一時序的輸入都是前一個時序的輸出,從整體上來看就是:我通過不同時序輸入“How are you <EOL>”,模型就能自動一個字一個字的輸出“W I am fine <EOL>”,這裡的W是一個特殊的標識,它既是編碼器最後的輸出,同時又是解碼器的一個觸發訊號

那麼我們訓練的時候輸入的X,Y應該是什麼呢?X="How are you <EOL>",Y="W I am fine <EOL>"?

這是不行的,因為在解碼器還沒有訓練出靠譜的引數之前,我們無法保證第一個時序的輸出就是“I”,那麼傳給第二個時序的輸入就不一定是I,同樣第三、四個時序的輸入就無法保證是am和fine,那麼是無法訓練出想要的模型的

我們要這樣來做:我們直接把解碼器中每一時序的輸入強制改為"W I am fine",也就是把這部分從我們訓練樣本的輸入X中傳過來,而Y依然是預測輸出的"W I am fine <EOL>",這樣訓練出來的模型就是我們設計的編碼器解碼器模型了

那麼在使用訓練好的模型做預測的時候,我們改變處理方式:在解碼時以前一時序的輸出為輸入做預測,這樣就能輸出我們希望輸出的"W I am fine <EOL>"了

基於以上的原理,下面開始我們的工程實踐

語料準備工作

準備至少300w的聊天語料用於詞向量的訓練和seq2seq模型的訓練,語料越豐富訓練出來的詞向量質量越好,如果想通過影視劇字幕來獲取語料可以參考《自己動手做聊天機器人 二十九-重磅:近1GB的三千萬聊天語料供出》

獲取到原始語料後需要做一些加工處理,首先需要做切詞,方法如下:

python word_segment.py ./corpus.raw ./corpus.segment

其中word_segment.py是我寫的切詞工具,僅供參考

之後要把切詞好的檔案轉成“|”分隔的問答對,如下:

cat ./corpus.segment | awk '{if(last!="")print last"|"$0;last=$0}' | sed 's/| /|/g' > ./corpus.segment.pair

這樣語料準備工作就齊全了

訓練詞向量

我們直接利用google的word2vec來訓練詞向量,如下:

word2vec -train ./corpus.segment -output vectors.bin -cbow 1 -size 200 -window 8 -negative 25 -hs 0 -sample 1e-5 -threads 20 -binary 1 -iter 15

其中corpus.raw是原始語料資料,vectors.bin是生成的詞向量二進位制檔案

瞭解word2vec的原理請見《自己動手做聊天機器人 二十五-google的文字挖掘深度學習工具word2vec的實現原理》

生成的詞向量二進位制載入方法可以參考我寫的:word_vectors_loader.py

建立模型

下面就是重點的模型建立過程,這裡面我們直接使用tensorflow+tflearn庫來實現:

# 首先我們為輸入的樣本資料申請變數空間,如下。其中self.max_seq_len是指一個切好詞的句子最多包含多少個詞,self.word_vec_dim是詞向量的維度,這裡面shape指定了輸入資料是不確定數量的樣本,每個樣本最多包含max_seq_len*2個詞,每個詞用word_vec_dim維浮點數表示。這裡面用2倍的max_seq_len是因為我們訓練是輸入的X既要包含question句子又要包含answer句子
input_data = tflearn.input_data(shape=[None, self.max_seq_len*2, self.word_vec_dim], dtype=tf.float32, name = "XY")
# 然後我們將輸入的所有樣本資料的詞序列切出前max_seq_len個,也就是question句子部分,作為編碼器的輸入
encoder_inputs = tf.slice(input_data, [0, 0, 0], [-1, self.max_seq_len, self.word_vec_dim], name="enc_in")
# 再取出後max_seq_len-1個,也就是answer句子部分,作為解碼器的輸入。注意,這裡只取了max_seq_len-1個,是因為還要在前面拼上一組GO標識來告訴解碼器我們要開始解碼了,也就是下面加上go_inputs拼成最終的go_inputs
decoder_inputs_tmp = tf.slice(input_data, [0, self.max_seq_len, 0], [-1, self.max_seq_len-1, self.word_vec_dim], name="dec_in_tmp")
go_inputs = tf.ones_like(decoder_inputs_tmp)
go_inputs = tf.slice(go_inputs, [0, 0, 0], [-1, 1, self.word_vec_dim])
decoder_inputs = tf.concat(1, [go_inputs, decoder_inputs_tmp], name="dec_in")
# 之後開始編碼過程,返回的encoder_output_tensor展開成tflearn.regression迴歸可以識別的形如(?, 1, 200)的向量;返回的states後面傳入給解碼器
(encoder_output_tensor, states) = tflearn.lstm(encoder_inputs, self.word_vec_dim, return_state=True, scope='encoder_lstm')
encoder_output_sequence = tf.pack([encoder_output_tensor], axis=1)
# 取出decoder_inputs的第一個詞,也就是GO
first_dec_input = tf.slice(decoder_inputs, [0, 0, 0], [-1, 1, self.word_vec_dim])
# 將其輸入到解碼器中,如下,解碼器的初始化狀態為編碼器生成的states,注意:這裡的scope='decoder_lstm'是為了下面重用同一個解碼器
decoder_output_tensor = tflearn.lstm(first_dec_input, self.word_vec_dim, initial_state=states, return_seq=False, reuse=False, scope='decoder_lstm')
# 暫時先將解碼器的第一個輸出存到decoder_output_sequence_list中供最後一起輸出
decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
decoder_output_sequence_list = [decoder_output_tensor]
# 接下來我們迴圈max_seq_len-1次,不斷取decoder_inputs的一個個詞向量作為下一輪解碼器輸入,並將結果新增到decoder_output_sequence_list中,這裡面的reuse=True, scope='decoder_lstm'說明和上面第一次解碼用的是同一個lstm層
for i in range(self.max_seq_len-1):
next_dec_input = tf.slice(decoder_inputs, [0, i+1, 0], [-1, 1, self.word_vec_dim])
decoder_output_tensor = tflearn.lstm(next_dec_input, self.word_vec_dim, return_seq=False, reuse=True, scope='decoder_lstm')
decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
decoder_output_sequence_list.append(decoder_output_tensor)
# 下面我們把編碼器第一個輸出和解碼器所有輸出拼接起來,作為tflearn.regression迴歸的輸入
decoder_output_sequence = tf.pack(decoder_output_sequence_list, axis=1)
real_output_sequence = tf.concat(1, [encoder_output_sequence, decoder_output_sequence])
net = tflearn.regression(real_output_sequence, optimizer='sgd', learning_rate=0.1, loss='mean_square')
model = tflearn.DNN(net)

至此模型建立完成,讓我們彙總一下里面的思想:

1)訓練輸入的X、Y分別是編碼器解碼器的輸入和預測的輸出;

2)X切分兩半,前一半是編碼器輸入,後一半是解碼器輸入;

3)編碼解碼器輸出的預測值用Y做迴歸訓練

4)訓練時通過樣本的真實值作為解碼器輸入,實際預測時將不會有上圖中WXYZ部分,因此上一時序的輸出將作為下一時序的輸入(後面會詳述預測的實現)

訓練模型

下面我們來例項化模型並喂資料做訓練,如下:

model = self.model()
model.fit(trainXY, trainY, n_epoch=1000, snapshot_epoch=False, batch_size=1)
model.load('./model/model')

這裡的trainXY和trainY通過載入上面我們準備的語料來賦值

首先我們載入詞向量並存到word_vector_dict中,然後讀取語料檔案並挨個詞查word_vector_dict並賦值向量給question_seq和answer_seq,如下:

def init_seq(input_file):
"""讀取切好詞的文字檔案,載入全部詞序列
"""
file_object = open(input_file, 'r')
vocab_dict = {}
while True:
question_seq = []
answer_seq = []
line = file_object.readline()
if line:
line_pair = line.split('|')
line_question = line_pair[0]
line_answer = line_pair[1]
for word in line_question.decode('utf-8').split(' '):
if word_vector_dict.has_key(word):
question_seq.append(word_vector_dict[word])
for word in line_answer.decode('utf-8').split(' '):
if word_vector_dict.has_key(word):
answer_seq.append(word_vector_dict[word])
else:
break
question_seqs.append(question_seq)
answer_seqs.append(answer_seq)
file_object.close()

有了question_seq和answer_seq,我們來構造trainXY和trainY,如下:

def generate_trainig_data(self):
xy_data = []
y_data = []
for i in range(len(question_seqs)):
question_seq = question_seqs[i]
answer_seq = answer_seqs[i]
if len(question_seq) < self.max_seq_len and len(answer_seq) < self.max_seq_len:
sequence_xy = [np.zeros(self.word_vec_dim)] * (self.max_seq_len-len(question_seq)) + list(reversed(question_seq))
sequence_y = answer_seq + [np.zeros(self.word_vec_dim)] * (self.max_seq_len-len(answer_seq))
sequence_xy = sequence_xy + sequence_y
sequence_y = [np.ones(self.word_vec_dim)] + sequence_y
xy_data.append(sequence_xy)
y_data.append(sequence_y)
return np.array(xy_data), np.array(y_data)

構造了訓練資料也建立好了模型,訓練的效果如下:

[root@centos #] python my_seq2seq_v2.py train
begin load vectors
words = 70937
size = 200
load vectors finish
---------------------------------
Run id: 9PZWKM
Log directory: /tmp/tflearn_logs/
---------------------------------
Training samples: 368
Validation samples: 0
--
Training Step: 47 | total loss: 0.62260
| SGD | epoch: 001 | loss: 0.62260 -- iter: 047/368

最終會生成./model/model模型檔案

效果預測

訓練好模型,我們希望能輸入一句話來預測一下回答,如下:

predict = model.predict(testXY)

因為我們只有question沒有answer,所以testXY中是沒有Y部分的,所以需要在程式中做一些改變,即用上一句的輸出作為下一句的輸入,如下:

for i in range(self.max_seq_len-1):
# next_dec_input = tf.slice(decoder_inputs, [0, i+1, 0], [-1, 1, self.word_vec_dim])這裡改成下面這句
next_dec_input = decoder_output_sequence_single
decoder_output_tensor = tflearn.lstm(next_dec_input, self.word_vec_dim, return_seq=False, reuse=True, scope='decoder_lstm')
decoder_output_sequence_single = tf.pack([decoder_output_tensor], axis=1)
decoder_output_sequence_list.append(decoder_output_tensor)

因為詞向量是多維浮點數,預測出的詞向量需要通過餘弦相似度來匹配,餘弦相似度匹配方法如下:

def vector2word(vector):
max_cos = -10000
match_word = ''
for word in word_vector_dict:
v = word_vector_dict[word]
cosine = vector_cosine(vector, v)
if cosine > max_cos:
max_cos = cosine
match_word = word
return (match_word, max_cos)

其中的vector_cosine實現如下:

def vector_cosine(v1, v2):
if len(v1) != len(v2):
sys.exit(1)
sqrtlen1 = vector_sqrtlen(v1)
sqrtlen2 = vector_sqrtlen(v2)
value = 0
for item1, item2 in zip(v1, v2):
value += item1 * item2
return value / (sqrtlen1*sqrtlen2)

其中的vector_sqrtlen實現如下:

def vector_sqrtlen(vector):
len = 0
for item in vector:
len += item * item
len = math.sqrt(len)
return len

預測效果如下:

輸入是“真 討厭”

預測結果:

[root@centos #] python my_seq2seq_v2.py test test.data
begin load vectors
words = 70937
size = 200
load vectors finish
predict answer
竟然 0.796628661264 8.13188244428
是 0.361905373571 4.72316883181
你 0.416023172832 3.78265507983
啊 0.454288467277 3.13229596833
不是 0.424590214456 2.90688231062
你 0.489174557107 2.62733802498
啊 0.501460288258 2.87990178439
你 0.560230783333 3.09066126524

輸出的第一列是預測的每個時序產生的詞,第二列是預測輸出向量和最近的詞向量的餘弦相似度,第三列是預測向量的歐氏距離

因為我們設計的max_seq_len是定長8,所以輸出的序列最後會多餘一些字,可以根據餘弦相似度或者其他指標設定一個閾值來截斷

以上列出的是部分程式碼,全部程式碼分享在https://github.com/warmheartli/ChatBotCourse/blob/master/chatbotv2/my_seq2seq_v2.py歡迎觀看


原網站:http://www.toutiao.com/i6373769687162946049/

相關文章