Rasa代码修改
Rasa的核心就是NLU和Core。NLU是为了处理语言、即NLP的部分。Core是为了构建整个对话。因此,我们暂时可以将要做的事简化成一来一回的QA对话模式,这样可以先不用考虑多轮对话、意图跳转、上下文关联等等复杂的情况。
在简单的QA对话模式下,那主要就是要识别一句话的意图以及这句话里的主要信息点。
天气处理例子:
问:北京今天天气怎么样?
答:北京今天天气晴,温度18℃~27℃,出门请注意防晒。
为了实现上述对话,网上各类开放的天气查询接口还是很多的,但是查询之前,需要获取的信息包括:
1 地点: 北京
2 日期: 今天
因此,我们需要实现两个点:
- 识别意图: 天气查询
- 识别命名实体: 地点、日期
在这个部分,其实标准的Rasa的pipeline会是这样的:
Rasa pipeline
常见的办法,第一个是“分词”,将整句话分成特定的词,然后根据词提取特征,将特征输入下一层的神经网络实现特征与意图的对应。
这是我曾经尝试过的一个pipeline,对于中文处理,jieba分词非常常见,另外还有哈工大、清华、北大等的开源分词,当然它们额外还提供了很多词性、句法分析等等功能,但是后来我没想到怎么用更合适。
Used
后来看到bert以后,觉得我们如果按照美国人的思路这么搞,就把事情搞复杂了。真要把中午的一句话往下分拆、一个字一个词的分析,加上中国人说话又不那么标准,我个人感觉这就没头了。
后来包括baidu的ernie等,也是依赖整句话的输入,然后通过mark掉一些词来训练。其实bert也是这样的能力。所以后来的思路,就是直接将pipeline简化成基于bert整句输入,直接识别。
新的pipeline就是这样了
bert pipeline
这个pipeline里面前面的两个就是识别命名实体,识别语句意图。最后一个是为了识别同义词进行映射。毕竟中国人很多时候:“今天”, “今儿”, “今儿个”等等
需要在Rasa代码中增加的部分
主要的内容就是增加基于bert的意图识别和命名实体的识别
BERTClassifier: 按照Rasa的要求实现一个custom component
class BERTClassifier(Component):
defaults = {
"port": 5555,
"port_out": 5556
}
def __init__(self, component_config=None):
super(BERTClassifier, self).__init__(component_config)
self.port = self.component_config.get("port")
self.port_out = self.component_config.get("port_out")
self.class_bc = BertClient(port=self.port, port_out=self.port_out, show_server_config=False, check_version=False, check_length=False, mode='CLASS')
def process(self, message, **kwargs):
# type: (Message, **Any) -> None
rst = self.class_bc.encode([message.text])
intent = rst[0]['pred_label'][0]
confidence = rst[0]['score'][0]
intent = {"name": intent, "confidence": confidence}
logger.debug("classifier intent:{}, confidence:{}".format(intent, confidence))
message.set("intent", intent, add_to_output=True)
BERTCrfNerExtractor: 也是一样的目的
class BERTCrfNerExtractor(EntityExtractor):
name = "bert_crf_ner_extractor"
defaults = {
"port": 5455,
"port_out": 5456
}
def __init__(self, component_config=None):
# type: (Optional[Dict[Text, Text]]) -> None
super(BERTCrfNerExtractor, self).__init__(component_config)
self.component_config = component_config
self.port = self.component_config.get("port")
self.port_out = self.component_config.get("port_out")
self.ner_bc = BertClient(port=self.port, port_out=self.port_out, show_server_config=False, check_version=False, check_length=False, mode='NER')
def train(self, training_data, config, **kwargs):
# type: (TrainingData) -> None
pass
def process(self, message, **kwargs):
# type: (Message, **Any) -> None
extracted = self.add_extractor_name(self.parse(message))
message.set("entities", extracted, add_to_output=True)
def parse(self, example):
raw_entities = example.get("entities", [])
logger.debug("current entities: {}".format(raw_entities))
# start_t = time.perf_counter()
rst = self.ner_bc.encode([list(example.text)], is_tokenized=True)
labels = rst[0]
logger.debug("extract labels: {}".format(labels))
start = 0
end = 0
entity = ""
for i in range(len(labels)):
logger.debug("i:{}, start:{}, end:{}, entity:{}".format(i, start, end, entity))
logger.debug("label: {}, pos 0:{}".format(labels[i], labels[i][0]))
if labels[i] == "O":
continue
elif labels[i][0] == "B":
if entity != "":
end += 1
logger.debug("extract entity: start:{}, end:{}, value:{}, entity:{}".format(start, end, example.text[start:end], entity))
raw_entities.append({
'start': start,
'end': end,
'value': example.text[start:end],
'entity': entity
})
start = i
end = start
entity = labels[i][2:]
elif labels[i][0] == "I":
end = i
if entity != "":
end += 1
logger.debug("extract entity: start:{}, end:{}, value:{}, entity:{}".format(start, end, example.text[start:end], entity))
raw_entities.append({
'start': start,
'end': end,
'value': example.text[start:end],
'entity': entity
})
# print('rst:', rst)
# print(len(rst[0]))
# print(time.perf_counter() - start_t)
logger.debug("after parse entities: {}".format(raw_entities))
return raw_entities