オブジェクトの広場は株式会社オージス総研グループのエンジニアによる技術発表サイトです

AI

はじめての自然言語処理

第2回 Rasa NLU を用いた文書分類と固有表現抽出
技術部 アドバンストテクノロジセンター
鵜野 和也
2019年4月23日

前回は、単語のカウントや分散表現を用いて文書の類似性評価をする手法を紹介しました。今回はチャットボット構築の必須技術である NLU (Natural Language Understanding=自然言語理解) について OSS の Rasa NLU を題材に、 NLU とは何か、Rasa NLU の使用方法と日本語で利用する際のポイント、日本語データセットでの実験結果を紹介します。

1. 始めに

本記事では OSS の Rasa NLU を題材に NLU(自然言語理解)、特に文書分類と固有表現抽出について説明します。Rasa NLU の使用方法と日本語で利用する際のポイントを解説し、日本語のデータで精度評価を行った結果を紹介します。今回も各手法の数学的な細かい説明などは省くので概念的な考え方を理解してもらえればと思います。

2. NLU (Natural Language Understanding=自然言語理解)

まず、NLU について少し整理しておきます。NLU という用語の一般的な意味合いは「計算機に自然言語で記述された文の意図を抽出させる技術」というところでしょう。「意図を抽出する」の具体的な内容には幅広い解釈があると思いますが、この記事ではスマートスピーカに見られるような、比較的短い自然言語文から、その意図、対象や条件を抽出する処理を対象とします。

NLU を謳う製品やサービスは、DialogFlow, Lex, LUIS, Snips NLU, Rasa NLU 等々ありますが、基本的にはどれもユーザの意図(=インテント)を判断して、その対象(=エンティティ)を抽出する機能を有しています。「エアコンの設定温度を28度にして」であれば、インテントは “AdjustTemprature"、エンティティは"28度"になるでしょう。チャットボットの自然言語インタフェースのバックエンドに複数のサービス(エアコンの温度を変えたり、TVのチャンネルを変えたり)が待機しており、インテントで呼び出すサービスを、エンティティでパラメータを決定するイメージです。インテントの判断、エンティティの抽出に用いられる手法がそれぞれ「文章分類」と「固有表現抽出」です。

3. Rasa NLU

Rasa NLU はドイツの Rasa Technologies GmbH が開発するオープンソースソフトウェアで、Apache 2.0 ライセンスで公開されています。 Rasa 製品の全体像を説明しておくと、OSS の Rasa Stack とその商用版の Rasa Platform があり、Rasa Stack は Rasa NLU と Rasa Core から構成されるという形です。文章分類、固有表現抽出といった NLU の中核機能が Rasa NLU で実装されており、Rasa Core では対話管理や Facebook Messenger, Slack 等との連携機能が提供されています。この記事では Rasa NLU 以外には特に触れませんので、興味のある方は Rasa 製品のページを参照してください*1。またこの記事は rasa-nlu 0.13.7 を対象に記述しています。

Rasa製品の構成

Rasa NLU の内部はコンポーネント化されており、データは複数のコンポーネントが数珠つなぎになったパイプラインで処理されます。

Rasa NLU のパイプライン

3.1 Rasa NLU の基本的な使用方法

まずは簡単に Rasa NLU の使用法を説明していきたいと思います。とりあえず動かす程度の基本的な内容だけ記述しますので、より詳しく知りたい方は Rasa NLU のドキュメントを参照してください。また、日本語を使用する場合は一部のコンポーネントを差し替える必要がありますので、実行前に 3.2 Rasa NLU での日本語の利用 を参照してください。なお、コマンド出力の内容はイメージを伝える為のダミーですので、データや設定の内容とは無関係です。

設定ファイルのフォーマット

パイプラインを構成するコンポーネントやその順序は設定ファイルにYAMLで記述します。以下は既定のパイプラインとして用意されている tensorflow_embedding の内容です *2

language: "en"

pipeline:
- name: "tokenizer_whitespace"
- name: "ner_crf"
- name: "ner_synonyms"
- name: "intent_featurizer_count_vectors"
- name: "intent_classifier_tensorflow_embedding"

上記の設定は以下のように処理されます。

  1. "tokenizer_whitespace" : 処理対象の文章をトークンに分割
  2. "ner_crf" : エンティティを抽出`
  3. "ner_synonyms" : 抽出されたエンティティをシノニムに置き換え
  4. "intent_featurizer_count_vectors" : 文章を固定長のベクトルに変換
  5. "intent_classifier_tensorflow_embedding" : 文章ベクトルをインテントに分類

学習/テストデータのフォーマット

学習/テストデータは以下のように MarkDown フォーマットで記述します。

## intent:BookRestaurant
- [寿司](served_dish)のある[パブ](restaurant_type)を予約して下さい。
- [東京](state)で[十人](party_size_number)の席を予約してください。
- 私は[刈谷](city)の[カンテサンス](restaurant_name)を予約して欲しいです

## intent:thankyou
- サンキュー
- ありがとう
- おおきに

"## intent:***" でインテントを定義し、以降の行に "- " 始まりで文例を記述します。文例中では "[寿司](served_dish)" のようにエンティティを定義します。"寿司" が文中のエンティティ、"served_dish" がエンティティの種別になります。

学習の実行

設定ファイル(sample.yml)と学習データ(train_ja.md)が用意できたら以下のようにして学習を実行します。

$ python -m rasa_nlu.train --config sample.yml --data train_ja.md \
  --path projects --project sample_proj \ --fixed_model_name sample_model \
  --verbose

学習が終わると ./projects/sample_proj/sample_model に生成されたモデルが保存されています。

学習済みモデルの評価

以下のコマンドでテストデータ(test_ja.md)で学習済みモデルの評価を行います。

python -m rasa_nlu.evaluate --data test_ja.md --model projects/sample_proj/sample_model \
  --errors errors.json --histogram hist.png --confmat confmat.png 

実行すると以下のように文章分類(Intent evaluation)と固有表現抽出(Entitiy evaluation)の評価結果が出力されます。また、文章分類に関しては、評価エラーの内容、ヒストグラム、混同行列が指定したファイルに出力されます。評価結果で望ましい精度が得られない場合は、学習データを追加する、設定パラメータを変更してみる等の調整の上で再学習・評価を行います。

...
2019-03-15 07:23:39 INFO     __main__  - Intent evaluation results:
...
2019-03-15 07:23:40 INFO     __main__  - F1-Score:  0.8333333333333333
2019-03-15 07:23:40 INFO     __main__  - Precision: 1.0
2019-03-15 07:23:40 INFO     __main__  - Accuracy:  0.75
2019-03-15 07:23:40 INFO     __main__  - Classification report: 
                precision    recall  f1-score   support

                     0.00      0.00      0.00         0
BookRestaurant       1.00      1.00      1.00         2
      thankyou       1.00      0.50      0.67         2

   avg / total       1.00      0.75      0.83         4
...
2019-03-15 07:23:41 INFO     __main__  - Confusion matrix, without normalization: 
[[0 0 0]
 [0 2 0]
 [1 0 1]]
2019-03-15 07:23:43 INFO     __main__  - Entity evaluation results:
...
2019-03-15 07:23:43 INFO     __main__  - F1-Score:  0.44999999999999996
2019-03-15 07:23:43 INFO     __main__  - Precision: 0.36
2019-03-15 07:23:43 INFO     __main__  - Accuracy:  0.6
2019-03-15 07:23:43 INFO     __main__  - Classification report: 
                   precision    recall  f1-score   support

             city       0.00      0.00      0.00         2
        no_entity       0.60      1.00      0.75        21
party_size_number       0.00      0.00      0.00         4
  restaurant_name       0.00      0.00      0.00         5
            state       0.00      0.00      0.00         3

      avg / total       0.36      0.60      0.45        35

2019-03-15 07:23:43 INFO     __main__  - Finished evaluation

Rasa NLU サーバーの起動と REST API 呼び出し

以下のコマンドで Rasa NLU をサーバとして起動します。

$ python -m rasa_nlu.server --path ./projects &

サーバを起動したら、以下の要領で REST API を呼び出すことができます。

curl -X POST localhost:5000/parse \
  -d '{"q":"茨城の料亭を5人で予約して下さい。", "project": "sample_proj", "model": "sample_model"}'

以下のようなイメージで結果が返ります。インテントとして "BookRestaurant" が検出され、その確信度は 94% です。 また、エンティティとして 「茨城」,「料亭」,「5人」がそれぞれ state, restaurant_type, party_size_number として抽出されています。

{
  "model": "sample_model",
  "intent": {
    "confidence": 0.9418181777000427,
    "name": "BookRestaurant"
  },
  "text": "茨城の料亭を5人で予約して下さい。",
  "project": "sample_proj",
  "intent_ranking": [
    {
      "confidence": 0.9418181777000427,
      "name": "BookRestaurant"
    },
    {
      "confidence": 0.06678754091262817,
      "name": "PlayMusic"
    },
    {
      "confidence": 0.0,
      "name": "GetWeather"
    },
    {
      "confidence": 0.0,
      "name": "RateBook"
    },
    {
      "confidence": 0.0,
      "name": "SearchScreeningEvent"
    },
    {
      "confidence": 0.0,
      "name": "SearchCreativeWork"
    },
    {
      "confidence": 0.0,
      "name": "AddToPlaylist"
    }
  ],
  "entities": [
    {
      "end": 2,
      "entity": "state",
      "extractor": "mecab_crf_extractor.MecabCRFEntityExtractor",
      "confidence": 0.9630082473691354,
      "value": "茨城",
      "start": 0
    },
    {
      "end": 5,
      "entity": "restaurant_type",
      "extractor": "mecab_crf_extractor.MecabCRFEntityExtractor",
      "confidence": 0.5849810723150879,
      "value": "料亭",
      "start": 3
    },
    {
      "end": 8,
      "entity": "party_size_number",
      "extractor": "ml_rasa_nlu.extractors.mecab_crf_entity_extractor.MecabCRFEntityExtractor",
      "confidence": 0.9920665074072829,
      "value": "5人",
      "start": 6
    }
  ]
}

3.2 Rasa NLU での日本語の利用

既定のパイプラインである tensorflow_embedding は処理の先頭で文章をトークンと呼ばれる単位に分割するのですが、空白で単語が区切られていることが前提( tokenizer_whitespace )となっていますので、日本語で利用する場合は、最低限この部分を差し替える必要があります。以下のコードを mecab_tokenizer.py として保存します。

# -*- coding: utf-8 -*-
import MeCab
from rasa_nlu.components import Component
from rasa_nlu.tokenizers import Tokenizer, Token

class MecabTokenizer(Tokenizer, Component):
    name = "mecab_tokenizer.MecabTokenizer"
    provides = ["tokens", "mecab_features"]

    def __init__(self, component_config=None):
        super(MecabTokenizer, self).__init__(component_config)
        self.mecab = MeCab.Tagger ("-Ochasen")
        self.mecab.parse("")

    def train(self, training_data, config, **kwargs):
        for example in training_data.training_examples:
            tokens, mecab_features = self.tokenize(example.text)
            example.set("tokens", tokens)
            example.set("mecab_features", mecab_features)

    def process(self, message, **kwargs):
        tokens, mecab_features = self.tokenize(message.text)
        message.set("tokens", tokens)
        message.set("mecab_features", mecab_features)

    def tokenize(self, text):
        words = []
        mecab_features = []
        node = self.mecab.parseToNode(text).next
        while node:
            words.append(node.surface)
            mecab_features.append(node.feature.split(','))
            node = node.next
        running_offset = 0
        tokens = []
        for word in words:
            word_offset = text.index(word, running_offset)
            running_offset = word_offset + len(word)
            tokens.append(Token(word, word_offset))
        return tokens, mecab_features

次に設定ファイルを以下のように記述します( sample.yml )*3

language: "ja"

pipeline:
- name: "mecab_tokenizer.MecabTokenizer"
- name: "ner_crf"
- name: "ner_synonyms"
- name: "intent_featurizer_count_vectors"
- name: "intent_classifier_tensorflow_embedding"

あとは学習の実行時に config オプションで設定ファイルを指定すればOKです*4

$ python -m rasa_nlu.train --config ./sample1.yml ...

次章からは Rasa NLU において、文章分類と固有表現抽出がどのような手法で実装されているかを説明していきます。

4. 文章分類

文章分類についてのモデルや手法は書ききれないので、今回は Rasa NLU で実装されている手法について紹介します。

Rasa NLU の場合、まず intent_featurizer_count_vectors で文章を固定長の文章ベクトルに変換、intent_classifier_tensorflow_embedding で文章ベクトルをインテントに分類します。

intent_featurizer_count_vectors は内部的に sklearnCountVectorizer を利用しており、前回の記事で紹介した Bag of Words (BOW) ベクトルを生成してくれます*5

次に intent_classifier_tensorflow_embedding で文章ベクトルを入力、対応するインテントを教師ラベルとした教師あり学習を行います。 具体的な手法としては、Facebook AI Research により2017年に提案された StarSpace *7 をアレンジして用いています。 StarSpace自体は文書分類に特化している訳ではなく、異なる種類(文章、画像、…)を同一ベクトル空間上に写像して相互比較可能にする手法なのですが、Rasa NLU では文章分類において以下のような使い方をしています。

StarSpaceによる文章分類

学習時のサンプルは文章ベクトル("Restaurant has great food”)とその正解ラベル(“#dinner”)、及び全不正解ラベルからランダム抽出された不正解ラベル集合(“sport”,“#animal”)を1セットとします。 文章ベクトルを2層MLP、正解/不正解ラベルを1層MLPで20次元のベクトルにエンコードし、以下の目的関数で最適化します*8

文章分類の目的関数

L_margin は文章ベクトルと正解ラベルの類似度が0.8未満の場合、文章ベクトルと最も近い不正解ラベルとの類似度が0.4より大きい場合に損失が増大する関数、L_emb は正解ラベルと最も近い不正解ラベルとの類似度になります。つまり以下のような作用になります。

  • 文章ベクトルと正解ラベルを近づける(①)
  • 文章ベクトルと最も近い不正解ラベルを遠ざける(②)
  • 正解ラベルと最も近い不正解ラベルを遠ざける(③)

学習後の推論フェーズでは文章ベクトルとラベル(インテント)を同一空間上に写像、写像先で文章ベクトルに一番類似度の高いラベルを選ぶことで文章分類を行います。

5. 固有表現抽出

固有表現抽出は「明日の東京の天気はどうなりますか?」のような文章から「明日」(日時)、「東京」(場所)のようなエンティティ(とその種別)を抽出する処理です。Rasa NLU では ner_crf で実装されており、 Linear Chain CRF(線形連鎖条件付き確率場:以後、単にCRFと表記)が用いられています。

CRFは構造学習により系列ラベリングを行う手法です。系列ラベリングはデータ系列を入力し、系列の各要素に識別結果のラベルを与える処理で、以下の例は「国立競技場でサッカーの試合があります」という文書に対し、「国立競技場」に“place"、「サッカー」に"sport"という種類のエンティティが抽出される場合のイメージです。"place”, “sport” の先頭につく"B",“I”, “L”, “U"、及びそれ以外の"O"はそれぞれ、"Begin”, “Inside”, “Last”, “Unit”, “Outside” の頭文字です。抽出するエンティティが複数トークンに跨るケースがあるので、エンティティの開始、途中、終了の意味合いまで含めてラベル付けを行います。

系列ラベリングの例

構造学習は、系列全体を入力として渡し、系列全体として最も適切と思われる出力系列を推論する手法のことです。上記の例では「競技」を単体で評価してもエンティティなのか、エンティティだとしても先頭か、真ん中か、末尾か適切に判断することは困難です。系列全体をまとめて処理することで「国立競技場」というエンティティの真ん中の部分だと判断できる訳です。

ここで、データ系列 X に対しラベル系列 Y が得られたとすると、

データ系列とラベル系列

CRF ではその確率を以下のように算出します。

CRFの確率

複雑なように見えますが、exp()の中身は上下で同じなので、落ち着いて見ればそれ程でもありません。 K 個の fk 、fk それぞれに対応する重み λk があり、長さ T の先頭から末尾まで fk の値に λk を掛けながら足しこむだけです。 Z0 は P(Y|X) を 0.0 ~1.0 の範囲にする為のもので分配関数と呼ばれます。CRFにおける学習は fk に対する重み λk の調整ですので、素性関数と呼ばれる fk がポイントになってきます(素性関数に関しては後述します)。 λk の学習は Forward-Backword アルゴリズム、学習後の推論は Viterbi アルゴリズムで行います。説明は省きますが、uchiumi氏の Crfと素性テンプレート*6 でわかりやすく解説されているので、興味のある方は参照して見て下さい。

5.1 素性関数

素性関数はデータ系列からその特徴を抽出する役目を担います。 素性関数 fk は入力にデータ系列 X、系列内での位置 t、位置 t におけるラベル y_t、位置 t-1 におけるラベル y_t-1 をとり、ある条件が成立した場合のみ 1、そうでなければ 0 を返す関数です。素性関数の例としては以下のような物が考えられます。

  • f1 : 単語が「サッカー」かつラベルが “U-sport” であれば 1、そうでなければ 0
  • f2 : 一つ前のラベルが “B-place"、現時点のラベルが "I-place” であれば1、そうでなければ0

f1 に対応する重み λ1 が大きければ、「サッカー」という単語に遭遇した際に “U-sport” というラベルを付ける傾向が、f2 に対応する λ2 が大きければ、"B-place" の次に “I-place” を付ける傾向が強くなります。f1 のようにデータ列中で観測された値とラベルを組み合わせた素性を観測素性、f2 のようにラベル間の遷移で表現される素性を遷移素性と呼びます。

5.2 素性テンプレート

文章の特徴を認識するには多数の素性関数が必要です。遷移素性はラベル全種類の遷移パターンを網羅すれば良いので問題ありませんが、観測素性を手動で用意するのは大変な為、素性テンプレートと呼ばれる手法が用いられます。

例えば素性テンプレートとして「位置 t のトークン」を定義したとすると、 前述の例では「位置 t のトークン」の値として「国立」、「競技」、「場」、「で」、「サッカー」、「の」、「試合」、「が」、「あり」、「ます」の 10種類がありえます。ラベルは “B-place”, “I-place”, “L-place”, “U-sport”, “O” の5種類です。この二つを掛け合わせ、トークンの値とラベルの全ての組み合わせを以下の表のような 5 × 10 の 50個の観測素性として生成します。

素性テンプレート

5.3 Rasa NLU の実装

ここで Rasa NLU における素性テンプレートの定義を確認します。 ner_crf の実体は crf_entity_extractor.py で定義されており、以下のようになっています。

        "features": [
            ["low", "title", "upper"],
            ["bias", "low", "prefix5", "prefix2", "suffix5", "suffix3",
             "suffix2", "upper", "title", "digit", "pattern"],
            ["low", "title", "upper"]
        ],

features が3行になっているのは位置 t のトークンを処理する際、以下のように素性を生成するという意味です。

  • t-1 のトークンについて low, title, upper の3つの素性テンプレートを適用
  • t のトークンについて bias, low, prefix5, prefix2, suffix5, suffix3, suffix2, upper, title, digit, pattern の11の素性テンプレートを適用
  • t+1 のトークンについて low, title, upper の3つの素性テンプレートを適用

features の行数を増やして 5行にすれば、位置 t の前後2のトークンを参照して素性を作ることになります。 low, title, upper 等はそれぞれ lambda 式に紐ついており、データ系列の当該位置に対する処理が記述されています。

素性テンプレートの名前で察しがついていると思いますが、 ner_crf で用意されている素性テンプレートは欧米の言語を前提とした素性になっています。日本語のデータで動作しない訳ではないですが、日本語の品詞情報等の素性を利用したい場合は、 ner_crf の実装をベースにコンポーネントを自作、設定ファイルで差し替えたほうが良い精度が期待できるでしょう。

長くなるので、crf_entity_extractor.py に対する変更箇所だけ示しますが、"features"function_dict の定義、_from_text_to_crf() メソッドの末尾の修正だけでOKです。

"features" の定義
        "features" : [
            # t-2
            ["token", "pattern", "length", "prefix", "suffix",
             "mecab_pos0", "mecab_pos1", "mecab_pos2",
             "has_hiragana", "has_katakana", "has_kanji"],
            # t-1
            ["token", "pattern", "length", "prefix", "suffix",
             "mecab_pos0", "mecab_pos1", "mecab_pos2",
             "has_hiragana", "has_katakana", "has_kanji"],
            # t
            ["token", "pattern", "length", "prefix", "suffix",
             "mecab_pos0", "mecab_pos1", "mecab_pos2",
             "has_hiragana", "has_katakana", "has_kanji", "bias"],
            # t+1
            ["token", "pattern", "length", "prefix", "suffix",
             "mecab_pos0", "mecab_pos1", "mecab_pos2",
             "has_hiragana", "has_katakana", "has_kanji"],
            # t+2
            ["token", "pattern", "length", "prefix", "suffix",
             "mecab_pos0", "mecab_pos1", "mecab_pos2",
             "has_hiragana", "has_katakana", "has_kanji"],
        ],
function_dict の定義
    function_dict = {
        'token': lambda doc: doc[0],
        'length': lambda doc: len(doc[0]),
        'mecab_pos0': lambda doc: doc[1][0],
        'mecab_pos1': lambda doc: doc[1][1],
        'mecab_pos2': lambda doc: doc[1][2],
        'has_hiragana': lambda doc:
             re.search('[ぁ-ん]', doc[0]) is not None,
        'has_katakana': lambda doc:
             re.search('[ァ-ン]', doc[0]) is not None,
        'has_kanji': lambda doc:
             re.search('[一-龥]', doc[0]) is not None,
        'prefix': lambda doc: doc[0][:1],
        'suffix': lambda doc: doc[0][-1:],
        'bias': lambda doc: 'bias',
        'pattern': lambda doc: doc[3],
    }
_from_text_to_crf() の変更
        ...snip...
        for i, token in enumerate(tokens):
            pattern = self.__pattern_of_token(message, i)
            entity = entities[i] if entities else "N/A"
            feature = message.get("mecab_features")[i]                 # この2行を修正
            crf_format.append((token.text, feature, entity, pattern))  # append される tuple が function_dict での doc に対応   
        return crf_format

6. 日本語データセットでの実験

ここまでで、NLU とは何か、Rasa NLU の使用方法と日本語を扱う際のポイントを紹介してきました。ここからは日本語のデータセットを用い実験を行ったので、その結果を紹介します。

6.1 データセット

例によって適当な日本語データセットを見つけられなかったので、Natural Language Understanding benchmark データセット*9を手作業で日本語化して用いています。ただ明らかなラベルの間違いや細かい表現等を修正している為、純粋な翻訳データセットではありません。また、Cross Validation は実施していません。データセット中のインテントの内訳は以下のとおりです。

インテントの内訳

また、エンティティは同ジャンルの日本語に手作業で置き換えています。データセット中のエンティティ37種の内訳は以下のとおりです。

エンティティの内訳

6.2 実験パターン一覧

実験パターンの一覧は以下のとおりです。今回のデータにはアーティスト、楽曲、映画等々の固有名詞が非常に多く含まれる為、 MeCabの辞書として IPADIC に加えて NEologd*10 も使用してみました。 文章分類における文章のベクトル化手法として、intent_featurizer_count_vectors による単語カウントベクトルに加え、前回紹介した Sent2Vec によるベクトル化を試しています。固有表現抽出に関しては、標準機能の ner_crf5.3 Rasa NLU の実装 で紹介した修正版で検証を行いました。

実験パターン

6.3 使用した主なソフトウェア

以下を使用しています。

ソフトウェア バージョン
Python 3.5.2
MeCaB 0.996-1.2ubuntu1
IPADIC 2.7.0-20070801+main-1
NEologd 20181112-01
rasa-nlu 3.4.0
sent2vec https://github.com/epfml/sent2vec/tree/a3c4cda47de

6.4 実験結果

文章分類の実験結果は以下の通りです。全てのパターンで F1 スコア 0.98前後ですので、ほぼ正確に分類が行えています。Sent2Vec を使用することで学習データに含まれない単語に対する対応力が向上することを期待したのですが、今回のデータセットでは効果がありませんでした。また NEologd も目立った違いは認められません。「予約する」、「上映する」、「予報」、「プレイリスト」等々の特徴的な単語でほぼ判別ができてしまうということかもしれません。

文章分類の結果

固有表現抽出の実験結果は以下の通りです。こちらは 0.95 が最良値ですが、エンティティの種別には「アルバム名」と「楽曲名」など人間でも判別の苦しいものが混じっていることを考えるとまずまずの数値ではないかと思います。Rasa NLU の実装("default_*")に比較して、素性を日本語向けに修正したもの("mcb_*")は 2.8 ~ 3.0 ポイント程度、スコアが向上しています。また、NEologd にもやや効果が認められました。NEologd では「丸の内ピカデリー」や「君の膵臓をたべたい」等の抽出対象が固有名詞として辞書登録されており、1トークンとして扱われる為、幾分認識が容易になったと考えられます。前後2トークンの幅で素性関数を生成しているので、「君の膵臓をたべたい」が 1 トークンになるか、「君」、「の」、「膵臓」、「を」、「たべ」、「たい」の 6 トークンになるかはそれなりに違いそうです。

固有表現抽出の結果

学習データ数が精度に与える影響

文章分類で 0.98、固有表現抽出で 0.95 という結果が得られました。学習データの分量として認識したいインテント毎に300サンプルというのは、がんばれそうな気がする数ですが、「もっと少ないデータ量でなんとかならないの?」という気になったので追加で調べてみました。今回の学習データはインテント毎に300サンプルが全量になりますが、これを 100, 50, 20 と減らしながら同様の実験を行いました。

文章分類の結果は以下の通りです。インテント毎に100サンプルでもほぼ精度の低下が発生しないことが確認できます。またサンプル数が少なくなるに従い BOW("cv_*") が不利になっていきます。BOW は「ある単語が何回出現したか」によるので学習時のサンプル数が少すぎるとカバーできる単語が減ってしまい認識力に影響がでるのでしょう。

学習データ量と文章分類

続いて、固有表現抽出の結果です。文章分類に比べて学習サンプル数減少の影響が強くでています。詳しく確認はしていませんが、今回のデータでは固有表現抽出はエンティティの種別毎のサンプルにかなりバラつきがある為、一部の種別では極端に学習サンプルが不足する等の状況が発生していたかもしれません。

学習データ量と固有表現抽出

StarSpace によるベクトル表現

前述のとおり文章分類では StarSpace を内部的に使用しているので、StarSpaceで得られた分散表現を PCA*11 で次元削減のうえプロットしてみました。なお、文章ベクトルには NEologd で分かち書きして Sent2Vec でベクトル化したものを用いています("s2v_neologd")。"●"は文章ベクトルの埋め込み、"◆"はラベルの埋め込みです。RateBook の周辺が見た目少し混じり気味ですが概ねインテント毎に集団が形成されています。

埋め込み表現の可視化

実験結果のサンプル

棒グラフだけでは雰囲気が伝わらないので、実験結果のサンプルを載せておきます。下線部は抽出対象のエンティティ、緑字は正しい予測、赤字は誤検知や検知漏れ、"→"表記が正解ラベルです。NEolod で分かち書きし、CountVector での文章ベクトル(“cv_neologd”)、固有表現抽出の素性を日本語向けに修正(“mcb_neologd”)したモデルでの結果です。

実験結果のサンプル

文章分類では “SearchCreativeWork” を “Play Music” に誤検知していますが確信度(0.55)を見ると微妙な判定だったようです。"PlayMusic" の学習データ中に「アルバム」を含むものが複数存在するので、そちらに引っ張られたようです。

固有表現抽出でも検知漏れと誤検知が確認できます。"album"->“track"、"album”->“object_name"の誤検知については、人間でもなかなか判定できない類なので仕方ないかな?というところでしょうか。"music_item”->“object_type"の誤検知ですが、学習データを確認すると以下の表のように「アルバム」が "music_item” と “object_type” の両方に入っているので、これはデータの問題です*12

実験結果のサンプル

これらの事情を踏まえると、適切な設計と量のデータを用意できれば実用的なモデルが得られそうな印象です。

7. おわりに

今回は Rasa NLU を用いて日本語の文章からインテントを識別しエンティティを抽出する方法を紹介しました。NLU の OSS だけあり、機械学習のアルゴリズムを実装した Python のライブラリを直接使うよりも格段に利用しやすいと思ってもらえたのではないでしょうか? 次回は昨年末に盛り上がりを見せた BERT について紹介する予定です。

1: https://rasa.com/
2: Rasa NLU では既定のパイプラインとして "spacy_sklearn""tensorflow_embedding" が用意されています。"spacy_sklearn" は、著名なNLPライブラリである spaCyを利用します。 spaCy(https://spacy.io/) は GiNZA(https://megagonlabs.github.io/ginza/) で日本語が利用可能になったのですが記事執筆とのタイミングの関係上、今回は検証していません。
3: spacy_sklearn を使用しない場合 language: "ja" は特に意味ありません。気持ちです。
4: 今回は試していませんが、文書頻度やストップワードの設定をしたりベクトルにNGramのカウントを含めたりできます。
5: https://www.ogis-ri.co.jp/otc/hiroba/technical/similar-document-search/part1.html
6: https://www.slideshare.net/uchumik/crf-8416551
7: StarSpace: Embed All The Things! https://arxiv.org/abs/1709.03856
8: このあたりの層数、次元数等は設定で調整が可能になっています。
9: https://github.com/snipsco/nlu-benchmark
10: NEologd は新語・固有表現に強い MeCab用の辞書です。https://github.com/neologd/mecab-ipadic-neologd
11: PCAはデータの次元削減の手法です。元データの持つ情報量を出来るだけ維持しつつ、その次元数を減らすことができます。
12: 執筆中に気づいたのですが英語の元データはインテント毎に固有表現抽出をする想定のようです。