最近在做dialog policy相關(guān)的研究,實(shí)現(xiàn)就用了rasa的輪子,看源碼順便寫篇文章。水平有限,還請(qǐng)指正。
先吹一波
rasa core的代碼質(zhì)量非常高非常高非常高!我知道有許多中國工程師參與了開發(fā),牛逼!
整體思路
我們先從執(zhí)行的角度來分析tracker的源碼
- 是什么?如何初始化
- 輸入是什么
- 跟蹤了什么內(nèi)容
- 如何更新狀態(tài)
- 狀態(tài)如何表達(dá)
如何初始化 —— init
下面是init的全部代碼,非常簡(jiǎn)單,我做了一些注釋方便理解,其實(shí)源碼里面的注釋很多,原來的注釋大家就直接看源碼吧。多說一句,rasa的源碼寫得太漂亮了,注釋詳細(xì),格式規(guī)范,讀起來就是享受。
def __init__(self, sender_id, slots,
max_event_history=None):
"""Initialize the tracker.
A set of events can be stored externally, and we will run through all
of them to get the current state. The tracker will represent all the
information we captured while processing messages of the dialogue."""
# 可以跟蹤的最長(zhǎng)歷史,tracker記錄狀態(tài)是以event為單位的
self._max_event_history = max_event_history
# 歷史事件列表
self.events = self._create_events([])
# 這個(gè)id和rasa的chenel特性有關(guān)系
self.sender_id = sender_id
# slot列表
self.slots = {slot.name: copy.deepcopy(slot) for slot in slots}
###
# current state of the tracker - MUST be re-creatable by processing
# all the events. This only defines the attributes, values are set in
# `reset()`
###
# 暫停標(biāo)志
self._paused = None
# 一些action記錄
self.followup_action = ACTION_LISTEN_NAME
self.latest_action_name = None
self.latest_message = None
# bot的上一個(gè)返回內(nèi)容
self.latest_bot_utterance = None
self._reset()
從init函數(shù)中我們可以知道些什么呢?
- tracker是記錄一個(gè)用戶對(duì)話狀態(tài)的對(duì)象
- tracker基于Event對(duì)象跟蹤對(duì)話狀態(tài)
Event
既然tracker是基于Event的,我們就來看看Event是啥
簡(jiǎn)單來說,Event就是對(duì)bot一切行為的抽象,每一個(gè)具體的事件類都繼承自Event基類
class Event(object):
"""Events describe everything that occurs in
a conversation and tell the :class:`DialogueStateTracker`
how to update its state."""
type_name = "event"
def __init__(self, timestamp=None):
self.timestamp = timestamp if timestamp else time.time()
這種設(shè)計(jì)很優(yōu)秀,使得tracker可以跟蹤系統(tǒng)預(yù)定義以外的事件,只要你自己實(shí)現(xiàn)一個(gè)Event的子類就行。說起來這是應(yīng)該是面向?qū)ο蟮幕驹O(shè)計(jì)思維,但是真正編碼的時(shí)候很難考慮周全。
rasa-core內(nèi)部實(shí)現(xiàn)了以下Event

名字一看就知道大概什么意思了
下面我們看一下Event核心的方法apply_to()
class UserUttered(Event):
def apply_to(self, tracker):
# type: (DialogueStateTracker) -> None
tracker.latest_message = self
tracker.clear_followup_action()
看一個(gè)就行,這是在干嘛呢?就是給tracker改屬性,把一些和自己有關(guān)的內(nèi)容更新了。
為什么要有這個(gè)方法呢?因?yàn)槊總€(gè)Event需要修改的屬性不一樣,把這部分邏輯放到子類自己實(shí)現(xiàn),調(diào)用邏輯在tracker實(shí)現(xiàn),最大化復(fù)用代碼。這同樣應(yīng)該屬于基礎(chǔ)思維,那么自己做到了么(逃
狀態(tài)更新 —— update
def update(self, event):
# type: (Event) -> None
"""Modify the state of the tracker according to an ``Event``. """
if not isinstance(event, Event): # pragma: no cover
raise ValueError("event to log must be an instance "
"of a subclass of Event.")
self.events.append(event)
event.apply_to(self)
就是這么簡(jiǎn)單
輸出
上面說的內(nèi)容就是tracker的核心部分了,抽象非常優(yōu)美。題外話,推薦大家讀一讀Flask的源碼,我讀了一部分,說賞心悅目不為過,那種架構(gòu)設(shè)計(jì)的嚴(yán)謹(jǐn)優(yōu)雅看著是真tm舒服。
tracker記錄了整個(gè)交流的過程,提供了生成Story的接口和生成Dialog的接口
def export_stories(self):
# type: () -> Text
"""Dump the tracker as a story in the Rasa Core story format.
Returns the dumped tracker as a string."""
from dqn_policy.training.structures import Story
story = Story.from_events(self.applied_events())
return story.as_story_string(flat=True)
def as_dialogue(self):
# type: () -> Dialogue
"""Return a ``Dialogue`` object containing all of the turns.
This can be serialised and later used to recover the state
of this tracker exactly."""
return Dialogue(self.sender_id, list(self.events))
其他接口
tracker還實(shí)現(xiàn)了很多接口,涉及到了rasa的各個(gè)部分,就不一一細(xì)說了。里面很多是用來featurize的輔助接口,我也還沒把這部分研究透,后面會(huì)再寫一篇聊featurize,這是rasa core的核心組件
總結(jié)一哈
tracker是rasa core中承上啟下的一環(huán),它記錄來自前端輸入的數(shù)據(jù),又為模型訓(xùn)練的featurize提供基礎(chǔ)。從tracker出發(fā)基本能摸清楚整個(gè)rasa core的框架結(jié)構(gòu)。rasa core抽象做得非常好,代碼質(zhì)量賊高,必須吹一波。這部分源碼相對(duì)比較簡(jiǎn)單,注釋非常詳細(xì),讀起來很舒服,推薦大家都讀一讀。