Help us understand the problem. What is going on with this article?

DialogFlowで会話をし、MobileNetで画像認識を行う自家製チャットサイトの構築

はじめに

 第一回 LINE風チャットサイトの構築(基本編)
 第二回 LINE風チャットサイトの構築(完成編)
 ということで、第三回目はこの自家製チャットサイトにボット機能と画像認識機能を加えたいと思います。

ボット機能(DialogFlow) 画像認識機能(MobileNet)
10.png 07.png

Dialogflow

 googleのdialogflowを使います。会話の設定などはあらかじめ作成したものがあるので、それを読み込んで使います。
 ここではDialogflowの詳しい解説は行いませんが、サンプルエージェント(英語版)を手軽に読み込めますので学習の手始めにいいと思います。なお今回のエージェントはそのサンプルエージェント coffee-shopを大雑把に日本語翻訳したものです。

エージェント構築手順

まずは私の方で作成したエクスポートzipファイルを、ご自分のローカルマシンにダウンロードしてください。

CafeMaster.zip

ファイル名はそのままでOKです。

そしてDialogflowコンソールへアクセスします
https://dialogflow.cloud.google.com/?hl=ja#/login

中央の「Sign-in with Google」をクリックしてログインします。

1Dialogflow-login.png

IDの指定と連携確認が行われた後、下記の画面になります。
上部のお知らせは消してしまいましょう
サービス規約を確認します

2Dialogflow-login2.png
ACCEPTをクリックするとWelcome画面になります
早速エージェント作成ボタンを押します
3Welcome.png
エージェント作成画面になりますので
1. エージェント名に「CafeMaster」
2. 言語設定に「Japanese-ja」を選択
3. CREATEをクリック
4CreateAgent.png
しばらくするとインテント作成画面になりますが、ここではあらかじめ作成しておいたエージェントを読み込むために左上の三本線メニューを選択します
5CreateAgent2.png
左のメニューが開いたら、CafeMasterと書かれている右横の歯車マークをクリックしてエージェント設定画面に移ります
6ClickDialog.png
エージェント設定画面が開いたら、真ん中あたりの「Export and Import」タブを選択します
7MasterEntry.png
メニューから「RESTORE FROM ZIP」を選択します
8selectMenu.png
アップロードするファイルの選択画面になりますので、先ほどダウンロードしたCafeMaster.zipをドラッグ&ドロップしてください
9selectFile.png
ファイルが選択されると下記の画面になります。ここで①の部分に「RESTORE」と打ち込み、②のRESTOREボタンを押します
10RestoreFile.png
zipファイルが読み込まれ学習が実行されます。しばらくすると完了するので「DONE」を押します
11FinishRestore.png
これで、設定は完了です
12CreatedAgent.png

参考にさせていただいたサイト

Qiita:Dialogflow入門
Geekfeed:DialogFlowのFulfillmentを使ったチャットボット作成
Qiita:サルにもわかる Dialogflow FAQ
ゆたかみわーく:【Dialogflowの使い方】Fulfillmentを使ってみる(超基礎編)

キーファイルの作成

 このAgentをプログラムから利用するため GCPのサービス アカウントを作成し、秘密鍵ファイルをダウンロードします。
 公式の案内   https://cloud.google.com/dialogflow/docs/quick/setup

GCPからのログインして作成することもできますが、ここではDialogflowコンソールから作成してみたいと思います。

まずはエージェントの設定画面のところにあるGoogle Projectの欄を見てください。ここに自動発行されたProject IDとService Accountが表示されています。ここで、このService Accountをクリックします。

51.png
初回時は規約への同意画面を経てGCPのコンソール画面へと遷移します。
この画面から、先ほどの自動設定されたService Accountの右横にある操作欄の…ボタンから「編集」を選択します
52.png

サービスアカウントの設定画面になりますので「+キーを作成」を選択します
53.png
秘密鍵の作成ダイアログに代わりますので、作成タイプがJSONであることを確認して「作成」ボタンを押します
54.png
秘密鍵ファイルが作成されると「秘密鍵がパソコンに保存されました」メッセージが出て、cafemaster-xxxxxxxxx~.jsonがローカルのパソコンに自動でダウンロードされます。

このファイルを後ほどチャットサーバーにアップロードし、プログラムから参照させるため、ファイル名を dialogflow.json に変更してください。

ファイル名を変更したら保存ボタンを押してください。

55.png

次にこの画面からDialogflow APIの利用許可の設定を確認します。

GCPコンソールのメニューから 「APIとサービス」を選び「ライブラリ」を選択します
56.png
APIようこそ画面に遷移しますので、中央の検索ボックスに「dialogflow」と入力します。
57.png

検索の結果からDialogflow APIを選択します
58.png

下記の画面になっていればOKですのでチャットサーバーの構築に手順を移してください。

追加01.png


もしもAPIの有効化設定がされていない場合

プロジェクトを作成と書かれているドロップダウンリストから「CafeMaster」を選択します
59.png

「続行」をクリック
60.png

画面にAPIは有効になってます。と表示がでればOKです
61.png

そのまま×で閉じます

62.png

以上でDialogflowの秘密鍵の作成とAPIの有効化が完了です。

サーバー環境構築

前回に引き続きお手軽なクラウドサービスを使って環境構築を行います。
いつものようにPaizaを使います。

Paiza Cloud

paiza表紙.png
Paiza Cloudeにアクセスしてメールアドレスを登録すると、すぐに環境構築ができるようになります。

リンク先
https://paiza.cloud/ja/

サーバー作成

アカウントを作成したらサーバー作成ボタンを押しましょう
まだサーバーはありません.png
新規サーバー作成のポップアップで、Node.jsとMongoDBを選択してください。
サーバー設定.png
数秒間待っているとサーバー環境ができあがります。
ちなみに無料プランの場合は
* サーバーの最長利用時間は24時間
* サービスは外部へ公開されない
逆に言うと練習にはもってこいという事でしょうか。

アプリケーション構築

次に各種インストールを行い、アプリケーションの実行環境を構築します。
まずは画面からターミナルのアイコンをクリックしてください。

s_ターミナル.png

起動したターミナルに下記のコマンドを入れます
① git clone によるファイル展開

git clone https://github.com/nstshirotays/chatapp-shot3.git

② ディレクトリを移動し
③ npmによるパッケージのインストール
これにより実行に必要なモジュールなどがpackage.jsonに従って自動的にインストールされます。

cd chatapp-shot3
npm install

01.png

次に、先ほど作成したDialogflowに接続するための秘密鍵ファイルをアップロードします。
/home/ubuntuと書かれているところを右クリックするとメニューが表示されますので、そこから前の工程で作成したdialogflow.jsonファイルをアップロードします。

02.png

ファイル名は必ずdialogflow.jsonとして/home/ubuntu/に配置されていることを確認してください

03.png

以上で必要な準備が整いましたので、あとはnodejsを起動してアプリを立ち上げます。

npm start

エラーがでなければ、左側に緑色のブラウザアイコンが新しく点滅し始めます。
s_ブラウザ3000.png

このアイコンをクリックするとチャットアプリが起動します。

アプリ実行

ログイン画面

まずはログイン画面です。
login.png
初回は誰も登録されていないので、Create an Account を押してユーザー登録画面に移ります。

ユーザー登録画面

NickNameとPassCodeを入れてユーザーを登録しましょう。
NickNameは英文字で4から12文字。PassCodeは数字で6から12文字です。
regisit.png

お好みでFaceIconを変更(png 32kbまで)できます。

ユーザーを登録したら実際にログインしてみましょう。

友達選択画面

04.png

 前回はEchoさんだけでしたが、今回はCafeMasterが追加されています。
 このCafeMasterさんの実体がDialogflowとなっています。

DialogFlowによる会話生成

 それでは、実際にCafeMasterさんと会話してみましょう。

05.png

 最初の「いらっしゃいませ。コーヒーはいかがですか?」はチャットサイト側で出力しており、次の「アメリカンコーヒーを一杯お願いします」というTEXTからDialogflowにて処理をしています。
 Dialogflowでは渡されたTEXTから該当するインテントを割り出し処理をしていきます。ここではおそらくアメリカンコーヒーという単語に反応してorder.drinkインテントが発動されています。
 なので、店主の挨拶の後に「いつもの」などと入れるとorder.lastインテントが発動します。
 その他、Dialogflow側の処理については、DialogFlow Console 画面の右側にチャットボックスがあり自由にテストができますので、いろいろと試してみてください。

プログラム解説

 それでは早速Dialogflowとの連動についてプログラムを見てみましょう。

起動時の処理

 Google APIを利用するためには秘密鍵による認証が必要です。先に配置したdialogflow.jsonファイルがそれです。
 この秘密鍵ファイルは環境変数の設定により必要とされるプログラムからアクセスされます。
 このため起動する前に当該環境変数の設定をする必要があります。ここでは、npm startで最初に参照されている package.jsonで設定を行っています。

package.json
{
  "name": "chatapp2",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "GOOGLE_APPLICATION_CREDENTIALS='/home/ubuntu/dialogflow.json' node ./bin/www"
  },
  "dependencies": {
    "@tensorflow-models/mobilenet": "^2.0.4",
    "@tensorflow/tfjs": "^1.5.2",
    "@tensorflow/tfjs-node": "^1.5.2",
    "basic-auth-connect": "^1.0.0",
    "bcryptjs": "^2.4.3",
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "dialogflow": "^1.2.0",
    "ejs": "~2.6.1",
    "express": "~4.16.1",
    "express-validator": "^6.2.0",
    "http-errors": "~1.6.3",
    "jsonwebtoken": "^8.5.1",
    "mongo-sanitize": "^1.0.1",
    "mongoose": "^5.7.6",
    "mongoose-sequence": "^5.2.2",
    "morgan": "~1.9.1",
    "pb-util": "^0.1.2",
    "randomstring": "^1.1.5",
    "redis": "^2.8.0",
    "save": "^2.4.0",
    "uuid": "^3.4.0"
  }
}

Startの値に環境変数の設定を追加しています。

GOOGLE_APPLICATION_CREDENTIALS='/home/ubuntu/dialogflow.json'

会話の送信と受信

チャットサーバーとDialogFlowとのやりとりは helper/bot.cafe.jsにて実装してあります。

helper/bot.cafe.js
// CafeMasterの処理
var db = require('../helper/db');
var Chat = db.Chat;
const chatsv = require('../models/chat.service');

//DialogFlowの設定
const dialogflow = require('dialogflow');
const uuid = require('uuid');
var sessionId = "";

//console.log(process.env.GOOGLE_APPLICATION_CREDENTIALS);
var fs = require("fs");
var use_readFile_json = JSON.parse(fs.readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf8'));
const project_id = use_readFile_json.project_id;
//console.log(project_id);



exports.start = function(){
    //  最初の挨拶を登録
    if(FrName == 'CafeMaster') {
        // CafeMaster
        sessionId = uuid.v4();
        console.log('Session ID = ' + sessionId);

        chatsv.create({
            fromAddress : FrID,
            toAddress : MyID,
            message : 'いらっしゃいませ。コーヒーはいかがですか?'
        });
        // 2秒ごとに新しいメッセージを検索する
        botTimer = setInterval(function(){
            serachNewMessages();
        },2000);
    }
};


var resentMsg ="";

async function serachNewMessages() {
    if(FrName == 'CafeMaster') {
        // CafeMasterの最後の発言を取得する
        var query = { "fromAddress": FrID ,"toAddress": MyID};

        Chat.find(query,{},{sort:{timeStamp: -1},limit:1}, function(err, data){
            if(err){
                console.log("serachNewMessages err",err);
            }
            if(data.length > 0){
                // CafeMasterの最後の発言以降の自分の発言を取得する.
                var lastMessageTime = data[0].timeStamp;
                query = { "fromAddress": MyID ,"toAddress": FrID,"timeStamp": {$gt : lastMessageTime}};
                Chat.find(query,{},{sort:{timeStamp: -1},limit:1}, async function(err, data){
                    if(err){
                        console.log("serachNewMessages err",err);
                    }
                    if(data.length > 0){
                        // 応答メッセージの設定
                        var resMessage="";

                        const {struct} = require('pb-util');
                        // Create a new session
                        const sessionClient = new dialogflow.SessionsClient();
                        const sessionPath = sessionClient.sessionPath(project_id, sessionId);
                        // The text query request.
                        const request = {
                            session: sessionPath,
                            queryInput: {
                                text: {
                                    // The query to send to the dialogflow agent
                                    text: data[0].message,
                                    // The language used by the client (en-US)
                                    languageCode: 'ja-JP', //'ja'
                                },
                            },
                        };


                        //DialogFlowへ会話を送って返事を待つ
                        const responses = await sessionClient.detectIntent(request);
                        const result = responses[0].queryResult;

                        const parameters = JSON.stringify(struct.decode(result.parameters));
                        var obj = JSON.parse(parameters);    
                        console.log(`  Query: ${result.queryText}`);
                        console.log(`  Response: ${result.fulfillmentText}`);
                        resMessage = result.fulfillmentText;

                        // 退避メッセージと異なってれば発話する
                        if ( resMessage !== "" && resentMsg != data[0].timeStamp + data[0].message) {
                            resentMsg = data[0].timeStamp + data[0].message;
                            chatsv.create({
                                fromAddress : FrID,
                                toAddress : MyID,
                                message : resMessage
                            });
                        }
                    }
                });
            }
        });
    }
}

 この処理は友達選択でCafeMasterが呼ばれると起動されます。
 初期処理にて、先ほどの環境変数で示された鍵ファイルからproject_idを取得し、ランダムに得られた番号をセッション番号としてDialogflowに接続します。
 最初の挨拶として「いらっしゃいませ。コーヒーはいかがですか?」を発話したのち、serachNewMessage関数を2秒ごとに実行しユーザーの会話を待ちます。

 新しい会話を検知すると、その内容をDialogFlowに渡して返事を待ちます。そして返事がきたらそのTEXTをmongoDBに書き込みます。

 以上によりチャットサイトとDialogFlowが連動して会話処理を行うことができます。

Fulfillmentの設定

 さて、このdorder.drinkインテントですが、ここでは「飲み物」「サイズ」「デリバリー」の3要素を必須入力としています。この3要素が揃うと次のorder.drink-yesというインテントが発動し、いつもの支払い方法でよいか聞かれます。ここで「はい」と答えるとorder.drink.same_cardに移動し最終確認が行われます。
 初期段階ではFulfillmentが設定されていませんので、単純にこのインテントに設定されたResponse「この度はご注文ありがとうございました。またのご来店をお待ちしております。」というメッセージのみが戻されます。

08.png

 本来であれば実際の注文処理が動くと思いますので、ここでFulfillmentによる外部サーバーの呼び出しを行ってみます。
 ここでは注文処理用の外部サーバー処理を先ほどのチャットサーバー上に同居させています。このためまずは呼び出すサーバーアドレスを取得します。
 なお、この設定を行うためには本チャットサーバーがインターネットに公開されていることが前提となります(Paiza.cloudの無料枠では利用できません)。
 まず公開されているアドレスを控えておきます。

09.png

次にDialogFlow Consoleの左側のメニューからFulfillmentを選択します

webhookスイッチをオンにして、呼び出すサーバーの設定を行います。
① 先ほど控えたURLの末尾に cafeを追加して設定
② IDに「userid」英字6文字を設定
③ PWDに「passcode」英字8文字を設定
④ 最後に「SAVE」ボタンをクリック

01.png

 そして、設定されたWebhookを呼び出すタイミングをインテントに指定します。

 Intentsを選択し、 order.drink -> order.drink - yes を展開し order.drink.samecardインテントを選択します。

02.png

 そして、①画面の下方にあるfuifillmentのタブを開いて②、「Enable webhook call for this intent」のスイッチをオン③にします。
 それから最後に④のSAVEを押して内容を反映させます。

03.png

 これでFulfillmentによるwebhookが設定できましたので、先ほどのCafeMasterさんを再度よびだしてみると、最終の挙動が変わっているのがわかると思います。

10.png

webhookプログラムの解説

 それでは、受け取り側のwebhookプログラムについて解説します。実装はroutes/cafe.jsです。

routes/cafe.js 
var express = require('express');
var router = express.Router();
var db = require('../helper/db');
var User = db.User;
var Chat = db.Chat;
const verifyToken = require('../helper/VerifyToken');
var { check, validationResult } = require('express-validator');
var fs = require('fs');
const chatsv = require('../models/chat.service');
var basicAuth = require('basic-auth-connect');


//DialogFlowからのコールバックを処理する
//ベーシック認証
router.post('/', basicAuth('userid','passcode'), function (req, res, next) {

    //console.log("req.body.session "+ req.body.session);   
    //console.log(req.body.queryResult.outputContexts[1].parameters.drink);
    //console.log(req.body.queryResult.outputContexts[1].parameters.size);
    //console.log(req.body.queryResult.outputContexts[1].parameters.delivery);

    var cafeImageFile = req.body.queryResult.outputContexts[1].parameters.drink + ".png";
    var baseStr = "data:image/png;base64,";
    var cafeImage;

    fs.readFile('./public/files/' + cafeImageFile, function (err, data) {
        if (err) throw err;
        cafeImage = baseStr.concat(new Buffer(data).toString('base64'));
        chatsv.create({
            fromAddress : FrID,
            toAddress : MyID,
            image : cafeImage,
        })
    });

    let responseObj={
     "fulfillmentText":'おまたせいたしました。'
    }
    return res.json(responseObj);

});

module.exports = router;

 ここでDialogFlowからのpost要求を受け付けます。ここではbasic認証を入れてあります。本来であればmTLSによるサーバーの相互認証(これにより接続してきたDialogflow側の証明書を認証することができる)をすべきなのですが、paiza.cloudが仮想環境による実装のため、サーバー証明書を入れることができませんので実装は試せませんでした。
 処理的にはシンプルでpostされたデータにより画像ファイルを選択してmongoDBに格納しています。そしてDialogFlowへの返却値として「おまたせしました」というTEXTを送り返しています。

 以上がDialogFlowとの連動プログラムです。プログラム自体は単純なのですが、Dialogflow側の設定は慣れないと自分が何をしているかわからなくなります。このあたりはDialogflowのサンプルエージェントを眺めてみるるとその実装形態の思想が見えてくると思います。

MobileNetによる画像識別

 2017年に誕生した畳み込み画像認識のmobileNetは、小型軽量でモバイル等での利用も盛んです。今回はこれをnode.jsに組み込んでいます。
 友達選択からEchoさんを選択し、画像を送ると1000個の分類から識別してくれます。

11.png

 このように識別してくれていますが、分類名称が英語ですので、これを日本語ファイルで置き換えます。日本語ファイルはいろいろなサイトを参考にして作成しました。

06.png

 chatapp-shot3/imagenet_classes.jsが和訳した内容ですので、node_module/@tensorflow-modules/mobilenet/distにある英文内容と差し替えます。
 差し替えたのちに npm startで再度起動すると和訳文が読み込まれます。

12.png

 余談ですが、この1000分類を見ていただくとわかりますが、昆虫と魚と犬が多いなと思いました。
犬種に関してはかなりマニアックな感じがしました。

07.png

プログラム解説

 実装は helper/bot.echo.jsにあります。

helper/bot.echo.js
// Echoさんの処理
var db = require('../helper/db');
var Chat = db.Chat;
const chatsv = require('../models/chat.service');

//tensorflow.jsの設定
var randomstring = require("randomstring");
const tf = require('@tensorflow/tfjs');
const mobilenet = require('@tensorflow-models/mobilenet');
const tfnode = require('@tensorflow/tfjs-node');
var fs = require('fs');


exports.start = function(){
    //  最初の挨拶を登録
    if(FrName == 'Echo') {
        // echo
        chatsv.create({
            fromAddress : FrID,
            toAddress : MyID,
            message : 'こんにちは'
        });
        // 1秒ごとに新しいメッセージを検索する
        botTimer = setInterval(function(){
            serachNewMessages();
        },1000);
    }
};


var resentMsg ="";

function serachNewMessages() {
    if(FrName == 'Echo') {
        // Echo さんの最後の発言を取得する
        var query = { "fromAddress": FrID ,"toAddress": MyID};

        Chat.find(query,{},{sort:{timeStamp: -1},limit:1}, function(err, data){
            if(err){
                console.log("serachNewMessages err",err);
            }
            if(data.length > 0){
                // Echoさん最後の発言以降の自分の発言を取得する.
                var lastMessageTime = data[0].timeStamp;
                query = { "fromAddress": MyID ,"toAddress": FrID,"timeStamp": {$gt : lastMessageTime}};
                Chat.find(query,{},{sort:{timeStamp: -1},limit:1}, async function(err, data){
                    if(err){
                        console.log("serachNewMessages err",err);
                    }
                    if(data.length > 0){
                        var resMessage="";
                        // 応答メッセージの設定
                        if (data[0].image.length > 0) {
                            // MobileNetの呼び出し
                            var matches = data[0].image.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/),
                            imageBuffer = {};
                            imageBuffer.type = matches[1];
                            imageBuffer.data = new Buffer(matches[2], 'base64');

                            const mobilenetModel = await mobilenet.load({version: 2, alpha: 1.0});
                            const predictions = await mobilenetModel.classify(tfnode.node.decodeImage(imageBuffer.data));

                            resMessage = predictions[0].className + "ですね";

                        } else if (data[0].stampTitle !== undefined) {
                            resMessage = data[0].stampTitle + "ですね";
                        } else if(data[0].message.length > 0){
                            resMessage = data[0].message + "ですね";
                        } else {
                            resMessage =  "無言ですね";
                        }
                        // 退避メッセージと異なってれば発話する
                        if ( resMessage !== "" && resentMsg != data[0].timeStamp + data[0].message) {
                            resentMsg = data[0].timeStamp + data[0].message;
                            chatsv.create({
                                fromAddress : FrID,
                                toAddress : MyID,
                                message : resMessage
                            });
                        }
                    }
                });
            }
        });
    }
}

 このボットが1秒ごとにメッセージを検索し、TEXTであれば末尾に「ですね」を付け加え、スタンプであればそのファイル名を返却し、画像であればmobilenetで画像識別を行ってその結果を書き込んでいます。
コードにすればほんの数行で画像認識を行うことができます(モデルの読み込みはここでなくてもよいかもしれません)。

 その他にもいろいろと呼び出せるライブラリがあります。

  TensorFlow.js

最後に

 いかがだったでしょうか。相変わらず前回から間が空いてしまいましたが、Echoさんが画像の識別をしてくれるようになり、ボットサイトを使った会話もできるようになりました。
 今回はGoogleのDialogflowを利用する関係で手順がとても多くなってしまいました。Step by Stepで記載しましたが、半年後には画面も変わったりするので何時ごろまで有効かはわかりませんが、ひとまずはこの手順で動くと思います。
 いずれにせよ、外部のサイトと連携したり、開発されたライブラリを活用することで、簡単に機能追加ができるのが実感できました。

次回は...

 次回はこの画像識別をさらにパワーアップさせて、画像のキャプションを自動生成するロジックを追加したいと思っています。

記事一覧

WEBでLINE風のチャットサイトを作る-その1
WEBでLINE風のチャットサイトを作る-その2

nstshirotays
NTTデータシステム技術 デジタル技術推進室の城田です。よろしくお願いいたします。
http://www.nttdst.com/employee/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした