Rasa 详解自定义 NLU 组件

Rasa 详解自定义 NLU 组件_第1张图片
rasa_tutorial_custom_nlu_background.png

我们认为,定制 ML 模型对于构建成功的 AI 助手至关重要。开源的 Rasa 为你构建用于意图分类和实体提取的良好 NLU 模型提供了坚实的基础,但是如果你想通过自定义组件(比如:情感分析器、拼写检查器、字符分词器、字节匹配编码器等)来增强现有 Rasa NLU 模型,我们在 Rasa NLU 模块化方面投入了大量工作,以便可以做到这一点。本文教程中,你将学习如何在 Rasa 中自定义 NLU 组件,你可以创建一个自定义组件来执行当前 Rasa NLU 组件中没有提供的功能,以便你的 AI 助手达到一个全新的水平。

本文的目录结构:

  1. Rasa NLU pipeline 介绍
  2. 自定义组件介绍
  3. 将自定义的情感分析组件添加到 Rasa NLU 中
  4. 总结

1. Rasa NLU pipeline 介绍

处理 pipeline 是 Rasa NLU 模型的主要构建模块,它定义了传入的用户消息必须经过哪些处理阶段,直到模型输出为止。这些阶段可以是:分词阶段、特征提取阶段、意图分类阶段、实体提取阶段、模式匹配阶段等等。默认情况下,Rasa NLU 附带了一堆预构建的组件供你使用,下面是以 Rasa NLU pipeline 的配置示例:

pipeline:
    - name: "SpacyNLP"
    - name: "SpacyTokenizer"
    - name: "SpacyFeaturizer"
    - name: "RegexFeaturizer"
    - name: "CRFEntityExtractor"
    - name: "EntitySynonymMapper"
    - name: "SklearnIntentClassifier" 

一旦 pipeline 被定义后,每个组件都将被另一个组件调用,并产生输出,该输出可以直接添加到 Rasa NLU 模型输出中,也可用作其他组件的输入。如何在配置文件中定义组件,十分重要,例如:在 pipeline 中定义了三个组件 ['Component1', 'Component2', 'Component3'],则将首先调用 Component1 的方法,下图显示了组件的生命周期:

Rasa 详解自定义 NLU 组件_第2张图片
rasa_tutorial_custom_nlu_01.png

组件经历了三个主要阶段:

  • 创建:在训练之前初始化组件
  • 训练:该组件使用上下文和可能的先前组件的输出进行自我训练
  • 持久化:将经过训练过的组件保存到磁盘上以备将来使用

在初始化第一个组件之前,会创建一个上下文,该上下文用于组件之间传递信息。例如:一个组件可以计算训练数据的特征向量,将其存储在该上下文中,另一个组件可以从该上下文中检索这些特征向量并进行意图分类。当创建、训练、持久化了所有组件后,将创建一个描述整个 NLU 模型的模型元数据。

2. 自定义组件介绍

使用预构建的 Rasa NLU 组件,你可以很方便的自定义模型。但是,在某些情况下,你可能需要添加一个在 Rasa NLU 组件中未本地实现的组件。例如:添加一个情感分析器,用于根据用户的心情做出不同的响应,或者你想添加一个拼写检查器来更正用户消息中的拼写错误,然后对其进行意图分类和实体提取,接着用于 API 调用或者数据库中查找记录。在 NLU pipeline 中添加自定义组件,需要实现自定义组件类,该类需实现必须的方法,并在 Rasa NLU pipeline 配置文件中引用。通常,通过以下两种情况来自定义组件:

  • 预训练模型中的组件(例如:在不同的数据集上进行训练并打包为 python 库、.pkl 文件等)
  • 在你的 Rasa NLU 训练数据集上进行训练,并对训练示例进行更改或添加更多的训练示例来优化的组件

下面将介绍如何在实践中实现这两种情况。

3. 将自定义的情感分析组件添加到 Rasa NLU 中

我们将以情感分析器添加到 Rasa NLU pipeline 中为例,首先,你将学习如何设计它,以便在添加更多训练示例时提高组件的性能。由于情感分析是有监督的分类问题,因此,对于这个特定的示例,你将不得不为 NLU 训练示例分配更多情感值标签(正、负、中性),添加这些新标签的方法之一是将它们存储在一个单独文件中。比如:你的 Rasa NLU 训练数据看起来如下所示:

## intent: feedback
- It’s very helpful
- I had the best experience speaking with you
- no feedback
- ok
- You are the most stupid bot I have ever seen
- the worst

相应的标签如下所示:

pos
pos
neu
neu
neg
neg

3.1 构建一个自定义情感分析组件类

自定义组件类将定义如何训练组件,将什么内容作为输入以及将产生什么内容。要自定义组件,先创建一个新文件(例如:sentiment.py),然后从设置自定义组件类名以及定义该组件的主要内容描述开始:

  • name:组件名
  • provides:组件产生的输出内容
  • requires:此组件需要哪些消息属性
  • defaults:组件的默认配置参数
  • language_list:组件兼容的语言列表

在下面的示例中,自定义组件类名为 SentimentAnalyzer,组件名为 sentiment。为了使对话管理模型能够访问此组件的详细内容,并根据用户的心情使用它来驱动对话,情感分析结果将另存为实体。因此,情感分析组件包括了提供 entities。由于情感模型将分词作为输入,因此可以从其他负责分词的 pipeline 组件中获取详细内容。这就是为什么下面的组件配置中需要分词的原因,最后,由于此示例仅适用于英语的情感分析模型,因此,在 language_list 中包含 en

定义好之后,接着实现该类的主要方法:

  • __init__:初始化组件
  • train:该方法负责训练
  • process:该方法用于解析传入的用户消息
  • persist:该方法用于将训练过的组件保存到磁盘上以备将来使用

下面的代码显示了该特定示例的实现方法,我们将逐步进行介绍:

  • __init__:初始化自定义组件类,设置自定义组件的主要内容描述。
  • train():前面的组件产生分词作为输入,训练数据,由加载情绪标签和格式化数据,形成一个情感分类。
  • process():该函数用于将新用户消息的分词和情感分析模型预测作为实体消息类。
  • persist():该函数以 .pkl 文件保存训练后的情感分析模型作为以后使用
  • load():该函数定义了持久化的情感分析模型如何被加载。
from rasa.nlu.components import Component
from rasa.nlu import utils
from rasa.nlu.model import Metadata

import nltk
from nltk.classify import NaiveBayesClassifier
import os

import typing
from typing import Any, Optional, Text, Dict

SENTIMENT_MODEL_FILE_NAME = "sentiment_classifier.pkl"

class SentimentAnalyzer(Component):
    """自定义情感分析组件"""
    name = "sentiment"
    provides = ["entities"]
    requires = ["tokens"]
    defaults = {}
    language_list = ["en"]
    print('initialised the class')

    def __init__(self, component_config=None):
        super(SentimentAnalyzer, self).__init__(component_config)

    def train(self, training_data, cfg, **kwargs):
        """从文本文件中加载情感标签,
           检索训练分词并格式化,
           形成情感分类器。"""

        with open('labels.txt', 'r') as f:
            labels = f.read().splitlines()

        training_data = training_data.training_examples #list of Message objects
        tokens = [list(map(lambda x: x.text, t.get('tokens'))) for t in training_data]
        processed_tokens = [self.preprocessing(t) for t in tokens]
        labeled_data = [(t, x) for t,x in zip(processed_tokens, labels)]
        self.clf = NaiveBayesClassifier.train(labeled_data)


    def convert_to_rasa(self, value, confidence):
        """将模型输出转换为 Rasa NLU 兼容的输出格式。"""

        entity = {"value": value,
                  "confidence": confidence,
                  "entity": "sentiment",
                  "extractor": "sentiment_extractor"}

        return entity
        

    def preprocessing(self, tokens):
        """创建训练示例的词袋表示。"""
        
        return ({word: True for word in tokens})


    def process(self, message, **kwargs):
        """检索新消息的分词,并将其传给分类器,
           将预测结果追加到 message 中。"""
        
        if not self.clf:
            # component is either not trained or didn't
            # receive enough training data
            entity = None
        else:
            tokens = [t.text for t in message.get("tokens")]
            tb = self.preprocessing(tokens)
            pred = self.clf.prob_classify(tb)

            sentiment = pred.max()
            confidence = pred.prob(sentiment)

            entity = self.convert_to_rasa(sentiment, confidence)

            message.set("entities", [entity], add_to_output=True)


    def persist(self, file_name, model_dir):
        """将此模型持久化传入的目录中。"""
        classifier_file = os.path.join(model_dir, SENTIMENT_MODEL_FILE_NAME)
        utils.json_pickle(classifier_file, self)
        return {"classifier_file": SENTIMENT_MODEL_FILE_NAME}

    @classmethod
    def load(cls,
             meta: Dict[Text, Any],
             model_dir=None,
             model_metadata=None,
             cached_component=None,
             **kwargs):
        file_name = meta.get("classifier_file")
        classifier_file = os.path.join(model_dir, file_name)
        return utils.json_unpickle(classifier_file)

就是这样!你刚刚实现了一个自定义组件,该组件可以解析传入的用户消息,并将情感作为一个叫做 sentiment 的实体返回。要使用此组件,请确保在 Rasa NLU pipeline 配置中引用它。你可以像引用 Python 模块 -module_name.class_name 一样引用自定义组件。由于此自定义组件需要 tokens,因此应将其添加到产生 tokens 的组件之后。下面是示例流水线化的配置,情感分析组件的方法将在 SpacyTokenizer 之后调用组件,负责句法分析成 tokens

pipeline:
- name: "SpacyNLP"
- name: "SpacyTokenizer"
- name: "sentiment.SentimentAnalyzer" 
- name: "SpacyFeaturizer"
- name: "RegexFeaturizer"
- name: "CRFEntityExtractor"
- name: "EntitySynonymMapper"
- name: "SklearnIntentClassifier"

使用自定义的情感分析组件训练 Rasa NLU 模型之后,你可以测试其性能!

注意:为了确保 Rasa 能够使用你的组件,请确保将项目目录添加到 PYTHONPATH 中,因此,你可以使用以下命令:

export PYTHONPATH=/path_to_your_project_dir/:$PYTHONPATH

下面是一个带有自定义情感分析组件的 Rasa NLU 模型输出的示例,当一个礼貌的用户向助手打招呼时:

{  
   'intent':{  
      'name':'greet',
      'confidence':0.44503513568867775
   },
   'entities':[  
      {  
         'value':'neg',
         'confidence':0.9933702940854111,
         'entity':'sentiment',
         'extractor':'sentiment_extractor'
      }
   ],
   'intent_ranking':[  
      {  
         'name':'greet',
         'confidence':0.44503513568867775
      },
      {  
         'name':'chitchat',
         'confidence':0.20129539551108508
      },
      {  
         'name':'inform',
         'confidence':0.09576408290307896
      },
      {  
         'name':'goodbye',
         'confidence':0.08987117551991658
      },
      {  
         'name':'decline',
         'confidence':0.08840002616908385
      },
      {  
         'name':'affirm',
         'confidence':0.04842063587016189
      },
      {  
         'name':'restaurant',
         'confidence':0.03121354833799584
      }
   ],
   'text':'Hello stupid bot'
}

3.2 使用预训练的情感分析模型

上面的自定义组件示例是一个比较简单的情感分析模型,当你添加更多的 NLU 训练数据时,模型性能会有所改善。如果你更愿意使用与训练模型,那么实现方式与自定义组件类的实现非常相似,除了以下一些细节:

  • 你不必实现 train()persist() 方法,因为你的组件已经训练和持久化过了。
  • 你可以在 process() 方法中更改哪些详细内容传送给你的模型,比如:你的预训练情感分析模型将未经处理的文本消息作为输入而不是 tokens

为了演示这种情况,让我们使用 NLTK 自然语言工具包提供的预训练模型:SentimentIntensityAnalyzer,然后修改之前自定义组件的代码。

from rasa.nlu.components import Component
from rasa.nlu import utils
from rasa.nlu.model import Metadata

import nltk
from nltk.sentiment.vader import SentimentIntensityAnalyzer
import os

class SentimentAnalyzer(Component):
    """预训练情感分析组件"""

    name = "sentiment"
    provides = ["entities"]
    requires = []
    defaults = {}
    language_list = ["en"]

    def __init__(self, component_config=None):
        super(SentimentAnalyzer, self).__init__(component_config)


    def train(self, training_data, cfg, **kwargs):
        """不需要实现该方法,因为预训练模型已经训练了。"""
        pass


    def convert_to_rasa(self, value, confidence):
        """将模型输出转换为 Rasa NLU 兼容的输出格式。"""
        
        entity = {"value": value,
                  "confidence": confidence,
                  "entity": "sentiment",
                  "extractor": "sentiment_extractor"}

        return entity


    def process(self, message, **kwargs):
        """检索文本消息,并将其传给分类器,
           将预测结果追加到 message 中。"""

        sid = SentimentIntensityAnalyzer()
        res = sid.polarity_scores(message.text)
        key, value = max(res.items(), key=lambda x: x[1])

        entity = self.convert_to_rasa(key, value)

        message.set("entities", [entity], add_to_output=True)

        
    def persist(self, model_dir):
        """不需要实现该方法,因为预训练模型已经持久化了。"""

        pass

在这种情况下,train()persist() 方法都是 pass,因为预训练模型已经训练和持久化过了。此外,由于模型将未处理的文本作为输入,因此在 process() 方法中,将检索实际消息并将其传递给模型,该模型负责所有处理工作并进行预测。

4. 总结

本教程中,学习了如何创建自定义组件并将其添加到 Rasa NLU pipeline 中,你可以添加任意自定义组件,但重要的是要了解它们如何与其他处理组件配合使用,以及它们应该产生什么输出,将某些内容传递给 pipeline 中的其他组件或者向模型的输出添加某些内容。


作者:关于我

备注:转载请注明出处。

如发现错误,欢迎留言指正。

你可能感兴趣的