基於詞向量的主題匹配

2016 will be the year of conversational commerce
Chris Messina

在 Facebook 釋出 Messenger API 後,形形色色的 Chatterbot 如雨後春筍般湧現:

messenger

Messenger chatbot - from Facebook

「以對話式介面取代圖形化介面」、「2016年將成為對話式商務元年」,種種呼聲儼然令聊天機器人成為了一個 NLP 熱點,這將是一場介面革命,我們所專注的不再是色調與元素的微調,而是去思考如何令機器人在談吐間富含溫度,在語意上理解透徹,在思維上,更貼近一個真實的人。

嘛,開場白也講得差不多了,太企業導向的東西先放一邊,讓我們來點輕鬆的,瞧瞧這篇文章出來的成果會是什麼樣子:

1
使用者:明天早上叫我起床。
相似度 概念 匹配元
0.4521 鬧鐘 起床
0.3904 天氣 早上
0.3067 住宿 起床
0.1747 病症 起床

這個表是算法推論出的對話主題,機器看到「明天早上叫我起床」,就知道該幫你「訂鬧鐘」了,接下來,就能基於自己的語料為使用者提供服務。這個例子有趣的點在使用者壓跟沒提到「鬧鐘」,那麼,機器是怎麼推斷出「設鬧鐘」這個結論的?

其實標題已經先劇透了,沒涉及到 LSTM 或 ESA 這類高端技術,只不過是把匹配的單元從「詞」替換成詞「向量」罷了,也就是基於向量間的餘弦相似度計算,來去推測每個詞可能會有的隱含主題。

一切的起點

其實以前我也開發過聊天機器人,方法跟目前很多框架一樣,定義了幾種匹配模式再去檢查關鍵字,用程式語言來講大概是:

1
2
3
4
5
6
7
if user_input.contain(keyword):
say_something(keyword.domain)

if user_input.start_with(keyword):
say_something(keyowrd.domain, start=True)

...

這麼做精度很高,且因對話是基於人工定義,答覆的品質也是一等一的,可惜缺點也相當明顯,即是開發彈性相當低,畢竟要表達一個主題的方式實作太多太多,我們不能總是指望關鍵字有辦法包山包海,人力與時間的花費都是個問題。
於是後來看上了LDA,要找主題問主題模型,這麼想也是理所當然的,可嘆機率模型是盤難解的棋,除此之外,無監督學習出的主題也未必是應用上需要的,再說像聊天這種頂多二十幾個字的短語,我認為還是回歸到關鍵字比對會靠譜一些,重點是怎麼比呢?

試著歸納主題

自然語言處理最難的就是語言的多樣性,我想沒有人會否認這個說法,那麼化繁為簡就成為了關鍵,這讓我想到 IR 的 Stemming ,或許可以透過 domain tree 來規約一個句子的含義,比方說:

1
2
User:這附近哪裏能買到泡沫紅茶?
Domains:{附近|近距離}{哪裡|疑問}{買到|購買}{泡沫紅茶|飲料}

這能透過廣義知網實現,如此一來就把關鍵字擴充同義詞集,但是這樣還不夠,我們更關注的應該會是詞之間的主題關聯性,比如說提到肚子餓,就會想到該吃飯了,口渴了,那你可能會想買飲料。想要保存關聯,又想要建立同義關係,就我所學中最恰當的方法就是Word2Vec,至於如何將詞向量化呢?可以參見我另一篇文章的紀錄。

其實我本來對這個想法還頗有疑慮,直到偶然拜訪了這個 Word2Vec的視覺化專案,它隨機將文章中某個詞替換成了與該詞相似度最高的詞,這與我目前要做的有異曲同工之妙,不過它是將文句發散出去,我是想讓概念收斂回來,就演示的結果來看,大方向應該是沒有問題的,現在該是寫程式的時候了。

先定義規則

既然是基於匹配,我們就得先決定好匹配的結構才行,我簡單定義了一個json的格式:

1
2
3
4
5
6
7
8
9
10
{
"domain": "代表這個規則的抽象概念",
"response": [
"機器人給予的回覆"
],
"concepts": [
"要被比對向量的關鍵詞集"
],
"children": ["該規則的子規則們"]
}

實際建立起來可能像是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"domain": "購買",
"response": [
"正在將您導向購物模組"
],
"concepts": [
"購買","購物","訂購"
],
"children": [
"購買生活用品",
"購買家電",
"購買食物",
"購買飲料",
"購買鞋子",
"購買衣服",
"購買電腦產品"
]
}

開始匹配

現在我們就能基於規則開發資料結構,基於個人後處理的考量,接下來都會用python 3.5.0開發,使用到的套件有主題模型gensimjieba斷詞。規則類的程式碼可以參見rulebase.py,我創建了RuleRuleBase,由於我python是新手入門,開發上又幾經波折,程式裏頭有很多無關緊要的玩意兒還請見諒。我想重點是在匹配的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    def match(self, sentence, threshold=0):

max_sim = 0.0
matchee = ""

for word in sentence:
for term in self.terms:
try:
sim = self.model.similarity(term,word)
if sim > max_sim and sim > threshold:
max_sim = sim
matchee = word
except Exception as e:
self.log.write(repr(e)+ ". Try to hard-match.")
if term == word:
max_sim = 1
matchee = word

return [max_sim, self.id_term, matchee]

sentence是使用者輸入的斷詞結果,self.termsRule的關鍵詞的列表,透過雙層迴圈讓詞袋中每一個詞和關鍵詞表中的每一個關鍵字計算相似度,最後迴傳最優的分數、自己的 ID 與匹配到的字詞。RuleBase的工作也沒什麼,就負責把一堆Rulejson檔中讀入,並提供了一個對外的匹配接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    def match(self, sentence, topk=1, threshold=0, root=None):

assert self.model is not None, "Please load the model before any match."

result_list = []
at_leaf_node = False
term_trans = ""

if root is None: # then search from roots of forest.
focused_rule = self.forest_base_roots
else:
focused_rule = [self.rules[root]]

while not at_leaf_node:

at_leaf_node = True

for rule in focused_rule:
result_list.append(rule.match(sentence, threshold))

result_list = sorted(result_list, reverse=True , key=lambda k: k[0])
top_domain = result_list[0][1] # get the best matcher's domain.

if self.rules[top_domain].has_child():
result_list = []
term_trans += top_domain+'>'
at_leaf_node = False

# travel to the best node's children.
focused_rule = []
for rule_id in self.rules[top_domain].children:
focused_rule.append(self.rules[rule_id])

return [result_list,term_trans]

我儲存規則的資料結構是字典,鍵為Rule.domain,值則是Rule的實例,那麼你可能就好奇了,怎麼不用單層迴圈就搞定?

1
2
3
for rule in self.rules.values():
grade = rule.match(sentence, threshold)
...

其實那是我的初號機,因為以前聊天機器人就是這麼寫的,可惜當詞變成了詞向量,這個老方法就有點力不從心。弱匹配雖然彈性很大,但規則設計上就得小心翼翼,很容易發生搶佔分數的情形,比如「附近」、「哪裡」總拿高分,讓「購物」或「觀光」這類有明確意圖的只能敬陪末座,也不能武斷的將其歸類為停用詞,因為某種程度上他們表達了「詢問地點」這層含義,那怎麼辦呢?就做一顆分類樹吧!

規則很模糊?那就分而治之

因為概念詞會與斷好的每個詞作匹配,那麼匹配的順序就會顯得很重要,我的想法是先從大方向抽取主題,比如「訂房」這類有明確意圖的關鍵詞,再向下觀察地點、時間及其他細項,這很像一個分類器,我們將模糊的大領域切割成若干小領域,再將它們各個擊破:
Chatbot

如果一句話得出了最高相似概念是購物,我們會走向與購物相關的規則,以避免一些雜項干擾了對核心概念的評估。這樣出來的結果確實會很好多,在設計新規則時也比較不擔心與原有的抵觸。

實際使用看看

我另外寫了一個 console.py 把斷詞、去除停用詞、匹配這些功能都包覆起來,可以前往Chatbot進行測試,不過要先準備好一份自己的詞向量才可以唷,我演示所使用的是 300 維的 skipgram,以維基百科和 A+ 醫學百科跟幾本小說作為訓練語料,如果一切都就緒了,就試著調用看看吧:

1
2
3
4
5
6
7
8
from Chatbot.console import Console

console = Console(model_path="詞向量模型的相對路徑")

speech = input('Input a sentence:')
res,path = console.rule_match(speech)

console.write_output(speech,res,path)
1
2
3
4
5
6
7
8
9
10
11
Case# 我的肚子好餓啊
------------------

0.5014 吃喝玩樂 餓
0.3816 鬧鐘 肚子
0.3736 病症 肚子
0.1807 住宿 餓
0.1580 天氣 餓
0.0000 觀光
0.0000 購買
0.0000 股票
------------------

一些展望

如果還有後續的話,希望先實現看看各平台的 adapter,或者用機器學習來調適規則的權重,目前有些關鍵字的實用性是有些不如預期,我想這跟語料的選用也有很大的關係,在取得更多有效資源前,就先在這告個段落吧!