Rasa+GiNZAによるお手軽チャットボット作成

R&Dチーム所属の伊藤です。GiNZAについて検索しようとして(地名の)銀座についての結果が出てくると悲しくなります。

今回はチャットボットの作成についてです。前から気になっていたRasaを試してみたので備忘録がてらまとめてみました。

はじめに

RasaはRasa Technologies GmbHより提供されるオープンソースの対話システム作成フレームワークです。 Pythonで書かれており、自然言語理解のモデル訓練・推論環境や対話管理ツール、データベースやAPIに接続するためのエンドポイントなど、チャットボットのような対話システムを作るための機能が一通り用意されています。

チャットボット作成における(英語のような言語と比べた)日本語の問題点として、入力文章を自然言語理解モデルに適用される際の前処理として必要なトークナイズ(分かち書き)が難しいということが挙げられます。 MeCabをはじめとしてトークナイズを行うためのツールは数多く存在しますが、それらのツールを実際に前処理に適用するという行為は面倒であることが多いです。 そのような中、Rasaは自然言語処理ライブラリのSpaCyを使用した前処理・特徴抽出のためのパイプラインを提供しています。 そのため、前回の記事でもご紹介したSpaCyベースの日本語NLPツールであるGiNZAを使えば簡単に日本語対応のチャットボットを作成できると考えたのが本記事を書いた動機となります。

本記事はRasaを触ったことのない人を対象として、下の画像のような時間を指定して予約を行うための簡単なチャットボットを構築することを目的としています。

f:id:optim-tech:20211116124701p:plain

ここで紹介するRasaの機能はほんの一部分であり、Rasaは公式ドキュメントが充実しているため興味を持った方はそちらも参照していただけると幸いです。

準備

まずはRasaとGiNZAをインストールします。 Rasaが推奨するPythonのバージョンは3.6、3.7、3.8のいずれかですが、今回は3.7を使用しています。

$ pip install rasa
$ pip install ginza ja-ginza

次にRasaプロジェクトの初期化を行います。下記のコマンドを実行するとプロジェクトに必要なファイルが生成されると同時に、サンプルモデルが訓練されます。

$ rasa init --no-prompt

初期化によって生成されるファイルは下記のようになります。基本的にはこれらのファイルを編集するだけで、チャットボットを作成することができます。

.
├── actions
│   ├── __init__.py
│   └── actions.py
├── config.yml
├── credentials.yml
├── data
│   ├── nlu.yml
│   ├── rules.yml
│   └── stories.yml
├── domain.yml
├── endpoints.yml
├── models
│   └── *.tar.gz
└── tests
    └── test_stories.yml

ちなみに、ここで訓練されたチャットボットのサンプルは、rasa shellというコマンドで実行することが可能です(このモデルの対応言語は英語になります)。

$ rasa shell

(省略)

Your input -> Hello
Hey! How are you?
Your input ->  Bye
Bye
# Ctrl+C で終了

ドメインの設定

ドメインはボットを構築するのに使用する要素のことを指し、domain.ymlファイルに記述されます。 下記が今回使用するファイルの中身です。 いくつかの要素はデフォルトのままですが、変更点について解説していきます。

version: "2.0"

intents:
  - greet
  - reserve

entities:
  - Time

slots:
  Time:
    type: text
    influence_conversation: false

responses:
  utter_greet:
    - text: "こんにちは!"
  utter_reserve:
    - text: "予約したい時間を入力してください。"
  utter_not_understand:
    - text: "すみません。よくわかりませんでした。"

forms:
  reservation_form:
    required_slots:
      Time:
        - type: from_entity
          entity: Time

actions:
  - action_reservation_time

session_config:
  session_expiration_time: 60
  carry_over_slots_to_new_session: true

intents

intentsはボットを使用するユーザの入力の種類を記述します。 今回は予約チャットボットに対する挨拶であるgreetと、予約のトリガーとなるreserveの2つを使用することを宣言しています。 具体的にどのような言葉をどのintentとして定義するかはモデルの訓練時(後述)に決定されます。

entities

entitiesには、ユーザの入力の中からボットが抽出したい固有表現を宣言します。 これらの表現はデータを用意して学習させることが可能なのですが、RasaではSpaCyの固有表現抽出機能をそのまま使用するためのパイプラインが整備されています(設定方法は後述)。 今回は入力された時間表現を抽出するために、GiNZAの固有表現名として定義されているTimeを使用します(GiNZAで抽出される固有表現はこちらを参照)。

slots

slotsはボットが使用する変数を宣言します。 このslotの名前がentityと同じ名前である場合、entityが抽出された時に同名のslotに中身が自動的に挿入されます。 今回は、抽出されたentityであるTimeを保存するために用いる、テキストを値とするslotを作成しています。

responses

responsesには、チャットボットがユーザへ返す内容を宣言します。 ボットへの挨拶の返答となるutter_greet、予約を促すutter_reserve、ユーザの入力が理解できなかった場合に返すutter_not_understandの3つを記述しています。

forms

formsはユーザの入力から何かしらの情報を収集したい場合に使用します。 これらのformsは、required_slotsで指定されたslotsが全て埋まったかどうかを自動で判定します。 今回構築する予約チャットボットでは、予約時間Timeを保存するためのformを宣言しました。

actions

Rasaではチャットボット自身のサーバとは別に、Action Serverと呼ばれるresponsesなどでは定義できないチャットボットの処理を定義したサーバと通信を行います。 このような処理はカスタムアクションと呼ばれ、このカスタムアクションの名前を宣言するのがactionsになります。 今回は予約の完了を通知するためのaction_reservation_timeというactionを用意しました。

カスタムアクションはactionディレクトリ以下の.pyファイルに定義されます。 今回は、プロジェクトに最初から用意されていたactions.pyを以下のように書き換えてカスタムアクションを定義しました。

from typing import Any, Text, Dict, List

from rasa_sdk import Action, Tracker
from rasa_sdk.executor import CollectingDispatcher


class ActionReservationTime(Action):

    def name(self) -> Text:
        return "action_reservation_time"

    def run(self, dispatcher: CollectingDispatcher,
            tracker: Tracker,
            domain: Dict[Text, Any]) -> List[Dict[Text, Any]]:

        time = tracker.slots['Time']
        dispatcher.utter_message(text="{}に予約を完了しました!".format(time))

        return []

カスタムアクションはActionクラスを継承したクラスであり、最低でもname(self)run(self, dispatcher, tracker, domain)の2つのメソッドが定義されている必要があります(参考)。

Action.nameはカスタムアクションの名前を返すメソッドです。 これはdomain.ymlで宣言したアクション名からカスタムアクションを探すのに用いられるため、今回はaction_reservation_timeを返り値にしています。

Action.runにはカスタムアクションとして実際に実行される処理を書きます。
このメソッドの引数は以下の用途で使用されます。

  • dispatcher: ユーザーにメッセージを返すために使用します。
  • tracker: slotや過去のメッセージ等の状態を保持します。
  • domain: domain.ymlに書かれたドメインの情報について保持しています。

ActionReserveTimeでは、trackerからslotであるTimeの中身を受け取って返信メッセージを作成しdispatcherに渡すことでボットへの返信を実現しています。 このように、ボットとの対話の状況に応じた複雑な処理を記述するためにカスタムアクションを使用することが可能です。

ちなみに、action_reservation_timeではslotの値に応じたメッセージを生成するためにカスタムアクションを使用しましたが、実際には同じ処理をresponseとして実現することが可能です。 今回はカスタムアクションの例を見せるためにカスタムアクションとして実装しました。

responses:
  utter_reservation_time:  # action_reservation_timeと同じ挙動
    - text: "{Time}に予約を完了しました!"

モデルの設定

Rasaは自然言語理解(Natural Language Understanding, NLU)モデルを利用して、ユーザの入力したテキストからintentを予測したり、入力に対するactionの選択を行います。このモデルの設定はconfig.ymlというファイルに記載されます。

今回用いる設定は以下の通りです。この設定ファイルはlanguagepipelinepoliciesをキーに持つ連想配列となります。

language: ja

pipeline:
  - name: SpacyNLP
    model: 'ja_ginza'
  - name: SpacyTokenizer
  - name: SpacyFeaturizer
  - name: SpacyEntityExtractor
  - name: RegexFeaturizer
  - name: LexicalSyntacticFeaturizer
  - name: CountVectorsFeaturizer
  - name: CountVectorsFeaturizer
    analyzer: "char_wb"
    min_ngram: 1
    max_ngram: 4
  - name: DIETClassifier
    epochs: 100
  - name: EntitySynonymMapper
  - name: ResponseSelector
    epochs: 100

policies:
  - name: MemoizationPolicy
  - name: RulePolicy
  - name: TEDPolicy
    max_history: 5
    epochs: 100

language

チャットボットで使用する言語を定義します。今回は日本語なのでjaを指定しています。

pipeline

ユーザの入力テキストを処理するためのパイプラインを記述します。 前述の通りRasaはSpaCyとの強力な連携機能を有しており、SpaCyのトークナイズ機能・特徴量抽出機能・固有表現抽出機能を簡単に使うことが可能です。 例として、面倒な日本語でのトークナイズはパイプラインにname: SpacyTokenizerを追加するだけで解決してしまいます。

今回はRasaの公式がおすすめしている設定を元にSpacyNLPmodelをGiNZAに変更、加えて固有表現抽出にGiNZAを用いるためのSpacyEntityExtractorを追加しました。

また、パイプラインの最後にFallbackClassifierを追加しています。 これを追加していると、ユーザの入力から予測できるintentの精度が閾値を下回る場合には、入力をnlu_fallbackというintentとみなすようになります。 予想外の入力がユーザからされた場合の対処を行うのに便利です。

その他のパイプラインの詳細については公式ページに詳しく記載されているため、そちらをご覧ください。

policies

policiesはユーザの入力に対してボットがどのような対応をするかを決定するために使用されます。 今回の設定は推奨設定RulePolicyを加えたものです。 RulePolicyを適用することで、モデル訓練のためのデータ(後述)にrulesを追加することが可能です。

モデルの訓練データ

チャットボットのNLUモデルを訓練するためのデータはデフォルトではdataディレクトリ下に用意することになります。 決まったフォーマットに沿ったYAMLファイルであれば、ファイルをいくつに分けても問題はないようですが、今回は初期プロジェクトのファイル構成そのままに内容を変更しました。

nlu

nluの値には、intentの例となる文を用意します。 今回のプロジェクトではdata/nlu.ymlとして保存しました。

ドメインで設定したように、ボットに対する挨拶となるgreetと予約をするためのトリガーであるreserveの2つのintentを宣言しているので、それらに対する入力の例を与えています。

version: "2.0"

nlu:

- intent: greet
  examples: |
    - こんにちは
    - おはようございます
    - こんばんは

- intent: reserve
  examples: |
    - 予約をする
    - 予約をお願いします
    - 予約をしたい

rules

rulesは、あるintentや条件に対しての決まったアクションを提供します。 これはconfig.ymlにおいてRulePolicyを指定していない限り無効となります。

今回は、ユーザの入力がgreetreserveのintentのどちらにも当てはまらない(Fallbackが発生する)場合にutter_not_understandレスポンスを返すように設定しています。

version: "2.0"

rules:

- rule: Fallback
  steps:
  - intent: nlu_fallback
  - action: utter_not_understand

stories

storiesはユーザとボットの対話の例を示す訓練データです。 基本的にはユーザからのintentに対するボットの対応を記述していきます。

今回は、ユーザから挨拶をされた時に挨拶を返すgreetingと、予約をするためのreservationという2つのstoryを用意しています。

version: "2.0"

stories:

- story: greeting
  steps:
  - intent: greet
  - action: utter_greet
  - action: action_back


- story: reservation
  steps:
  - intent: reserve
  - action: utter_reserve
  - action: reservation_form
  - active_loop: reservation_form
  - active_loop: null
  - action: action_reservation_time
  - action: action_restart

以下に、それぞれのstoryがどのように進行するかを示します。

  • greeting
    1. greetと予測されるintentが入力される
    2. utter_greetレスポンスを返す
    3. action_backアクションによりボットの状態を入力前に戻す
  • reservation
    1. reserveと予測されるintentが入力される
    2. utter_reserveレスポンスを返す
    3. reservation_formをアクティブにする
    4. ユーザがTimeとなるentityを入力するまでループ
    5. Time entityが入力されたらaction_reservation_timeによるメッセージを表示
    6. action_restartアクションによりスロットを初期化する

エンドポイント設定

モデルの設定やデータの準備は終わりましたが、最後にエンドポイントの設定をendpoints.ymlに書く必要があります。 今回はカスタムアクションを設定しているため、アクションサーバのエンドポイントを定義しておきます。

action_endpoint:
  url: "http://localhost:5055/webhook"

モデル訓練

モデルの訓練は、プロジェクトのルートディレクトリで下記のコマンドを実行するだけです。 このコマンドはdomain.ymlconfig.ymlの変更を気にしないため、もしこれらのファイルのみを変更して再学習を行いたい場合は、--forceをオプションを付ける必要があります。

rasa train

訓練されたモデルはmodelsディレクトリに置かれます。 何もオプションを指定していない場合はyyyymmdd-hhmmss.tar.gzという名前で保存されます。

チャットボット実行

モデルを学習すると、実際にチャットボットを実行することが可能です。 ただし今回のようにカスタムアクションを定義している場合は、アクションサーバーを立てている必要があります。

$ rasa run actions  &  # アクションサーバをバックグラウンドで実行

CLIで対話的にチャットボットを実行するには、shellコマンドを使用します。

$ rasa shell

(省略)

Your input ->

実際に動いているかどうか確かめてみます。

Your input -> こんにちは
こんにちは!
Your input -> おはようございます
こんにちは!
Your input -> おはよう
こんにちは!  # data/nlu.ymlに定義されていない例でも正しく理解している
Your input -> あ
すみません。よくわかりませんでした。  # Fallbackの発生
Your input -> 予約したい
予約したい時間を入力してください。
Your input -> 15時
15時に予約を完了しました!
Your input -> 予約
予約したい時間を入力してください。  # data/nlu.ymlに定義されていない例でも正しく理解している
Your input -> 20:00にお願いします
20:00に予約を完了しました!  # 時間表現のみを正しく抽出している

おわりに

今回はRasaとGiNZAを利用したチャットボットを作成してみました。 Rasaは今回初めて触りましたが、カスタムアクションを書かなければPythonのコードすら書かずにチャットボットを作成できることが判明したため、かなりお手軽に使えるフレームワークであるという印象を受けました。 当初の目的であったRasaとGiNZA(SpaCy)の連携部分も設定ファイルを少し書くだけで完了したため、正直なところGiNZAを使っているという感覚は全くありませんでした。

本記事で使用したコードは以下のリポジトリに公開していますので、興味のある方は是非覗いてみて下さい。

f:id:optim-tech:20211116124659p:plain