聊天機器人(chatbot)終極指南:天然語言處理(NLP)和深度機器學習(Deep Machine Learning)

 

在過去的幾個月中,我一直在收集天然語言處理(NLP)以及如何將NLP和深度學習(Deep Learning)應用到聊天機器人(Chatbots)方面的最好的資料。python

時不時地我會發現一個出色的資源,所以我很快就開始把這些資源編製成列表。 不久,我就發現本身開始與bot開發人員和bot社區的其餘人共享這份清單以及一些很是有用的文章了。git

在這個過程當中,個人名單變成了一個指南,通過一些好友的敦促和鼓勵,我決定和你們分享這個指南,或許是一個精簡的版本 - 因爲長度的緣由。github

這個指南主要基於Denny Britz所作的工做,他深刻地探索了機器人開發中深度學習技術的利用。 文章中包含代碼片斷和Github倉,好好利用!算法

閒話不扯了…讓咱們開始吧!數據庫

概述:聊天機器人開發中的深度學習技術

deep learning

聊天機器人是一個熱門話題,許多公司都但願可以開發出讓人沒法分辨真假的聊天機器人,許多人聲稱可使用天然語言處理(NLP)和深度學習(Deep Learning)技術來實現這一點。 可是人工智能(AI)如今吹得有點過了,讓人有時候很難從科幻中分辨出事實。ubuntu

在本系列中,我想介紹一些用於構建對話式代理(conversational agents)的深度學習技術,首先我會解釋下,如今咱們所處的位置,而後我會介紹下,哪些是可能作到的事情,哪些是至少在一段時間內幾乎不可能實現的事情。數組

模型分類

hand

基於檢索的模型 VS. 生成式模型

基於檢索的模型(retrieval-based model)更容易實現,它使用預約義響應的數據庫和某種啓發式推理來根據輸入(input)和上下文(context)選擇適當的響應(response)。 啓發式推理能夠像基於規則(rule based)的表達式匹配同樣簡單,或者像機器學習中的分類器集合(classifier ensemble)同樣複雜。 這些系統不會產生任何新的文本,他們只是從固定的集合中選擇一個響應。網絡

成式模型(generative model)要更難一些,它不依賴於預約義的響應,徹底從零開始生成新的響應。 生成式模型一般基於機器翻譯技術,但不是從一種語言翻譯到另外一種語言,而是從輸入到輸出(響應)的「翻譯」:架構

seq2seq model

兩種方法都有明顯的優勢和缺點。 因爲使用手工打造的存儲庫,基於檢索的方法不會產生語法錯誤。 可是,它們可能沒法處理沒有預約義響應的場景。 出於一樣的緣由,這些模型不能引用上下文實體信息,如前面提到的名稱。 生成式模型更「更聰明」一些。 它們能夠引用輸入中的實體,給人一種印象,即你正在與人交談。 然而,這些模型很難訓練,並且極可能會有語法錯誤(特別是在較長的句子上),而且一般須要大量的訓練數據。app

深度學習技術既能夠用於基於檢索的模型,也能夠用於生成式模型,可是chatbot領域的研究彷佛正在向生成式模型方向發展。 像seq2seq這樣的深度學習體系結構很是適合l來生成文本,研究人員但願在這個領域取得快速進展。 然而,咱們仍然處於創建合理、良好的生成式模型的初期階段。如今上線的生產系統更多是採用了基於檢索的模型。

對話的長短

chat cloud

對話越長,就越難實現自動化。 一種是短文本對話(更容易實現) ,其目標是爲單個輸入生成單個響應。 例如,你可能收到來自用戶的特定問題,並回復相應的答案。 另外一種是很長的談話(更難實現) ,談話過程會經歷多個轉折,須要跟蹤以前說過的話。 客戶服務中的對話一般是涉及多個問題的長時間對話。

開放領域 VS. 封閉領域

domain over model

開放領域的chatbot更難實現,由於用戶 不必定有明確的目標或意圖。 像TwitterReddit這樣的社交媒體網站上的對話一般是開放領域的 - 他們能夠談論任何方向的任何話題。 無數的話題和生成合理的反應所須要的知識規模,使得開放領域的聊天機器人實現至關困難。

「開放領域 :能夠提出一個關於任何主題的問題,並期待相關的迴應,這很難實現。考慮一下,若是就抵押貸款再融資問題進行交談的話,實際上你能夠問任何事情「 —— 馬克·克拉克

封閉領域的chatbot比較容易實現,可能的輸入和輸出的空間是有限的,由於系統試圖實現一個很是特定的目標。 技術支持或購物助理是封閉領域問題的例子。 這些系統不須要談論政治,只須要儘量有效地完成具體任務。 固然,用戶仍然能夠在任何他們想要的地方進行對話,但系統並不須要處理全部這些狀況 - 用戶也不指望這樣作。

「封閉領域 :能夠問一些關於特定主題的有限的問題,更容易實現。好比,邁阿密天氣怎麼樣?「

「Square 1邁出了一個聊天機器人的可喜的第一步,它代表了可能不須要智能機器的複雜性,也能夠提供商業和用戶價值。

」Square 2使用了能夠生成響應的智能機器技術。 生成的響應容許Chatbot處理常見問題和一些不可預見的狀況,而這些狀況沒有預約義的響應。 智能機器能夠處理更長的對話而且看起來更像人。 可是生成式響應增長了系統的複雜性,並且每每是增長了不少的複雜性。

咱們如今在客服中心解決這個問題的方法是,當有一個沒法預知的狀況時,在自助服務中將沒有預約義的迴應 ,這時咱們會把呼叫傳遞給一個真人「 Mark Clark

共同的挑戰

在構建聊天機器人時,有一些挑戰是顯而易見的,還有一些則不那麼明顯,這些挑戰中的大部分都是如今很活躍的研究領域。

使用上下文信息

dialog

爲了產生明智的反應,系統可能須要結合語言上下文和實物上下文 。 在長時間的對話中,人們會跟蹤說過的內容以及所交換的信息。上圖是使用語言上下文的一個例子。最多見的實現方法是將對話嵌入到向量(vector)中,可是長時間的對話對這一技術帶來了挑戰。兩個相關的論文:「使用生成式層級神經網絡模型構建端到端的對話系統」以及「在神經網絡對話模型中使用有目的的注意力」,都在朝着這個方向發展。此外,還可能須要在上下文中合併其餘類型的數據,例如日期/時間,位置或關於用戶的信息。

一致的個性

personality

理想狀況下,當生成響應時代理應當對語義相同的輸入產生一致的答案。 例如,對於這兩個問題:「你幾歲了?」和「你的年齡是?」,你會指望獲得一樣的回答。 這聽起來很簡單,可是如何將這種固定的知識或者說「個性」歸入到模型裏,仍是一個須要研究的問題。許多系統學能夠生成語言上合理的響應,可是它們的訓練目標並不包括產生語義一致的反應。 一般這是由於它們接受了來自多個不一樣用戶的大量數據的訓練。 相似於論文「基於角色的神經對話模型」中的模型,正在向爲個性建模的方向邁出第一步。 
persona

模型的評估

評估聊天代理的理想方法是衡量是否在給定的對話中完成其任務,例如解決客戶支持問題。 可是這樣的標籤(label)的獲取成本很高,由於它們須要人爲的判斷和評估。有時候沒有良好定義的目標,就像在開放領域域的模型同樣。通用的衡量指標,如BLEU, 最初是用於機器翻譯的,它基於文本的匹配,所以並非特別適合於對話模型的衡量,由於一個明智的響應可能包含徹底不一樣的單詞或短語。 事實上,在論文「 對話響應生成的無監督評估指標的實證研究」中,研究人員發現,沒有任何經常使用指標與人類的判斷具備真正相關性。

siri chatbot

意圖和多樣性

生成式系統的一個常見問題是,它們每每會生成一些相似於「很棒!」或「我不知道」之類的沒有養分的響應,這些響應能夠應對不少輸入。 谷歌智能答覆的早期版本傾向於用「我愛你」來回應幾乎任何事情。這一現象的部分根源在於這些系統是如何訓練的,不管是在數據方面仍是在實際的訓練目標和算法方面。 一些研究人員試圖經過各類目標函數(Object function)來人爲地促進多樣性 。 然而,人類一般會產生特定於輸入的反應並帶有意圖。 由於生成式系統(特別是開放域系統)沒有通過專門的意圖訓練,因此缺少這種多樣性。

如今能實現到什麼程度?

基於目前全部前沿的研究,咱們如今處於什麼階段,這些系統的實際工做狀況到底怎麼樣? 再來看看咱們的模型分類。 基於檢索的開放領域系統顯然是不可能實現的,由於你永遠不可能手工製做足夠的響應來覆蓋全部的狀況。 生成式的開放域系統幾乎是通用人工智能(AGI:Artificial General Intelligence),由於它須要處理全部可能的場景。 咱們離這個的實現還很遠(可是在這個領域正在進行大量的研究)。

這就給咱們剩下了一些限定領域的問題,在這些領域中,生成式和基於檢索的方法都是合適的,對話越長,情境越重要,問題就越困難。

(前)百度首席科學家Andrew Ng 最近接受採訪時說:

現階段深度學習的大部分價值能夠體如今一個能夠得到大量的數據的狹窄領域。 下面是一個它作不到的例子:進行一個真正有意義的對話。 常常會有一些演示,利用一些精挑細選過的對話,讓它看起來像是在進行有意義的對話,但若是你真的本身去嘗試和它對話,它就會很快地偏離正常的軌道。

許多公司開始將他們的聊天外包給人力工做者,並承諾一旦他們收集了足夠的數據就能夠「自動化」。 這隻有在一個很是狹窄的領域運行時纔會發生 - 好比說一個叫Uber的聊天界面。 任何開放的領域(好比銷售電子郵件)都是咱們目前沒法作到的。 可是,咱們也能夠經過提出和糾正答案來利用這些系統來協助工做人員。 這更可行。

生產系統中的語法錯誤是很是昂貴的,由於它們可能會把用戶趕跑。 這就是爲何大多數系統可能最好採用基於檢索的方法,這樣就沒有語法錯誤和攻擊性的反應。 若是公司可以以某種方式掌握大量的數據,那麼生成式模型就變得可行 - 可是它們必須輔以其餘技術,以防止它們像微軟的Tay那樣脫軌。

用TENSORFLOW實現一個基於檢索的模型

本教程的代碼和數據在Github上。

基於檢索的博客

當今絕大多數的生產系統都是基於檢索的,或者是基於檢索的和生成式相結合。 Google的Smart Reply就是一個很好的例子。 生成式模型是一個活躍的研究領域,但咱們還不能很好的實現。 若是你如今想構建一個聊天代理,最好的選擇就是基於檢索的模型。

UBUNTU DIALOG CORPUS

在這篇文章中,咱們將使用Ubuntu對話語料庫( 論文 , github )。 Ubuntu 對話語料庫(UDC)是可用的最大的公共對話數據集之一。 它基於公共IRC網絡上的Ubuntu頻道的聊天記錄。 論文詳細說明了這個語料庫是如何建立的,因此在這裏我再也不重複。 可是,瞭解咱們正在處理的是什麼樣的數據很是重要,因此咱們先作一些數據方面的探索。

訓練數據包括100萬個樣例,50%的正樣例(標籤1)和50%的負樣例(標籤0)。 每一個樣例都包含一個上下文 ,即直到這一點的談話記錄,以及一個話語 (utterance),即對上下文的迴應。 一個正標籤意味着話語是對當前語境上下文的實際響應,一個負標籤意味着這個話語不是真實的響應 - 它是從語料庫的某個地方隨機挑選出來的。 這是一些示例數據:

udc dialogs

請注意,數據集生成腳本已經爲咱們作了一堆預處理 - 它使用NLTK工具對輸出進行了分詞(tokenize), 詞幹處理(stem)和詞形 規範化(lemmatize) 。 該腳本還用特殊的標記替換了名稱,位置,組織,URL和系統路徑等實體(entity)。 這個預處理並非絕對必要的,但它可能會提升幾個百分點的性能。 上下文的平均長度是86字,平均話語長17字。 使用Jupyter notebook來查看數據分析 。

數據集拆分爲測試集和驗證集。 這些格式與訓練數據的格式不一樣。 測試/驗證集合中的每一個記錄都包含一個上下文,一個基準的真實話語(真實的響應)和9個不正確的話語,稱爲干擾項(distractors) 。 這個模型的目標是給真正的話語分配最高的分數,並調低錯誤話語的分數。

udc data

有多種方式能夠用來評估咱們的模型作得如何。 經常使用的衡量指標是k召回(recall@k ),它表示咱們讓模型從10個可能的回答中選出k個最好的回答(1個真實和9個干擾)。 若是正在選中的回答中包含正確的,咱們就將該測試示例標記爲正確的。 因此,更大的k意味着任務變得更容易。 若是咱們設定k = 10,咱們獲得100%的召回,由於咱們只有10個回答。 若是咱們設置k = 1,模型只有一個機會選擇正確的響應。

此時你可能想知道如何選擇9個干擾項。 在這個數據集中,9個干擾項是隨機挑選的。 然而,在現實世界中,你可能有數以百萬計的可能的反應,你不知道哪個是正確的。 你不可能評估一百萬個潛在的答案,選擇一個分數最高的答案 - 這個成本過高了。 Google的「 智能答覆」使用集羣技術來提出一系列可能的答案,以便從中選擇。 或者,若是你只有幾百個潛在的迴應,你能夠對全部可能的迴應進行評估。

基準

在開始研究神經網絡模型以前,咱們先創建一些簡單的基準模型,以幫助咱們理解能夠期待什麼樣的性能。 咱們將使用如下函數來評估咱們的recall@ k指標:




1 def evaluate_recall(y, y_test, k=1):
2     num_examples = float(len(y))    
3     num_correct = 0 
4     for predictions, label in zip(y, y_test):   
5         if label in predictions[:k]:    
6             num_correct += 1    
7     return num_correct/num_examples

這裏,y是咱們按照降序排序的預測列表,y_test是實際的標籤。 例如,[0,3,1,2,5,6,4,7,8,9]中的ay表示話語0得分最高,話語9得分最低。 請記住,對於每一個測試樣例,咱們有10個話語,第一個(索引0)始終是正確的,由於咱們數據中的話語列位於干擾項以前。

直覺是,一個徹底隨機的預測器也應該能夠在recall@ 1指標上拿10分,在recall@2指標上得20分,依此類推。 讓咱們來看看是不是這種狀況:

 1 # Random Predictor
 2 def predict_random(context, utterances):
 3     return np.random.choice(len(utterances), 10, replace=False)
 4 
 5 # Evaluate Random predictor
 6 y_random = [predict_random(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
 7 
 8 y_test = np.zeros(len(y_random))
 9 for n in [1, 2, 5, 10]:
10     print(「Recall @ ({}, 10): {:g}」.format(n, evaluate_recall(y_random, y_test, n)))
 

測試結果:



1 Recall @ (1, 10): 0.0937632
2 Recall @ (2, 10): 0.194503
3 Recall @ (5, 10): 0.49297
4 Recall @ (10, 10): 1

 

 

很好,看起來符合預期。 固然,咱們不僅是想要一個隨機預測器。 原始論文中討論的另外一個基準模型是一個tf-idf預測器。 tf-idf表明「term frequency - inverse document frequency」,它衡量文檔中的單詞與整個語料庫的相對重要性。 這裏不闡述具體的的細節了(你能夠在網上找到許多關於tf-idf的教程),那些具備類似內容的文檔將具備相似的tf-idf向量。 直覺上講,若是上下文和響應具備類似的詞語,則它們更多是正確的配對。 至少比隨機更可能。 許多庫(如scikit-learn 都帶有內置的tf-idf函數,因此它很是易於使用。 如今,讓咱們來構建一個tf-idf預測器,看看它的表現如何。

 1 class TFIDFPredictor:
 2     def __init__(self): 
 3         self.vectorizer = TfidfVectorizer() 
 4 
 5     def train(self, data):
 6         self.vectorizer.fit(np.append(data.Context.values,
 7                                 data.Utterance.values))
 8     def predict(self, context, utterances):
 9         # Convert context and utterances into tfidf vector
10         vector_context = self.vectorizer.transform([context])
11         vector_doc = self.vectorizer.transform(utterances)
12 
13         # The dot product measures the similarity of the resulting vectors
14         result = np.dot(vector_doc, vector_context.T).todense()
15         result = np.asarray(result).flatten()
16 
17         # Sort by top results and return the indices in descending order
18         return np.argsort(result, axis=0)[::-1]
19 
20 
21 # Evaluate TFIDF predictor
22 pred = TFIDFPredictor()
23 pred.train(train_df)
24 
25 y = [pred.predict(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
26 
27 for n in [1, 2, 5, 10]:
28     print(「Recall @ ({}, 10): {:g}」.format(n, evaluate_recall(y, y_test, n)))

 

 

運行結果:

Recall @ (1, 10): 0.495032 Recall @ (2, 10): 0.596882 Recall @ (5, 10): 0.766121 Recall @ (10, 10): 1

咱們能夠看到tf-idf模型比隨機模型表現得更好。 儘管如此,這還不夠完美。 咱們所作的假設不是很好。 首先,響應不必定須要與上下文類似纔是正確的。 其次,tf-idf忽略了詞序,這多是一個重要的改進信號。 使用一個神經網絡模型,咱們應該能夠作得更好一點。

雙編碼器LSTM

咱們將在本文中構建的深度學習模型稱爲雙編碼器LSTM網絡(Dual Encoder LSTM Network)。 這種類型的網絡只是能夠應用於這個問題的衆多網絡之一,並不必定是最好的。 你能夠嘗試各類深度學習架構 - 這是一個活躍的研究領域。 例如,常常在機器翻譯中使用的seq2seq模型在這個任務上可能會作得很好。 咱們打算使用雙編碼器的緣由是由於據報道它在這個數據集上性能不錯。 這意味着咱們知道該期待什麼,而且能夠確定咱們的絲線代碼是正確的。 將其餘模型應用於這個問題將是一個有趣的項目。

咱們將創建的雙編碼器LSTM看起來像這樣( 論文 ): 
dual encoder lstm

它的大體工做原理以下:

  1. 上下文和響應文本都是按照單詞分割的,每一個單詞都嵌入到一個向量中。 詞嵌入是用斯坦福大學的GloVe矢量進行初始化的,而且在訓練過程當中進行了微調(注:這是可選的,而且沒有在圖片中顯示,我發現用GloVe進行初始化對模型性能沒有太大的影響)。
  2. 嵌入的上下文和響應都逐字地輸入到相同的遞歸神經網絡(Recurrent Neural Network)中。 RNN生成一個矢量表示,不嚴格地說,這個表示捕捉了上下文和響應(圖片中的c和r)中的「含義」。 咱們能夠自由選擇矢量的大小,不過先選擇256個維度吧。
  3. 咱們用矩陣M乘以c來「預測」一個響應r'。 若是c是256維向量,則M是256×256維矩陣,結果是另外一個256維向量,咱們能夠將其解釋爲產生的響應。 矩陣M是在訓練中學習到的。
  4. 咱們經過取這兩個向量的點積來度量預測響應r'和實際響應r的類似度。 大的點積意味着兩個向量更類似,所以應該獲得高分。 而後,咱們應用sigmoid函數將該分數轉換爲機率。 請注意,步驟3和4在圖中組合在一塊兒。

爲了訓練網絡,咱們還須要一個損失(成本)函數。 咱們將使用分類問題中常見的二項交叉熵損失(binary cross-entropy loss)。 讓咱們將上下文響應的真實標籤稱爲y。 這能夠是1(實際響應)或0(不正確的響應)。 讓咱們把上面第4條中提到的預測機率稱爲y'。 而後,交叉熵損的計算公式爲L = -y * ln(y') - (1-y)* ln(1-y')。 這個公式背後的直覺很簡單。 若是y = 1,則剩下L = -ln(y'),這意味着對遠離1的預測加以懲罰;若是y = 0,則剩下L = -ln(1-y'),這懲罰了遠離0的預測。

咱們的實現將使用numpy ,pandas , TensorflowTF Learn ( Tensorflow的高層API)的組合。

數據預處理

原始的數據集是CSV格式。 咱們能夠直接使用CSV,但最好將咱們的數據轉換成Tensorflow專有的example格式。 (順便說一下:還有一個tf.SequenceExample,但tf.learn彷佛不支持這一格式)。 example格式的主要好處是它容許咱們直接從輸入文件加載張量(tensor),並讓Tensorflow來對輸入進行隨機排序(shuffle),批次處理(batch)和隊列處理(queue)。 做爲預處理的一部分,咱們還建立了一個詞表。 這意味着咱們將每一個單詞映射到一個整數,例如「cat」可能變成2631.咱們將生成的TFRecord文件,存儲的就是這些整數而不是字串。 咱們會保留詞表,以便後續能夠從整數映射回單詞。

每一個樣例包含如下字段:

  • context:表示上下文文本的詞序列,例如[231,2190,737,0,912]
  • context_len:上下文的長度,例如上面例子中的5
  • utterance:表示話語(響應)的一系列單詞id
  • utterance_len:話語的長度
  • label:標籤,在訓練數據中才有。 0或1。
  • distractor_ [N]:僅在測試/驗證數據中。 N的範圍從0到8.表明干擾項的詞序列id。
  • distractor_ [N] _len:僅在測試/驗證數據中。 N的範圍是從0到8.發音的長度。

預處理由Python腳本prepare_data.py 完成,該腳本生成3個文件:train.tfrecordsvalidation.tfrecordstest.tfrecords。 你能夠本身運行腳本或者在這裏下載數據文件 。

建立一個輸入函數

爲了使用Tensorflow內置的訓練和評估支持,咱們須要建立一個輸入函數 - 一個返回批量輸入數據的函數。 事實上,因爲咱們的訓練和測試數據有不一樣的格式,咱們須要不一樣的輸入功能。 輸入函數應返回一批特徵和標籤(若是可用)。 模板以下:

def input_fn(): # TODO Load and preprocess data here return batched_features, labels

 

 

由於在訓練和評估過程當中咱們須要不一樣的輸入函數,而且由於咱們討厭複製代碼,因此咱們建立了一個名爲create_input_fn的包裝器,以便爲相應的模式(mode)建立一個輸入函數。 它也須要一些其餘參數。 這是咱們使用的定義:

 

完整的代碼能夠在udc_inputs.py中找到。 這個函數主要執行如下操做:

  1. 建立描述樣例文件中字段的特徵定義(feature definition
  2. 使用tf.TFRecordReader從輸入文件中讀取記錄
  3. 根據特徵定義解析記錄
  4. 提取訓練標籤
  5. 將多個樣例和培訓標籤構形成一個批次
  6. 返回批次

定義評估指標

咱們已經提到,咱們要使recall@ k指標來評估咱們的模型。 幸運的是,Tensorflow預置了不少咱們可使用的標準的評估指標,包括recall@ k。 要使用這些指標,咱們須要建立一個從指標名稱映射到函數(以預測和標籤爲參數)的字典:

 

上面代碼中,咱們使用functools.partial將一個帶有3個參數的函數轉換爲只帶有2個參數的函數。 不要讓名稱streaming_sparse_recall_at_k把你搞糊塗。 streaming只是意味着指標是在多個批次上累積的,而sparse則是指咱們標籤的格式。

這帶來了一個重要的問題:評估過程當中咱們的預測究竟是什麼格式? 在訓練期間,咱們預測樣例正確的機率。 可是在評估過程當中,咱們的目標是對話語和9個干擾項進行評分,並挑選分最高的一個 - 咱們不能簡單地預測正確仍是不正確。 這意味着在評估過程當中,每一個樣例都應該獲得一個有10個分值的向量,例如[0.34,0.1,0.22,0.45,0.01,0.02,0.03,0.08,0.33,0.11],每個分數分別對應於真實的響應和9個干擾項。 每一個話語都是獨立評分的,因此機率不須要加起來爲1.由於真正的響應在數組中老是爲0,因此每一個例子的標籤都是0。上面的例子將被recall@ 1指標視爲分類錯誤,由於第三個干擾項的機率是0.45,而真實的回答只有0.34。 然而,它會被recall@ 2指標視爲正確的。

brain

訓練代碼樣板

在編寫實際的神經網絡代碼以前,我喜歡編寫用於訓練和評估模型的樣板代碼。 這是由於,只要你堅持正確的接口,很容易換出你使用的是什麼樣的網絡。 假設咱們有一個模型函數model_fn,它以批次特徵,標籤和模式(訓練或評估)做爲輸入,並返回預測結果。 那麼咱們能夠編寫以下的通用代碼來訓練咱們的模型:

estimator = tf.contrib.learn.Estimator(
    model_fn=model_fn,
    model_dir=MODEL_DIR,
    config=tf.contrib.learn.RunConfig())

input_fn_train = udc_inputs.create_input_fn(
    mode=tf.contrib.learn.ModeKeys.TRAIN,
    input_files=[TRAIN_FILE],
    batch_size=hparams.batch_size)

input_fn_eval = udc_inputs.create_input_fn(
    mode=tf.contrib.learn.ModeKeys.EVAL,
    input_files=[VALIDATION_FILE],
    batch_size=hparams.eval_batch_size,
    num_epochs=1)

eval_metrics = udc_metrics.create_evaluation_metrics()

# We need to subclass theis manually for now. The next TF version will
# have support ValidationMonitors with metrics built-in.
# It’s already on the master branch.
class EvaluationMonitor(tf.contrib.learn.monitors.EveryN):
    def every_n_step_end(self, step, outputs):
        self._estimator.evaluate(
            input_fn=input_fn_eval,
            metrics=eval_metrics,
            steps=None)

eval_monitor = EvaluationMonitor(every_n_steps=FLAGS.eval_every)
estimator.fit(input_fn=input_fn_train, steps=None, monitors=[eval_monitor])

 

 

 

在這裏,咱們爲model_fn,訓練和評估數據的兩個輸入函數以及評估指標字典建立了一個估計器。 咱們還定義了一個監視器,在訓練期間每隔FLAGS.eval_every_every指定的步數對模型進行評估。 最後,咱們訓練模型。 訓練過程能夠無限期地運行,但Tensorflow能夠自動地將檢查點文件保存在MODEL_DIR指定的目錄中,所以能夠隨時中止訓練。 一個更炫的技巧是使用早期中止,這意味着當驗證集指標中止改進時(即開始過擬合),將自動中止訓練。 你能夠在udc_train.py中看到完整的代碼。

我想簡要說起的兩件事是FLAGS的使用。 這是給程序提供命令行參數的一種方法(相似於Pythonargparse)。 hparams是咱們在hparams.py中建立的一個自定義對象,它包含用來調整模型的參數、超參數。 咱們在實例化模型時將這個hparams對象賦予給模型。

建立模型

如今咱們已經創建了關於輸入,解析,評估和訓練的樣板代碼,能夠爲咱們的Dual LSTM神經網絡編寫代碼了。 由於咱們有不一樣格式的訓練和評估數據,因此我寫了一個create_model_fn包裝器,它負責爲咱們提供正確的格式。 它接受一個model_impl參數,應當指向一個實際進行預測的函數。 在咱們的例子中就是上面介紹的雙編碼器LSTM,可是咱們能夠很容易地把它換成其餘的神經網絡。 讓咱們看看是什麼樣的:

 1 def dual_encoder_model(
 2     hparams,
 3     mode,
 4     context,
 5     context_len,
 6     utterance,
 7     utterance_len,
 8     targets):
 9 
10     # Initialize embedidngs randomly or with pre-trained vectors if available
11     embeddings_W = get_embeddings(hparams)
12 
13     # Embed the context and the utterance
14     context_embedded = tf.nn.embedding_lookup(
15         embeddings_W, context, name=」embed_context」)
16 
17     utterance_embedded = tf.nn.embedding_lookup(
18         embeddings_W, utterance, name=」embed_utterance」)
19 
20     # Build the RNN
21     with tf.variable_scope(「rnn」) as vs:
22         # We use an LSTM Cell
23         cell = tf.nn.rnn_cell.LSTMCell(
24             hparams.rnn_dim,
25             forget_bias=2.0,
26             use_peepholes=True,
27             state_is_tuple=True)
28 
29     # Run the utterance and context through the RNN
30     rnn_outputs, rnn_states = tf.nn.dynamic_rnn(
31         cell,
32         tf.concat(0, [context_embedded, utterance_embedded]),
33         sequence_length=tf.concat(0, [context_len, utterance_len]),
34         dtype=tf.float32)
35 
36     encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)
37 
38     with tf.variable_scope(「prediction」) as vs:
39         M = tf.get_variable(「M」,
40         shape=[hparams.rnn_dim, hparams.rnn_dim],
41         initializer=tf.truncated_normal_initializer())
42 
43     # 「Predict」 a response: c * M
44     generated_response = tf.matmul(encoding_context, M)
45     generated_response = tf.expand_dims(generated_response, 2)
46     encoding_utterance = tf.expand_dims(encoding_utterance, 2)
47 
48     # Dot product between generated response and actual response
49     # (c * M) * r
50     logits = tf.batch_matmul(generated_response, encoding_utterance, True)
51     logits = tf.squeeze(logits, [2])
52 
53     # Apply sigmoid to convert logits to probabilities
54     probs = tf.sigmoid(logits)
55 
56     # Calculate the binary cross-entropy loss
57     losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))
58 
59     # Mean loss across the batch of examples
60     mean_loss = tf.reduce_mean(losses, name=」mean_loss」)
61 
62     return probs, mean_loss

 

完整的代碼在dual_encoder.py中 。 鑑於此,咱們如今能夠在咱們以前定義的udc_train.py的主例程中實例化咱們的模型函數。

model_fn = udc_model.create_model_fn( hparams=hparams, model_impl=dual_encoder_model)

 

好了! 咱們如今能夠運行python udc_train.py,它將開始訓練咱們的網絡,間或評估驗證數據的召回狀況(你能夠選擇使用-eval_every開關來選擇評估的頻率)。 要得到咱們使用tf.flagshparams定義的全部可用的命令行標誌的完整列表,你能夠運行python udc_train.py --help

1 INFO:tensorflow:training step 20200, loss = 0.36895 (0.330 sec/batch).
2 INFO:tensorflow:Step 20201: mean_loss:0 = 0.385877
3 INFO:tensorflow:training step 20300, loss = 0.25251 (0.338 sec/batch).
4 INFO:tensorflow:Step 20301: mean_loss:0 = 0.405653
5 6 INFO:tensorflow:Results after 270 steps (0.248 sec/batch): recall_at_1 = 0.507581018519, recall_at_2 = 0.689699074074, recall_at_5 = 0.913020833333, recall_at_10 = 1.0, loss = 0.5383
7

 

評估模型

在你訓練完模型以後,你能夠在測試集上使用python udc_test.py - model_dir = $ MODEL_DIR_FROM_TRAINING來評估它,例如python udc_test.py - model_dir =〜/ github / chatbot-retrieval / runs / 1467389151。 這將在測試集而不是驗證集上運行recall@ k評估指標。 請注意,你必須使用在訓練期間使用的相同參數調用udc_test.py。 因此,若是你用 - embedding_size = 128進行訓練,就須要用相同的方法調用測試腳本。

通過約20,000步的訓練(在快速GPU上一個小時左右),咱們的模型在測試集上獲得如下結果:

1 recall_at_1 = 0.507581018519
2 recall_at_2 = 0.689699074074
3 recall_at_5 = 0.913020833333

 

 

雖然recall@ 1接近咱們的TFIDF模型,recall@ 2recall@ 5顯着更好,這代表咱們的神經網絡爲正確的答案分配了更高的分數。 原始論文中recall@1recall@2recall@5的值分別是0.55,0.72和0.92,可是我還沒能重現。 也許額外的數據預處理或超參數優化可能會使分數上升一點。

預測

你能夠修改並運行udc_predict.py,以獲取不可見數據的機率得分。 例如python udc_predict.py — model_dir=./runs/1467576365/,將獲得輸出:

1 Context: Example context
2 Response 1: 0.44806
3 Response 2: 0.481638

 


你能夠想象爲,在一個上下文中輸入100個潛在的響應,而後選擇一個最高分的。

結論

在這篇文章中,咱們已經實現了一個基於檢索的神經網絡模型,能夠根據對話上下文對潛在的響應打分。 然而,還有不少改進的餘地。 能夠想象,與雙LSTM編碼器相比,其餘神經網絡在這個任務上作得更好。 超參數優化還有不少空間,或者預處理步驟的改進。 本教程的代碼和數據在Github上,請查看

原文:Ultimate Guide to Leveraging NLP & Machine Learning for your Chatbot

相關文章
相關標籤/搜索