WonderPlanet Tech Blog

アドバンストテクノロジー部やCTO室のメンバーを中心に最新の技術情報を発信しています。

Dialogflowでちょっとしたチャットボットを作ろう

アドバンストテクノロジー部の近藤です。
Amazon EchoやGoogle Homeなどのスマートスピーカーが最近テレビCMするなどしていて普及し始めています。
今回はGoogle Homeにも利用できるDialogflowを使ってちょっとしたチャットボットのようなものの作り方を紹介します。

必要なもの

・Googleアカウント(DialogflowとActions on Googleのログインに利用)
・連携したいメッセージアプリのアカウント
・Google Homeで動かしたい場合はその実機

Dialogflowとは

https://dialogflow.com
Dialogflowは音声およびテキストベースの自然言語での会話型インターフェイスを構築するためのサービスです。
たとえば「誕生日おめでとう」という呼びかけに対して、「ありがとう」という返答をさせるようなものを作ることができます。
Dialogflowのコンソールではどのような言葉に対してどのように返答するかの設定していきます。
Dialogflowで作成したチャットボットは、Googleアシスタント、Facebook Messenger、Slackなどのサービスに対応していて、それらのサービス上でも動作させることができます。
スマートスピーカーのGoogle Homeで音声による会話をすることも可能です。

Actions on Googleとは

https://console.actions.google.com
Actions on Google では、Dialogflowで作成したアプリをシミュレーターで動作確認をしたり、アプリを公開するための設定・審査への提出をすることができます。
審査を通過すると、Google Homeを使っているユーザーがアプリを利用することができるようになります。
Google Home以外にもSlackやFacebookメッセンジャーなどのメッセージアプリに組み込んで公開する方法があります。

ハンズオン

「元気ですか?」と聞いたら、いくつかのパターンで返答してくれるボットを作ってみましょう。

Agentを作る

Dialogflowを開いたら、左側のメニューから「Create new agent」を選択し、Agentを作成します。 f:id:HidehikoKondo:20180226211344p:plain

今回のAgentの設定は下記のように設定しましょう。
Agent name:HowAreYou?(日本語不可です)
DEFAULT LANGUAGE:Japanese
DEFAULT TIME ZONE:(GMT+9:00)Asia/Tokyo
GOOGLE PROJECT:Create a new Google project
設定項目を記入したら「CREATE」ボタンを押しましょう。
f:id:HidehikoKondo:20180220171719p:plain

Intentsの設定

メニューから「Intents」を開き、「CREATE INTENT」ボタンでインテントを作成します。
一番上の「Intent name」の欄にインテントの名前を設定します。

「User Says」の項目に、反応させたい言葉を記入します。
「元気ですか?」と設定します。
複数設定することもできるのでせっかくなので「調子はどう?」も設定しておきましょう。
f:id:HidehikoKondo:20180220171013p:plain

Text responseの欄にGoogleアシスタントが返答する言葉を記入します。
複数記入すればそのなかからランダムでどれかひとつを返してくれます。
「今日は元気いっぱいです!!」「ちょっと具合が悪いです」「まぁまぁ元気です」
今回はこの3つを設定しましょう
f:id:HidehikoKondo:20180301192155p:plain

設定したら忘れずに右上の「SAVE」ボタンを押します。

Integrationsの設定

「Integrations」のメニューを開きます。
Dialogflowで連携できるサービスの一覧が表示されていますが、ここではGoogle Assistantの欄の「INTEGRATION SETTINGS」のリンクをクリックします。 f:id:HidehikoKondo:20180301182826p:plain
ここで先程設定したIntentをGoogle Assistantで利用するための設定をします。
f:id:HidehikoKondo:20180220190111p:plain

Explicit invocationの項目でさきほど設定した「元気ですか?」を選択します。
Google Assistantで呼び出す時に言う「OK Google」に続く言葉として設定されます。

上記の設定ができたら「TEST」をクリックしてActions on Googleの画面に遷移します。

Actions on Googleの設定

ここではアプリの動作確認をするためのシミュレータを利用することができます。
文字入力により確認ですが、DialogflowのIntentsで設定したキーワードに反応するか確認することができます。

f:id:HidehikoKondo:20180226182903p:plain 日本語で利用する場合はLanguageの項目は「Japanese(Japan)」を選びましょう。
Inputの項目には予め「テスト用アプリにつないで」と入力されています。この言葉がテスト用のウェイクワードとなります。「OK Google」ではありませんので注意しましょう。(Overviewの画面で任意の言葉に変更できます)
Inputの項目にカーソルを合わせてEnterキーを押せばアプリが立ち上がります。

f:id:HidehikoKondo:20180226183635p:plain アプリが立ち上がったら、Inputの項目にDialogflowで設定した「元気ですか?」「調子はどう?」と入力してみましょう。
設定したレスポンスを返してくれます。
画面右側ではJSON形式でレスポンスがやり取りされていることが分かります。

Google Homeの実機でテスト

シミュレーターでの動作が確認画できたら、やっぱり実機で試してみたいですよね。
DialogflowにログインしているGoogleアカウントと、Google Homeに紐付けているGoogleアカウントが同じである必要があります。
この状態になっていれば、すでにテストでGoogle Homeを利用することができます。

テストで実行するにはさきほどActions on Googleでも利用したテスト用ウェイクワード「テスト用アプリにつないで」とGoogle Homeに話しかけます。
アプリが起動したら「元気ですか?」「調子はどう?」と話しかけます。
そうすると「今日は元気いっぱいです!!」「まぁまぁ元気です」などと返してくれるはずです。

メッセージアプリで動かしてみる

Dialogflowで作ったアプリはGoogle Home以外にもFacebookメッセンジャーやSlackなどのメッセージアプリに対応していたり、WEBページ内に埋め込むことも可能です。
今回はSlackでの利用例を紹介します。

Dialogflowの「Integrations」の画面でSlackを選択します。
右上のスイッチをONにします。
次に「TEST IN SLACK」ボタンを押して、Slackのワークスペースにログインします。

ログインできたら、Slackのアプリを作ります。
Launchの項目の「Create a new Slack app.」のリンクをクリックすると、Slackのアプリの作成画面に遷移します。
f:id:HidehikoKondo:20180226201648p:plain アプリ名を「HowAreYou?」と設定し、「Create App」をクリックします。
するとClient ID、Client Secret、Verification Tokenが発行されます。
これをDialogflow側の設定画面のSlack Client ID、Slack Client Secret、Slack Verification Tokenに入力します。 f:id:HidehikoKondo:20180226201835p:plain

これらの項目が入力できたら「Start」ボタンを押して設定完了です。 f:id:HidehikoKondo:20180226201555p:plain

では、Slackを立ち上げて、「Apps」の項目を見ると、「dialogflow_bot」というのが追加されています。
これを開き、Dialogflowで設定した「元気ですか?」「調子はどう?」と聞いてみましょう。
すると設定したとおりに返事を返してくれます。

f:id:HidehikoKondo:20180226202156p:plain

まとめ

このようにしてDialogflowでチャットボットを作ることができます。
今回は単純に決められたテキストをランダムで返すものを作りましたが、Dialogflowで入力されたメッセージに対してJavaScriptで動的に返答するテキストを変えることもできます。
たとえばよくある質問に対して回答をするようなものも作れるかもしれないですね。

Google Homeで音声で操作できたり、WEBページに埋め込んだり、メッセージアプリと連携したりと、使える場面もいろいろあります。
アイデア次第で面白いことが色々できそうですね。

AlexaとZoom Roomsでスマート会議スキルを開発する方法

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回はZoom RoomsのAPIを使ってAlexaからZoom Roomsを操作するスキルを開発してみましたのでご紹介します。
先日ご紹介したAlexa for Businessで公開されているZoom for Alexaと連携させる方法とは別のものです。
開発したスキルは、以下の動作例のように日本語で繋げたい会議室を指定すると、接続や切断をすることが可能です。
これにより、ミーティングツールの使い方をよく分かっていないために会議の開始が遅くなるといった時間の無駄を削減することができます。

動作例

・「アレクサ、ズームルームで''ホール''に繋いで!」
 → "発言者の会議室" と "指定した会議室(ホール)" のZoom Roomsを接続

・「アレクサ、ズームルームで''ミーティング''を終了して!」
 →"発言者の会議室" のZoom Roomsを終了

実際の動作結果

実際の動作は先にみたほうが分かりやすいため動画でご紹介します。
youtu.be

Zoomについては紹介記事を投稿しているので、ご興味があればご覧ください。
tech.wonderpla.net

またAlexa for Businessについても、ご興味があれば合わせてご覧ください。 tech.wonderpla.net


概要&必要なもの

AWS LambdaでZoom Rooms APIを呼び出してZoom Roomsを操作します。
AlexaとZoom Roomsを活用するため、開発には以下のものを用意しておく必要があります。

Alexaスキル開発用

・Alexa 端末
・Amazon Developerアカウント
・Amazon ショッピングアカウント
(混乱を避けるため全て.co.jpリージョンであることが望ましい)
・AWS アカウント

Zoom Rooms用

・Zoom Roomsアカウント x 2
・Zoom Rooms用PC x 2
・Zoom Rooms用タブレット x 2
(接続先と接続元用に各2つずつ用意)

またPCにインストールするZoom Roomsクライアントは非公開のプレリリース版のビルドである必要があります。
Webで公開されている通常のZoom RoomsクライアントではZoom Rooms APIが使えないため、接続ができません。
こちらは非公開ではありますが、Zoomにメールするとダウンロード用のアドレスを共有してもらえました。
Zoom Rooms API - Zoom Developers

Note: These APIs need a pre-launch version of our new Zoom Room build – if you would like to try it, please send an e-mail to “developersupport@zoom.us” and we will provide you the link to download

こうして必要なものを並べると、導入のために必要なものが、かなりあるのが分かりますね。
以上の必要なものが揃ったらZoom RoomsをPCとタブレット両方にインストールしておいてください。
PCにインストールするZoom Roomsはメールで頂いたプレリリース版です。
タブレットにインストールするZoom Roomsはストアに公開されている通常のもので大丈夫です。


AlexaでZoom Roomsを操作するスキルの開発

Amazon開発者コンソールの設定では接続したい部屋名を取得するIntentを設定します。
とくに難しい設定はしなくても出来るので、詳しい解説は前回の記事をご参照ください。
tech.wonderpla.net

今回は以下のように部屋に接続するためにConnectRoomIntentを設定しました。 Alexa インテント

また{room}のスロットに部屋名として認識させたい単語を登録しておきました。
(ワンダープラネットでは会議室に名古屋名物の名前をつけているため、スロットに食べ物の名前が並んでいます笑)
Alexa スロット
この他にもミーティング開始時や終了時のインテントを追加しておきました。
ひとまず動作を見るだけであれば、先ほど設定した部屋名を取得するインテントだけで大丈夫です。


Zoom RoomsのAPI Keyの取得

次はZoom RoomsのAPIを使うために必要なKeyを取得します。
Zoom Developers – Power Up Your Apps with Video, Voice, and Screen Sharing
サインインを行ったら、画面右上のDevelopper Accountを開き、API Keyを発行しましょう。
Zoom Developer Account
Zoom Roomsのアクセストークン

Zoom Rooms APIの呼び出し

ここからは実際にソースコードを書いていきます。言語はNode.js6.10です。
JWT認証でZoom Rooms APIのAccess Tokenを発行してZoom Roomsへ命令を送ります。
ひとまずはLambdaに書く前にローカル環境で動作を確認してみます。

JWT認証

取得したAPI KeyとAPI Secretを使ってJWTでAccess Tokenを発行します。
JWTはnpmでインストールしておきましょう。

$ npm install jsonwebtoken

Access Tokenは以下のような記述で発行できます。

const jwt = require('jsonwebtoken');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

Zoom Roomsを起動

あらかじめnpmでsync-requestをインストールしておきます。

$ npm install sync-request

さきほどのコードで生成したtokenを用いてAPIを呼び出します。
以下のコードを叩くと設定したルームIDの会議室のインスタントミーティング(設定したルームIDの会議室をホストとして起動)が開始できます。
もし設定するルームIDが分からない場合は以下を参考にしてください。
Zoom RoomsのミーティングIDとルームIDを調べる

const syncrequest = require('sync-request');
const HOST_ZR_ID = "起動するZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + HOST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("起動成功");
}else{
    console.log("起動失敗");
}

methodjoinにすることでZoom Roomsを起動します。ただし接続先を指定していないため、設定したルームIDの会議室をホストとして開始するだけで、他の会議室には繋がりません。

また、paramsにはforce_acceptを設定しています。これが無いとAPI実行時にerror: { code: 4009, message: 'The room client is busy.' }と出力され起動できないことがあります。設定しておくとbusy状態でも接続を強制してくれるようになります。他と接続していないときでも稀にbusyが返ってきて悩まされることがあるので、ひとまずは設定しておくとよいでしょう。

Zoom Roomsを他の会議室と繋ぐ

他の会議室からホストとなっている会議室に接続したい場合は、
以下のようにoptionのparamsに接続先のミーティングIDを指定すると接続できます。
このとき、ホスト側はすでに起動させておいてインスタントミーティングの状態にしておく必要があります。

const syncrequest = require('sync-request');
const HOST_MEETING_ID = "ホスト(起動済み)のZoom RoomsのミーティングID";
const GUEST_ZR_ID = "新たに起動する接続したいZoom RoomsのルームID";
var option = {
   qs: {access_token: token},
   json: {method: "join",
          params: {meeting_number: HOST_MEETING_ID,
                   force_accept: true}}
};
var zr_join = "https://api.zoom.us/v2/rooms/" + GUEST_ZR_ID + "/meetings";
var syncres = syncrequest('POST', zr_join, option);
var syncBody = JSON.parse(syncres.getBody('utf8'));
console.log(syncBody);
if(syncBody.result){
    console.log("接続成功");
}else{
    console.log("接続失敗");
}

Zoom RoomsのミーティングIDとルームIDを調べる

設定するZoom RoomsのミーティングIDとルームIDが分からない場合、以下の方法で確認できます。

ミーティングIDの確認

以下のリンクより管理画面へログインして各部屋のミーティングIDを見ることができます。
https://zoom.us/signin

ログインしたら左サイドバーの「Zoom Rooms」から対象のルームの編集をクリックします。
Zoom Rooms 管理ページ

少しスクロールするとhttps://zoom.us/j/〇〇〇〇という形式でミーティングIDを確認できます。
またこれ以外にもタブレットのZoom RoomsアプリからSettingsを開いても確認することができます。
Zoom Rooms ミーティングID

ルームIDの確認

ルームIDは設定画面での記載箇所が見つからなかったため、Zoom Rooms APIのListを使って確認しました。
以下のコードでAPI KeyとAPI Secretを設定して実行すれば、アカウントに紐づくZoom RoomsのルームIDと名前の一覧が取得できます。

const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";

var payload = {
  iss: ZOOM_API_KEY,
  exp: ((new Date()).getTime() + 5000)
};
var token = jwt.sign(payload, ZOOM_API_SEC);

var option = {
   qs: {access_token: token},
   json: {method: "list"}
};

var zr_list = "https://api.zoom.us/v2/rooms/zrlist";
var syncres = syncrequest('POST', zr_list, option);
console.log(syncres.getBody('utf8'));

Alexaを使ってAWS Lambdaから実行

ローカル環境で動作することが確認できたら、ここからはAWS Lambdaにソースを移して声で実行してみます。
簡単にAlexa用に書くと以下のようになります。AWS Lambdaに登録が完了したらARNをAmazon開発者コンソールにコピーしてください。

"use strict";
const jwt = require('jsonwebtoken');
const syncrequest = require('sync-request');
const Alexa = require("alexa-sdk");
const SKILL_NAME = "ZOOM"
const APP_ID = "amzn1.ask.skill.xxxxx";
const ZOOM_API_KEY = "取得したAPI Key";
const ZOOM_API_SEC = "取得したAPI Secret";
const HOST_ZR_INFO = { zr_name: '起動するZoom Roomsの会議室名', zr_id: '起動するZoom RoomsのルームID', meeting_id: '起動するZoom Roomsのミーティング'ID };
const GUEST_ZR_INFO = { zr_name: '接続したいZoom Roomsの会議室名', zr_id: '接続したいZoom RoomsのルームID', meeting_id: '接続したいZoom RoomsのミーティングID' };

//ステートの定義
const states = {
  CONNECT: '_CONNECTMODE'
};

var languageString = {
    "ja-JP": {
        "translation": {
            "WELCOME_MESSAGE": "ズームルームへようこそ。",
            "HELP_MESSAGE": "ほかの部屋と接続したい場合は、ルーム名を教えてください。",
            "UNHANDLED_MESSAGE": "すみません、よく聞きとれませんでした。",
            "CONNECT_MESSAGE": "%sに接続します。",
            "ERROR_CONNECT_MESSAGE": "接続に失敗しました。",
            "EXIT_MESSAGE": "さようなら。"
        }
    }
};

exports.handler = function(event, context, callback) {
  var alexa = Alexa.handler(event, context);
  alexa.resources = languageString;
  alexa.dynamoDBTableName = 'ZoomRooms';
  alexa.registerHandlers(handlers,connectHandlers);
  alexa.execute();
};

// 初期ハンドラ
var handlers = {
  'LaunchRequest': function () {
    this.emit(':ask', this.t("WELCOME_MESSAGE") + this.t("HELP_MESSAGE"));
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', this.t("HELP_MESSAGE"));
  },
  'AMAZON.CancelIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'AMAZON.StopIntent': function() {
    this.emit(':tell', this.t("EXIT_MESSAGE"));
  },
  'ConnectRoomIntent': function() {
    this.handler.state = states.CONNECT;
    this.emitWithState("ConnectRoomIntent");
  },
  'Unhandled': function () {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
};

// 接続ステート
var connectHandlers = Alexa.CreateStateHandler(states.CONNECT, {
  'ConnectRoomIntent': function() {
    var response = "";
    var asyncToken = getToken(ZOOM_API_KEY, ZOOM_API_SEC);
    //ホストを起動
    var isOpenHost = connectionRoom(asyncToken, HOST_ZR_INFO);
    if(isOpenHost){
      //ホストが正常に起動出来ていたら、依頼された部屋と接続
      if(connectionRoom(asyncToken, GUEST_ZR_INFO, HOST_ZR_INFO)){
        response = this.t("CONNECT_MESSAGE", GUEST_ZR_INFO.zr_name);
      }
    }
    if(!response){
      // 失敗時はエラーメッセージを流す
      response = this.t("ERROR_CONNECT_MESSAGE");
    }
    this.emit(':tell', response);
  },
  'Unhandled': function() {
    this.emit(':ask', this.t("UNHANDLED_MESSAGE"));
  }
});

// 部屋を起動(AcessToken, 起動する部屋, 接続先の部屋)
function connectionRoom(token, startRoom, connectRoom){
  var option = {
     qs: {access_token: token},
     json: {method: "join",
            params: {force_accept: true}}
  };
  if(connectRoom){
    // 接続先がある場合、optionに接続先のミーティングIDを追加
    option.json.params.meeting_number = connectRoom.meeting_id;
  }
  var zr_join = "https://api.zoom.us/v2/rooms/" + startRoom.zr_id + "/meetings";
  var syncres = syncrequest('POST', zr_join, option);
  var syncBody = JSON.parse(syncres.getBody('utf8'));
  console.log(syncBody);
  if(syncBody.result){
      console.log("接続成功");
      return true;
  }else{
      console.log("接続失敗");
      return false;
  }
};

// ZoomRooms接続用トークンを発行
function getToken(api_key, api_sec){
  var payload = {
    iss: api_key,
    exp: ((new Date()).getTime() + 15000)
  };
  return jwt.sign(payload, api_sec);
}

上記のコードでは、Zoom Roomsの接続先がGUEST_ZR_INFOに固定となっているため、
実際に運用する場合はConnectRoomIntentで取得した部屋名からIDを割り出すコードは必要になるかと思います。
またendのmethodなどを駆使すればAlexaでZoom Roomsを終了したりすることも可能です。


Zoom Rooms API

上記のコードで扱ったZoom Roomsの起動、接続以外にも以下のようなことが可能です。
凝ったことはできませんが、基本的な機能は一通り使えるようです。

Zoom Rooms API
Restart POST https://api.zoom.us/v2/rooms/{roomId}/zrclient
"method": "restart"
ズームルームクライアントを再起動する。
List POST https://api.zoom.us/v2/rooms/zrlist
"method": "list"
登録されているズームルームのIDと名前の取得する。
Leave POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "leave"
進行中の会議をそのままにして会議から退出。指定の部屋がホストだった場合は会議を終了する。
Join POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "join"
指定の会議が開かれている場合はそこに参加。 指定の会議が開かれていない場合はホストとなりインスタントミーティングを開始する。
Invite POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "invite"
ズームルームへ連絡先を招待する。
Schedule POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "schedule"
会議をスケジュールする。
Cancel POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "cancel"
スケジュールされた会議をキャンセルする。
End POST https://api.zoom.us/v2/rooms/{roomId}/meetings
"method": "end"
指定した部屋がホストだった場合は会議を終了する。指定した部屋が他の部屋にJOINしていた場合は、会議をLeaveする。

最後に

今回はZoom RoomsのAPIを使ってAlexa用のスマート会議室スキルを開発しました。
現在は一部の会議室で検証中ですが、会議前のモタつきが軽減されるので大変便利です。
色々と必要なものが多いためハードルが高めではありますが、
もし既にZoom Roomを使っていたりスマート会議室に興味があるのであれば、チャレンジしてみてはいかがでしょうか。

参考

Zoom Rooms API - Zoom Developers
Zoom for Alexa - GitHub

Alexa for Business でスキルを有効化する方法。Zoom for Alexaを試してみた。

こんにちはアドバンストテクノロジー部の@y-matsushitaです。
今回は先日発表されたAlexa for Businessのスキルを試しに有効化するまで触ってみたので、ご紹介します。
有効化してみたスキルはオンライン会議サービスであるZoomのAlexa for Business用スキルです。
こちらのスキルを使って声で通話を始めるスマート会議の構築にチャレンジしてみます。
Zoom for Alexa以外のスキルを有効化する場合にも参考になるかと思います。

Zoomについては紹介記事を投稿しているので、ご興味があればご覧ください。 tech.wonderpla.net

Alexa for Businessとは

Alexaを搭載するスマートスピーカーを一元管理したり、自社専用の「プライベートスキル」を開発できます。
Alexa for Business用に公開されているスキルもあり、社内で活用できる様々なスキルが利用できます。

aws.amazon.com

注意点

現時点(2018/1/15)でAlexa for Businessは日本語未対応です。
端末をAlexa for Businessに登録すると端末が英語版になってしまうため、日本語での操作が一切できなくなります。
元に戻すことは可能ですが、端末を再セットアップする必要があります。

必要なもの

・Amazon Echo(Alexa対応端末)
・AWSアカウント
・Windows機 *1
 Microsoft Windows 7 or later, the AWS Tools for Windows PowerShell, and the .NET Framework 3.5 or later.)
・Zoom Roomsアカウント*2

手順

手順としては以下のように進めていきます。

端末登録
Alexa for Businessでの設定
 ・ Room profileの作成
 ・ Skill groupの作成
 ・ Roomの作成
 ・ Skillの追加と「Zoom for Alexa」の有効化
 ・ Zoom for AlexaをSkill groupに登録
 ・ Zoom for AlexaとZoomアカウントの連携
 ・ Provider設定にZoomを追加
設定内容の確認
Zoom for Alexaを使う


端末登録

それでは早速はじめていきます。
まずはAlexa for BusinessにEchoを端末登録します。
AWSのAlexa for Businessを開き、Shared devicesから「Set up devices」をクリックします。
Alexa for Businessを利用するにはリージョンをバージニア北部に設定しなければいけない点に注意してください。 Alexa for Business Shared devices

セットアップ用のexeのダウンロードリンクが出るのでクリックしてダウンロードします。
f:id:y-matsushita:20171222122541j:plain:w500

ダウンロードしたexeを実行します。当然ですが実行はWindows環境で行います。
f:id:y-matsushita:20171222122743j

もし以下のような画面が出てたらインターネット回線が繋がっているか確認しましょう。
An Error occurred attempting to install DeviceSetupTool.

Access key IDとSecret access keyを入力します。
入力するKeyはAWSのIAMロールで発行します。
Alexa for Businessへのアクセス権限とプログラムによるアクセス許可をしておきましょう。 Setup AWS security credentials

認証が成功したらEchoとセットアップ用のPCをWi-Fiで接続します。
Select Alexa-enabled devices

この状態でEchoをセットアップモードにすると、EchoとDevice Setup Toolを接続させることができます。
セットアップモードにするには下図のボタンをオレンジ色が点灯するまで長押しします。
Echoのセットアップ

上手く検出できるとDevice Setup ToolにAmazon-◯◯◯という名前でデバイスが表示されます。 Echoのセットアップ

Echoで使用するWi-Fiの接続情報を入力します。 f:id:y-matsushita:20171222143318p

設定内容に問題がなければStatusがSuccessに変わります。
ここまででEchoとAlexa for Businessの連携は完了です。
f:id:y-matsushita:20171222145212j:plain:w700


Alexa for Businessでの設定

ここからはAWS上での操作になります。
Alexa for Business側を開いてEcho Deviceが新たに追加されていることを確認しましょう。
f:id:y-matsushita:20171222145738p:plain:w700

ここからはDeviceにSkillを追加する手順を記載します。
DeviceはRoomごとに異なるSkillを設定可能です。
必要な作業は以下の通りです。設定内容は後から変更可能です。

・Room profileとSkill groupを作成。
・Skillを有効化してSkill groupに追加。
・Roomを作成してRoom profileとSkill groupとDeviceを設定。
・Providerの追加。(Zoom for Alexa用)

Room profileの作成

まずはRoom profileとSkill groupを作成しましょう。
画面左のConfigurationからRoom profilesを開き、「Create room profile」をクリックします。
f:id:y-matsushita:20171222152213j:plain:w700

「Create room profile」で設定内容を記入します。
f:id:y-matsushita:20171222152346j:plain:w700
・Profile name : Room profile名
・Address : 所在地
・Time zone : タイムゾーン
・Wake word : ウェイクアップワード(Alexa, Amazon, Echo, Computerから選択)
・Temperature units : 摂氏・華氏の選択
・Distance units : フィート・メートルの選択
・Maximum volume : デバイス音量の最大値(6~10)
・Device setup mode : セットアップモードの有効化。Offの場合、セットアップモードにならなくなる
・Outbound calling : 電話の発信の有効化

Skill groupの作成

次はスキルを管理するためのSkill groupを作成します。
あとで「Zoom for Alexa」のスキルを有効化してグループ化させるためのものです。
「Create skill group」をクリックしましょう。
Alexa for Business Skill groupの作成

Skill groupの名前を入力するだけで、ひとまずの作成は完了です。
f:id:y-matsushita:20171222155208j:plain:w500

Roomの作成

画面左上のRoomsから「Create room」をクリックしてRoomを作成します。
Alexa for Business Roomの作成

Room名と先ほど作成したRoom profileを設定します。
Alexa for Business Room名の設定

Roomに先ほど端末登録したEchoを追加します。
端末登録したEchoを設定

Roomに先ほど作成したSkill groupを設定します。 Skill groupを設定

問題なければ作成完了です。
Roomの作成完了


Skillの追加と「Zoom for Alexa」の有効化

画面左のSkillsをクリックします。
Alexa for Business スキルの設定

Alexa skills storeタブを開いて「zoom」で検索しましょう。
Zoom for Alexa がヒットしたら Enable をクリックします。
Zoom for Alexaを有効化

Zoom RoomsアカウントでログインしてZoom for Alexa を有効化します。
Zoomアカウントと連携

問題なく有効化できればEnable skillsにZoom for Alexa が現れます。
Zoom for Alexaが追加される

Zoom for AlexaをSkill groupに登録

Skill groupsから先ほど作成したSkill groupを選択します。
Zoom for AlexaをSkill groupに登録

Add skillsからSkill groupにZoom for Alexaを追加します。
Zoom for Alexaを追加

追加されると以下のようにSkill groupに追加されます。
Zoom for Alexaを追加完了

Zoom for AlexaとZoomアカウントの連携

SkillsのZoom for AlexaをクリックしてZoom for Alexaの詳細設定を確認します。
「Link account to this room」が未設定になっている場合は、設定を行います。
Zoom for Alexaとアカウント連携

Zoom Roomsアカウントでログインして連携します。
Zoom for Alexaとアカウント連携

上手くいけばボタンの表示が「Link master account」になります。
ここでの連携は結構失敗しやすいですが、失敗したときの履歴が残っていると連続で失敗してしまうため、
上手くいかない場合は以下のようなことを試すと上手くいくかもしれません。

・一旦ブラウザでZoomのサイトへ行ってログアウトしてから管理者のZoom roomsアカウントで試す。
・ブラウザのシークレットウィンドウで試す。
・Configurationに適当なテキストを設定する。

Zoom for Alexa設定完了

Provider設定にZoomを追加

画面左のConferencingを開き、「Add provider」をクリックします。
ProviderにZoomを追加

Providerの設定情報を入力します。特に設定を変更しなくても動作しますが、
ミーティングスタート時にMeeting PINの要求が必要であれば、OptionalかRequiredに変更します。
ZoomのProvider設定

追加が完了すると以下のようにProviderとしてZoomが表示されます。
ZoomProvider追加完了


設定内容の確認

大変長くなりましたが、以上で設定は完了です。
確認として以下の点が問題なければ設定は大丈夫です。

・RoomにDeviceが設定されているか。
・RoomにRoom profileとZoom for Alexa スキルの入ったSkill groupが設定されているか。
・Zoom for AlexaスキルとZoom Roomsアカウントが連携できているか。
・ProviderにZoomが追加されているか。

あとは最後に重要な点ですが、Alexa for Businessのスキルを有効化するために、Echoを再起動しましょう。
どうやらスキルの追加をした後に再起動をしないと、有効化されないようなので注意が必要です。
(私はここに気付かず、何時間も設定を見直すハメになりました。)

スキルが動かない場合は適当に別のスキルを試して動くか確認してみると良いでしょう。
Alexa for Businessには「Crazy Jokes」のような単純にジョークを言うだけのスキルもあるので、
それらを有効化して動くかどうかを見るだけでも見直す箇所が絞り込めるかと思います。


Zoom for Alexaを使う

それでは、Zoom for Alexaを使ってみましょう。
Echoに「アレクサ、スタート、ミーティング!」と話しかけると、「ミーティングIDは何ですか?」と英語で返してくれます。
ここで返すミーティングIDはZoom Roomsだけでなく個人アカウントのミーティングIDでもOKです。

今回は試しに個人アカウントのzoom.usに繋げてみます。
zoom.usを開き、以下の画面に書かれている番号をAlexaに向かって英語で読み上げてください。
ZoomのミーティングID

しばらく待っていると、zoom.usのほうがホスト状態になっていれば自然に繋がります。
ホスト状態じゃなかった場合は、Alexaから軽快な音楽が流れ始めて待機状態になるようです。
待機状態のときにzoom.us側でインスタントミーティングを始めるとAlexaとの接続が開始されます。
ただし待機状態になっていたときにzoom.us側には通知などがされなかったので、
事前に繋げることを伝えておくか、常にホスト状態にしておく等の運用をする必要がありそうです。 Zoom for Alexaを繋げた結果

実際に動かしてみた様子です。 Amazon EchoとPCのZoomのクライアントで通話ができました!
Echoに向かって話しかけると、ちゃんとPCから音声が返ってきます。 (映像は繋がってません。)
Zoom for Alexa実行中

まとめ

今回はAlexa for BusinessのスキルとしてZoom for Alexaを試してみました。
ハンズフリーで相手と通話を可能にするのは未来感がありますね。
ただし相手のミーティングIDを読み上げなければいけなかったり、言語が英語だったりとまだまだ使い辛さを感じます。

個人的には「アレクサ、ズームルームでホールに接続して!」と日本語で言うだけで映像と音声を繋げたいですね。
Zoom RoomsにはAPIがあるので、次はそれを使って上記のような会議室同士をつなぐAlexaアプリを開発してみます。
上手くいったら、またこちらのブログで皆様にご紹介いたします!

参考

Alexa for Business Administration Guide
Zoom with Alexa for Business: Meet Without Lifting a Finger

*1:EchoをAlexa for Businessに端末登録するためにexeの実行環境が必要です。

*2:Zoom for Alexaを有効化するために必要。他のスキルであれば不要です。

Kerasで複数のGPUを使い、学習を高速化してみた

こんにちは。アドバンストテクノロジー部のR&Dチーム所属岩原です。
今回はKerasで複数のGPUを使う方法を書きたいと思います。

Keras 2.0.9から簡単に複数GPUを使用した高速化が可能に。

Keras2.0.9からtraining_utilsというモジュールにmulti_gpu_modelという関数が追加されました。
コレを使うと、学習を複数のGPUで行わせることが可能になります。
inputをGPUの数だけ分割することによって、並列化を実現しているようです。
keras/training_utils.py at master · keras-team/keras

では、実際に試してみましょう。

環境

  • AWS EC2(p2.8xlarge) -> GPU8本
  • Deep Learning Base AMI (Ubuntu) Version 2.0 (ami-041db87c) -> CUDAやCuDNN、Python3.5などがセットアップ済み
  • Keras 2.1.2
  • tensorflow-gpu 1.4.1
  • Python 3.5.2

今回使用するコード

Kerasの例にあるcifar10_cnn.pyを複数GPUに対応させてみたいと思います。
keras/cifar10_cnn.py at master · keras-team/keras

まずはGPU1つのみの場合はどれくらいかかったのかを以下に示します。

~~~ 省略 ~~~
Epoch 90/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7667 - acc: 0.7457 - val_loss: 0.6644 - val_acc: 0.7838
Epoch 91/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7694 - acc: 0.7457 - val_loss: 0.6627 - val_acc: 0.7803
Epoch 92/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7692 - acc: 0.7449 - val_loss: 0.7553 - val_acc: 0.7680
Epoch 93/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7721 - acc: 0.7448 - val_loss: 0.7210 - val_acc: 0.7862
Epoch 94/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7751 - acc: 0.7436 - val_loss: 0.6743 - val_acc: 0.7811
Epoch 95/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7781 - acc: 0.7412 - val_loss: 0.7047 - val_acc: 0.7725
Epoch 96/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7781 - acc: 0.7427 - val_loss: 0.6371 - val_acc: 0.7909
Epoch 97/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7720 - acc: 0.7452 - val_loss: 0.6331 - val_acc: 0.7949
Epoch 98/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7917 - acc: 0.7399 - val_loss: 0.7105 - val_acc: 0.7699
Epoch 99/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7829 - acc: 0.7414 - val_loss: 0.6481 - val_acc: 0.7859
Epoch 100/100
1563/1563 [==============================] - 21s 13ms/step - loss: 0.7868 - acc: 0.7404 - val_loss: 0.6266 - val_acc: 0.8005

ということで、学習に21 sec / 1epoch かかっていることになります。
では、コレを単純に8つのGPUで実行した場合にどのような結果になるのか試してみましょう。

単純に8つのGPUで実行した場合

importの追加

まずは必要なモジュールのimportを追加します。
モデルのビルドはCPUで行う必要があるため、Tensorflowをimportします。

import tensorflow as tf # add
from keras.utils.training_utils import multi_gpu_model # add

GPU数の定数を追加とbatch_sizeの変更

GPUの数を定数として定義しておきます。今回は8つ使用するので、8を指定します。
また、batch_sizeは並列で処理を行うために元々のbatch_sizeをGPUの数だけ掛けます。

gpu_count = 8 # add

batch_size = 32 * gpu_count # modify

モデルの構築はCPUで行う

モデルの構築はOOMエラー対策のため、CPUで明示的に行う必要があるので、
tf.deviceを使用します。

with tf.device("/cpu:0"): # add
    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same',
                     input_shape=x_train.shape[1:]))
# 以下略

複数GPU対応する

modelをコンパイルする直前に、multi_gpu_model関数を呼び出すようにします。
引数gpusにはGPUの数を指定します。1を指定すると実行時エラーになるので注意して下さい。

model = multi_gpu_model(model, gpus=gpu_count) # add

コード全体

追記変更を含めたコード全体は以下のとおりです。

'''Train a simple deep CNN on the CIFAR10 small images dataset.

It gets to 75% validation accuracy in 25 epochs, and 79% after 50 epochs.
(it's still underfitting at that point, though).
'''

from __future__ import print_function
import keras
from keras.datasets import cifar10
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D
import os
import tensorflow as tf # add
from keras.utils.training_utils import multi_gpu_model # add

gpu_count = 8 # add

batch_size = 32 * gpu_count # modify
num_classes = 10
epochs = 100
data_augmentation = True
num_predictions = 20
save_dir = os.path.join(os.getcwd(), 'saved_models')
model_name = 'keras_cifar10_trained_model.h5'


# The data, shuffled and split between train and test sets:
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
print('x_train shape:', x_train.shape)
print(x_train.shape[0], 'train samples')
print(x_test.shape[0], 'test samples')

# Convert class vectors to binary class matrices.
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
with tf.device("/cpu:0"): # add
    model = Sequential()
    model.add(Conv2D(32, (3, 3), padding='same',
                     input_shape=x_train.shape[1:]))
    model.add(Activation('relu'))
    model.add(Conv2D(32, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Conv2D(64, (3, 3), padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(64, (3, 3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Dropout(0.25))

    model.add(Flatten())
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(num_classes))
    model.add(Activation('softmax'))
model = multi_gpu_model(model, gpus=gpu_count) # add
# initiate RMSprop optimizer
opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)

# Let's train the model using RMSprop
model.compile(loss='categorical_crossentropy',
              optimizer=opt,
              metrics=['accuracy'])

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255

if not data_augmentation:
    print('Not using data augmentation.')
    model.fit(x_train, y_train,
              batch_size=batch_size,
              epochs=epochs,
              validation_data=(x_test, y_test),
              shuffle=True)
else:
    print('Using real-time data augmentation.')
    # This will do preprocessing and realtime data augmentation:
    datagen = ImageDataGenerator(
        featurewise_center=False,  # set input mean to 0 over the dataset
        samplewise_center=False,  # set each sample mean to 0
        featurewise_std_normalization=False,  # divide inputs by std of the dataset
        samplewise_std_normalization=False,  # divide each input by its std
        zca_whitening=False,  # apply ZCA whitening
        rotation_range=0,  # randomly rotate images in the range (degrees, 0 to 180)
        width_shift_range=0.1,  # randomly shift images horizontally (fraction of total width)
        height_shift_range=0.1,  # randomly shift images vertically (fraction of total height)
        horizontal_flip=True,  # randomly flip images
        vertical_flip=False)  # randomly flip images

    # Compute quantities required for feature-wise normalization
    # (std, mean, and principal components if ZCA whitening is applied).
    datagen.fit(x_train)

    # Fit the model on the batches generated by datagen.flow().
    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=4)

# Save model and weights
if not os.path.isdir(save_dir):
    os.makedirs(save_dir)
model_path = os.path.join(save_dir, model_name)
model.save(model_path)
print('Saved trained model at %s ' % model_path)

# Score trained model.
scores = model.evaluate(x_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

結果

~~~ 省略 ~~~

Epoch 90/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7307 - acc: 0.7453 - val_loss: 0.6331 - val_acc: 0.7831
Epoch 91/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7301 - acc: 0.7457 - val_loss: 0.6280 - val_acc: 0.7817
Epoch 92/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7324 - acc: 0.7445 - val_loss: 0.6149 - val_acc: 0.7870
Epoch 93/100
196/196 [==============================] - 18s 91ms/step - loss: 0.7239 - acc: 0.7494 - val_loss: 0.6257 - val_acc: 0.7822
Epoch 94/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7247 - acc: 0.7472 - val_loss: 0.6101 - val_acc: 0.7879
Epoch 95/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7207 - acc: 0.7482 - val_loss: 0.6233 - val_acc: 0.7860
Epoch 96/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7191 - acc: 0.7489 - val_loss: 0.6349 - val_acc: 0.7798
Epoch 97/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7111 - acc: 0.7514 - val_loss: 0.6057 - val_acc: 0.7912
Epoch 98/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7118 - acc: 0.7524 - val_loss: 0.6084 - val_acc: 0.7894
Epoch 99/100
196/196 [==============================] - 18s 90ms/step - loss: 0.7126 - acc: 0.7523 - val_loss: 0.6026 - val_acc: 0.7887
Epoch 100/100
196/196 [==============================] - 18s 89ms/step - loss: 0.7035 - acc: 0.7567 - val_loss: 0.6052 - val_acc: 0.7932

21秒よりは少し早くなりましたが、思ってたよりは早くなっていません。

仮説

実際に学習をしている様子を眺めているとわかるのですが、進捗が頻繁に止まります。
これはImageDataGeneratorが画像を作る処理がボトルネックになっていそうです。
画像を生成する処理が追いついていないのではないか、という仮説に基づいて、対応してみましょう。
例のコードではfit_generatorを使うようになっているので、それをfit関数を使ってImageDataGeneratorが関与しないように変更してみましょう。

fit_generatorではなくfitを使うようにした場合

data_augmentation = True

data_augmentation = False

に変えるだけでOKです。

なお、サンプル数は変わらず50000になります。

Epoch 90/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.5075 - acc: 0.8223 - val_loss: 0.6516 - val_acc: 0.7786
Epoch 91/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4984 - acc: 0.8247 - val_loss: 0.6293 - val_acc: 0.7859
Epoch 92/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4987 - acc: 0.8237 - val_loss: 0.6290 - val_acc: 0.7860
Epoch 93/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4925 - acc: 0.8277 - val_loss: 0.6383 - val_acc: 0.7849
Epoch 94/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4922 - acc: 0.8274 - val_loss: 0.6283 - val_acc: 0.7839
Epoch 95/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4892 - acc: 0.8292 - val_loss: 0.6435 - val_acc: 0.7832
Epoch 96/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4850 - acc: 0.8298 - val_loss: 0.6449 - val_acc: 0.7820
Epoch 97/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4823 - acc: 0.8325 - val_loss: 0.6250 - val_acc: 0.7878
Epoch 98/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4741 - acc: 0.8337 - val_loss: 0.6227 - val_acc: 0.7902
Epoch 99/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4692 - acc: 0.8345 - val_loss: 0.6559 - val_acc: 0.7794
Epoch 100/100
50000/50000 [==============================] - 6s 113us/step - loss: 0.4684 - acc: 0.8354 - val_loss: 0.6374 - val_acc: 0.7840

今度は6秒になりました。やはりImageDataGeneratorがネックとなっていそうです。
メモリが潤沢にある場合やサンプル数が沢山用意できる環境であればよいのですが、
大抵はそのような恵まれた環境ではないかと思います。
ImageDataGenerator自体を並列化することで対応してみたいと思います。

ImageDataGeneratorのプロセス並列化

実は、ImageDataGeneratorのflowメソッドがSequenceを継承したIteratorオブジェクトを返すので、プロセス並列化出来たりします。

ImageDataGeneratorのプロセス並列化をした場合

    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=4)

    model.fit_generator(datagen.flow(x_train, y_train,
                                     batch_size=batch_size),
                        epochs=epochs,
                        validation_data=(x_test, y_test),
                        workers=32,
                        max_queue_size=64,
                        use_multiprocessing=True)

に変え、

data_augmentation = False

data_augmentation = True

に戻します。

結果

Epoch 90/100
196/196 [==============================] - 7s 37ms/step - loss: 0.7232 - acc: 0.7481 - val_loss: 0.6056 - val_acc: 0.7893
Epoch 91/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7233 - acc: 0.7486 - val_loss: 0.5949 - val_acc: 0.7953
Epoch 92/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7175 - acc: 0.7488 - val_loss: 0.5963 - val_acc: 0.7908
Epoch 93/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7159 - acc: 0.7502 - val_loss: 0.6006 - val_acc: 0.7905
Epoch 94/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7119 - acc: 0.7528 - val_loss: 0.6063 - val_acc: 0.7882
Epoch 95/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7115 - acc: 0.7502 - val_loss: 0.5908 - val_acc: 0.7923
Epoch 96/100
196/196 [==============================] - 7s 36ms/step - loss: 0.7057 - acc: 0.7556 - val_loss: 0.5935 - val_acc: 0.7925
Epoch 97/100
196/196 [==============================] - 7s 36ms/step - loss: 0.6995 - acc: 0.7586 - val_loss: 0.5920 - val_acc: 0.7910
Epoch 98/100
196/196 [==============================] - 7s 36ms/step - loss: 0.6939 - acc: 0.7586 - val_loss: 0.5929 - val_acc: 0.7967
Epoch 99/100
196/196 [==============================] - 7s 37ms/step - loss: 0.6946 - acc: 0.7573 - val_loss: 0.5752 - val_acc: 0.8007
Epoch 100/100
196/196 [==============================] - 7s 37ms/step - loss: 0.6966 - acc: 0.7566 - val_loss: 0.5837 - val_acc: 0.7966

7秒まで早くなりました。並列化することでボトルネックをある程度解消できた事になります。

まとめ

複数GPUによる高速化は可能です。
ただし、fit_generatorを使う場合、generator部がボトルネックにならないように気をつける必要があります。

注意

今回提示したソースコードだと、modelのsave時にエラーになります。
原因はmulti_gpu_modelのDocに書いてあります。

# On model saving
    To save the multi-gpu model, use `.save(fname)` or `.save_weights(fname)`
    with the template model (the argument you passed to `multi_gpu_model`),
    rather than the model returned by `multi_gpu_model`.

したがって、モデルをsaveしたい時は、multi_gpu_model返すmodelではなく、
multi_gpu_model渡したmodelをsaveするようにしてください。

今回は本質と関係ない部分なので、対応は省略しました。

ファインチューニングをやってみた

こんにちは。アドバンストテクノロジー部のR&Dチーム所属岩原です。
今回はファインチューニングについて色々と調査しました。

ファインチューニング(fine tuning)とは

既存のモデルの一部を再利用して、新しいモデルを構築する手法です。
優秀な汎用モデル(VGG16など)を使い、自分たち用のモデルを構築したり出来ます。
少ないデータ(といっても数十〜数百ぐらいは必要ですが)で、結構精度の良いモデルが構築できたりします。

全く違う方向性(写真画像系のモデルを元に、イラストの判定モデルを作るなど)だと余り効果が出てこないようですが、
元のモデルより更に詳細な特徴を抽出したい、などの用途だと効果が高いようです。

転移学習(transfer learning)という呼び方もされるみたいですが、使い分けとかどんな感じなんでしょうね?

実際にやってみた

環境

  • Ubuntu16.04(GTX1080Ti)
  • Keras 2.0.8
  • Tensorflow 1.3.0
  • nvidia-docker 1.0.1

AWSのGPU Computeインスタンス(p2やp3)でも使いまわせるようにDockerizeしています。

使用するデータセット

Food-101 -- Mining Discriminative Components with Random Forests
101ラベルの料理画像を計101000枚(1ラベル1000枚)用意しているデータセットです。
枚数としては心もとない気がしますが、ファインチューニングを試してみるにはちょうどよい枚数かと思います。

3つのデータパターン

データの数によってどこまで変わるか、という検証のため、パターンを3つ用意しました。

  • データ数を訓練データ100枚、検証データ25で行うパターン(パターン1)
    データ数を少なくし、ファインチューニングの効果を検証するパターンです。

  • パターン1のデータをImageDataGeneratorで水増ししたパターン(パターン2)
    限られたデータ数を水増しし、どこまで効果が出るのかを検証するパターンです。
    fit_generatorの引数steps_per_epochを2000(batch_sizeは32なので、2000 * 32で64000枚)、引数validation_stepsを500(同じく500 * 32で16000枚)に設定しました。

  • データセットの全てのデータ(訓練データ75750枚、検証データ25250枚)で学習を行うパターン(パターン3)
    データセット全てを学習&検証に回し、水増しとの違いを検証するパターンです。
    バッチサイズは32を設定しました。

3つのモデル

ファインチューニングの方法によってどこまで差が出るのかの検証のため、さらにモデルを3つ用意しました。

  • ピュアなVGG16(モデル1)
    重みを初期化したVGG16構造のモデルです。
    ファインチューニングしない場合の検証を行うパターンになります。

f:id:m_iwahara:20171212170350p:plain

Kerasを使用したコードはこんな感じになります。

def create_none_weight_vgg16_model(size):
    model_path = "./models/vgg16_none_weight.h5py"
    if not os.path.exists(model_path):
        input_tensor = Input(shape=(224,224,3))
        model = VGG16(weights=None, include_top=True, input_tensor=input_tensor, classes=size)
        model.save(model_path) # 毎回ダウンロードすると重いので、ダウンロードしたら保存する
    else:
        model = load_model(model_path) 
    model.compile(loss='categorical_crossentropy',
                  optimizer='adam',
                  metrics=['accuracy'])
    return model
  • VGG16の全結合層のみを取り替え(モデル2)
    全結合層を取っ払い、新たに全結合層をくっつけたモデルです。
    全結合層以外の層の重みは学習済みのパラメータをそのまま使用し、学習しないようにします。
    全結合層のみファインチューニングした場合の検証を行うパターンになります。
    ついでに、全結合層にDropoutを付けてみたりしています。
    なお、最適化関数は学習率を極端に抑えたSGDを使用しています。

f:id:m_iwahara:20171212170417p:plain

Kerasを使用したコードはこんな感じになります。

def get_vgg16_model():
    model_path = "./models/vgg16.h5py"
    if not os.path.exists(model_path):
        input_tensor = Input(shape=(224,224,3))
        # 出力層側の全結合層3つをモデルから省く
        model = VGG16(weights='imagenet', include_top=False, input_tensor=input_tensor)
        model.save(model_path) # 毎回ダウンロードすると重いので、ダウンロードしたら保存する
    else:
        model = load_model(model_path)
    return model

def create_fullconnected_fine_tuning(classes):
    # vgg16モデルを作る
    vgg16_model = get_vgg16_model()

    input_tensor = Input(shape=(224,224,3))

    for layer in vgg16_model.layers:
        layer.trainable = False

    x = vgg16_model.output
    x = Flatten()(x)
    x = Dense(2048, activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(1024, activation='relu')(x)
    predictions = Dense(classes, activation='softmax')(x)
    model = Model(inputs=vgg16_model.input, outputs=predictions)


    model.compile(loss='categorical_crossentropy',
                  optimizer=SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])
    return model
  • VGG16の最後の畳み込み層と全結合層を取り替え(モデル3)
    最後の畳み込み層の重みを初期化して学習するようにし、全結合層を取り替えたモデルです。
    上記以外の層は再学習をしないようにします。
    最後の畳み込み層と全結合層をファインチューニングした場合の検証を行うパターンになります。

f:id:m_iwahara:20171212170432p:plain

Kerasを使用したコードはこんな感じになります。

def create_last_conv2d_fine_tuning(classes):
    # vgg16モデルを作る
    vgg16_model = get_vgg16_model()

    input_tensor = Input(shape=(224,224,3))

    x = vgg16_model.output
    x = Flatten()(x)
    x = Dense(2048, activation='relu')(x)
    x = Dropout(0.5)(x)
    x = Dense(1024, activation='relu')(x)
    predictions = Dense(classes, activation='softmax')(x)
    model = Model(inputs=vgg16_model.input, outputs=predictions)
    # 最後の畳み込み層より前の層の再学習を防止
    for layer in model.layers[:15]: 
        layer.trainable = False

    model.compile(loss='categorical_crossentropy',
                  optimizer=SGD(lr=1e-4, momentum=0.9),
                  metrics=['accuracy'])
    return model

その他

過学習に陥る前に学習を止めるEarlyStoppingを導入済みです。

結果

上記データパターンとモデルパターンの組み合わせを検証。

パターン1

モデル1

精度

f:id:m_iwahara:20171213091424p:plain

損失

f:id:m_iwahara:20171213091443p:plain

モデル2

精度

f:id:m_iwahara:20171213091528p:plain

損失

f:id:m_iwahara:20171213091541p:plain

モデル3

精度

f:id:m_iwahara:20171213091609p:plain

損失

f:id:m_iwahara:20171213091622p:plain

パターン2

モデル1

精度

f:id:m_iwahara:20171213091648p:plain

損失

f:id:m_iwahara:20171213091700p:plain

モデル2

精度

f:id:m_iwahara:20171213091714p:plain

損失

f:id:m_iwahara:20171213091725p:plain

モデル3

精度

f:id:m_iwahara:20171213091740p:plain

損失

f:id:m_iwahara:20171213091749p:plain

パターン3

モデル1

精度

f:id:m_iwahara:20171213091803p:plain

損失

f:id:m_iwahara:20171213091813p:plain

モデル2

精度

f:id:m_iwahara:20171213091825p:plain

損失

f:id:m_iwahara:20171213091837p:plain

モデル3

精度

f:id:m_iwahara:20171213091902p:plain

損失

f:id:m_iwahara:20171213091914p:plain

結果まとめ

「データ数は多いほうが良い。ファインチューニングはゼロから学習させるよりもかなり有効で、収束も早い。最後の畳み込み層からファインチューニングした方が精度が良い。」という感じですね。
枚数が足りないのか、過学習の傾向はどれも見られますが…。
なお、1 epoch辺りの学習時間は モデル1 > モデル3 > モデル2 といった結果になりました。
ファインチューニングを行うと学習時間も節約できるのでおすすめです。

参考

VGG16のFine-tuningによる犬猫認識 (2) - 人工知能に関する断創録