深度學習應用於聊天機器人(二) ——在Tensorflow上搭建基於檢索的模型

深度學習應用於聊天機器人(二) ——在Tensorflow上搭建基於檢索的模型

來自專欄 行政妹子的python進階之路

基於檢索的聊天機器人

這篇文章將介紹如何實現基於檢索的聊天機器人。基於檢索的模型有一個預定義回答的資料庫可以利用,然而生成模型可以生成從未碰到過的內容,這就是基於檢索模型和生成模型的不同點。更正式地來講,輸入數據在基於檢索的模型中對應了一個上下文和潛在回答。模型的輸出結果就是相應的分數的輸出,因為為了在模型中找出一個更好的答案,你就不得不計算所有回答的分數,並且選擇一個分數最高的答案。

但是為什麼在我們有能力搭建一個生成模型時,還要選擇搭建一個基於檢索的模型呢?生成模型似乎是更加靈活,因為它不需要一個預定義回復的資料庫,到底是不是這樣的呢?

問題是生成模型在實際的操作中效果並不好,至少現在是這樣的。這是因為生成模型在產生新的內容時有太多的自由空間,就會出現一些語法錯誤,並且還會出現回復一些不相關、寬泛的、不一致的內容的情況。他還需要大量的訓練數據並且還很難優化。現如今大部分的生成系統都是基於檢索的模型,或者是基於檢索模型與生成模型的結合。谷歌的Smart Reply就是一個很好的例子。生成模型在研究領域很火,但是我們現在還沒達到一定的水平。現在想搭建一個會話系統的話,最好的方法還是基於檢索的模型。

Ubuntu 對話語料庫

在這篇文章中我們將利用Ubuntu對話語料庫(paper, github)。Ubuntu對話語料庫(UDC)是最大可用的公共對話數據集之一。它內容來自公共IRC網路上的Ubuntu頻道的聊天日誌。該文將詳細的解釋語料到底如何創建的,所以這裡不在重複。然而,需要了解一下我們正在使用的是什麼樣的數據,所以先進行一些探索。

訓練數據包括1,000,000個實例,50%陽性(標記1)和50%陰性(標記0)。各實施例包括一個的上下文,話語,上下文的響應。正標籤意味著話語是對上下文的實際回答,而負標籤意味著話語不是實際回答——它是從語料庫中的某處隨機挑選的。這裡是一些示例數據。

值得注意的是該數據集生成腳本已經為我們做了一堆預處理的工作,它已經使用輸出NLTK工具進行標記,去除和lemmatized。腳本還用特殊標記替換了名稱,位置,組織,URL和系統路徑等實體。這種預處理不是絕對必要的,但它可能會提高性能幾個百分點。每個上下文語境的平均字數是86,每個句子包含17個字。查看Jupyter筆記以查看數據分析。

數據集包含測試和驗證集。它們的格式不同於訓練數據的格式。測試/驗證集中的每個記錄由一個上下文語境,真實話語(真實的回答)和9個被稱為干擾項不正確話語組成。模型的目的是將最高得分分配給真實話語,並將較低分數匹配到錯誤話語。

有多種方法來評估模型的執行效果。一個常用的度量方法是recall @ k。Recall @ k意味著我們在模型的10個可能的回答中挑選K個最佳回答(1個真實和9個干擾)。如果真實的回答也被挑選,那麼我們就將該測試示例標記為正確。因此,k的值越大就意味著任務變得更容易。如果我們設置k=10我們的成功率將會是100%,因為我們只有10個回答選項。如果我們設置k = 1,模型只有一個機會選擇正確的回復。

你可能想知道這9個干擾項是如何選擇的呢?這9個干擾項是在數據集中隨機挑選的。但是在現實世界中,你可能有數百萬的可能的回答,你不知道哪一個是正確的。你不可能對這數百萬個回答都進行評分選出最高分,這樣做的代價太大了。Google的Smart Reply使用聚類技術生成一組可能回復的數據集,然後再從這個數據集中挑選最優回答。如果你只有幾百個可能的回答,那就把所有的都評估一遍。

基線

在開始使用神經網路模型之前,讓我們構建一些簡單的基線模型,幫助了解我們可以期許或者達到什麼樣的執行效果。我們將使用以下函數來測評我們的recall@k

y是按預測分數列表的降序排列的,並且y_test是真實標籤。例如,一個y的[0,3,1,2,5,6,4,7,8,9]列表中,0所對應的語句是得分最高的,9代表得分最低。在對於每個測試示例都10個話語,第一個(索引0)總是正確的,因為它是列在數據中的其他干擾項列之前的。

憑直覺來說,如果k的取值為1,即recall@1,完全隨機預測的得分將會是整個分數的10%,recall@2將會是20%,等等。讓我們看看是不是這樣。

從結果來看好像是這樣的。當然,我們想要的不只是一個隨機預測。在原文件中討論的另一個基線是tf-idf預測器。tf-idf代表詞頻 ——逆文檔」頻率,它測量詞相對於整個語料庫的重要程度。在這裡就不多贅述一些細節(你可以在網路上找到關於tf-idf的許多教程),具有相似內容的文檔將具有類似的tf-idf向量。如果上下文語境和回答具有相似的單詞,則它們可能是一組正確的匹配,至少比隨機的可能性更高。許多圖書館(如scikit-learn)帶有內置的tf-idf函數,所以它很容易使用。讓我們構建一個tf-idf預測器,看看它的性能。

我們可以看到,tf-idf模型的性能明顯優於隨機模型。當然這離完美還很遠。我們做的假設不是那麼好。首先,回答並不一定跟上下文相似才是正確的。其次,tf-idf忽略字序,這可能是一個重要的信號。有了神經網路模型,我們可以做得更好。

雙編碼器LSTM

在這篇文章中我們介紹的深度學習模型為雙編碼器LSTM網路,他只是可以解決這個問題的模型之一,並不是最好的一種。你可以嘗試所有尚未嘗試過的深度學習架構,這是一個活躍的研究領域。例如,經常被用於機器翻譯的seq2seq模型可能在此任務上做得很好。我們要用雙編碼器的原因是它在數據集上表現出了良好的性能。這意味著我們知道期望什麼,並且可以確保我們的實現是正確的。將其他模型應用於此問題將是一個有趣的項目。

我們將構建的雙編碼器LSTM看起來像這樣:

它的工作流程大概是這樣的:

1. 上下文和回答內容都由詞分割,每個詞都嵌入到向量中。嵌入的詞用斯坦福的GloVe向量初始化,並在訓練過程中進行微調(邊註:這是可選的,在圖片中沒有顯示。我發現用GloVe初始化嵌入詞並沒有在模型性能方面有很大改觀)。

2. 嵌入的上下文和回答都被逐字地輸入到相同的遞歸神經網路中。RNN生成向量表示捕獲上下文語境和回答的「含義」(c和r出現在圖片里)。我們可以選擇這些向量應該有多大,但是我們選擇了256維。

3. 我們乘以c一個矩陣M來「預測」一個回答r。如果c是256維向量,則M是256×256維矩陣,回答就是另一個256維向量,我們可以將其解釋為生成的回答。矩陣M在訓練中學習。

4. 我們通過取這兩個向量的點積來測量預測回答r和實際回答r的相似性。點積越大表示向量越相似,並且回答會有一個很高的評分。然後,我們應用Sigmoid函數將該分數轉換為概率。在圖中步驟3和4是結合在一起的。我們將用損失(成本)函數來訓練神經網路,二次交叉熵損失解決分類問題。讓我們將上下文——回答對標為y。它可以是1(真是回答)或0(錯誤回答)。我們把上面第四步得到的概率稱為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、Tensorflow和TF Learn(對Tensorflow來說這是高級便利函數的組合)。

數據預處理

該數據集最初是CSV格式。我們可以直接使用CSV,但最好將數據轉換為Tensorflow的專有示例格式。(邊註:tf.learn似乎不支持tf.SequenceExample)。這種格式的主要好處是,它允許我們直接從輸入文件載入tensors,讓Tensorflow直接處理輸入數據的混排,批處理和排序的問題。作為預處理的一部分,我們還創建一個辭彙表。這意味著我們將每個詞映射到一個整數,例如「cat」可能變成2631。我們生成的文件TFRecord將存儲這些整數而不是字串。我們還將保存辭彙,以便我們可以從整數映射回詞。每個Example包含以下欄位:

context:表示上下文文本的單詞id的序列,例如 [231, 2190, 737, 0, 912]

context_len:上下文的長度,例如5對於上述示例

utterance:表示話語(響應)的字ids的序列。

utterance_len:話語的長度

label:僅在訓練數據中。0或1。

distractor_[N]:只有在測試/驗證數據。N的範圍從0到8.表示牽引器話語的詞ids的序列。

distractor_[N]_len:只有在測試/驗證數據。N的範圍從0到8.話語的長度。

我們將利用prepare_data.py做預處理並且生成3個文件:train.tfrecords,validation.tfrecords和test.tfrecords。您可以自己運行腳本或在此處下載數據文件。

創建輸入函數

為了使用Tensorflow的內置支持進行訓練和評估,我們需要創建一個輸入函數 - 一個返回我們輸入數據的批次的函數。事實上,因為我們的訓練和測試數據具有不同的格式,我們需要不同的輸入函數。輸入函數應返回一些特徵和標籤(如果可行的話)。例如:

因為我們在訓練和評估期間需要不同的輸入函數,並且由於我們討厭代碼重複,我們創建一個包裝器create_input_fn,為相應的模式創建一個輸入函數。它還需要幾個其他參數。這裡是我們使用的定義:

完整的代碼可以在這裡udc_inputs.py找到。該函數還可以執行以下操作:

1. 創建描述Example文件中欄位的要素定義

2. 用tf.TFRecordReader來閱讀input_files裡面的記錄

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.11, 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的話,就是正確的。

訓練代碼樣板

在編寫實際的神經網路代碼之前,我喜歡編寫用於訓練和評估模型的樣板代碼。這是因為,只要你的介面是正確的,很容易換出你正在使用的網路。假設有一個模型函數model_fn可以批量輸入特徵,標籤和模式(訓練或評測),並返回預測。我們可以編寫通用代碼來訓練我們的模型,代碼如下:

這裡我們為model_fn創建一個估計值,兩個輸入函數用於訓練和評估數據,以及評估指標字典。我們還定義了一個監視器,用於評測FLAGS.eval_every在訓練期間的每個步驟。最後一步,訓練模型。訓練將會無限期地運行,但Tensorflow會將查驗點文件自動保存在MODEL_DIR中,因此你可以隨時停止訓練。一個更好的技術可以用於早期停止,這意味著當驗證集度量停止改進(例如,出現過度擬合情況)時,你自動停止訓練。您可以在中查看完整代碼udc_train.py。在使用FLAGS時,我想簡單提兩點。這是一種向程序提供命令行參數的方法(類似於Python的argparse)。hparams是我們在hparams.py中創建的自定義對象,用於保存模型超參數、調整nobs。我們實例化時過程中將hparams對象賦予模型。

創建模型

現在我們已經設置了圍繞輸入,解析,評估和訓練的樣板代碼,接下來就是為Dual LSTM神經網路編寫代碼了。因為我們有不同格式的訓練和評估數據,我寫了一個create_model_fn包裝器,它負責為我們提供正確的格式數據。它接受一個model_impl參數,它是一個實際進行預測的函數。在我們的例子中,它是我們上面描述的雙編碼器LSTM,但我們可以輕鬆地交換出來一些其他神經網路。下面是實際操作代碼:

完整的代碼在dual_encoder.py中。鑒於此,現在可以在我們之前定義的udc_train.py的主線程中實例化模型函數。

我們現在可以運行python udc_train.py了,它可以訓練我們的網路,偶爾評估驗證數據(你可以選擇用--eval_every交換機進行評估的頻率)。你可以通過運行pythonudc_train.py --help來獲取我們用tf.flags,hparams定義的所有命令行標籤。

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.405653

...

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

...

模型評測

模型訓練完成之後,您可以使用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上大概一個小時),我們的模型在測試集上獲得以下結果:

recall_at_1 = 0.507581018519

recall_at_2 = 0.689699074074

recall_at_5 = 0.913020833333

雖然recall @ 1跟TFIDF模型相近,但是recall@ 2和recall@ 5明顯更好,這表明我們的神經網路將更高的分數到分配給了正確的答案。在原來的文章中recall@ 1、recall@ 2、recall@ 5的得分分別是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編碼器做得更好。在超參數優化,預處理步驟方面也還有改進的空間。

aHR0cDovL3dlaXhpbi5xcS5jb20vci95empPMXFQRVN2NDFyZWUxOTIzWg== (二維碼自動識別)

If you like it, send it to your friends;

if not, send it to your enemies.


推薦閱讀:

怎麼從與陌生人聊天的愉悅感中抽離出來?
和男朋友不是一類,總是聊不起來怎麼辦?
怎樣和女生聊天不冷場,讚美女孩獨闢蹊徑!
性格內向 面對面不太會聊天 如何改善這種問題?
看完獵頭挖人對話,我深深的嘆口氣,這樣的手法,憑什麼挖走人才

TAG:深度學習DeepLearning | 聊天 | 機器人 |