自定义nlu组件(详细版)

Enhancing Rasa NLU models with Custom Components

源文地址:https://blog.rasa.com/enhancing-rasa-nlu-with-custom-components/?_ga=2.115773509.2118304263.1591599801-549374689.1567321116

自定义组件增强nlu模型

我们认为自定义的机器学习模型对于构建成功的AI助手至关重要。开源的rasa给你为意图分类和实体提取构建好的nlu模型提供给你一个坚实基础,但是如果你想用你自己自定义组件来增强现有的rasa nlu模型,我们做了很多的工作使得rasa nlu模块化,这样你就可以做到这一点。想要学习如何去实现它?在这篇文章,你将学习到如何如何自定义组件并且将其添加到nlu管道让你的AI助手到达一个新的水平。

概要

1.介绍rasa nlu管道

2.介绍自定义组件

3.添加自定义的情感分析组件到rasa nlu

  • 建立自定义情感分析组件类

  • 如果我想使用一个预先训练好的情绪分析模型呢?

4.总结

5.资源

介绍rasa nlu管道

一个处理管道是rasa nlu模型的主要的基本构成要素。它定义传入的用户消息在产生模型输出之前必须经过处理阶段。这些阶段可以是符号化,特征化,意图分类,实体提取,模式匹配等等。默认的,rasa nlu已经给你自带了一些预先构建的组件。下面是一个rasa nlu的管道配置。

pipeline:

  • name: "SpacyNLP"

  • name: "SpacyTokenizer"

  • name: "SpacyFeaturizer"

  • name: "RegexFeaturizer"

  • name: "CRFEntityExtractor"

  • name: "EntitySynonymMapper"

  • name: "SklearnIntentClassifier"

一旦定义了管道,每个组件都会被一个接一个的调用,并且产生的输出要么直接添加到rasa nlu模型的输出,或者是被用做其他管道的输入。这是非常重要的,如何在配置文件中定义组件。例如,如果你在你的管道中定义了三个组件[‘Component1’, ‘Component2’, ‘Component3’],component1的方法将在第一个被调用,下面这个图片展示了组件的生命周期:

img

组件通过三个主要的阶段:

  • 创建:训练之前初始化组件

  • 训练:组件使用上下文和前面组件的输出对自己进行训练

  • 持久化:保存训练组件到磁盘,以备将来使用

在初始化第一个组件之前,创建一个所谓的上下文,用于组件之间传递信息。例如,一个组件可以计算训练数据的特征向量,将其储存在上下文中,并且可以从上下文中检索那些特征向量并进行意图分类。一旦所有组件创建,训练和持久化完成,就会创建描述整个nlu模型的模型元数据。

介绍自定义组件

使用预构建的rasa nlu组件,你可以自由的定制模型。但是在有些情况下,你想要添加的组件并没有在rasa nlu中预先实现。例如,你可能想添加情感分析让你的助手根据用户的心情产生不同的响应,或者是希望添加一个拼写检查器来纠正用户信息拼写错误,然后对意图进行分类并提取实体并用于API调用或数据库查找。向nlu管道添加自定义组件是使用必需的方法实现自定义组件类,并在Rasa NLU管道配置文件中引用它的过程。通常,您可能要使用两种类型的自定义组件:

  • 已经预训练模型的组件(例如:在不同的数据集或包上训练,如 python libraries, .pkl files, etc)

  • 在你的rasa nlu数据上训练的组件,并在你更改训练示例或添加更多训练示例时得到改进

在这篇文章的下一步,你将学习如何在实践中实现这两种情况。

添加自定义的情感分析组件到rasa nlu

我们拿一个情感分析的实际例子添加到rasa nlu管道中。首先, 你应该学习如何去设计它,当你添加更多的训练数据时提高你组件的性能。因为情感分析是有监督的分类问题,这意味着对于这个特例,你将必须在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

接下来,这都是关于构建实际组件的。我们开始如何来实现!

建立自定义情感分析组件类

自定义组件类将定义如何训练组件,哪些详细信息作为输入,以及成哪些详细信息作为输出。定义组件,创建一个新的文件(如:sentiment.py),并且开始设置你自定义组件类的名字以及描述该组件的细节如下:

  • name: 组件命名

  • provides: 自定义组件产生哪些输出

  • require: 该组件需要消息的哪些属性

  • defaults: 一个组件的默认配置参数

  • language_list:与组件兼容的语言列表

在下面的例子中,自定义组件类命名为 SentimentAnalyzer ,组件的真实名字为 sentiment。为了对话管理模型访问该组件的细节,并使它能基于用户情绪驱动对话,情感分析的结果将作为实体进行保留。处于这个原因,情感分析的组件配置包括组件提供的entities。因为情感模型接受字符作为输入,所以这些细节可以从其他负责符号化的管道组件获取。这就是为什么下面的组件配置声明自定义组件需要的tokens。最后,因为这个例子包含情感分析模型仅仅在英文运行,语言列表中包括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):
    """A custom sentiment analysis 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):
        """Load the sentiment polarity labels from the text
           file, retrieve training tokens and after formatting
           data train the classifier."""

        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):
        """Convert model output into the Rasa NLU compatible output format."""

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

        return entity

    def preprocessing(self, tokens):
        """Create bag-of-words representation of the training examples."""

        return ({word: True for word in tokens})

    def process(self, message, **kwargs):
        """Retrieve the tokens of the new message, pass it to the classifier
            and append prediction results to the message class."""

        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):
        """Persist this model into the passed directory."""
        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管道配置文件中引用它。你引用自定义组件,可以像引用python模块一样引用它 —module_name.class_name。由于此自定义组件需要tokens,所以应该将其添加到生成tokens的组件之后。下面的示例管道配置意味着,将在SpacyTokenizer之后调用sentiment组件的方法。

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'
 }

如果我想使用一个预先训练好的情绪分析模型呢?

上面的自定义组件示例包括一个相当简单的sentiment模型,当您添加更多的NLU训练示例时,它将得到改进。如果你更喜欢使用预先训练过的模型,自定义组件类的实现会非常相似,除了一些细节:

  • 您不必实现train()和persist()方法类,因为您的组件已经经过了训练和持久化(可能作为python模块或持久化模型)。

  • 您可以在process()方法中更改传递给模型的细节。例子: 你的预先训练情感模型将未经处理的文本消息作为输入,而不是tokens。

为了说明这种情况,让我们修改之前实现的自定义组件的代码,代替训练自定义的情感模型,我们使用一个由NLTK自然语言工具包提供的预训练的情感分析器模型:

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):
    """A pre-trained sentiment 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):
        """Not needed, because the the model is pretrained"""
        pass

    def convert_to_rasa(self, value, confidence):
        """Convert model output into the Rasa NLU compatible output format."""

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

        return entity

    def process(self, message, **kwargs):
        """Retrieve the text message, pass it to the classifier
            and append the prediction results to the message class."""

        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 because a pre-trained model is already persisted"""

        pass

在这种情况下,train()和persist()方法都是pass,因为模型已经被预先训练并持久化为NLTK方法。另外,由于模型将未处理的文本作为输入,所以process()方法检索实际的消息并将它们传递给模型,由模型执行所有处理工作并进行预测。

总结

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

有用的资源

  • ****Custom Components documentation****

  • ****Rasa NLU pipeline component configuration****

  • ****Rasa Community Forum****

你可能感兴趣的:(自定义nlu组件(详细版))