Rasa Core是Rasa框架提供的对话管理模块,它类似于聊天机器人的大脑,主要的任务是维护更新对话状态和动作选择,然后对用户的输入作出响应。所谓对话状态是一种机器能够处理的对聊天数据的表征,对话状态中包含所有可能会影响下一步决策的信息,如自然语言理解模块的输出、用户的特征等;所谓动作选择,是指基于当前的对话状态,选择接下来合适的动作,例如向用户追问需补充的信息、执行用户要求的动作等。举一个具体的例子,用户说“帮我妈妈预定一束花”,此时对话状态包括自然语言理解模块的输出、用户的位置、历史行为等特征。在这个状态下,系统接下来的动作可能是:
下面是Rasa Core文档中给出的一个对话场景:
由前面描述的对话管理模块了解到,它应该是负责协调聊天机器人的各个模块,起到维护人机对话的结构和状态的作用。对话管理模块涉及到的关键技术包括对话行为识别、对话状态识别、对话策略学习以及行为预测、对话奖励等。下面是Rasa Core消息处理流程:
Interpreter(NLU模块)
,该模块负责识别Message中的"意图(intent)“和提取所有"实体”(entity)数据;Tracker对象
,该对象的主要作用是跟踪会话状态(conversation state);policy
记录Tracker对象的当前状态,并选择执行相应的action,其中,这个action是被记录在Track对象中的;注:整个执行过程由Rasa Core框架中的
rasa_core.agent.Agent
类处理。
pip install rasa_core
Story样本数据就是Rasa Core对话系统要训练的样本,它描述了人机对话交互过程成可能出现的故事情节,通过对Stories样本和domain的训练得到人机对话系统所需的对话模型。每个story的格式基本是一样的,只是组成的内容不一样,即以##
开头的行表示一个story的开始,跟随的文本只用于描述;以*
开始的行表示一个意图和填充的slot;以缩进 -
开始表示Rasa NLU识别到该意图后Rasa Core要执行的action。以下是stories.md文件的部分内容:
## story1:greet only
* greet
- utter_answer_greet
> check_greet
## story2:
* goodbye
- utter_answer_goodbye
## story3:thanks
* thanks
- utter_answer_thanks
## story4:change address or data-time withe greet
> check_greet
* weather_address_date-time{"address": "上海", "date-time": "明天"}
- action_report_weather
## story5:change address or data-time withe greet
> check_greet
* weather_address_date-time{"address": "上海", "date-time": "明天"}
- action_report_weather
- utter_report_weather
* weather_address{"address": "北京"} OR weather_date-time{"date-time": "明天"}
- action_report_weather
- utter_report_weather
...
...
其中,> check_*
用于模块化和简化训练数据,即story复用;OR Statements
用于处理同一个story中可能出现2个以上走向(意图),这有利于简化story,但是相应的训练时间相当于训练了两个以上故事,但也不建议使用的太密集。
Visualizing Stories:可视化Stories
Rasa Core中提供了rasa_core.visualize模块可视化故事,这有利于我们更容易掌握设计故事流程。
命令如下:
python -m rasa_core.visualize -d domain.yml -s data/stories.md -o graph.html -c config.yml
其中,-m
指定运行模块;-d
指定domain.yml文件路径;-s
指定story路径;-o
指定输出文件名;-c
指定Policy配置文件。最终,在项目根目录得到一个graph.html,用浏览器打开可见:
当然,除了使用命令生成stories的可视化关系图,我们还可以创建visualize.py代码实现。
from rasa_core.agent import Agent
from rasa_core.policies.keras_policy import KerasPolicy
from rasa_core.policies.memoization import MemoizationPolicy
if __name__ == '__main__':
agent = Agent("domain.yml",
policies=[MemoizationPolicy(), KerasPolicy()])
agent.visualize("data/stories.md",
output_file="graph.html", max_history=2)
domain.yml定义了对话机器人应知道的所有信息,它相当于大脑框架,指定了意图intents
、实体entities
、插槽slots
以及动作actions
,其中,intents、entities应与NLU模型训练样本中标记的致,slots应与标记的entities一致,actions为对话机器人对应用户的请求需作出的动作。此外,domain.yml中的templates
部分针对utter_
类型action定义了模板消息,便于对话机器人对相关动作自动回复。假如我们要做一个天气资讯的人机对话系统,并且定义一个查询天气和污染程度的action,那么我们要这么做。domain.yml示例:
intents:
- greet
- goodbye
- thanks
- whoareyou
- whattodo
- whereyoufrom
- search_weather
- search_weather_quality
- search_datetime
- search_city
slots:
city:
type: text
datetime:
type: text
matches:
type: unfeaturized
entities:
- city
- datetime
actions:
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_introduce_self
- utter_introduce_selfcando
- utter_introduce_selffrom
- action_search_wether
- action_search_weather_quality
templates:
utter_answer_goodbye:
- text: "再见"
- text: "拜拜"
- text: "虽然我有万般舍不得,但是天下没有不散的宴席~祝您安好!"
- text: "期待下次再见!"
- text: "嗯嗯,下次需要时随时记得我哟~"
- text: "88"
utter_answer_thanks:
- text: "嗯呢。不用客气~"
- text: "这是我应该做的,主人~"
- text: "嗯嗯,合作愉快!"
utter_introduce_self:
- text: "您好!我是您的AI机器人呀~"
utter_introduce_selfcando:
- text: "我能帮你查询天气信息"
utter_introduce_selffrom:
- text: "我来自xxx"
utter_ask_city:
- text: "请问您要查询哪里的天气?"
utter_ask_datetime:
- text: "请问您要查询哪天的天气"
utter_report_search_result:
- text: "{matches}"
utter_default:
- text: "小x还在学习中,请换种说法吧~"
- text: "小x正在学习中,等我升级了您再试试吧~"
- text: "对不起,主人,您要查询的功能小x还没学会呢~"
说明:
intents |
things you expect users to say. See Rasa NLU |
---|---|
actions |
things your bot can do and say |
templates |
template strings for the things your bot can say |
entities |
pieces of info you want to extract from messages. See Rasa NLU |
slots |
information to keep track of during a conversation (e.g. a users age) - see Using Slots |
intents,即意图,这里枚举了在训练NLU模型样本时,样本中标出的所有intent。
intents:
- greet
- goodbye
- thanks
- search_weather
- search_weather_quality
- search_datetime
- search_city
当Rasa NLU识别到用户输入Message的意图后,Rasa Core对话管理模块就会对其作出回应,而完成这个回应的模块就是action。Rasa Core支持三种action,即default actions、utter actions以及 custom actions,它们的作用和区别如下:
1. default actions
DefaultAction是Rasa Core默认的一组actions,我们无需定义它们,直接可以story和domain中使用。包括以下三种action:
2. utter actions
UtterAction是以utter_
为开头,仅仅用于向用户发送一条消息作为反馈的一类actions。定义一个UtterAction很简单,只需要在domain.yml文件中的actions:
字段定义以utter_
为开头的action即可,而具体回复内容将被定义在templates:
部分,这个我们下面有专门讲解。定义utter actions示例如下:
actions:
- utter_answer_greet
- utter_answer_goodbye
- utter_answer_thanks
- utter_introduce_self
- utter_introduce_selfcando
- utter_introduce_selffrom
3. custom actions
CustomAction,即自定义action,允许开发者执行任何操作并反馈给用户,比如简单的返回一串字符串,或者控制家电、检查银行账户余额等等。它与DefaultAction不同,自定义action需要我们在domain.yml文件中的actions
部分先进行定义,然后在指定的webserver中实现它,其中,这个webserver的url地址在endpoint.yml
文件中指定,并且这个webserver可以通过任何语言实现,当然这里首先推荐python来做,毕竟Rasa Core为我们封装好了一个rasa-core-sdk
专门用来处理自定义action。关于action web的搭建和action的具体实现,我们在后面详细讲解,这里我们看下在在Rasa Core项目中需要做什么。假如我们在天气资讯的人机对话系统需提供查询天气和空气质量两个业务,那么我们就需要在domain.yml文件中定义查询天气和空气质量的action,即:
actions:
...
- action_search_weather
- action_search_weather_quality
在前面我们说的,domain.yml的templates:
部分就是描述UtterActions具体的回复内容,并且每个UtterAction下可以定义多条信息,当用户发起一个意图,比如"你好!",就触发utter_answer_greet
操作,Rasa Core会从该action的模板中自动选择其中的一条信息作为结果反馈给用户。templates部分示例如下:
templates:
utter_answer_greet:
- text: "您好!请问我可以帮到您吗?"
- text: "您好!请说出您要查询的具体业务,比如跟我说'查询身份证号码'"
- text: "您好!
utter_answer_goodbye:
- text: "再见"
- text: "拜拜"
- text: "虽然我有万般舍不得,但是天下没有不散的宴席~祝您安好!"
- text: "期待下次再见!"
- text: "嗯嗯,下次需要时随时记得我哟~"
- text: "88"
utter_answer_thanks:
- text: "嗯呢。不用客气~"
- text: "这是我应该做的,主人~"
- text: "嗯嗯,合作愉快!"
utter_introduce_self:
- text: "您好!我是您的AI机器人呀~"
utter_introduce_selfcando:
- text: "我能帮你查询天气信息"
utter_introduce_selffrom:
- text: "我来自xxx"
utter_ask_city:
- text: "请问您要查询哪里的天气?"
utter_ask_datetime:
- text: "请问您要查询哪天的天气"
utter_report_search_result:
- text: "{matches}"
utter_default:
- text: "小x还在学习中,请换种说法吧~"
- text: "小x正在学习中,等我升级了您再试试吧~"
- text: "对不起,主人,您要查询的功能小x还没学会呢~"
注:utter_default是Rasa Core默认的action_default_fallback,当Rasa NLU识别该意图时,它的置信度低于设定的阈值时,就会默认执行utter_default中的模板。
除了回复简单的Text Message,Rasa Core还支持在Text Message后添加按钮
和图片
,以及访问插槽中的值(如果该插槽的值有被填充的话,否则返回None)
。举个栗子:
utter_introduce_self:
- text: "您好!我是您的AI机器人呀~"
image: "https://i.imgur.com/sayhello.jpg"
utter_introduce_selfcando:
- text: "我能帮你查询天气信息"
buttons:
- title: "好的"
payload: "ok"
- title: "不了"
payload: "no"
utter_ask_city:
- text: "请问您要查询{ datetime }哪里的天气?"
utter_ask_datetime:
- text: "请问您要查询{ city }哪天的天气"
当然,上面描述的template reponse是我们通过训练后,对话机器人识别到相应的意图后,自动触发对应的UtterAction,然后选取一条template文本作为回复消息。其实,我们还可以在自定义action中使用dispatcher.utter_template("utter_greet")
函数生成一条消息Message反馈给用户。示例代码:
from rasa_core_sdk.actions import Action
class ActionGreet(Action):
def name(self):
return 'action_greet'
def run(self, dispatcher, tracker, domain):
dispatcher.utter_template("utter_greet")
return []
entities,即实体,这里枚举了在训练NLU模型样本时,样本中标出的所有entity。一般而言,entities和slots的内容应该是slots包含entities关系。
entities:
- city
- datetime
Slots,即插槽,它就像对话机器人的内存,它通过键值对
的形式可用来收集存储用户输入的信息(实体)或者查询数据库的数据等。以天气查询为例,那就意味着对话机器人必须知道具体的时间和地点方能查询,因为在domain.yml文件中我们就需要在slots部分定义两个插槽,即city、datetime,而matches则用来存储最后查询的结果,其中,type
表示slot存储的数据类型;initial_value
为slot初始值,该值可有可无(无意义)。示例如下:
slots:
city:
type: text
initial_value: "北京"
datetime:
type: text
initial_value: "明天"
matches:
type: unfeaturized
initial_value: "none"
(1) slot类型
关于**rasa_core.slots.Slot
值的type**,Rasa Core为我们提供了多种类型,以满足不同情况需求,具体分析如下:
text
:存储文本信息;
bool
:存储布尔值,True or False;
categorical
:指定接收枚举所列的一个值,如:
slots:
risk_level:
type: categorical
values:
- low
- medium
- high
float
:存储浮点连续值,其中,max_value=1.0
, min_value=0.0
为默认值,当要存储的值大于max_value,那么slot只会存储max_value;当要存储的值小于min_value,那么slot只会存储min_value。示例如下:
slots:
temperature:
type: float
min_value: -100.0
max_value: 100.0
list
:存储列表数据,且列表的长度不影响对话;
unfeaturized
:用于存储不影响会话流程的数据。这个槽不会有任何的特性,因此它的值不会影响对话流,并且在预测机器人应该运行的下一个动作时被忽略。
自定义slot类型
:详见Custom Slot Types
(2) 填充slots的值
在一次对话中,存在多种方式填充slots的值,下面我们详细分析下这几种情况。
Slots Set from NLU
当我们在训练NLU模型时,标记了一个名为name
的实体,并且在Rasa Core的domain.yml文件中也包含一个具有相同名称的slot(插槽),那么当用户输入一条Message时,NLU模型会对这个name进行实体提取,并自动填充到这个名为name的slot中。示例如下:
# story_01
* greet{"name": "老蒋"}
- slot{"name": "老蒋"}
- utter_greet
注:在上述情况下,就算我们不包含- slot{"name": "老蒋"}
部分,name也会被自动填充。
Slots Set By Clicking Buttons
前面说到,在domain.yml的templates:部分,Rasa Core还支持在Text Message后添加按钮,当我们点击这个按钮后,Rasa Core就会向RegexInterpreter
发送以/
开头的Message,当然对于RegexInterpreter来说,NLU的输入文本的格式应该与story中的一致,即/intent{entities}
。假设对话机器人询问是否需要查询天气信息时,我们在NLU训练样本中标记一个choose意图和decision实体,然后再在domain.yml中将decision标记为slot,当我们点击按钮后,"好的"或“不了”会被自动填充到decision的这个slot中。也就是说,当我们想Rasa Core发送"/choose{"decision": "好的"}"后,会直接识别到意图choose,并提取实体decision的值。
templates:
utter_introduce_selfcando:
- text: "我能帮你查询天气信息"
buttons:
- title: "好的"
payload: "/choose{"decision": "好的"}"
- title: "不了"
payload: "/choose{"decision": "不了"}"
...
Slots Set by Actions
以查询天气质量
为例,先看下Rasa Core项目中domain.yml和stories.md:
# domain.yml
...
actions:
- action_search_weather_quality
slots:
weather_quality:
type: categorical
values:
- 优
- 良
- 差
...
# stories.md
* greet
- action_search_weather_quality
- slot{"weather_quality" : "优"}
- utter_answer_high
* greet
- action_search_weather_quality
- slot{"weather_quality" : "中"}
- utter_answer_midddle
* greet
- action_search_weather_quality
- slot{"weather_quality" : "差"}
- utter_answer_low
# 注:官方文档这里说,如果slot的类型是categorical时,在stories.md的故事情节中使用- slot设置值有利于提高正确action的执行率?
在自定义action中,我们先查询天气数据库,以json格式返回,然后提取出json中weather_quality字段的值填充到weather_quality slot中返回。代码如下:
from rasa_core_sdk.actions import Action
from rasa_core_sdk.events import SlotSet
import requests
class ActionSearchWeatherQuality(Action):
def name(self):
return "action_search_weather_quality"
def run(self, dispatcher, tracker, domain):
url = "http://myprofileurl.com"
data = requests.get(url).json
# 解析json,填充slot
return [SlotSet("weather_quality", data["weather_quality"])]
python -m rasa_core.train -d domain.yml -s data/stories.md -o models/current/dialogue -c config.yml
命令说明:
usage: train.py default [-h] [--augmentation AUGMENTATION] [--dump_stories] [--debug_plots] [-v] [-vv] [--quiet] [-c CONFIG] -o OUT (-s STORIES | --url URL) -d DOMAIN
-m mod
指定要运行的module
-d或--domain
指定对话机器人的domain.yml文件路径;
-s或--stories
指定stories.md文件路径,需要注意的是,我们可以将故事情节根据某种分类保存在多个.md文件中,比如将所有.md文件存放在data目录的stories目录下,此时命令行的参数应该改为-s data/stories/
;
-o或--out
指定对话模型输出路径,训练好的模型会自动保存到该路径下;
-c或--c
指定Policy规范文件,config.yml配置文件(默认参数)如下:
policies:
- name: KerasPolicy
epochs: 100
max_history: 5
- name: FallbackPolicy
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: FormPolicy
--augmentation AUGMENTATION
该参数默认开启,Rasa Core将通过将故事文件中的故事随机地粘合在一起来创建更长的故事。如果我们希望每次回复都执行相同的action,无论之前的会话历史是什么,可以通过--augmentation 0
关闭这种功能。(训练时该参数可选)。
--url URL
从URL网络中下载一个stories.md文件用于训练。
接下来,我们重点介绍下Policy(策略)模块。Policies是Rasa Core中的策略模块,即类rasa_core.policies.Policy
,它的作用就是预测对话中,而具体选择哪个action将由预测的置信度决定,哪个的置信度越高就执行哪个。下面是我在测试过程中的debug信息,当我向机器人输入"帮我查手机号码12345
"时,Policies模块就会预测到执行action_search_num_business
这个action:
2019-04-15 16:46:07 DEBUG rasa_core.processor - Received user message '帮我查手机号码12345' with intent '{'name': 'search_plate_number', 'confidence': 0.4848141755831445}' and enti
ties '[{'entity': 'item', 'value': '手机号码12345', 'start': 3, 'end': 12, 'confidence': None, 'extractor': 'ner_mitie'}]'
2019-04-15 16:46:07 DEBUG rasa_core.policies.memoization - There is no memorised next action
2019-04-15 16:46:07 DEBUG rasa_core.policies.form_policy - There is no active form
2019-04-15 16:46:07 DEBUG rasa_core.policies.ensemble - Predicted next action using policy_0_KerasPolicy
2019-04-15 16:46:07 DEBUG rasa_core.processor - Predicted next action 'action_search_num_business' with prob 0.98.
2019-04-15 16:46:07 DEBUG rasa_core.actions.action - Calling action endpoint to run action 'action_search_num_business'.
2019-04-15 16:46:08 DEBUG rasa_core.processor - Action 'action_search_num_business' ended with events '[]'
2019-04-15 16:46:08 DEBUG rasa_core.processor - Bot utterance 'BotUttered(text: ['手机号码12345', '123556', None] 所属人为张三,这是他的业务信息, data: {
"elements": null,
"buttons": null,
"attachment": null
})'
DPL(Dialogue Policy Learning
),即对话策略学习,也被称为对话策略(Policy
)优化,根据当前对话状态,对话策略决定下一步执行什么系统动作(action)。系统行动与用户意图类似,也由意图和槽位构成。DLP模块的输入时DST(Dialogue state tracker,对话状态跟踪
)输出的当前对话状态,通过预设的对话策略选择系统动作作为输出。Rasa Core中拥有不同的policy,且策略配置文件可以同时包含不同的policy。
Memoization Policy
MemoizationPolicy只记住(memorizes)训练数据中的对话。如果训练数据中存在这样的对话,那么它将以置信度为1.0预测下一个动作,否则将预测为None,此时置信度为0.0。下面演示了如何在策略配置文件config.yml文件中,配置MemoizationPlicy策略,其中,max_history
(超参数)决定了模型查看多少个对话历史以决定下一个执行的action。
policies:
- name: "MemoizationPolicy"
max_history: 5
注:max_history值越大训练得到的模型就越大并且训练时间会变长,关于该值到底该设置多少,我们可以举这么个例子,比如有这么一个Intent:out_of_scope
来描述用户输入的消息off-topic(离题),当用户连续三次触发out_of_scope意图,这时候我们就需要主动告知用户需要向其提供帮助,如果要Rasa Core能够学习这种模型,max_history应该至少为3。story.md中表现如下:
* out_of_scope
- utter_default
* out_of_scope
- utter_default
* out_of_scope
- utter_help_message
Keras Policy
KerasPolicy策略是Keras框架中实现的神经网络来预测选择执行下一个action,它默认的框架使用LSTM(Long Short-Term Memory,长短期记忆网络
)算法,但是我们也可以重写KerasPolicy.model_architecture
函数来实现自己的框架(architecture)。KerasPolicy的模型很简单,只是单一的LSTM+Dense+softmax
,这就需要我们不断地完善自己的story来把各种情况下的story进行补充。下面演示了如何在策略配置文件config.yml文件中,配置KerasPolicy策略,其中,epochs
表示训练的次数,max_history
同上。
policies:
- name: KerasPolicy
epochs: 100
max_history: 5
Embedding Policy
基于机器学习的对话管理能够学习复杂的行为以完成任务,但是将其功能扩展到新领域并不简单,尤其是不同策略处理不合作用户行为的能力,以及在学习新任务(如预订酒店)时,如何将完成一项任务(如餐厅预订)重新应用于该任务时的情况。EmbeddingPolicy
,即循环嵌入式对话策略(Recurrent Embedding Dialogue Policy,REDP),它通过将actions和对话状态嵌入到相同的向量空间(vector space)
能够获得较好的效果,REDP包含一个基于改进的Neural Turing Machine的记忆组件和注意机制,在该任务上显著优于基线LSTM分类器。EmbeddingPolicy包含以下的步骤:
(1) apply dense layers to create embeddings for user intents, entities and system actions including previous actions and slots(稠密嵌入,包括用户意图、实体和系统行为:以前的动作和槽)
(2) use the embeddings of previous user inputs as a user memory and embeddings of previous system actions as a system memory.(使用以前的用户输入作为用户memory和以前的系统行为作为系统memory)
(3) concatenate user input, previous system action and slots embeddings for current time into an input vertor to rnn(合并用户输入,以前系统行为和槽作为当前时间用户的输入向量给rnn模型)
(4) using user and previous system action embeddings from the input vector, calculate attention probabilities over the user and system memories(使用输入向量中用户输入和以前的系统行为嵌入,来计算用户和系统的注意力向量)
(5) sum the user embedding and user attention vector and feed it and the embeddings of the slots as an input to an LSTM cell(用户词嵌入和用户注意力向量相加再和槽向量一起作为LSTM的输入)
(6) apply a dense layer to the output of the LSTM to get a raw recurrent embedding of a dialogue(应用LSTM的输出来获得一个对话的原始循环嵌入)
(7) sum this raw recurrent embedding of a dialogue with system attention vector to create dialogue level embedding, this step allows the algorithm to repeat previous system action by copying its embedding vector directly to the current time output(将对话的原始循环嵌入和系统注意力向量相加,来创建对话层的嵌入。这一步允许算法通过直接拷贝它的向量到当前的输出来重复之前的系统行为)
(8) weight previous LSTM states with system attention probabilities to get the previous action embedding, the policy is likely payed attention to(加权以前的LSTM状态和系统注意力来获取以前的行为嵌入,policy最有可能需要注意的)
(9) if the similarity between this previous action embedding and current time dialogue embedding is high, overwrite current LSTM state with the one from the time when this action happened(如果以前的行为嵌入和当前的对话嵌入相似度很高,overwrite当前的LSTM状态)
(10) for each LSTM time step, calculate the similarity between the dialogue embedding and embedded system actions(对于LSTM的每一步,计算对话嵌入和系统行为嵌入的相似度)
所以EmbeddingPolicy效果上来说会比较好,但是它有个问题是耗时,而且尤其是官网的源码,它并没有使用GPU、没有充分利用CPU资源。下图为KerasPolicy和EmbeddingPolicy比较效果图,可见EmbeddingPolicy效果明显优于KerasPolicy:
配置EmbeddingPolicy参数:
policies:
- name: EmbeddingPolicy
epochs: 20
featurizer:
- name: FullDialogueTrackerFeaturizer
state_featurizer:
- name: LabelTokenizerSingleStateFeaturizer
注:详情请参考Embedding Policy
Form Policy
FormPolicy是MemoizationPolicy的扩展,用于处理(form)表单的填充事项。当一个FormAction
被调用时,FormPolicy将持续预测表单动作,直到表单中的所有槽都被填满,然后再执行对应的FormAction,详情可查阅Slot Filling。
Mapping Policy
MappingPolicy可用于直接将意图映射到要执行的action,从而实现被映射的action总会被执行,其中,这种映射是通过triggers
属性实现的。举个栗子(domain.yml文件中):
intents:
- greet: {triggers: utter_goodbye}
其中,greet是意图;utter_goodbye是action。一个意图最多只能映射到一个action,我们的机器人一旦收到映射意图的消息,它将执行对应的action。然后,继续监听下一条message。需要注意的是,对于上述映射,我们还需要要在story.md文件中添加如下样本,否则,任何机器学习策略都可能被预测的action_greet在dialouge历史中突然出现而混淆。
Fallback Policy
如果意图识别的置信度低于nlu_threshold
,或者没有任何对话策略预测的action置信度高于core_threshold,FallbackPolicy将执行fallback action。通俗来说,就是我们的对话机器人意图识别和action预测的置信度没有满足对应的阈值,该策略将使机器人执行指定的默认action。configs.yml配置如下:
policies:
- name: "FallbackPolicy"
# 意图理解置信度阈值
nlu_threshold: 0.3
# action预测置信度阈值
core_threshold: 0.3
# fallback action
fallback_action_name: 'action_default_fallback'
其中,action_default_fallback
是Rasa Core中的一个默认操作,它将向用户发送utter_default模板消息,因此我们需要确保在domain.yml文件中指定此模板。当然,我们也可以在fallback_action_name
字段自定义默认回复的action,比如my_fallback_cation
,就可以这么改:
policies:
- name: "FallbackPolicy"
nlu_threshold: 0.4
core_threshold: 0.3
fallback_action_name: "my_fallback_action"
运行Rasa Core模块命令:
python -m rasa_core.run -d models/dialogue -u models/nlu/current --port 5002 --credentials credentials.yml --endpoints endpoints.yml --debug -o out.log
参数说明:
-m mod
指定运行模块;-d modeldir
指定dialog对话路径;-u modeldir
指定nlu模型路径;--port
指定Rasa Core web应用运行的端口号;--credentials credentials.yml
指定通道(input channels)属性;----endpoints endpoints.yml
该文件用于指定Rasa Core连接其他web server的url地址,比如nlu web或custom action web;-o file
指定输出log日志文件路径;--debug
打印调试信息,在显示的信息中,我们可以了解到用户输入Message后NLU模块是否提出出实体、意图及其置信度;插槽的填充情况和使用哪个policy(策略)来预测要执行的下一个action,如果这个 exact story已经在训练数据中,并且MemoizationPolicy是集成的一部分,那么它将被用于预测下一次动作的概率为1。注意(重要):如果所有的插槽值和NLU信息均是符合预期,但是仍然预测执行错误的action,我们就需要检测是哪个policy决定的这个action,如果是MemoizationPolicy,则说明在stories.md中我们设计的故事情节有问题;如果是KerasPolicy ,说明我们的模型预测得不对,这里就建议开启交互式学习来创建故事相关(relevant stories)的数据,并添加到我们的stories中。
接下来,我们重要解释下credentials.yml和endpoints.yml。
(1) credentials.yml
当我们的AI对话系统需要跟外部世界联系时,比如需要连接到facebook, slack, telegram, mattermost and twilio时,就需要使用credentials.yml来存储对应的访问权限信息。如果要连接到这些channels,Rasa Core将从yaml格式的凭证文件中读取这些属性。示例枚举如下:
twilio:
account_sid: "ACbc2dxxxxxxxxxxxx19d54bdcd6e41186"
auth_token: "e231c197493a7122d475b4xxxxxxxxxx"
twilio_number: "+440123456789"
slack:
slack_token: "xoxb-286425452756-safjasdf7sl38KLls"
slack_channel: "@my_channel"
telegram:
access_token: "490161424:AAGlRxinBRtKGb21_rlOEMtDFZMXBl6EC0o"
verify: "your_bot"
webhook_url: "your_url.com/webhook"
mattermost:
url: "https://chat.example.com/api/v4"
team: "community"
user: "[email protected]"
pw: "password"
facebook:
verify: "rasa-bot"
secret: "3e34709d01ea89032asdebfe5a74518"
page-access-token: "EAAbHPa7H9rEBAAuFk4Q3gPKbDedQnx4djJJ1JmQ7CAqO4iJKrQcNT0wtD"
webexteams:
access_token: "ADD-YOUR-BOT-ACCESS-TOKEN"
room: "YOUR-WEBEXTEAMS-ROOM-ID"
rocketchat:
user: "yourbotname"
password: "YOUR_PASSWORD"
server_url: "https://demo.rocket.chat"
# socket通道
# 前两个配置值定义Rasa Core在通过socket.io发送或接收消息时使用的事件名称
socketio:
user_message_evt: user_uttered
bot_message_evt: bot_uttered
session_persistence: true/false
# rest通道
rest:
# you don't need to provide anything here - this channel doesn't
# require any credentials
# CallbackInput通道
callback:
# URL to which Core will send the bot responses
url: "http://localhost:5034/bot"
当我们需要从自己开发的客户端访问人机对话系统,可以通过使用socket和http通道来实现,它们分别对应socketio输入通道和rest输入通道,而callback与rest通道类似,均是走HTTP协议,但是它不会直接将bot消息返回给发送消息的HTTP请求,而是调用一个URL,我们可以指定该URL来发送bot消息。由于我们使用HTTP情况比较多,该情况下credentials.yml的配置如下:
rest:
# you don't need to provide anything here - this channel doesn't
# require any credentials
(2) endpoints.yml
在Rasa Core项目创建endpoint.yml
文件,该文件用于指定Rasa Core将要访问的CustomeAction web和nlu web,当rasa core需要执行意图、实体提取和执行action时,就会根据url找到对应的web进行执行。这里假设CustomeAction web和NLU web是独立的项目,其中,localhost,即IP地址,表示部署在本地,如果部署在其他终端,改成对应的IP即可;5055默认为CustomeAction web端口;5000默认为NLU web。
# 指定custom action web url
action_endpoint:
url: "http://localhost:5055/webhook"
# 指定nlu web url
nlu:
url: "http://localhost:5000"
# you can also specify additional parameters, if you need them:
# headers:
# my-custom-header: value
# token: "my_authentication_token" # will be passed as a get parameter
# basic_auth:
# username: user
# password: pass
# 指定models url,即从其他服务器获取模型数据
models:
url: http://my-server.com/models/default_core@latest
wait_time_between_pulls: 10 # [optional](default: 100)
注:HTTP的POST方式访问url,其中,POST方式的body举例如下:
{ "tracker": { "latest_message": { "text": "/greet", "intent_ranking": [ { "confidence": 1.0, "name": "greet" } ], "intent": { "confidence": 1.0, "name": "greet" }, "entities": [] }, "sender_id": "22ae96a6-85cd-11e8-b1c3-f40f241f6547", "paused": false, "latest_event_time": 1531397673.293572, "slots": { "name": null }, "events": [ { "timestamp": 1531397673.291998, "event": "action", "name": "action_listen" }, { "timestamp": 1531397673.293572, "parse_data": { "text": "/greet", "intent_ranking": [ { "confidence": 1.0, "name": "greet" } ], "intent": { "confidence": 1.0, "name": "greet" }, "entities": [] }, "event": "user", "text": "/greet" } ] }, "arguments": {}, "template": "utter_greet", "channel": { "name": "collector" } }
endpoint的response举例如下:
{ "text": "hey there", "buttons": [], "image": null, "elements": [], "attachments": [] }
前面说到,CustomAction的具体业务逻辑是实现在一个独立的web server中,当然,我们也可以直接写到Rasa Core项目中。但是考虑到代码的可维护性和模块化,这里还是建议重新创建一个web server项目。对于这个web server的开发语言,虽然Rasa官方基本没有什么限制,但是我还是建议使用python,因为Rasa Core专门为此提供了一个SDK,即rasa-core-sdk
,便于我们快速开发action web。基本步骤如下:
第一步:创建action web项目,安装rasa-core-sdk
pip install rasa_core_sdk
注:目前最新版本为0.13.0
。
第二步:在web项目的根目录下创建actions.py文件,该文件实现具体的action业务逻辑,当然这个文件的名字可以任意命名,也不必要一定要放在根目录下,只是在启动web时需要改下参数。这里仍然以查询天气和空气质量举例,actions.py代码如下:
from rasa_core_sdk import Action
from rasa_core_sdk.events import SlotSet
# 查询天气action
class ActionSearchWeather(Action):
def name(self):
# type: () -> Text
return "action_search_weather"
def run(self, dispatcher, tracker, domain):
city = tracker.get_slot('city')
datetime = tracker.get_slot('datetime')
# 执行天气查询业务逻辑
....
# 回复用户Message方法1:使用dipatcher
# dipatcher.utter_message(‘result if result is not None else []’)
# return []
# 回复用户Message方法2:使用SlotSet
return [SlotSet("matches", result if result is not None else [])
# 查询空气质量action
class ActionSearchWeatherQuality(Action):
def name(self):
# type: () -> Text
return "action_search_weather_quality"
def run(self, dispatcher, tracker, domain):
city = tracker.get_slot('city')
datetime = tracker.get_slot('datetime')
# 执行查询空气质量业务逻辑
....
# 回复用户Message方法1:使用dipatcher
# dipatcher.utter_message(‘result if result is not None else []’)
# return []
# 回复用户Message方法2:使用SlotSet
return [SlotSet("matches", result if result is not None else [])]
需要注意的是,编写action的响应代码,必须遵守以下三个规则
:
创建一个继承于Action
的类,这个类的名字可以任意,但是这里还是建议直接根据action的名使用驼峰命名法命名;
重写Action的name
函数,返回值必须为对应的action名,因为这是Rasa Core定位到该action类的关键所在;
重写Action的run
函数,这个函数就是我们具体的业务所在,即当Rasa Core匹配action成功后,会自动执行该函数完成具体的操作并返回响应给用户。run函数需要传递四个参数,即self
、dispatcher
、tracker
以及domain
,其中后三个尤其重要。
函数原型:Action.run(dispatcher, tracker, domain)
(1) 参数说明
dispatche:该对象用于向用户回复消息,通过
dipatcher.utter_message()
、dispatcher.utter_template
以及其他rasa_core_sdk.executor
.CollectingDispatcher
方法。tracker:该对象描述当前会话的状态,通过该对象的
tracker.get_slot(slot_name)
方法可以轻松地获得指定插槽中的值,或者通过tracker.latest_message.text
获取最新的用户信息等;domain:该对象即为domain.yml
(2)返回值
返回一个列表
[]
,该列表可包含多个rasa_core_sdk.events.Event
对象,比如上面代码中的SlotSet对象就是一个event,它的作用就是完成插槽值设定这么一个时间。
从分析Action.run()函数原型可知,它的返回值是一个包含多个rasa_core_sdk.events.Event对象的列表,下面我们就具体介绍下这个Event对象。Event对象是Rasa Core描述会话中发生的所有事件和明确rasa.core.trackers.DialogueStateTracker
该如何更新其状态的基类,因此不能被直接使用,而是通过它包含的具体事件(event)实现。具体如下:
(a) SlotSet
:设置插槽值
Class | rasa_core.events.SlotSet(key, value=None, timestamp=None) |
---|---|
描述 | 该事件用于实现设置对话tracker中插槽(slot)的值,其中,参数key表示插槽名,参数value表示要设置的值 |
JSON | { ‘event’: ‘slot’, ‘name’: ‘departure_airport’, ‘value’: ‘BER’ } |
(b) Restarted
:重置tracker
Class | rasa_core.events.Restarted(timestamp=None) |
---|---|
描述 | 该事件用于重置tracker,即初始化tracker的状态,包括所有的会话历史,slots值。 |
JSON | { ‘event’: ‘restart’ } |
(3) AllSlotsReset
:重置一次会话中所有的插槽
Class | rasa_core.events.AllSlotsReset(timestamp=None) |
---|---|
描述 | 该事件用于初始化会话中所有插槽(slots),当我们希望保留对话历史,仅仅是重置所有slot的值就可以使用AllSlotsReset事件。 |
JSON | { ‘event’: ‘reset_slots’ } |
(4) ReminderScheduled
:定时执行某个action
Class | rasa_core.events.ReminderScheduled(action_name, trigger_date_time, name=None, kill_on_user_message=True, timestamp=None) |
---|---|
描述 | 该事件用于设置定时执行某个事件,其中,参数action_name为需要执行的action名字,参数trigger_date_time为时间。 |
JSON | { ‘event’: ‘reminder’, |
'action': 'my_action',
'date_time': '2018-09-03T11:41:10.128172',
'name': 'my_reminder',
'kill_on_user_msg': True
} |
(5) ConversationPaused
:暂停会话
Class | rasa_core.events.ConversationPaused(timestamp=None) |
---|---|
描述 | 该事件用于暂停会话,即对话机器人忽略用户输入的Message,不会执行预测到的action,直到执行了resume事件。通过该事件,我们可以人工接管对用户输入的Message作出响应。 |
JSON | { ‘event’: ‘pause’, |
} |
(6) ConversationResumed
:设置插槽值
Class | rasa_core.events.ConversationResumed |
---|---|
描述 | 该事件用于恢复之前被暂停的会话,对话机器人继续负责对用户输入作出响应。 |
JSON | { ‘event’: ‘resume’, |
} |
(7) FollowupAction
:强制设置next action为指定的action
Class | rasa_core.events.FollowupAction(name, timestamp=None) |
---|---|
描述 | 该事件的作用是强制设定Rasa Core的next action为某个固定的action,而不再通过预测来确定next action是什么。 |
JSON | { ‘event’: ‘followup’, |
'name': 'my_action'
} |
(1) UserUttered
:用户向对话机器人发送一条Message
Class | rasa_core.events.UserUttered(text, intent=None, entities=None, parse_data=None, timestamp=None, input_channel=None, message_id=None)[source] |
---|---|
描述 | 该事件的作用为用户向对话机器人发送一个条Message,但是当执行该事件后,会自动在Tracker对象中创建一个Turn (注:不知道这个Turn是什么意思)。 |
JSON | { ‘event’: ‘user’, |
'text': 'Hey',
'parse_data': {
'intent': {'name': 'greet', 'confidence': 0.9},
'entities': []
}
} |
(2) BotUttered
:对话机器人向用户发送一条Message
Class | rasa_core.events.BotUttered(text=None, data=None, timestamp=None) |
---|---|
描述 | 该事件用于对话机器人向用户发送一条Message,需要注意的是,BotUttered并不需要训练,它被包含在ActionExecuted类中,Track对象默认有一个实体。 |
JSON | { ‘event’: ‘bot’, |
'text': 'Hey there!',
'data': {}
} |
(3) UserUtteranceReverted
:当最后一个UserUttered被执行后撤销所有 side effects
Class | rasa_core.events.UserUtteranceReverted(timestamp=None) |
---|---|
描述 | 当最后一个UserUttered被执行后,该事件用于撤销所有side effects |
JSON | { ‘event’: ‘rewind’ } |
(4) ActionReverted
:当最后一个action被执行后,撤销所有 side effects
Class | rasa_core.events.ActionReverted(timestamp=None) |
---|---|
描述 | 机器人撤消它的最后一个动作 |
JSON | { ‘event’: ‘undo’, |
} |
(5) ActionExecuted
:Logs an action the bot executed to the conversation. Events that action created are logged separately.
Class | rasa_core.events.ActionExecuted(action_name, policy=None, confidence=None, timestamp=None)[source] |
---|---|
描述 | An operation describes an action taken + its result.It comprises an action and a list of events. operations will be appended to the latest Turn in the Tracker.turns. |
JSON | { ‘event’: ‘action’, |
'name': 'my_action'
} |
第三步:启动action web服务器,其中bot即bot.py,为action具体实现文件。
python -m rasa_core_sdk.endpoint --actions bot
虽然我们可以容易的人工构建story样本数据,但是往往会出现一些考虑不全,甚至出错等问题,基于此,Rasa Core框架为我们提供了一种交互式学习(Interactive Learning)来获得所需的样本数据。在互动学习模式中,当你与机器人交谈时,你会向它提供反馈,这是一种强大的方法来探索您的机器人可以做什么,也是修复它所犯错误的最简单的方法。基于机器学习的对话的一个优点是,当你的机器人还不知道如何做某事时,你可以直接教它。
首先,我们创建一些story样本。
## story1:greet only
* greet
- utter_answer_greet
> check_greet
## story2:
* goodbye
- utter_answer_goodbye
## story3:thanks
* thanks
- utter_answer_thanks
## story4:change address or data-time withe greet
> check_greet
* weather_address_date-time{"address": "上海", "date-time": "明天"}
- action_report_weather
## story5:change address or data-time withe greet
> check_greet
* weather_address_date-time{"address": "上海", "date-time": "明天"}
- action_report_weather
- utter_report_weather
* weather_address{"address": "北京"} OR weather_date-time{"date-time": "明天"}
- action_report_weather
- utter_report_weather
...
...
其次,启动交互式学习,其中,第一行命令用于启动custom action服务器;第二行命令用于启动交互式学习模式。在交互模式下,机器人会要求你确认NLU和Core做出的每一个预测。
python -m rasa_core_sdk.endpoint --actions actions&
python -m rasa_core.train \
interactive -o models/dialogue \
-d domain.yml -c policy_config.yml \
-s data/stories.md \
--nlu models/current/nlu \
--endpoints endpoints.yml
# 或者直接使用已经训练好的对话模型
# python -m rasa_core.train \
# interactive --core models/dialogue \
# --nlu models/current/nlu \
# --endpoints endpoints.yml
当然,在交互式学习过程中,我们还可以通过Rasa Core提供的可视化图形跟踪会话状态,只需要在浏览器输入http://localhost:5005/visualization.html即可。
为了评估训练好的对话模型,Rasa Core提供了rasa_core.evaluate
模块来实现。当执行下列命令后,rasa_core.evaluate模块会将预测action不正确的所有story存储到results/failed_stories.md
文件中。此外,该命令还会自动生成一个混淆矩阵(confusion matrix)文件results/story_confmat.pdf
,所谓的混淆矩阵表示的是预测domain.yml文件中每个action被预测命中的频率以及一个错误action被预测的频率。
python -m rasa_core.evaluate --core models/dialogue --stories test_stories.md -o results
参数说明:
usage: evaluate.py default [-h] [-m MAX_STORIES] [-u NLU] [-o OUTPUT] [--e2e]
[--endpoints ENDPOINTS]
[--fail_on_prediction_errors] [--core CORE]
(-s STORIES | --url URL) [-v] [-vv] [--quiet]
-m module
指定运行模块;-- core dir
指定对话模型路径;-- stories file或-s file
指定用于测试的story文件路径;-o dir
指定评估结果输出路径;-m number或--max_stories number
指定要测试的story的最大数量;-u NLU或--nlu NLU
指定NLU模型路径;--url URL
指定训练对话模型的story文件URL;--endpoints ENDPOINTS
指定端点文件;--e2e, --end-to-end
为combined action和意图预测运行一个端到端(end-to-end)评估; 假设我们的机器人使用对话模型与Rasa NLU模型组合来解析意图,希望评估这两个模型如何在整个对话中一起执行。rasa_core.evaluate
通过使用--e2e
参数选项,允许将Rasa NLU意图预测和Rasa Core行为预测结合,端对端地评估对话模型。执行脚本如下:
python -m rasa_core.evaluate default --core models/dialogue --nlu models/nlu/current --stories e2e_stories.md --e2e
需要注意的是,用于端到端评估的story格式与标准的Rasa Core story格式略有不同,前者必须使用自然语言包含用户消息,而不仅仅是它们的意图,即*
。
e2e_stories.md
示例如下:
## end-to-end story 1
* greet: hello
- utter_ask_howcanhelp
* inform: show me [chinese](cuisine) restaurants
- utter_ask_location
* inform: in [Paris](location)
- utter_ask_price
## end-to-end story 2
...
在评估模型的时候,需要我们构造一些对话测试集,通常这些测试集从训练的对话数据中抽取而来。但是,作为一个刚启动的项目,毕竟用于训练的对话数据非常有限,肯定是舍不得抽一部分用于测试评估的。因此,为了缓解这个问题,Rasa Core为我们提供一些脚本,允许我们对使用的Policies
方案进行对比,以选择一个性能最好的方案来训练我们的数据。步骤如下:
(1) 创建多个Policies配置文件,即不同方案,执行下列脚本
python -m rasa_core.train compare -c policy_config1.yml policy_config2.yml -d domain.yml -s stories_folder -o comparison_models --runs 3 --percentages 0 5 25 50 70 90 95
其中,-c file1 file2..
指定要对比的方案;-o dir
指定输出结果路径;--run num
指定run次数,以确保结果一致;-- percentages
指定Rasa Core将使用0、5、25、50、70进行多次训练
(2) 评估训练得到的对比模型,确定那种policies方法更好
python -m rasa_core.evaluate compare --stories stories_folder --core comparison_models -o comparison_results
其中,--core
指定对比模型所在路径;-o
指定输出结果。需要注意的是,如果不确定要比较哪些策略,官方建议可以从EmbeddingPolicy和KerasPolicy入手,确定哪种更适合。
在Rasa Core中,当我们执行一个action需要同时填充多个slot时,可以使用FormAction
来实现,因为FormAction会遍历监管的所有slot,当发现相关的slot未被填充时,就会向用户主动发起询问,直到所有slot被填充完毕,才会执行接下来的业务逻辑。
在doamin文件下新增forms:
部分,并将所有用到的form名称添加到该字段下:
intents:
- request_restaurant
- request_weather
slots:
- cuisine
type: unfeaturized
- num_people
type: unfeaturized
- outdoor_seating
type: unfeaturized
- preferences
type: unfeaturized
- feedback
type: unfeaturized
actions:
...
# form action?
forms:
- restaurant_form
- weather_form
...
templates:
- utter_ask_cuisine
- utter_ask_num_people
- utter_ask_outdoor_seating
- utter_ask_preferences
- utter_ask_feedback
- utter_ask_continue
...
在story中,不仅需要考虑用户按照我们的设计准确的提供有效信息,而且还要考虑用户在中间过程改变要执行的意图情况或称输入无效信息,因为对于FormAction来说,如果无法获得预期的信息,就会报``ActionExecutionRejection异常,这里我们分别称这两种情况为happy path、unhappy path。示例如下:
## dhappy path for restaurant
* request_restaurant
- restaurant_form
- form{"name": "restaurant_form"}
- form{"name": null}
## unhappy path for restaurant,chitchat exclude ActionExecutionRejection
* request_restaurant
- restaurant_form
- form{"name": "restaurant_form"}
* chitchat
- utter_chitchat
- restaurant_form
- form{"name": null}
## unhappy path for restaurant,change mind suddenly
* request_restaurant
- restaurant_form
- form{"name": "restaurant_form"}
* stop
- utter_ask_continue
* deny
- action_deactivate_form
- form{"name": null}
...
注:* request_restaurant
为意图;- restaurant_form
为form action;- form{"name": "restaurant_form"}
为激活表单(form);- form{"name": null}
为禁止表单;- action_deactivate_form
为默认的action,它的作用是用户可能在表单操作过程中改变主意,决定不继续最初的请求,我们使用这个default action来禁止(取消)表单,同时重置要请求的所有slots。
class RestaurantForm(FormAction):
"""Example of a custom form action"""
def name(self):
# type: () -> Text
"""Unique identifier of the form"""
return "restaurant_form"
@staticmethod
def required_slots(tracker):
# type: () -> List[Text]
"""A list of required slots that the form has to fill"""
return ["cuisine", "num_people", "outdoor_seating",
"preferences", "feedback"]
def submit(self, dispatcher, tracker, domain):
# type: (CollectingDispatcher, Tracker, Dict[Text, Any]) -> List[Dict]
"""Define what the form has to do
after all required slots are filled"""
# utter submit template
dispatcher.utter_template('utter_submit', tracker)
return []
当form action第一被调用时,form就会被激活并进入FormPolicy策略模式。每次执行form action,required_slots
会被调用,当发现某个还未被填充时,会主动去调用形式为uuter_ask_{slotname}
的模板(注:定义在domain.yml的templates字段中
);当所有slot被填充完毕,submit方法就会被调用,此时本次form操作完毕被取消激活。
修改Rasa Core的Policy配置文件configs.yml,新增FormPolicy
策略:
policies:
- name: KerasPolicy
epochs: 500
max_history: 5
- name: FallbackPolicy
fallback_action_name: 'action_default_fallback'
- name: MemoizationPolicy
max_history: 5
- name: "FormPolicy"