どうも、shoheiです。
Nuxt.jsとAzure LUISを用いてカンタンなチャットボットの作り方をご紹介します。
仕事で使うことがあったので備忘録としてまとめます。
開発環境
- macOS High Sierra 10.13.4
- Google Chrome
- nuxt-community/typescript-template
- Nuxt.js
- TypeScript
- Azure LUIS
Azure LUISとは
Azure LUISとはMicrosoftが開発した自然言語処理を行うサービスです。
理解した自然言語に応じて結果を出力してくれます。
日本語対応済みであり、カンタンなチャットボットを作ることができます。
Azure LUIS
IntentとEntity
Azure LUISにはIntent(意図)とEntity(値)があります。
Intent(意図)
Intent(意図)とは、その言葉が意味する「やりたいこと・目的」を指します。
例えば「徳島県までの行き方を教えてほしい」や「徳島県がどこにあるか教えてほしい」といった質問があるとします。
それぞれの言葉の意図としては「徳島県の位置を知りたい」ということになります。
この場合、意図は「徳島県の位置を知りたい」となり、
意図を表す言葉は「徳島県までの行き方を教えてほしい」、「徳島県がどこにあるか教えてほしい」となります。
Azure LUISでは予め意図とその意図を表す言葉を設定し、学習させることで自然言語を理解することができます。
Entity(値)
Entity(値)とは、その名の通り言葉の中にある単語を示します。
例えば「徳島県までの行き方を教えてほしい」であれば「徳島県」「行き方」「教えてほしい」がEntityとして扱えることができます。
チャットボットを作る
さっそくチャットボットを作っていきます。
完成図はこんな感じです。徳島県について答えてくれるチャットボットです。
→ 遊んでみる
「小便小僧」と質問すると、
「小便小僧の案内サイト」を別タブで表示とその旨の回答をするシンプルなチャットボットです。
Azure LUISを使う
Azure LUISの使い方を紹介します。
まずはAzure LUISのアカウントを作成します(事前にMicrosoftのアカウントが必要です)。
Azure LUISのアカウント作成
「Create new app」又は「Import new app」でLUISアプリケーションを新規作成できます。
「Create new app」で作成できますが、決まったフォーマットのJSONファイルをインポートして作成することもできます。
開発効率を考えると、JSONファイルをインポートして作成したほうが良いのでここでは「Import new app」での作り方を解説します。
教師データを作成しインポートする
※本解説は以下の記事を参考にさせていただいております。
※また本解説で利用するpythonコードは以下の記事にあるpythonコードをベースにして変更を加えたものです。
Qiita – AzureのLUISの学習用テキストを、Pythonスクリプトで楽に入力する
Azure LUIS用の教師データを作り、インポートしてLUISアプリケーションを作成します。
予め以下のファイルを全てダウンロードしてください。
luis_learning_data
手順としては以下のとおりです。
- 教師データを記入したCSVファイルを作成
- CSVファイルをJSONファイルへ変換
- JSONファイルをAzure LUISへインポート
まずはCSVファイル(input.csv)を以下のように作成します。
Textに対してIntentとEntityを記入していきます。
注意として、Textにない文言をEntityに設定するとLUISへインポートする際にエラーとなるため、間違えないようにしてください。
また、中途半端な文言をEntityに設定してもLUIS側で単語区切りができないと言われエラーとなります。
例えば「教えてほしい」というTextに対して、Entityを 「教え」 「て」 「ほしい」 と言ったように区切ることはできません。この場合は必ず「教えて」「ほしい」と言ったように単語区切りにしてください(これでも実は区切れるか怪しい)。
CSVが作れたらJSONファイルへ変換します。
以下のコマンドを実行してください(実行にはpython3のインストールが必要です)。
1 |
$ python3 export_schema.py > output.json |
実行すると、output.jsonが作成されます。
次に作成されたoutput.jsonをAzure LUISへインポートします(ここの自動化はあるかどうかわからない)。
以下の画面で必要事項を記入し、インポートします。
無事、インポートされました。
Intentを開くと以下のようにIntentに対してTextとEntityが登録されています。
学習する
インポートした教師データを元に学習します。
画面右上の「Train」を選択と学習します。
学習が終わると画面右上の「Test」を選択できるようになります。
試しにちゃんと学習できているか確認します。
「Test」を選択し、「徳島」と入力してみます。
Tokushima (0.93) と結果がでています。
これは入力した「徳島」という言葉の意図を Tokushima と判断し、その認識率は0.93(93%)の結果を得たという意味です。
他にも適当に言葉を入力してみます。
学習させた言葉はほぼ狙い通りに判断できていますね。
REST APIを取得
ここまで学習させたLUISを他のアプリケーションから利用させるためにREST APIを発行します。
画面上の「PUBLISH」を選択します。
「Publish」を選択すると以下のようにREST APIが発行されます。
右側のurlがREST APIです。
使い方としては、urlの末尾に認識させたい言葉を設定してurlをコールするだけです。
https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/29408485-2606-4254-a510-87a1dabdcb90?subscription-key=fb014b9ac6ec46beaba59f22058991e5&verbose=true&timezoneOffset=0&q=”徳島県”
コールすると結果がJSONファイルで返ってきます。
Azure LUISの設定は以上です。
Nuxt.jsでチャットボットを作る
Nuxt.jsを用いてチャットUIを作成し、Azure LUISを使ってカンタンなチャットボットを作っていきます。
Nuxt.js + TypeScriptの環境構築
nuxt-community/typescript-templateを使い環境構築していきます。
nuxt-community/typescript-templateはNuxt.js(Vue.jsをより使いやすくしたフレームワーク)をTypeScriptで開発するためのひな形であり、必要な環境が揃っています。
yarnを使ってvue-cliをインストールします(yarnがインストールされていない場合は先にyarnをインストールしてください)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# vue-cliをインストール(あれば不要) $ npm install -g vue-cli # ひな形作成.随時質問に答えていく. $ vue init nuxt-community/typescript-template luis_demo # 作成したフォルダへ移動 $ cd luis_demo # package.jsonに書かれているライブラリをインストール $ yarn install # 開発モードで起動(localhostが立ち上がります) $ yarn dev |
yarn devで以下のような画面が立ち上げればインストール成功です。
うむ、ええ感じや。
ライブラリのインストールと設定
必要なライブラリをインストールし、設定していきます。必要なライブラリは以下のとおりです。
- vue-beautiful-chat
- @nuxtjs/dotenv
vue-beautiful-chatはチャットUIをカンタンに作成できるライブラリです。
@nuxtjs/dotenvは.envファイルに設定した環境変数の設定をしてくれるライブラリです。こちら無くてもいいんですが念の為入れておきます。
インストールしたライブラリを有効にするためコードを変更していきます。
該当箇所に「追加」のコメントアウトを入れています。
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
const parseArgs = require("minimist") 〜〜〜〜 省略 〜〜〜〜 /* ** Customize the progress-bar color */ loading: { color: "#3B8070" }, // 追加. plugins: [ { src: '~/plugins/chat.ts', ssr: false} ], vendor: ['vue-beautiful-chat'], generate: { dir: generateFolder }, /* ** Build configuration */ css: ["~/assets/css/main.css"], build: {}, // 追加. modules: [ "@nuxtjs/axios", '@nuxtjs/dotenv', "~/modules/typescript.js" ], axios: {} } |
plugins/chat.ts
pluginsフォルダを作成し、vue-beautiful-chatを使えるように設定したchat.tsを作成します。
1 2 3 |
import Vue from 'vue' import Chat from 'vue-beautiful-chat' Vue.use(Chat) |
.env
環境変数を設定できる.envを作成します。
.envで設定した変数を process.env.XXXXX として利用できます。
ここではAzure LUISのREST APIを設定します。
1 |
LUIS_API='https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/29408485-2606-4254-a510-87a1dabdcb90?subscription-key=fb014b9ac6ec46beaba59f22058991e5&verbose=true&timezoneOffset=0&q=' |
チャットUIとボット処理を実装
チャットUIとボット処理の部分を実装していきます。
pages配下にあるindex.vueを以下のように変更します。
pages/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
<template> <section> <h1 class="header">{{ title }}</h1> <div> <h2>徳島県について聞いてみよう!</h2> (多分)理解できる質問 <ul> <li>徳島県について</li> <li>徳島県の行き方</li> <li>かずら橋について</li> <li>小便小僧について</li> </ul> <div class="attention"> ※利用の際はブラウザのポップアップを「許可」してください。 </div> </div> <div> <button class="square_btn" @click="isChatOpen=true">チャットする!</button> <beautiful-chat :agentProfile="agentProfile" :onMessageWasSent="onMessageWasSent" :messageList="messageList" :newMessagesCount="newMessagesCount" :isOpen="isChatOpen" :close="closeChat" :open="openChat" :showEmoji="true" :showFile="true" /> </div> </section> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator" import axios from 'axios' @Component({ components: { } }) export default class extends Vue { title: string = 'Azure LUIS demo' message: string = 'test' agentProfile: any = { teamName: this.title, imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png' } messageList: Array<any> = [] newMessagesCount:number = 0 isChatOpen = false text: string = "" // ■ Luis API luisApi:any = process.env.LUIS_API // ■ Method. created () { console.log('created before DOM') } mounted () { console.log('mounted after DOM', process.env.LUIS_API) } sendMessage (msg: any) { console.log('sendMessage', msg) if (msg.data.text.length > 0) { this.newMessagesCount = this.isChatOpen ? this.newMessagesCount : this.newMessagesCount + 1 this.messageList.push(msg) } } onMessageWasSent (msg) { console.log('onMessageWasSent', msg) // 自分のメッセージを表示. this.messageList.push(msg) // LUISよろしく. this.luisAnalyze(msg.data.text) .then((result) => { // LUISの結果を元に処理. this.luisResult(result) }).catch((error) => { console.error('error', error) }) } openChat () { this.isChatOpen = true this.newMessagesCount = 0 } closeChat () { this.isChatOpen = false } async luisAnalyze(msg: string): Promise<any> { let url:string = this.luisApi + "\"" + msg + "\"" console.log('url', url) let answer = await axios.get(url) let result = answer.data return result } luisMessage (message: string) { let msg: any = { author: 'luis', type: 'text', data: { text: message } } this.sendMessage(msg) } luisResult(result: any): void { let msg = '' console.log('result', result) if ('topScoringIntent' in result) { let info:any= result.topScoringIntent // 認識率20%以上なら処理する(甘め) if ( info.score >= 0.2 ) { let url:string = 'https://www.google.co.jp/' if (info.intent === 'Tokushima') { msg = 'OK!徳島県の地図をだしたから見てね!' url = 'https://www.google.co.jp/maps/place/%E5%BE%B3%E5%B3%B6%E7%9C%8C/@34.1410563,133.7733708,8z/data=!4m5!3m4!1s0x35524d5baed737b3:0xfddd63f964cf310c!8m2!3d34.0657179!4d134.5593601' } else if (info.intent === 'HowToGetPlace') { msg = 'OK!徳島県の行き方のサイトを案内したから見てね!' url = 'https://www.awanavi.jp/access/' } else if (info.intent === 'Kazurabashi') { msg = 'OK!かずら橋の観光サイトを案内したから見てね!' url = 'http://miyoshinavi.jp/02miru/detail.php?genr=101&uid=SS000048' } else if (info.intent === 'Shonbenkozou') { msg = 'OK!小便小僧の観光サイトを案内したから見てね!' url = 'https://www.shikoku.gr.jp/spot/696' } else { msg = 'うーん....とりあえずGoogle開いたからそこから検索してね!' } window.open(url, '_blank') } else { msg = "ごめんなさい!質問が分からないんです。もっと徳島県について勉強します!" } this.luisMessage(msg) } } } </script> <style scoped> .header { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } /* 追加 */ .square_btn{ display: inline-block; padding: 0.5em 1em; text-decoration: none; background: #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 3px; } .square_btn:active { -ms-transform: translateY(4px); -webkit-transform: translateY(4px); transform: translateY(4px); border-bottom: none; } .attention { color: red; font-size: 0.8em; margin-bottom: 8px; } </style> |
チャットで送信したメッセージをAzure LUISへ認識させて、その結果を元に処理をしています。
メッセージをAzure LUISへ認識させている箇所は以下のコードです。
1 2 3 4 5 6 7 |
async luisAnalyze(msg: string): Promise<any> { let url:string = this.luisApi + "\"" + msg + "\"" console.log('url', url) let answer = await axios.get(url) let result = answer.data return result } |
axiosの非同期APIを用いてAzure LUISのREST APIをコールします。
awaitで応答待ちし、その結果をresultに格納しています。
結果を元にボット処理をする箇所は以下のコードです(かなりしょぼいですがw)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
luisResult(result: any): void { let msg = '' console.log('result', result) if ('topScoringIntent' in result) { let info:any= result.topScoringIntent // 認識率20%以上なら処理する(甘め) if ( info.score >= 0.2 ) { let url:string = 'https://www.google.co.jp/' if (info.intent === 'Tokushima') { msg = 'OK!徳島県の地図をだしたから見てね!' url = 'https://www.google.co.jp/maps/place/%E5%BE%B3%E5%B3%B6%E7%9C%8C/@34.1410563,133.7733708,8z/data=!4m5!3m4!1s0x35524d5baed737b3:0xfddd63f964cf310c!8m2!3d34.0657179!4d134.5593601' } else if (info.intent === 'HowToGetPlace') { msg = 'OK!徳島県の行き方のサイトを案内したから見てね!' url = 'https://www.awanavi.jp/access/' } else if (info.intent === 'Kazurabashi') { msg = 'OK!かずら橋の観光サイトを案内したから見てね!' url = 'http://miyoshinavi.jp/02miru/detail.php?genr=101&uid=SS000048' } else if (info.intent === 'Shonbenkozou') { msg = 'OK!小便小僧の観光サイトを案内したから見てね!' url = 'https://www.shikoku.gr.jp/spot/696' } else { msg = 'うーん....とりあえずGoogle開いたからそこから検索してね!' } window.open(url, '_blank') } else { msg = "ごめんなさい!質問が分からないんです。もっと徳島県について勉強します!" } this.luisMessage(msg) } } |
Azure LUISから送られてくる結果には「topScoringIntent」というものがあります。
これは一番認識率が良かったIntentとその認識率のデータです。
ここでは「topScoringIntent」で判断し、フィルタとして認識率20%以上のものを採用して(低w)処理を行っています。
実運用で使うことを考えると、ボット処理の部分が肝になります。
topScoringIntentの認識率のチューニングもそうですが、各IntentやそれぞれのEntityの認識率など組み合わせて更に細かな処理を行うことができます。
しかしこのまま書いていくとif文のオンパレードになるのですごい微妙ですね。
このif文の処理も教師データとして扱い、機械学習できれば自動化されたオリジナルのボット処理が作れそうです。
その場合は自然言語処理のLUISを使うよりは、他の機械学習を利用したほうが良さそうです。
GitHub Pagesの利用
せっかくなんでGitHubへアップロードして、GitHub Pagesで公開しましょう。
既にrepository作成、GitHub Pagesの設定済みであること前提で解説します。
Nuxt.jsの設定
Nuxt.jsをGitHub Pagesで公開するためにはnuxt.config.jsの設定が必要です。
コード変更
GitHub Pagesはホスティングサービスのurlが repository名 であることと、参照するフォルダが docs のため、その仕様に伴う設定をしていきます。
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const parseArgs = require("minimist") 〜〜〜〜 省略 〜〜〜〜〜 // 追加. ※pathはご自身のプロジェクト名にすること! const basePath = process.env.DEPLOY_ENV === 'GH_PAGES' ? '/luis_demo/' : '/' const routerBase = process.env.DEPLOY_ENV === 'GH_PAGES' ? { router: { base: basePath } } : {} const generateFolder = process.env.DEPLOY_ENV === 'GH_PAGES' ? 'docs' : 'dist' module.exports = { // 追加. mode: 'spa', ...routerBase, 〜〜〜〜 省略 〜〜〜〜〜 // 追加. generate: { dir: generateFolder }, |
GH_PAGESの環境変数によってGitHub Pages用にビルドするよう設定しています。
package.json
ビルドコマンドを追加します。
build-gh(無くても良いが一応)とgenerate-ghを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "name": "luis_demo", 〜〜〜〜 省略 〜〜〜〜〜 "scripts": { "dev": "nuxt", "build": "nuxt build", "start": "nuxt start", "generate": "nuxt generate", "build-gh": "DEPLOY_ENV=GH_PAGES nuxt build", "generate-gh": "DEPLOY_ENV=GH_PAGES nuxt generate" }, 〜〜〜〜 省略 〜〜〜〜〜 } |
.gitignore
こちらビルドには直接関係ありませんが、.envファイルをGitHubへデプロイしないように設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# dependencies node_modules # logs npm-debug.log # Nuxt build .nuxt # Nuxt generate dist # env .env |
ビルドする
以下のコマンドでビルドします。
ビルドが成功すると docsフォルダ が生成されます。
1 |
$ yarn generate-gh |
デプロイ
GitHub Desktop 又は gitコマンド を使ってプッシュするだけです。
URLにアクセスするとLuis demoのページが開きます。
https://hukusuke1007.github.io/luis_demo/
以上です。
最後に
いかがでしたでしょうか。
Azure LUISを利用することでカンタンにチャットボットを作ることができました。
Azure LUISを使うと次はGoogleやAWSが提供するサービスを使いたくなりますね。
日本語対応ができているのか?といったところが気になりますが、機会があれば他のサービスも利用してみたいと思います。