zake7749/Chatbot

記憶回覆功能

Closed this issue · 3 comments

你好,這個專案對我初學聊天機器人有很大的幫助,先謝謝你的分享。
我想了解一下這聊天機器人是怎樣達至記憶回覆功能?
demo
以左圖為例,聊天機器人如何記錄"高雄"這一個前面對話的選項呢?

您好,

因為許多麻煩的因素,屬於站點後端的更新我都沒有上傳至 Github,
其中也包括了記憶回覆、情境模組與資料庫的相關設計,我對此感到遺憾也相當抱歉 (´・_・`)

不過,我很樂意和您分享設計流程,就先從後端的主要結構開始吧,
為了讓說明更清楚,我先定義三個關鍵字:

  • user_id: 使用者的識別符,每個使用者都有不同的 user_id
  • situation: 應用情境,像是訂餐廳、訂飯店等等,而每個情境有若干個 slot,用於記錄使用者對該情境的需求,好比說訂餐廳的需求可能有「餐廳類型」、「餐廳地點」、「訂餐時間」三個 slots。只有使用者填滿所有 slots 時,他才會脫離該 situation。

理解這三個關鍵字後,關於後端的設計能考慮如下:

def handle_query(query, user_id):
    '''
    query: 使用者當次的問句
    user_id: 
    '''
    answer = None
    user_status = lookup_user_status(user_id) # db work
    if already_in_situation(user_status):
        situation_handler = get_situation_handler(user_status)
        filled_slots = get_filled_slot(user_id) # db work
        answer, updated_slots, user_status = situation_handler.parse(query, user_status, filled_slots)
        check_user_status(user_id, user_status, updated_slots)
    else:
        user_status = intent_classifier(query)
        if user_status is CHITCHAT:
            # do something else to handle chitchat     
        else:
            situation_handler = get_situation_handler(user_status)
            answer, updated_slots, user_status = situation_handler.parse(query, user_status, situation_handler.init_slots())
            check_user_status(user_id, user_status, updated_slots)       
    return answer

def check_user_status(user_id, user_status, updated_slots):
        if user_status is TERMINATED_STATE:
            clean_user_slots_and_status(user_id) # db work
        else:
            update_filled_slots(user_id, updated_slots) # db work
            update_user_status(user_id, user_status) # db work

跟資料庫處理相關的有以 # db work 標註,是以 json 格式儲存。

以此流程我們能知道左圖的結果是怎麼得出的,假設飯店情境的 slots 有地點與時間兩者,讓我們來分別考慮三組對話。

  1. user: 幫我挑間高雄的飯店:

因為這是使用者第一次進入對話,所以 user_status 為 already_in_situation 為 False,而 intent_classifier (預設採用) 得出該 user 的 user_status = RESERVE_HOTEL_INIT,我們挑出專門處理訂飯店的模組,接下來給定目前的 user_status,目前 user 說了什麼,以及一個全空的 slots,請該模組去 parse 出:

  • answer : 該回覆的答案,即「好的,請問要訂哪一天的飯店呢?」
  • updated_slots: 將「高雄」填入地點的 slot 中
  • user_status : 更新後的 user_status = RESERVE_HOTEL_ASK_DATE

parse() 的實作方式其實很彈性,我一開始選擇的是相當樸素的方法,先硬性的規定每個 situation 的詢問順序,好比說訂飯店總是先問地點再問時間(如果地點還沒被填的話),而抽取出實體的方式也很簡單,先濾除掉一些不可能為 slot 的詞,再將與 slot 值相關的可能說法填入一個表中,再看使用者的輸入是否有匹配到表中的某個值,如果有,就預設抽出這個地點或時間。

我後來在 intent_classifier 與 parse 上做了些改善,先是設計了一些情境模板來產生語料,用於訓練一個簡單的 query encoder,將目前的 query encode 成一個 query vector,再和之前的 user_status embedding concat 起來做為新的 state vector,最後用一個淺層分類器對 state vector 做分類,判斷其應該為哪一個 user_status,並跟據這個 user_status 去 lookup 該做什麼回應,比方說現在走到了 RESERVE_HOTEL_ASK_DATE,就該回覆「好的,請問要訂哪一天的飯店呢?」。

因為我是自己設計模板來產生語料的 (好比說:<時間>的<天氣狀態>怎麼樣),理論上也能用這個模板進行 sequence entity tagger 的訓練,不過不知為何,效果總是不怎麼優,恐怕是我的模板在設想上不過泛化所致。

最後是 check_user_status ,如果說 user_status 走到了 TERMINATED_STATE,也就是這個情境結束了,那我們就將使用者與該情境的互動記錄清空,如果還沒結束,就更新目前的 user_status 與其情境中已填的 slots,因為現在 user_status 不為 TERMINATED_STATE (還有時間沒問呢),所以我們選擇更新 slots

  1. user: 下禮拜二

因為 already_in_situation(user_status) 為真,這一次我們不會進行 intent 的分類,而是直接進行還未結束的那個情境,先恢復已填補的 slots :

situation_handler = get_situation_handler(user_status)
filled_slots = get_filled_slot(user_id) # db work

再來是延續之前的情境,繼續填補 slot

answer, updated_slots, user_status = situation_handler.parse(query, user_status, filled_slots)

這一次因為時間跟地點都被填滿了,situation_handler 能以 API 幫使用者找到飯店,並傳回了情境終止訊號 TERMINATED_STATE,清空使用者與 RESERVE_HOTEL 的所有互動,讓該 user 的 user_status 回到了 START_POINT

  1. user: 感謝 :)

因為目前的 user_status 為 START_POINT,already_in_situation 為假,我們要跑一下 intent_classifier,得出的結果為 CHITCHAT,進入了專門處理閒聊的模型,得出回覆「不客氣」。

後端的設計大致上是如此,因為是憑著古老的記憶敲出來的,說明上有顯得有些卡卡的,若有疑問也歡迎再留言,只是最近回覆可能會稍晚些,還請包涵。

什么时候更新这部分功能呢?

這部分的模組已不會更新。