R&Dチーム所属の伊藤です。GiNZAについて検索しようとして(地名の)銀座についての結果が出てくると悲しくなります。
今回はチャットボットの作成についてです。前から気になっていたRasaを試してみたので備忘録がてらまとめてみました。
はじめに
RasaはRasa Technologies GmbHより提供されるオープンソースの対話システム作成フレームワークです。 Pythonで書かれており、自然言語理解のモデル訓練・推論環境や対話管理ツール、データベースやAPIに接続するためのエンドポイントなど、チャットボットのような対話システムを作るための機能が一通り用意されています。
チャットボット作成における(英語のような言語と比べた)日本語の問題点として、入力文章を自然言語理解モデルに適用される際の前処理として必要なトークナイズ(分かち書き)が難しいということが挙げられます。 MeCabをはじめとしてトークナイズを行うためのツールは数多く存在しますが、それらのツールを実際に前処理に適用するという行為は面倒であることが多いです。 そのような中、Rasaは自然言語処理ライブラリのSpaCyを使用した前処理・特徴抽出のためのパイプラインを提供しています。 そのため、前回の記事でもご紹介したSpaCyベースの日本語NLPツールであるGiNZAを使えば簡単に日本語対応のチャットボットを作成できると考えたのが本記事を書いた動機となります。
本記事はRasaを触ったことのない人を対象として、下の画像のような時間を指定して予約を行うための簡単なチャットボットを構築することを目的としています。
ここで紹介する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
というファイルに記載されます。
今回用いる設定は以下の通りです。この設定ファイルはlanguage
とpipeline
、policies
をキーに持つ連想配列となります。
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の公式がおすすめしている設定を元にSpacyNLP
のmodel
を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
を指定していない限り無効となります。
今回は、ユーザの入力がgreet
とreserve
の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
- greetと予測されるintentが入力される
- utter_greetレスポンスを返す
- action_backアクションによりボットの状態を入力前に戻す
- reservation
- reserveと予測されるintentが入力される
- utter_reserveレスポンスを返す
- reservation_formをアクティブにする
- ユーザがTimeとなるentityを入力するまでループ
- Time entityが入力されたらaction_reservation_timeによるメッセージを表示
- action_restartアクションによりスロットを初期化する
エンドポイント設定
モデルの設定やデータの準備は終わりましたが、最後にエンドポイントの設定をendpoints.yml
に書く必要があります。
今回はカスタムアクションを設定しているため、アクションサーバのエンドポイントを定義しておきます。
action_endpoint: url: "http://localhost:5055/webhook"
モデル訓練
モデルの訓練は、プロジェクトのルートディレクトリで下記のコマンドを実行するだけです。
このコマンドはdomain.yml
やconfig.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を使っているという感覚は全くありませんでした。
本記事で使用したコードは以下のリポジトリに公開していますので、興味のある方は是非覗いてみて下さい。