ドコモの自然対話開発環境SUNABAで対話ボット作ってみた


はじめに

こんにちは。NTTドコモの白水です。
NTTドコモでは,独自に開発している対話型AIサービスのプラットフォームである「自然対話プラットフォーム」の記述言語であるxAIML(エックスエーアイエムエル)の言語仕様と,開発環境SUNABA公開しています。SUNABAでは,ドコモが開発中の「自然対話プラットフォーム」を誰でも簡単に試すことができます。

自然対話プラットフォームとは

自然対話プラットフォームは,ユーザの発話に対して,その発話の意図を理解した上で回答する対話型AIプラットフォームです。ドコモが提供する「my daiz」など様々な対話サービスのコア技術として利用されています。

自然体話プラットフォームは以下の3つの機能で構成されています。

シナリオ対話
あらかじめ設定されたシナリオによって,発話者とシステムとの対話を実現します。XMLタグを組み合わせるだけで多様なシナリオを作成することができます。
意図解釈
発話の意図を理解し,機械学習により定義されているタスクに自動で分類します。SUNABAではデフォルトのタスク(乗換案内や知識検索)を利用できます。
サービス連携
インターネット上の外部サービスと連携します。xAIMLで解決できない処理や,天気などの外部コンテンツを取得する際に利用します。この機能を利用してボットと既存のサービスを連携させ,高機能なボットを作成することができます。

今回は,上記3つのうち「シナリオ対話」と「意図解釈」の機能を使って,簡単な対話ボットを作ってみます。

SUNABAで遊んでみよう

準備

ボットのシナリオ(対話フロー)は,xAIMLという言語を利用して記述します。xAIMLはAIML(Artificial Intelligence Markup Language)をベースに,ドコモが機能を拡張したXMLライクな記述言語です。詳細な言語仕様が公開されています
xAIMLは標準のメモ帳でも記述できますが,Atomエディタがおすすめです。Atomであれば,サポートパッケージである xaiml-editorを利用できます。オートコンプリートや構文チェック,シンタックスハイライトなどが使えるようになるので,xAIMLの開発には欠かせません。

ボットの作成

チュートリアルが丁寧なので,こちらを参照します。

大まかな手順は以下です。

  1. 新規登録・ログイン
  2. シナリオファイル(.aimlファイル)の作成
  3. シナリオファイルをサーバへアップロード
  4. サーバのシナリオファイルをコンパイル(DATファイルの生成)
  5. DATファイルを対話サーバに転送

ログイン,サーバへのアップロード,コンパイル,転送はブラウザを通して行いますので,本記事では省略します。

シナリオを作成しよう

xAIMLを使って,「ユーザが『山』と言ったら『川』と返す」というボットを作ってみます。以降,ユーザの発話を「ユーザ発話」,ボットの発話を「システム発話」とします。

<?xml version="1.0" encoding="UTF-8"?>
<aiml version="xaiml1.0.0" xmlns="http://www.nttdocomo.com/aiml/schema" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nttdocomo.com/aiml/schema/AIML.xsd">
    <category>
        <pattern></pattern>
        <template></template>
    </category>
</aiml>

最初の2行(と最後の1行)はいわゆるおまじないです。サポートパッケージをインストールしていれば,<と入力するだけでオートコンプリートされます。
対話フローは,ユーザ発話・システム発話のペアを<category>タグの中に記入していくことで作成していきます。ユーザ発話が<pattern>タグで囲まれた文にマッチした場合,<template>タグで囲われたシステム発話が返ってきます。上記の例では,ユーザ発話が「山」の場合のみ,システム発話として「川」が返ってきます。複数の<pattern>タグを書くことも可能なので,「海」や「谷」などにもマッチさせたい場合は<pattern>海</pattern><pattern>海</pattern>なども併せて記述します。

ちなみに,xAIMLは記述量を減らす種々の工夫がこらされています。例えば<pattern>タグにはマッチングレベルを指定できるlevel属性があります。適切なlevelを指定することで,語の読み,類議語,上位概念語などにも反応してくれるようになります。マッチングレベルの制御については,Tipsにまとまっているので,ぜひ目を通してみてください。

ボットと対話しよう

シナリオをコンパイルして対話サーバへ転送したら,ボットと対話できるようになります。チュートリアルではPostmanというアプリケーションを利用していますが,本記事では,よりアプリケーションとの連携がしやすいPythonを利用します。Pythonのバージョンは2.7です。requestsモジュールを使いますので,事前にpipなどでインストールしておいてください。

対話を行う前に,まずアプリIDを取得します。アプリIDは,ボット毎に払い出される固有の文字列です。継続して利用可能なので,初めて対話するときだけ取得します。

import requests
import json

headers = {'Content-Type': 'application/json;charset=UTF-8'}
bot_id = 'Project_Bot'
data = {
    'bot_id': bot_id,
    'app_kind': 'Test',
    'notification': False
}
url = u'https://sunaba.xaiml.docomo-dialog.com/UserRegistrationServer/users/applications'
response = requests.post(url, headers=headers, data=json.dumps(data))
app_id = response.json()['app_id']

bot_idには,作成したBOT IDを入れてください。「プロジェクト名_ボット名」になると思います。
取得したアプリIDを使ってボットと対話してみます。

text = u'山'
url = u'https://sunaba.xaiml.docomo-dialog.com/SpontaneousDialogueServer/dialogue'
data = {
    'clientVer': '1.0.4',
    'language': 'ja-JP',
    'bot_id': bot_id,
    'app_id': app_id,
    'voiceText': text
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print response.json()['systemText']['expression']  # 川

アプリIDとアプリIDの取得の際に指定したBOT IDを,それぞれapp_idbot_idに入れてください。また,voiceTextにはユーザ発話を入れてください。voiceTextが「山」のときに,「川」と返ってくれば対話成功です。

意図解釈を使ってみよう

例えば乗換案内をボットにさせたいとき,<pattern>に「○○駅から××駅まで」や「△△駅に行きたい」などと、ユーザが発話しそうなパターン文をずらずら書き並べるのは大変です。そこで,意図解釈の出番です。意図解釈とは,タスク(ユーザが求めている動作)をユーザ発話から判定し,必要な情報の抽出を行う技術です。抽出した情報は,タスクごとに用意されているスロットに入ります。スロットが全て埋まったら(必要な情報が全てそろったら),タスクが実行されます。例えば乗換案内タスクのスロットは「出発駅」「到着駅」などです(「出発駅」や「到着駅」が分からなければ,乗換案内はできませんよね)。ユーザ発話は,機械学習モデルによって適切なタスク(乗換案内や天気検索など)に振り分けられるので,上手に使えば記述量をぐっと減らすことができます。

それでは,意図解釈を使ったボットを作ってみましょう。意図解釈のチュートリアルには天気検索と乗換案内の利用例があるので,本記事ではレシピ検索タスクを使って,レシピを紹介するボットを作ってみます。必要なスロットはrecipeGenre(料理名や食材名)だけです。

<?xml version="1.0" encoding="UTF-8"?>
<aiml version="xaiml1.0.0" xmlns="http://www.nttdocomo.com/aiml/schema" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.nttdocomo.com/aiml/schema/AIML.xsd">
    <category>
        <pattern label="ST001005002"/>
        <template>
            <think>
                <set name="_task_keep_dialog_status">true</set>
            </think>
            料理もしくは食材は何にしますか?
        </template>
    </category>
    <category>
        <pattern label="ST001005003"/>
        <template>
            <condition name="_task_slot_recipeGenre">
                <li value="肉じゃが">おいしい肉じゃがの作り方を紹介します。材料は……</li>
                <li value="カレー">おいしいカレーの作り方を紹介します。材料は……</li>
                <li>おいしい<get name="_task_slot_recipeGenre"/>の作り方は知らないんです……。</li>
            </condition>
        </template>
    </category>
</aiml>

<pattern label="ST001005002"/><pattern label="ST001005003"/>が意図解釈の部分です。あらかじめ決められているコマンドIDlabel属性に指定すると,意図解釈機能によって振り分けられたユーザ発話が,適切な<pattern>のところに入ります。

例えば,「レシピを教えて」や「レシピを知りたい」のようなユーザ発話は,意図解釈機能によってレシピ検索タスクだと判定されます。しかし,このユーザ発話だけではレシピ検索に必要なスロットrecipeGenreが埋まらない(何のレシピを検索したらよいか分からない)ので,レシピ検索タスクを実行できません。そこで,意図解釈機能は,「聞き返し」を行ってスロットrecipeGenreを埋めようとします。レシピ検索タスクの場合,聞き返し時には料理名食材名要求コマンドST001005002が実行されるので,ユーザ発話は<pattern label="ST001005002"/>のところに入ります。したがって,<template>に書くシステム発話はrecipeGenreを埋めてもらえるような内容にしたほうが良いでしょう。今回は「料理もしくは食材は何にしますか?」と返答するようにしました。

data['voiceText'] = u'レシピを教えて'
response = requests.post(url, headers=headers, data=json.dumps(data))
print response.json()['systemText']['expression']  # 料理もしくは食材は何にしますか?

<set name="_task_keep_dialog_status">true</set>のように,_task_keep_dialog_statustrueに設定してあげると,聞き返しの状態を保持できます。
聞き返しを保持した状態で,「肉じゃが」や「カレー」のように,recipeGenreを埋めてあげるようなユーザ発話を入力してあげると,必要なスロットがすべて埋まるので,検索実施コマンドST001005003が実行されます。

data['voiceText'] = u'肉じゃが'  # 聞き返されたので,recipeGenreスロットを埋めてあげる
response = requests.post(url, headers=headers, data=json.dumps(data))
# recipeGenreスロットが埋まったので,検索実施コマンドが実行される
print response.json()['systemText']['expression']  # おいしい肉じゃがの作り方を紹介します。材料は……

また,「カレーのレシピを教えて」のように,タスクがレシピ検索で,かつrecipeGenreスロットが埋まる(何のレシピを検索したらよいか分かる)ユーザ発話の場合は,直接,検索実施コマンドST001005003が実行されます。

data['voiceText'] = u'カレーのレシピを教えて'
response = requests.post(url, headers=headers, data=json.dumps(data))
print response.json()['systemText']['expression']  # おいしいカレーの作り方を紹介します。材料は……

上記の例では,<condition>タグを使って,recipeGenreスロットに入っている語が「肉じゃが」か「カレー」か「それ以外」かで処理を分岐させました。<li>タグの中に該当する料理名のレシピを記述すればユーザにレシピを紹介できそうですが,世に数多ある料理のレシピをひとつひとつ書いていくのは大変ですし,シナリオのメンテナンスも大変になります。そこで,CGSを使って外部サービス(例えばレシピ検索API)と連携させることで,記述量を減らしながらも高度な処理をさせることができます。

おわりに

NTTドコモが公開している,対話型AIサービスの開発環境SUNABAを使ってみました。記述言語であるxAIMLはドコモによって機能が拡張されており,少ない記述量で柔軟な対話を実現することができます。作成したボットはPythonからも利用できるので,例えばslackなどと連携させることで簡単なチャットサービスがすぐ作れそうです。ドキュメントもどんどん充実させていく予定ですので,今後も引き続きご注目ください。