聊天機器人的開發思路

幾天前,我收到了一份來自 San Jose 的 intern 邀請,對方正致力於用深度學習的相關技術,來開發商務用途的對話式介面,因此特別著重在「潛在語意分析」與「自學習」這兩個議題上,這剛好和我目前的研究方向挺類似的,而與對方攀談了不少開發與改進的方向,趁著現在記憶還算清楚,趕緊把這些想法記錄下來。

my-chatbot

聊天機器人「眠寶」的雛形,目前仍在開發中

從最基本的出發

在切入聊天機器人的商務應用之前,我們先來談談怎麼開發一個聊天機器人吧!一般來說,實作上大抵脫離不了這三種模型:

  • 樣板式模型 (Rule-based model)
  • 檢索式模型 (Retrieval-based model)
  • 生成式模型 (Generative model)

接下來,我會先介紹一下各個模型的出發點與實作方式,和往常一樣,這篇教學並不著重在深究每個算法的實現原理,而是會專注在應用層面,告訴大家怎麼用簡單的思維與套件來實現各種模型,也會偶而延伸一些自然語言處理和機器學習的基礎概念,算是綜合了我這半年來的學習歷程,願能幫助到有志於此的朋友們。

樣板式模型

先從樣板式模型說起吧,因為它是最平易近人的,我們可以透過設計「規則」來讓機器人知道遇到什麼詞時,就該說什麼話,打個比方:

1
2
if "天氣" in user.query:
chatbot.say("今天天氣真好")

簡單明瞭對吧?不過要設計這個就得勞心勞力的寫規則,還要考慮到規則間的優先順序。如果你對這方面有興趣,我推薦從AIML入手,他以樣板語言的形式來紀錄規則,就像是:

1
2
3
4
5
6
7
8
<aiml version = "1.0.1" encoding = "UTF-8"?>
<category>
<pattern> HELLO </pattern>
<template>
WORLD !
</template>
</category>
</aiml>

當你說 HELLO,機器人就說 WORLD!,除此之外,AIML 也提供了簡單的記憶語學習功能,官方文件寫得挺詳細的,我這邊就不重複造輪子了。除了 AIML 外,我在語意圖中,也會提及另一種基於圖的走訪式比對,主要是在解決樣板式模型中比對搶佔的問題。

檢索式模型

再來聊聊檢索式模型吧,其實這東西的原理跟搜尋引擎很像,但是搜尋引擎給我們的是「頁面」,檢索式模型告訴我們的則是「答案」,用學術上的語言來說,檢索式模型其實就是類似問答系統,我們會維護問題與答案的配對:

$$
Questions = { q_1 , q_2 ,……, q_n }
$$

$$
Answers = { a_1 , a_2 ,……, a_n }
$$

再看看使用者的輸入和 Questions 中的哪一個 q_k 最接近,就把 a_k 裡的回覆丟給使用者。在這裡補充一下,正統的問答系統 q_ka_k 是經過分析後算出,它背後可能藏著一個知識庫,透過歸類使用者的句型與知識庫推理來計算出最適合的答案。

那麼你可能會好奇,先前我怎麼假設已經有建好的問答配對?能這麼做的原因是我們要開發的,並不是像 Watson 那種能參加益智節目的超級電腦,而是一個只會面向特定領域的應用,會被問到的問題也就不是包山包海,聊天機器人只要記住一些「常見問答」就足夠了。

IBM Waston

看看 Watson 大顯神通

話說回來了,這個模型的技術成分很顯然是「相似度」計算,那麼,我們該如何計算語句的相似度呢?

OKapi BM25

通常第一個浮出的想法是用「TF * IDF」來計算詞的權重,再取出同時出現在使用者問句與問題集裡的詞,計算出兩者的相似分數。

而你可能會問,有沒有東西能比 TFIDF 做得更好呢?有,答案是 OKapi BM25,這是搜尋引擎的一種實作手段,不過既然都提到搜尋引擎了,就讓我再跑題一下吧,以前 Google 在搜尋引擎實作中是使用 PageRank,這個方法在自然語言處理上能調整成 TextRank,可用於從一串詞中挑選出核心的關鍵詞,算是有別於 TF/IDF 另一種尋找主題的方式。

現在我們拉回 BM25 吧,看了 Wiki 的連結後,你可能會被那些繁雜的公式嚇到,實際上他想說的事情很簡單,我們將一個句子分成了很多個「詞」qi,每個詞都會一個「IDF」分數,「f(qi,D)」指的是在 D 這篇文檔中,qi 這個詞一共出現了幾次,撇開停用詞不談,顯然一個詞在文章裡出現次數越多,他就顯得越重要。

BM25

到目前為止都是 TFIDF 的概念,BM25 引入了 b 與 k1 這兩個基於經驗調整的參數,兩者中 b 又會顯得重要些,從分母的地方能看出來,b 越大,文章長度就會顯得越重要,那為什麼要將文章長度列入考慮呢?我們可以這麼想,一個文章越長,它包含 qi 的機率就會越高,這樣對短文本就不太公平了,或許前者是一段冗長又包山包海的廢文,後者則是一針見血的精闢見解,所以 BM25 引入 b ,就是為了要懲罰這種情形。

嗯…好像太過偏向理論了,讓我們看點應用吧, BM25 在 SnowNLP 中已經實作完了,可以直接調用,容我直接引用演示案例:

1
2
3
4
5
from snownlp import SnowNLP
s = SnowNLP([[u'这篇', u'文章'],
[u'那篇', u'论文'],
[u'这个']])
s.sim([u'文章'])# [0.3756070762985226, 0, 0]

最終傳回的是分數列表,結果是[u'这篇', u'文章']獲得最高分,如果想參考 BM25 的實作方式,可以看看SnowNLP是怎麼做的

編輯距離

什麼是編輯距離,我們能想成句子 A 要變成句子 B 要「最少」改幾個字,好比說:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我要成為海賊王
我要成為火影

改一個字
---

我要成為海賊王
我要成為海火影

改兩個字
---

我要成為海賊王
我要成為海賊影

改三個字
---

我要成為海賊王
我要成為海賊王

那這兩個句子的編輯距離就是 3 ,我們會發現編輯距離越短,某種程度上「句型」會越相似(雖然主題就不一定了),也會發現這其實是個動態規劃問題,正因如此,我們就直接從應用層出發吧(?)

燃燒你的小宇宙吧

抱歉了,我最討厭打表格了

在此推薦fuzzywuzzy,這是在 python 上基於萊文斯坦距離的實作,使用上非常直觀:

1
2
3
from fuzzywuzzy import fuzz
score = fuzz.ratio("this is a test", "this is a test!")
# score = 97

話又說回來了,剛剛不是說到這相似評估好像沒辦法涉及語意,沒錯,最基本的編輯距離是沒辦法,但如果把距離的定義從「字變成別的字」換成「詞變成別的詞的相似度差異」就又是另一回事了。

說好的機器學習呢?

你可能已經發現了,如果說我們把檢索的目標從一個一個q_k,換成某種對話的主題,我們就能把文本分類的技巧套用到檢索式模型上。以經典的新聞分類來看:

1
2
3
4
5
訓練集(文本、標籤):

("小熊續留賽揚強投? 明年1月與艾瑞塔談延長合約","體育")
("韓推古裝劇花郎小鮮肉演員吸睛","娛樂")
......

當用戶說「今天洋基隊的比分是多少」,我們的聊天機器人就知道用戶是在講體育賽事,用技術的語言來說,用戶的「意圖」是問體育賽事,這個句子還有「洋基隊」、「今天」、「比分」這三個特徵,所以我們能這麼做:

1
2
3
4
5
6
7
8
9
query = "今天洋基隊的比分是多少"

class = classifier.classify(query)

handler = get_task_handler(class)
features = handler.extract_features(query)
response = handler.get_response(features)

chatbot.say(response)

明白了運作流程後,顯然能看出技術成分有兩處:「分類」與「特徵抽取」,我先來談談分類吧,特徵抽取請參見後續的「實體識別」。

樸素貝葉斯分類

文本分類的方法很多,其中最簡單的莫過於樸素貝葉斯 (Naive bayes) 分類了,我覺得這算是條件機率的應用,我們透過 「Given」 某些詞來計算這串詞是某個類別的機率,比如說我們現在有兩個類別 AB,使用者的輸入是:

1
Naive bayes

我們就會去計算:

$$
P_a = P(class A \ | \ Naive \ bayes \ in \ the \ query)
$$

$$
P_b = P(class B \ | \ Naive \ bayes \ in \ the \ query)
$$

如果 Pa > Pb 就是 A 類別,反之為 B 類別,你可能會好奇這怎麼辦的到,簡單來說的話,我們會在訓練時去求得:

$$
P(Naive \ bayes \ in \ the \ query \ | \ class A)
$$

$$
P(Naive \ bayes \ in \ the \ query \ | \ class B)
$$
所以能夠過條件機率的公式反過來推算,想要更進一步的了解,我向各位推薦 Udacity 的機器學習課程,以及從這篇妙趣橫生的文章來複習一下機率的相關概念。而關於樸素貝葉斯的直接應用,可以直接採用 TextBlob這個簡單的套件,它專門為了分類器的用法寫了一篇完整的教學,若有興趣不妨參考一下吧。

詞的表示法

對了,我剛剛疏忽了一點,機器學習中很注重「資料的表示法」,那麼我們該怎麼表示「一個句子」呢?很簡單,套用詞袋模型就成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
s1 = This is my life
s2 = This is my book

1.用集合表示上面兩個句子(this,is,my,life,book)
2.賦予每個詞一個 id ([this,1],[is,2],[my,3],[life,4],[book,5])

3.從而產生一個陣列
This is my life book
[flag, flag, flag, flag, flag]

4.現在我們就能將語句向量化,1表示有出現這個詞,0表示沒出現
s1 = [1,1,1,1,0]
s2 = [1,1,1,0,1]

如此一來,一個句子就變成陣列,更多基於監督式學習的方法都能助我們一臂之力。

深度學習

但是詞袋模型有一個很明顯的缺點,因為我們給每個詞都分配出了一個不同的id,這造成每個詞都是「語意獨立」的,彼此的相似度不是 1 就是 0。為解決這個問題,誕生出了 word2vec 這個將詞袋降維的技巧,要學習如何使用 word2vec,可以參見我之前寫的一篇教學

這時你可能就會問了,有詞向量,那有沒有句向量,答案是有的,好比說把句子中所有的詞加總起來就會是個句向量,但印象中這東西的效能會是 lower bound ,正統的句向量實作可以參考sentence2vec,這是依照 word2vec 作者 Tomas Mikolov 新發表的論文實作出來的。在我們有了句向量後,就只要計算兩個句子的 cosine 相似度,便可以得知哪些問答配對會是我們需要的,哈哈,還記得嗎,我們正在討論聊天機器人的「檢索式模型」。

而在我們將詞袋密集化後,就能走入更「深」一點的應用,好比說可以將他們塞進 LSTM ,除了做分類外,也能夠用來幫助我們解決序列標註的問題。又好比說把詞向量一個一個堆疊起來,便能得到一張語句「圖片」 ,我們就可以將捲積層應用上去,因為我們能彈性地去學權重,捲積層中的 Filters 就好像是強化版的 N-Grams 一樣,最後將捲積出的 Feature maps 串接起來,丟入神經網路,也能用來推測句子會屬於哪個類別。若對這方面有更深的興趣,不妨在 Google 敲敲 CNN NLP,或是看看史丹佛大學的深度學習與自然語言處理公開課。

生成式模型

這個東西的技術含量比較高吶,無論是資料前處理還是模型架構應該都是最麻煩的,但我個人認為生成式模型並不偏向特定領域的聊天,比較像是在實現一般的生活對話,因此不太符合這篇的主題,為了避免這篇文章太過冗長,這裡就簡介一些基本思維,並提供學習方向後便點到為止,改天再專文來聊聊 Sequence to Sequence with Attention model 吧。

自 google 的論文發表後,用 Sequence to Sequence 來實現聊天機器人就成為一股熱潮,Github 上有不少像 DeepQA 看起來高完成度的應用。Sequence to Sequence 的基本概念是串接兩個 RNN/LSTM,一個當作編碼器,把句子轉換成隱含表示式,另一個當作解碼器,將記憶與目前的輸入做某種處理後再輸出,不過這只是最直觀的方式,其實解碼器還有很多種作法,如果想了解細節與效能上的差異,我推薦這篇文章

Sequence to Sequence

所謂的序列生成,就是把前一刻的輸出當成下一刻的輸入

這其實就是另類的機器翻譯手段,在 TensorFlow 機器翻譯篇裡便有搭建 Sequence to Sequence 模型的完整教學,就不在此贅述細節了。

商務式聊天機器人

具備一些基本認識後,讓我們開始向「錢」看齊吧,現行的聊天機器人應用能切分成兩大類,一類是在整合其他應用,好比說 Siri微軟小冰,它能替你問天氣、查股票、找餐廳等等,若換個角度來看,它就是幫你把氣象資料、股票資料、以及 Google Map 等等資訊給整合起來,讓用戶能用「同個介面」拜訪「不同應用」,就自然語言的定義來看也就是偏向通用領域(General domain)的聊天機器人。

至於第二類就是相對第一類而言,特定領域(Specified domain)的聊天機器人囉!如果將整合多個應用這個思維稱為「寬」的話,特定領域則是偏向深入特定領域,好比之前說的客服機器人,以能回覆保險訊息的聊天機器人舉例吧,它顯然要具備與保險相關的知識才能實現,好比我們問:

1
2
哪個保險會適合我?
XXX 保險有什麼好處嗎?

顯然,無論是要認識顧客並以此推薦服務,或是了解自家公司的產品等等,這些都是在對「保險」這個議題深入處理。

LineBot

搞定語意與任務處理後,我們就能將其發佈於 Line/Messenger 上

除了客服應用外,我認為特定領域的聊天機器人還有一個更有價值的議題 -「多輪式對話」

多輪式對話

所謂的多輪式對話,或者有人說是對話情境,指的就是聊天機器人能對之前的談話有所「記憶」,並接續之前的服務,比如說:

1
2
3
4
5
6
7
User :幫我找間飯店
Bot :想找哪裡的飯店呢?


---

User :臺南的吧,會在十二月二十五號入住
Bot :好的,向您推薦台南大飯店,要透過 Agoda 進行訂房服務嗎?

這是個很有趣的問題,聊天機器人不能將每句對話都視作獨立的。接下來所有的篇幅我都會用來談論 Specified domain 的聊天機器人,並用簡單又直觀的方法來設計一個實現多輪式對話的架構。

語意分析

在正式切入多輪式對話前,我們得先明白語意分析是在變什麼花樣。語意分析其實蠻多人在做的,而且也存在 Wit.ai、luis、微軟的認知服務等很成熟的應用,它們的大方向通常是讓使用者編修一些句子,並標記句子裡的意圖(intent)與實體(entity),再透過後訓練調整問答模型。比如說:

1
2
Training set:
我想要購買一瓶紅酒

我們可以定義這句話的意圖是「購買物品」,其中的實體為「紅酒」與「一瓶」讓系統從訓練集來認識我們的需求,完成模型的訓練後,每當使用者輸入一句新的對話,系統就會去評比可能的意圖是什麼。

看到這裡,有沒有種似曾相似的感覺?沒錯,這跟我們先前在樸素貝葉斯分類中看過得相當相似,我們的目的在於將一句「對話」,通過使用者定義好的意圖,來分配至某一個「情境」。這看起來是種監督式學習,顯而易見的,隨著標記的實體量與句型越來越多,聊天機器人就會越來越厲害。

遺憾的是,我們看不到人家的後端是在怎麼搞,或許是基於某種神經網路的預測、或許是 LDA 的變型去做分類,也或許是以 WordNet 切入,從一個句型的架構著手,在語法與語意兩方面都進行處理,決定出最可能的主題是什麼。

當然啦,這些都只是個假設,也許背後只是一堆繁複的規則也說不定呢。既然我們無法摸清別人在搞什麼花樣,那麼就自己試著做做語意分析吧,先別走太高端的技術,讓我們一步一腳印,從最簡單的部分著手。

語意圖

這是我在專題中進行分析的一個技巧,你說什麼是語意圖?沒聽過也是挺正常的,其實這是我在報告中自己定義出的一個名詞,只是因為教授也沒說什麼,所以就這樣定下來了(・ω・ )…

類似的概念我在基於詞向量的主題匹配有提過了,將其抽象化後就成為了語意圖,長的會是這個樣子:

語意圖概觀

隨手畫出的一張語意圖

我們直接看看怎麼應用吧,以要識別:「這附近有什麼好吃的」為例:

語意圖分析

我們能這樣分析一句話

一言以蔽之,就是從 root 一直往下走到葉節點,每個節點都代表著某種語意訊息,我們不斷的從子節點中,挑選出與句子最相關的語意節點,這最終就形成了一條語意路徑。我們便能從這條路徑認知出這句話的含義,像「這附近有什麼好吃的」經過走訪後,就得出了想「找賣飯的餐廳」這個意圖。

1
(意圖是)飲食 -> (是想吃)食物 ->  (想吃)飯 -> (因為附近跟地點有關,所以想找) 餐廳

你或許會感到疑問,為什麼是「食物」接下來是往「飯」走而不是往「麵」走,這其實與模型、語料、客層三者相關,在後續的自學習篇章中,我會對這點稍做補充。除此之外,你可能會這樣質疑我:「你又知道能走到餐廳,是因為『餐廳』匹配到了『附近』,也可能是因為『餐廳』匹配到了『好吃』啊」,問的好,實際上我對語意圖定義了兩種運算行為:

其一為找尋進入點,「比如說這附近有什麼好吃的」,我們從「好吃」這個詞對應到了進入點「飲食」,可以想做因為你提到了好吃,所以我們認為有飲食相關的意圖。

其二為詞義解析,像是「好吃」一直在變色,這個是想找出系統內與「好吃」最相關的概念是什麼,結果是「飯」。

當一個詞完成了詞義解析後,這個詞就算處理完了,也「不會」再次被語意圖視為進入點,我想這也是蠻直觀的,總不能一直重複進去同一個點,這樣要其他的詞如何是好吶!

方唐鏡

不想被打的話,就別在同個點進進出出的

我剛剛沒說的是,我們要怎麼去找尋進入點與做詞義解析?換言之,那張圖到底是個怎麼樣的走法?其實這實現方法非常多種,因為每個語意節點本質上就是個打分器,只要有辦法讀入一個詞組,輸出一個分數,那就是一個合格的語意節點,換句話說,語意圖也容許不同評估方式的節點存在,以最大化迎合匹配的需求。

最簡單的,我們能建立一個同義詞集合,去匹配看看這些東西有沒有出現在使用者語句裡,如果有就給 1 分,沒有就給 0 分。我們也能從詞向量的角度出發,這還能另外抓出一些隱含語意,詳情請參見以往寫的基於詞向量的主題匹配。我們甚至能用把語意節點設計為一個分類器,結果輸出一個信心值,父節點會走向信心值最高的子結點,某種意義上,這很像是一個手動搞出來的決策森林或神經網路模型,同樣都會一層一層的,嘗試抽取出階段性的語意特徵。

總而言之,我們在分析完一句話的語意之後,就能得到了一條「語意路徑」,這條路徑能概念性的說明我們話中的涵義。若我們為每條路徑給予一個id,再依這個id去找出對應「欲詢問特徵的集合」,這麼一來,我們就有辦法去設計多輪式對話情境了。

多輪式對話

只要記住目前知道什麼,以及還要問些什麼,多輪式對話的架構就浮現了

在未滿足所有需求之前,我們的機器人就依照缺損的需求,一直問、一直問、一直問下去,直到需求滿足為止,這就是最為簡單的多輪式對話架構。

例外處理

當我報告這個想法時,有被詢問到一個問題:「如果在多輪式對話中,使用者並沒有依照我們預期般行動該怎麼辦?」

比方說機器人在向對方推薦酒類時,機器人會簡單的陳述一下每種酒的口感,這時使用者從中見到了一個不太確定意思的單字,而向機器人反問了一句:「什麼是 smooth?」這顯然會出現某種問題,因為我們預期收到的是使用者想喝的是什麼酒,但是現在卻收到了一個「詢問」。

red-wine

也請別問我什麼是 smooth,我沒有品酒的習慣。

我當時沒有想到解決的方法,不過現在回想起來,答案似乎意外的簡單,出現了預料之外的情形,這不就是「例外」嗎?要解決例外,那做「例外處理」不就行了嗎?

而所謂的例外,就是我們沒辦法從目前回應中抽出預期的「答案」(假設實體識別沒問題)。我之所以會有這個想法,是因為半年前在做 Watson 訂購披薩的 API 測試時,發現如果我說的話跟披薩摸不著邊,Watson 就會很無奈的說「我不太明白你想做什麼,我能做的就只有幫你訂披薩。」

話又說回來了,發生例外時該怎麼辦呢?

上述問題的解法其實挺生活化的,我們如果遇到一個不會的問題,去問問其他人會不會不就好了嗎?套用到聊天機器人上,如果一個模型不夠,那用兩個模型不就成了。

當使用者問說:「什麼是 smooth ?」時,因為我們抽取不到任何與答案相關的訊息,所以我們可以從匹配模型轉移到了「檢索式模型」,用來從既定 FAQ 裡找出答案告訴使用者。當例外處理結束後,再從檢索式模型轉回匹配模型,繼續之前的多輪式對話。

自學習

說穿了,面向特定領域的聊天機器人通常與推薦系統類似,它之所以回答你的問題,其實也就是想讓你老實地把錢掏出來,既然如此,讓系統能在與使用者對話的過程中,「越來越認識這名使用者」,就會顯得極其重要,這就是所謂的自學習。

「你認為聊天機器人該怎麼做到自學習?」

其實被問到這個問題時我驚訝了一下,因為這是我專題已經棄了蠻久的坑,因為時間啊、課業啊、推甄啊,還有很多很多原因就被我埋藏在記憶的深處了,現在終於又重見天日了。

向量權重調整

當初選擇用詞向量做語意圖的開發基底,就是為了線上學習而鋪的伏筆,在剛剛路徑的走訪中,我的假設是 sim('好吃','飯') > sim('好吃','麵'),但實際上,若系統中的使用者常常提到麵或與麵相關的概念好吃的話,sim('好吃','麵')便會產生變化,到達某一個程度後,能超過sim('好吃','飯'),改為以推薦麵食為主,這某種程度上,就好像我們的系統在學習一般。

行為記憶

之前曾經說過,自然語言處理最困難的就是多樣性,我們永遠都不會知道,下一秒使用者會突然說出什麼新花樣,既然如此,不妨試著把問題限縮在我們的業務範圍內吧!好比說像這樣:

MESSENGER-BOT

透過浮出按鈕,幫助使用者做決定,往好處想,它能夠限縮涵蓋範圍,也不用讓使用者自己打字,還算是挺貼心的,不過我想這不只如此而已,按按鈕這個動作不僅僅是把資料傳送給後端而已,更重要的是它反應出了使用者的行為,以上面那個例子來說,我們有 ActionComedyDramaDocument 這四個按鈕,透過結果,我們能知道使用者更喜歡喜劇片,更精確的講,是跟其他三者比起來,更喜歡喜劇片。也就是說,我們能夠為每個多輪式對話設計需求按鈕,再記錄每名用戶的偏好行為,以此評估使用者可能會更喜歡什麼。至於具體的分析方式,這就偏向推薦系統的實作了,在這邊就點到為止吧。