1. 程式人生 > >聊天機器人(chatbot)終極指南:自然語言處理(NLP)和深度機器學習(Deep Machine Learning)

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

為了這份愛

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

時不時地我會發現一個出色的資源,因此我很快就開始把這些資源編製成列表。 不久,我就發現自己開始與bot開發人員和bot社群的其他人共享這份清單以及一些非常有用的文章了。

在這個過程中,我的名單變成了一個指南,經過一些好友的敦促和鼓勵,我決定和大家分享這個指南,或許是一個精簡的版本 - 由於長度的原因。

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

閒話不扯了…讓我們開始吧!

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

deep learning

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

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

模型分類

hand

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

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

成式模型(generative model)要更難一些,它不依賴於預定義的響應,完全從零開始生成新的響應。 生成式模型通常基於機器翻譯技術,但不是從一種語言翻譯到另一種語言,而是從輸入到輸出(響應)的“翻譯”:

seq2seq model

兩種方法都有明顯的優點和缺點。 由於使用手工打造的儲存庫,基於檢索的方法不會產生語法錯誤。 但是,它們可能無法處理沒有預定義響應的場景。 出於同樣的原因,這些模型不能引用上下文實體資訊,如前面提到的名稱。 生成式模型更“更聰明”一些。 它們可以引用輸入中的實體,給人一種印象,即你正在與人交談。 然而,這些模型很難訓練,而且很可能會有語法錯誤(特別是在較長的句子上),並且通常需要大量的訓練資料。

深度學習技術既可以用於基於檢索的模型,也可以用於生成式模型,但是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召回([email protected] ),它表示我們讓模型從10個可能的回答中選出k個最好的回答(1個真實和9個干擾)。 如果正在選中的回答中包含正確的,我們就將該測試示例標記為正確的。 所以,更大的k意味著任務變得更容易。 如果我們設定k = 10,我們得到100%的召回,因為我們只有10個回答。 如果我們設定k = 1,模型只有一個機會選擇正確的響應。

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

基準

在開始研究神經網路模型之前,我們先建立一些簡單的基準模型,以幫助我們理解可以期待什麼樣的效能。 我們將使用以下函式來評估我們的[email protected] k指標:

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

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

直覺是,一個完全隨機的預測器也應該可以在[email protected] 1指標上拿10分,在[email protected]指標上得20分,依此類推。 讓我們來看看是否是這種情況:

# Random Predictor
def predict_random(context, utterances):
    return np.random.choice(len(utterances), 10, replace=False)

# Evaluate Random predictor
y_random = [predict_random(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]

y_test = np.zeros(len(y_random))
for n in [1, 2, 5, 10]:
    print(“Recall @ ({}, 10): {:g}”.format(n, evaluate_recall(y_random, y_test, n)))

測試結果:

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

很好,看起來符合預期。 當然,我們不只是想要一個隨機預測器。 原始論文中討論的另一個基準模型是一個tf-idf預測器。 tf-idf代表“term frequency - inverse document frequency”,它衡量文件中的單詞與整個語料庫的相對重要性。 這裡不闡述具體的的細節了(你可以在網上找到許多關於tf-idf的教程),那些具有相似內容的文件將具有類似的tf-idf向量。 直覺上講,如果上下文和響應具有相似的詞語,則它們更可能是正確的配對。 至少比隨機更可能。 許多庫(如scikit-learn 都帶有內建的tf-idf函式,所以它非常易於使用。 現在,讓我們來構建一個tf-idf預測器,看看它的表現如何。

class TFIDFPredictor:
    def __init__(self): 
        self.vectorizer = TfidfVectorizer() 

    def train(self, data):
        self.vectorizer.fit(np.append(data.Context.values,
                                data.Utterance.values))
    def predict(self, context, utterances):
        # Convert context and utterances into tfidf vector
        vector_context = self.vectorizer.transform([context])
        vector_doc = self.vectorizer.transform(utterances)

        # The dot product measures the similarity of the resulting vectors
        result = np.dot(vector_doc, vector_context.T).todense()
        result = np.asarray(result).flatten()

        # Sort by top results and return the indices in descending order
        return np.argsort(result, axis=0)[::-1]


# Evaluate TFIDF predictor
pred = TFIDFPredictor()
pred.train(train_df)

y = [pred.predict(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]

for n in [1, 2, 5, 10]:
    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的預測。

我們的實現將使用numpypandasTensorflowTF 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)建立一個輸入函式。 它也需要一些其他引數。 這是我們使用的定義:

def create_input_fn(mode, input_files, batch_size, num_epochs=None):
    def input_fn():
        # TODO Load and preprocess data here
        return batched_features, labels

    return input_fn

完整的程式碼可以在udc_inputs.py中找到。 這個函式主要執行以下操作:

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

定義評估指標

我們已經提到,我們要使[email protected] k指標來評估我們的模型。 幸運的是,Tensorflow預置了很多我們可以使用的標準的評估指標,包括[email protected] k。 要使用這些指標,我們需要建立一個從指標名稱對映到函式(以預測和標籤為引數)的字典:

def create_evaluation_metrics():
    eval_metrics = {}
    for k in [1, 2, 5, 10]:
        eval_metrics[“recall_at_%d” % k] = functools.partial(
            tf.contrib.metrics.streaming_sparse_recall_at_k,
            k=k)
    return eval_metrics

上面程式碼中,我們使用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。上面的例子將被[email protected] 1指標視為分類錯誤,因為第三個干擾項的概率是0.45,而真實的回答只有0.34。 然而,它會被[email protected] 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,但是我們可以很容易地把它換成其他的神經網路。 讓我們看看是什麼樣的:

def dual_encoder_model(
    hparams,
    mode,
    context,
    context_len,
    utterance,
    utterance_len,
    targets):

    # Initialize embedidngs randomly or with pre-trained vectors if available
    embeddings_W = get_embeddings(hparams)

    # Embed the context and the utterance
    context_embedded = tf.nn.embedding_lookup(
        embeddings_W, context, name=”embed_context”)

    utterance_embedded = tf.nn.embedding_lookup(
        embeddings_W, utterance, name=”embed_utterance”)

    # Build the RNN
    with tf.variable_scope(“rnn”) as vs:
        # We use an LSTM Cell
        cell = tf.nn.rnn_cell.LSTMCell(
            hparams.rnn_dim,
            forget_bias=2.0,
            use_peepholes=True,
            state_is_tuple=True)

    # Run the utterance and context through the RNN
    rnn_outputs, rnn_states = tf.nn.dynamic_rnn(
        cell,
        tf.concat(0, [context_embedded, utterance_embedded]),
        sequence_length=tf.concat(0, [context_len, utterance_len]),
        dtype=tf.float32)

    encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)

    with tf.variable_scope(“prediction”) as vs:
        M = tf.get_variable(“M”,
        shape=[hparams.rnn_dim, hparams.rnn_dim],
        initializer=tf.truncated_normal_initializer())

    # “Predict” a response: c * M
    generated_response = tf.matmul(encoding_context, M)
    generated_response = tf.expand_dims(generated_response, 2)
    encoding_utterance = tf.expand_dims(encoding_utterance, 2)

    # Dot product between generated response and actual response
    # (c * M) * r
    logits = tf.batch_matmul(generated_response, encoding_utterance, True)
    logits = tf.squeeze(logits, [2])

    # Apply sigmoid to convert logits to probabilities
    probs = tf.sigmoid(logits)

    # Calculate the binary cross-entropy loss
    losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))

    # Mean loss across the batch of examples
    mean_loss = tf.reduce_mean(losses, name=”mean_loss”)

    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

INFO:tensorflow:training step 20200, loss = 0.36895 (0.330 sec/batch).
INFO:tensorflow:Step 20201: mean_loss:0 = 0.385877
INFO:tensorflow:training step 20300, loss = 0.25251 (0.338 sec/batch).
INFO:tensorflow:Step 20301: mean_loss:0 = 0.405653INFO: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

評估模型

在你訓練完模型之後,你可以在測試集上使用python udc_test.py - model_dir = $ MODEL_DIR_FROM_TRAINING來評估它,例如python udc_test.py - model_dir =〜/ github / chatbot-retrieval / runs / 1467389151。 這將在測試集而不是驗證集上執行[email protected] k評估指標。 請注意,你必須使用在訓練期間使用的相同引數呼叫udc_test.py。 所以,如果你用 - embedding_size = 128進行訓練,就需要用相同的方法呼叫測試指令碼。

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

recall_at_1 = 0.507581018519
recall_at_2 = 0.689699074074
recall_at_5 = 0.913020833333

雖然[email protected] 1接近我們的TFIDF模型,[email protected] 2[email protected] 5顯著更好,這表明我們的神經網路為正確的答案分配了更高的分數。 原始論文中[email protected][email protected][email protected]的值分別是0.55,0.72和0.92,但是我還沒能重現。 也許額外的資料預處理或超引數優化可能會使分數上升一點。

預測

你可以修改並執行udc_predict.py,以獲取不可見資料的概率得分。 例如python udc_predict.py — model_dir=./runs/1467576365/,將得到輸出:

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

.
你可以想象為,在一個上下文中輸入100個潛在的響應,然後選擇一個最高分的。

結論

在這篇文章中,我們已經實現了一個基於檢索的神經網路模型,可以根據對話上下文對潛在的響應打分。 然而,還有很多改進的餘地。 可以想象,與雙LSTM編碼器相比,其他神經網路在這個任務上做得更好。 超引數優化還有很多空間,或者預處理步驟的改進。 本教程的程式碼和資料在Github上,請檢視