フレクトのクラウドblog re:newal

http://blog.flect.co.jp/cloud/からさらに引っ越しています

SalesforceのプラットフォームイベントをPostmanから発行する

こんにちは、技術開発室の藤野です。

プラットフォームイベント(Platform Events)はSalesforceでイベント駆動型のアーキテクチャを実現する仕組みです。 特に、Salesforce外部からのレコード操作を疎結合にする手段として有用なのではと個人的に注目しています。

詳しくは公式ガイドを参照してください。

本記事では例として下記のシチュエーションを実現する一連の手順を紹介します。

  • Salesforceは「デバイス」カスタムオブジェクトを持つ
  • 「デバイス」カスタムオブジェクトは「シリアルナンバー」「状態」カスタム項目を持つ
  • バイスの識別は「シリアルナンバー」カスタム項目で行う
  • 「状態」カスタム項目をイベントプラットフォーム経由で更新する
  • REST APIで上記イベントを公開(publish)する

目次:

  • カスタムオブジェクトの作成
  • プラットフォームイベントの作成
  • プロセスの作成
  • 接続アプリケーションの作成
  • イベントの発行によるレコードの更新
    • 更新対象レコードの準備
    • PostmanでのREST APIの送信
  • まとめ
  • 補足

カスタムオブジェクトの作成

まず、イベントで更新するカスタムオブジェクトを作成します。 シリアルナンバーと状態を持つだけの単純なものです。

Salesforce上で[設定]-タブの[オブジェクトマネージャ]-[作成]-[カスタムオブジェクト]と進み、新規カスタムオブジェクトを作成します。

続きを読む

Amazon Alexaスキル内課金を公開したらEcho Show 5が届きました!

こんにちは、技術開発室のオクダです。本日のテーマは「Amazon Alexaスキル内課金を公開したらEcho Show 5が届きました!」についてお話しします。

f:id:flect170:20200131145950j:plain
Echo Show 5が10個、会社に届きました!

ことの発端

Amazonでは、2ヶ月〜3ヶ月毎に、スキル開発キャンペーンが実施されています。昨年11月にスキル開発キャンペーンを確認すると

「10月1日~12月10日までにスキル内課金機能を公開スキル(新規スキル・既存スキルどちらでも可)に実装し、かつキャンペーンにお申し込みをいただいた方には、Echo Show 5をもれなくプレゼントします。複数スキルが該当した場合は、その数分のEcho Show 5をプレゼント(最大1開発者につき10台まで)!」

developer.amazon.com

というものでした。「お〜、なんと太っ腹。」と感じた私は、これはスキル内課金を勉強して、Echo Show 5も貰えるのなら「一石二鳥」と思い、既存の公開済みスキルにスキル内課金機能を組み込み公開申請することにしました。

スキル内課金の概要と設計

スキル内課金には「買い切り型」「サブスクリプション型」「消費型」の3種類が存在します。

  • 買い切り型: 買い切り型は、制限されているスキル内の機能やコンテンツにアクセスできるようにします。買い切り型には有効期限がありません。

  • サブスクリプション型: 一定期間プレミアムコンテンツまたは機能にアクセスできるようにします。サブスクリプションをキャンセルするまで、繰り返し課金できます。

  • 消費型: 何度も課金、消費できるコンテンツや機能です。たとえば、ゲームのヒント、ゲーム内通貨、または追加ライフなどです。

ということです。作成前の設計で、どのスキル内課金を採用するか考えた際、「買い切り型」と「サブスクリプション型」はキャンセル処理を作成しなければならない。購入しているユーザーがいる限り、こちらの都合でスキルのサービスを打ち切ることができないし、スキルを公開し続けている限りは、スキルのメンテナンスも行っていかないといけない。それって面倒だな。と考えた私は「消費型」のスキル内課金を作成することにしました。

支払設定と納税情報

スキル内課金を作成して公開した場合、そのスキルをユーザーが使用してスキル内課金の支払いに応じると、作成者には当然収益が発生いたします。

スキル内課金を作成する場合は、スキル内課金を作成する前に、まず「支払い設定」と「納税情報」の提出が必要となってきます。

「支払い設定」と「納税情報」は、Alexa Developer Consoleの「支払い」から設定することができます。

f:id:flect170:20200130113341p:plain

支払い設定に進むと、2段階認証が求められます。

f:id:flect170:20200130114903p:plain

指示通りに、電話番号と入力し「ワンタイムパスワードを送信」を押すと、「支払い情報」入力画面が表示されますので、お手持ちの銀行口座情報を入力します。

f:id:flect170:20200130130906p:plain

入力が終わったら保存を押して、次に「税に関する情報」タブを押します。

f:id:flect170:20200130132345p:plain
スクリーンショットは ステータス:完了 となっていますが、最初訪れたときは、 ステータス:未登録 です。

「ロイヤリティ収入の納税者確認インタビュー」と「サービス収入の納税者確認インタビュー」の2つがありますが、スキル内課金の納税情報は「ロイヤリティ収入の納税者確認インタビュー」の承認だけで大丈夫です。

「ロイヤリティ収入の納税者確認インタビュー」入力画面です。

f:id:flect170:20200130140017p:plain

名前住所等を記入します。「私は米国以外のTINを持っています」は「税制上の居住国から発行を受けたされた納税者IDを指定」と書いてあるので、恐らく日本であれば「マイナンバー」がそれに該当すると思います。

マイナンバー」を所有していない場合は、「TINが指定されていない場合、米国源泉所得に対する法定源泉税率(30%)の減免措置は適用されません。」と警告されます。マイナンバーを登録することをお勧めします。

そして、必要事項を全て記入したら、オンライン上で署名して送信します。記入事項が全て正しければ、後日「ロイヤリティ収入の納税者確認インタビュー」の ステータス:完了 に変わるはずです。

(注記: 筆者は米国Social Security Numberを持っているので、即座に ステータス:完了 に変わりました。日本国のマイナンバーを登録した場合については、今回は確認しておりません。)

スキルの作成

「支払設定」と「納税情報」の登録が終わったら、スキルの作成に取り掛かります。

スキルの作成に関しては、

Alexaスキルのデザイン・開発・公開 - フレクトのクラウドblog re:newal

に詳しく、記載しておりますので、そちらのご確認よろしくお願いします。

スキル内課金の実装

今回は、スキル内課金を既存のスキルに組み込むので、スキル内にスキル内商品を登録しなければなりません。なぜ、商品と呼ばれているのか?1つのスキルに複数のスキル内課金を登録でき、ユーザーはお金を支払って購入するからです。

スキルの作成画面から「スキル内商品」を選択します。

f:id:flect170:20200130143402p:plain

画面右上の「スキル内商品の作成」を押します。

f:id:flect170:20200130143931p:plain

「新しいスキル内商品を作成」画面で「スキル内商品名」を入力し「スキル内商品の種類を選択」で今回は「消費型」を選択します。入力が終わったら再度、画面右上の「スキル内商品の作成」を押します。

f:id:flect170:20200130144155p:plain

スキル内商品画面です。商品IDは自動的に割り振られています。商品IDはコードでの開発の際に必要になってきます。

サポートされている言語を「日本語」を選択し「編集」を押します。

f:id:flect170:20200130172104p:plain

表示名は既に入力済みなので、それ以外を全て入力。アイコン画像をドラッグ&ドロップで登録後、右上の「保存」ボタンを押します。

f:id:flect170:20200130155455p:plain

「価格設定と購入可能状況」は、日本語スキルなので、 Amazon.co.jp を選択し、価格を日本円で設定します。今回は「99 ¥ JPY」と最安値を設定しています。99円より安い価格設定はできません。

「テスト」購入する際のテスト手順をここに記載します。全て入力が完了したら、右上の「保存」ボタンを押します。

f:id:flect170:20200130172346p:plain

スキル内商品画面に戻ると、先ほど作成した商品が「リンク可能な商品」に登録されており、商品右端のアクションの「有効化」をクリックします。

すると画面のようになり「リンクした商品」に作成した商品が一覧として表示されます。有効化しておかないとコードから呼び出すことができません。

f:id:flect170:20200130161417p:plain

開発者コンソールでスキルにスキル商品を組み込む

スキル商品を購入してもらうため、購入フローに移動するためのインテントを追加する必要があります。

今回追加したカスタムインテントは、購入用のインテントと返金用のインテントです。

購入しますか?の質問に対して「はい」「いいえ」とユーザーが言うと予想されるので、ビルドインインテントである「AMAZON.YesIntent」と「AMAZON.NoIntent」を追加します。

f:id:flect170:20200130164141p:plain

AMAZON.YesIntent」「AMAZON.NoIntent」には「はい」「いいえ」以外に「そうです」「いいよ」「そうじゃない」のような一般的な肯定語、否定語が含まれており、別途カスタムインテントを作成する必要がないので便利です。

標準ビルトインインテント | Alexa Skills Kit

AWS Lambdaコードにスキル商品を組み込む

スキル作成時に「Alexa-hostedスキル」として作成すると、開発者コンソールの「コードエディタ」で、AWS Lambdaコードを編集、デプロイできます。

しかし、Persistent Attributes(永続属性)を保存するのにAmazon DynamoDBを使用するなど、手の込んだことをする場合は「ユーザー定義のプロビジョニング」でスキルを作成することをお勧めします。

詳しいAWS Lambdaコードの実装に関しては、

Alexaスキルのデザイン・開発・公開 - フレクトのクラウドblog re:newal

に詳しく、記載しておりますので、そちらのご確認よろしくお願いします。

AWS Lambdaコードに購入リクエストハンドラーを追加します。下記コードでは「購入しますか?」の質問に対し、ユーザーが「はい」と応答した場合のコードです。

f:id:flect170:20200203084532p:plain
購入リクエストハンドラー

SendRequestDirectiveのnameに "Buy" を指定します。

payloadのInSkillProductにスキル商品であるproductIDを渡します。これにより、購入フローが呼び出され、購入処理に入ります。

tokenは必須ですが、値は任意で何でも構いませんが、推測されにくいものにしましょう。

スキル開発者は購入リクエストを呼び出すだけで、特に購入処理を作成する必要がありません。

次は、AWS Lambdaコードに購入レスポンスハンドラーを追加します。下記コードでは、購入レスポンスハンドラーから購入処理結果を取得しています。

f:id:flect170:20200201100123p:plain
購入レスポンスハンドラー

まず、購入処理が正常に行われたか確認します。確認方法は、request.status.code == "200" であることを確認します。"200" でない場合は「購入に失敗しました」というメッセージを表示するようにしています。

次に、request.payload.get("purchaseResult") が ACCEPTED.value であるか ALREADY_PURCHASED.value であるかを確認しています。購入処理が受理された、またはもう既に購入済みである場合、サービスを提供しています。それ以外の場合は、購入処理が却下されたということで、スキルの終了処理を行っています。

次は、AWS Lambdaコードに返金リクエストハンドラーを追加します。下記コードではユーザーが「返金して」の発話に対し、スキルが返金リクエストを行います。

f:id:flect170:20200203084540p:plain
返金リクエストハンドラー

SendRequestDirectiveのnameに "Cancel" を指定します。

payloadのInSkillProductにスキル商品であるproductIDを渡します。これにより、返金フローが呼び出され、返金処理に入ります。

tokenは必須ですが、値は任意で何でも構いませんが、推測されにくいものにしましょう。

スキル開発者は返金リクエストを呼び出すだけで、特に返金処理を作成する必要がありません。

最後に、AWS Lambdaコードに返金レスポンスハンドラーを追加します。下記コードでは、返金レスポンスハンドラーから返金処理結果を取得しています。

f:id:flect170:20200203084547p:plain
返金レスポンスハンドラー

購入処理と同様、返金処理が正常に行われたか確認します。確認方法は、request.status.code == "200" であることを確認します。"200" でない場合は「キャンセルリクエスト処理中にエラーが発生しました。」というメッセージを表示するようにしています。

返金処理が正常に処理された場合、返金レスポンスのpayloadから返金処理が正しく行われたか確認し、その後の処理を割り振っています。

購入リクエスト、返金リクエストを追加した際の注意点

スキル開発で、1つ前の会話を記録させるためにSession Attributes(セッション属性)を使用して開発している場合、購入処理や返金処理に移動した際、一旦それまでの会話のセッションは切断されてしまいます。購入処理や返金処理が終了し、レスポンスが返ってきた時には、新たなセッションが作成されています。購入処理や返金処理の前の会話を保持しておきたい場合は、Persistent Attributes(永続属性)に変更してAmazon DynamoDBに会話内容を保存するようにしましょう。

いよいよ、公開申請

スキルを作成し、AWS Lambdaの実装も終わりましたら、いよいよ公開申請です。

スキルの公開申請に関して、詳しくは

Alexaスキルのデザイン・開発・公開 - フレクトのクラウドblog re:newal

をご覧ください。スキル内課金を含むスキルを公開申請するときは、いくつか異なる点があります。

まず、Alexa Developer Console画面の、公開 > スキルのプレビュー > プライバシーとコンプライアンス で「このスキルを使って何かを購入をしたり、実際にお金を支払うことができますか? *」の質問に対し、

「はい」を選択しなければなりません。

f:id:flect170:20200131090857p:plain

あとは、無料スキル公開申請と同じですが、最初に説明した「支払設定」と「納税情報」、先ほど説明した「プライバシーとコンプライアンス」に不備があると、認証 > 検証 でエラーメッセージが表示されますので気を付けてください。

認証 > 検証 も無事に成功しましたら、いよいよ、申請です。申請画面で新たに追加された選択項目についてご説明いたします。

f:id:flect170:20200131091303p:plain

以前は「公開の詳細設定」と言う項目がありませんでした。今までは認定されると即公開となっていましたが「公開の詳細設定」で「認定と公開」か「認定のみ」を選べるようになりました。

新たに設けられた「認定のみ」の使いどころは、例えば、○月×日の夜中0時からスキルを公開して、先着10名様にスキル内で使用できるスペシャルアイテムをプレゼントみたいな使い方が可能です。

スキルの認定作業は昼間しか行われておらず、今までは、いつ公開されるかは認定されるまではわかりませんでした。「認定のみ」が選択できるようになり、公開開始日時をこちらでコントロールできるのは、非常に有り難い機能です。

「公開の詳細設定」を選択したら右上の「はい、審査を申請します」ボタンを押します。審査は大抵は翌営業日に行われ、問題なければ審査日と同じ日に認定を受けることができます。

f:id:flect170:20200206165952p:plain
公開承認を受けると開発者宛に上記のようなemailが届きます。

Alexaスキルの認定に関するフィードバック

ここでは「認定に関するフィードバック」について、スキル内課金特有のフィードバックを、具体例を交えながら説明していきます。

多数のフィードバックがあり長くなりますが、参考になると思いますので書かせていただきます。

その1

フィードバック:スキル内課金機能を提供しているスキルは、スキルコンテンツの一部を無料で提供する、もしくはスキルの無料トライアルのバージョンを提供する必要があります。スキル内課金についての詳細はこちらを参照してください。

商品、コンテンツ、サービスを購入できるスキルの要件 | Alexa Skills Kit

原因:作成したスキルが、スキル呼び出し後、スキル商品を購入しないとサービスを利用できないスキル構成になっていました。

対処方法:スキルを1日3回までは無料で利用できるように変更し、4回目からスキル商品の購入を促すように変更しました。

その2

フィードバック:スキル内課金商品のアップセル中に金額情報の提示はできません。スキルの商品購入プロンプトと同様の内容を繰り返さないよう、適宜削除、及び変更してください。

スキルガイドラインでは直接説明されていませんが、以下の文脈から必要ということなのでしょう。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:金額は購入フロー後、Alexa側で提示するので、スキル側では金額を表示しないでください。ということです。

対処方法:購入リクエストから購入フローを呼び出す際に表示していたメッセージから金額を全て削除しました。

その3

フィードバック:スキル内課金商品のアップセル中には、スキルは消費型の課金商品について、明確な数量や時間等の情報を提供する必要があります。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:消費型の課金商品を作成したため、まとめ買いができたりするので、数量等の情報を提供してください。と指摘を受けました。消費型故の指摘内容です。

対処方法:今更、買い切り型やサブスクリプション型に変更するのは面倒だったので、1日4回目からでないと購入リクエストできない。購入後直ちにサービスが実施されスキル商品の在庫がいつも0である状態を説明し、数量情報を提示する義務は無いと説明し、再申請を行いました。

その4

フィードバック:申請いただいたスキルはユーザーに対し、直接スキル内課金商品を購入する方法を提供していません。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:スキル内課金商品の購入方法を提示していませんでした。

対処方法:スキル内課金商品購入ヘルプインテントを作成し、ユーザーが購入ヘルプインテントを呼び出した際に「1日4回以上ご利用になった場合のみ、購入することができます。」というメッセージを追加しました。

その5

フィードバック:申請いただいたスキルはユーザーに対し、購入したスキル内課金商品の返金やキャンセルについての情報を提供していません。

スキルコードにISPのサポートを追加する | Alexa Skills Kit

原因:スキル内課金は消費型であり返金処理は必要ないと考えていたが、返金情報を提供してください。と指摘されました。

対処方法:このスキル内課金商品は常に在庫を抱えない状態になっているので、返金情報を提供する義務はありません。と再申請を行ったが「例え在庫が常に存在しない状態のスキルであっても、返金処理を提供する義務はあり、その情報を提供していないスキルを公開認定することはできません。」と言われました。渋々、返金リクエスインテントを作成し、返金情報を提供するメッセージを追加しました。

反省点:常に在庫が発生しない状況であれば、返金処理を作る必要はないと考えていましたが、甘かったです。初動態勢、スキル内課金の設計をミスしてしまいました。

その6

フィードバック:言い回しの異なるサンプル発話が不足しています。スキルを確実に起動させ、多くのユーザーのリクエストに対応するためには、サンプル発話を充実させる必要があります。今回ご提示いただいたサンプル発話のみでは、スキルが確実に起動しない可能性があります。以下のようなサンプル発話を追加して、再度ご申請ください。

サンプル発話とカスタムスロットタイプの値を作成する際のベストプラクティス | Alexa Skills Kit

原因:スキル内課金「しりとり待った」を購入しますか?の質問に対し、AMAZON.YesIntentとAMAZON.NoIntentしか用意していなかったので、ユーザーが「購入します」「購入する」「購入しません」「購入しない」と言った場合、購入リクエストやキャンセルリクエストが呼ばれなかった。

対処方法:AMAZON.YesIntentに「購入します」「購入する」とAMAZON.NoIntentに「購入しません」「購入しない」を追加し、ユーザーが「購入します」や「購入しません」と言った場合でも、購入リクエストやキャンセルリクエストを呼び出されるように変更しました。

補足:ビルドインインテントにカスタムサンプルフレーズを追加できるので便利です。

その7

フィードバック:申請いただいたスキルは、一度スキルを終了し、再度起動した際に、すでに購入されているスキル内課金商品の使用をユーザーに提供していません。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:サービスを提供した後、スキル内商品は消費されているという説明がありませんでした。

対処方法:その4 の追加で、スキル内課金商品購入ヘルプインテントが呼び出された際のメッセージに「この商品は消費型であり1回利用すると、この商品は消費され無くなります。」というメッセージを追加しました。

その8

フィードバック:スキル審査の結果、下記のスキル内課金商品についてアップセルが提供されていないと判断されました。現在、スキルから提示される情報だけでは、ユーザーがどのようにスキル内課金商品を購入できるかが不明瞭です。購入プロセスへ誘導するためのフレーズをスキルからのプロンプトにご提示ください。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:その2 とその3 に対応しましたが、その結果として、スキル内課金商品説明だけでなく、購入プロセスに誘導するフレーズをメッセージとして提示してくださいと指摘されました。

対処方法:購入リクエストハンドラーを呼び出すためのフレーズを、スキル内課金商品購入ヘルプインテントが呼び出された際のメッセージに追加しました。

その9

フィードバック:次のインテント名を使用してスキルを起動した際、スキルの応答にリクエストとの関連性がありません。

原因:しりとりスキルで、ユーザーがしりとりの途中で「返金」と発話した際に「んで終わる言葉を言いましたね。あなたの負けです。しりとりを終了します。」としりとりを終了してしまい、返金処理に応じなかったのが原因でした。

対処方法:ユーザーが「返金」と言った場合に、返金リクエストハンドラーを呼び出し、返金処理に応じるように変更しました。

その10

フィードバック:スキルのホームカードにスキル内課金商品の金額を含めないようにしてください。

原因:その2 と類似却下理由。金額は返金フロー後、Alexa側で提示するので、スキル側では金額を表示しないでください。ということです。

対処方法:返金リクエストから返金フローを呼び出す際に表示していたメッセージから金額を全て削除しました。

その11

フィードバック:スキル内課金商品のアップセル中に、そのスキル内課金商品をスキルでどのように使用できるかの情報を提供する必要があります。「1回だけしりとりを待ったできます。」という情報だけでは、1回負けた後も継続することが可能ということがユーザーにわからない可能性があります。課金商品の機能がアップセルで明確にわかるよう修正してください。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:「しりとりを待った、を購入すると、1回だけしりとりを待ったできます。」という説明が不適切と判断されました。

対処方法:「しりとりを待った、を購入すると、しりとりに負けた時、1回だけしりとりをやり直すことができます。」に変更しました。

その12

フィードバック:スキルは、スキル内課金商品の購入が正常に完了したにも関わらず、その商品の機能をユーザーに提供していません。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:ユーザーがスキル内課金「しりとり待った」を購入後、即しりとりを終了し、再度しりとりスキルを起動し「ん」で終わる言葉でわざと負けた場合に、購入したはずの「しりとり待った」を使用できませんでした。

対処方法:スキル内課金商品の在庫状況を「AWS Lambdaコードにスキル商品を組み込む」で説明したPersistent Attributes(永続属性)を保存するようにAmazon DynamoDBを使用しました。

「Alexa-hostedスキル」で作成した場合、Alexa Developer ConsoleにhostされたAWS Lambdaを使用するため、自前で用意したAmazon DynamoDBへのアクセスは困難となります。

「ユーザー定義のプロビジョニング」でスキルを作成した場合、AWS LambdaもAmazon DynamoDBも同じユーザーであるためIAMロール設定一発で、Amazon DynamoDBへのアクセスが自由にできます。

Persistent Attributes(永続属性)を使用するスキルを作成する場合は、「ユーザー定義のプロビジョニング」でスキルを作成することをお勧めします。

その13

フィードバック:申請いただいたスキルは、ユーザーが購入した消費型の課金商品の使用可能な残量を示すオプションを提供していません。

スキル内課金の認定ガイド | Alexa Skills Kit

原因:その3 と類似却下理由。購入後、スキル側がサービスを提供して終了するスキルは問題無かったが、「しりとり」のように商品購入後、しりとり再開直後にユーザーの意思でスキルを終了できてしまう場合は、再開後、課金商品の残量を示す義務がありました。

対応方法:スキル内課金商品の在庫をAmazon DynamoDBに保存し、再開後、課金商品の残量をAmazon DynamoDBから取得しユーザーに提示するように変更しました。

その14

フィードバック:申請いただいたスキルは、アップセルメッセージが提供された後の取引のキャンセルが適切に処理されていません。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:ユーザーが課金商品を「購入する」と言い、スキルが「...同意して、購入しますか?」と確認した後、ユーザーが「いいえ」と言った場合に「そうですか、お役に立てず残念です。」とスキルを終了してしまった。

対応方法:スキルを終了する前に「購入はキャンセルされました。」というメッセージを追加しました。

その15

フィードバック:スキル内課金商品は、無料版のスキルに何らかの機能が追加される必要があります。スキル内で負けた際にもアップセル後に購入せずに単語を発話するとしりとりが続けられました。上記を修正、アップデートした上で再申請してください。

原因:ユーザーがしりとりに負けた後、スキルが課金商品を購入するか確認した際に「AMAZON.YesIntent」と「AMAZON.NoIntent」以外の言葉を話しかけた場合、しりとりを再開できてしまったのが問題でした。

対応方法:しりとりに負けた後、課金商品を購入するか確認中の時は「AMAZON.YesIntent」と「AMAZON.NoIntent」だけを受け付けるようにし、その他の言葉を話しかけられた場合は、再度「...同意して、購入しますか?」と確認するように変更しました。

その16

フィードバック:スキル審査の結果、下記のスキル内課金商品についてアップセルが提供されていないと判断されました。スキルを申請する際には、すべてのスキル内課金商品にアクセス出来るよう、開発者ポータル上のテストの手順でそちらの手順の記載をお願いします。アップセルを通さず、購入プロンプトが返ってきます。アップセルを追加してください。またその際には消費型の課金商品について、明確な数量等の情報を一緒に提供する必要があります。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:購入リクエスインテントが呼ばれたら直ぐに購入フローに移っていた。購入フローに移る前にその課金商品について説明しなければならなかった。

対応方法:購入リクエスインテントが発話された場合は、購入リクエストハンドラーを呼び出して購入フローに移動する前に、その課金商品についての説明を追加し、必要に応じて課金商品の在庫がある場合は、その残量をお知らせするようにしました。

その17

フィードバック:スキルは、ユーザーがアップセルに同意したにも関わらず、スキル内課金プロセスを正常に完了する事が出来ませんでした。スマートフォンのAlexaアプリや Alexa for PC ではスキル内商品を購入できません。ただ、ユーザーが購入済みのコンテンツを利用することは可能です。そのため、4回目以上使用の場合は、Echoデバイス側で商品の購入ができて、且つスマートフォン端末で購入済みの商品を使用できるようにする必要があります。ご確認後、修正をお願いします。

スキル内課金の優れたカスタマーエクスペリエンスのデザイン | Alexa Skills Kit

原因:「ここどこなの」というスキルを無料公開していた時は、位置情報が取得できるスマートフォンのAlexaアプリのみ利用可能にしていたが、Alexaアプリからはスキル内課金商品が購入できないため、1日4回以上利用することができなくなっていました。

解決方法:Echoデバイス側でも「ここどこなの」のスキル利用を有効にし、スキル内課金商品の購入のみ利用可能としました。購入したユーザー情報と課金商品の在庫数はAmazon DynamoDBに格納し、スマートフォンのAlexaアプリでは1日4回以上利用する場合は、購入したユーザー情報と課金商品の在庫数を確認後、購入済みの場合のみ、現在の位置情報から住所をお知らせするサービスを提供するように変更しました。

その18

フィードバック:Amazonにとって知的財産権の保護は非常に重要です。スキル内で使用されているコンテンツを販売する権利があることを確認できませんでした。販売する権利を持っていることを示す文書を提示してください。

原因:「年度別新入社員」というスキルで、年度別の新入社員のタイプを説明するスキルを作成しました。元ネタは「公益財団法人・日本生産性本部」の「新入社員意識調査・特徴とタイプ」を引用させていただいていました。

www.jpc-net.jp

以前同名のスキルを無料スキルとして公開していた時は「公益財団法人・日本生産性本部」より「我々が公開しているコンテンツに関して引用していただいて問題ございません。」とお墨付きをいただいていたが、有料スキルでの引用に関して、今回尋ねたところ「有料コンテンツでの引用は認めておりません。」との回答でした。

対応方法:前回スキルを公開した時は、あっさりコンテンツ利用を認めていただいたので、今回も認めていただけると思ったが、甘かったです。「年度別新入社員」のスキル内課金化を断念しました。

スキル内課金の組み込んだスキル一覧

  • サブリナの通訳

Amazon CAPTCHA

  • 全国の郵便番号検索

Amazon CAPTCHA

  • よく当たるタロット占い

Amazon CAPTCHA

https://www.amazon.co.jp/Flect-%E8%A5%BF%E6%9A%A6%E3%83%BB%E5%85%83%E5%8F%B7%E5%A4%89%E6%8F%9B/dp/B07PYQ3Z94/

  • 意地悪しりとり

https://www.amazon.co.jp/Flect-%E6%84%8F%E5%9C%B0%E6%82%AA%E3%81%97%E3%82%8A%E3%81%A8%E3%82%8A/dp/B0823JL3RK/

  • よく分かる全国の天気

Amazon CAPTCHA

  • ボー読み般若心経

https://www.amazon.co.jp/Flect-%E3%83%9C%E3%83%BC%E8%AA%AD%E3%81%BF%E8%88%AC%E8%8B%A5%E5%BF%83%E7%B5%8C/dp/B07PMNK53T/

  • 星座占い

Amazon CAPTCHA

  • 太陽系星座チェック

Amazon CAPTCHA

  • ここどこなの

https://www.amazon.co.jp/Flect-%E3%81%93%E3%81%93%E3%81%A9%E3%81%93%E3%81%AA%E3%81%AE/dp/B07Q5D55G8/

2020年1月27日(月)、アマゾンジャパン合同会社から、Echo Show 5が10個届きました。

f:id:flect170:20200131145509j:plain
アマゾンジャパン合同会社より荷物が届きました。

f:id:flect170:20200131145923j:plain
荷送人はAlexaスキル開発者キャンペーンです。

f:id:flect170:20200131145935j:plain
確かにEcho Show 5が10個入っていました。

f:id:flect170:20200131145950j:plain
勢い余って、Echo Show 5を10個、ピラミッド状に並べてみました。

スキル内課金を作成した感想

今回、Alexaスキル開発者キャンペーンが引き金で、Echo Show 5欲しさに、スキル内課金の勉強がてら既存の無料スキルにスキル内課金を組み込み、公開申請しました。

無料スキル公開申請同様、スキル内課金の公開申請ならではの、フィードバック(申請却下)をたくさん頂きました。

今回もサンドバック状態で滅多打ちにされましたが、雨にも負けず、風にも負けず、めでたく10個のスキル内課金が組み込まれたスキルを無事に公開することができました。

このブログを最後まで、読んでいただき、ありがとうございます。AMAZON Alexaに限らず、音声アシストを作成依頼したいところをお探しの場合は、是非弊社までご連絡ください。

クラウド最安 GPU で Jupyter Notebook を実行するには

技術開発室の佐藤です。こんにちは。

皆さんは GPU を使っていますか?筆者の周辺では年を追うごとに GPU の需要が高まっています。用途のほとんどはディープラーニングのモデルトレーニングです。特に画像系AI機能のモデルを作成する場合は、 GPU は必須です。

しかしこの GPU には問題があります。価格です。AWS などのクラウドベンダー各社は GPU インスタンスをラインアップしていますが、CPU インスタンスと比較するとかなりの高価格です。(設備の調達価格や電気料金を考えると仕方ないのかもしれませんが。。)最小販売単位が大きいことも悩みどころで、AWS と Azure は最低 4 CPU 構成からとなっています。本番稼働では必要でしょうが、学習用には過剰なのです。

筆者の知る限り、 GPU を一番小口で提供しているのは GCP で、1コアの n1-standard-1 に NVIDIA Tesla T4 をひとつ接続する構成が最小です(昨年までは K80 が最安だったが、最近 T4 が値下げされた)。これまで筆者はこのぐらいのインスタンスで Jupyter Notebook (以下、 notebook)を使って各種の手元検証をしていました。しかしこのような使い方では稼働率が低く、高額な GPU がしばしばアイドルになっていることを心苦しく思っていました。

この状況を改善するひとつの方法は「余剰割引」の利用です。AWS なら Spot Instance、 GCP なら Preemptible GPU になります。通常価格の半額以下で調達できます。ただしこの余剰割引にも問題があり、ベンダの都合で突然解放されてしまうのです(直前に予告通知はある)。元々待機状態の設備の格安提供なので、ベンダの都合でいつでも取り上げられる約束なのです。その時が来たら、ローカルストレージも全て解放されてしまいます。手元検証作業をしていたら突然中止、設定やり直し、というのは、心理的にもよろしくありません。

廉価な余剰割引を、安心して使う方法はないものでしょうか。

バッチ化、コンテナ化、GKE ジョブ化 で挑戦

そこで筆者が思いついた解決方法は、以下のようなものです。

  • Notebook は廉価な CPU インスタンスで基本調査した後、バッチ実行する。
  • このバッチ実行をコンテナ化する。
  • このコンテナを、余剰割引 GPU (preemptible GPU) ノードで GKE ジョブとして実行する。

図に書くと以下のような感じです。
f:id:masashi-sato-flect:20200131130140p:plain

GKE (Google Kubernetes Engine) は GCP が提供する Kubernetes ランタイムです。 今回は GCPGPUを利用しますので、KubernetesGCP を使うのが最も手間がないと考えました。

こうすれば、以下のメリットが得られるはずです。

  • GPU がアイドルでも preemptible GPU なので出費が少ない。
  • Kaggle や巷ブログに豊富に出回っている notebook をそのまま評価できる。
  • ジョブ実行中にノードが解放されたら(この動作は preemption と呼ばれる)、ノード障害として検出され、 GKE がジョブを自動で再実行する。

結果を先にお話しすると、この仕組みは一通り動作しました。筆者は今後はこの方法で GPU 技術の評価をしていきたいと考えています。もしかしたら同じような悩みをお持ちの方もいらっしゃるかもしれないと思いましたので、以下にその手法を説明させていただきます。

Notebook をバッチ実行する

これは実は楽勝です。Jupyter Notebook がインストールされた環境で、シェルコマンドで以下のように打つだけです。

jupyter nbconvert --ExecutePreprocessor.timeout=600 --to notebook --execute mynotebook.ipynb

Pythonコードで実行することもできます。

import nbformat
from nbconvert.preprocessors import ExecutePreprocessor

with open(LOCAL_FILE_PATH) as fIn:
    nb = nbformat.read(fIn, as_version=4)
    ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
    ep.preprocess(nb)
    with open(PROCESSED_FILE_PATH, 'w', encoding='utf-8') as fOut:
        nbformat.write(nb, fOut)

既定のタイムアウトは短い(60秒)ので、指定するようにします。

バッチ実行をコンテナ化する

Jupyter Notebook のコンテナは各種ありますが、今回は以下の TensorFlow 公式コンテナを使用しました。

  • tensorflow/tensorflow:latest-gpu-py3-jupyter

この上で、以下のような Python スクリプトを実行します。環境変数で Cloud Storage の所在を受け取ってダウンロード、 Notebook を実行してアップロードして返すだけの簡単な内容です。

import os
from google.cloud import storage
# https://nbconvert.readthedocs.io/en/latest/execute_api.html#executing-notebooks-using-the-python-api-interface
import nbformat
from nbconvert.preprocessors import ExecutePreprocessor

def download_blob(bucket_name, source_blob_name, destination_file_name):
    # 中略
    # https://cloud.google.com/storage/docs/downloading-objects
# end of download_blob

def upload_blob(bucket_name, source_file_name, destination_blob_name):
    # 中略
    # https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-python
# end of upload_blob

def main():
    LOCAL_FILE_PATH     = 'test.ipynb'
    PROCESSED_FILE_PATH = 'test_processed.ipynb'
    BUCKET_NAME         = os.environ['BUCKET_NAME']
    DOWNLOAD_FILE_PATH  = os.environ['DOWNLOAD_FILE_PATH']
    UPLOAD_FILE_PATH    = os.environ['UPLOAD_FILE_PATH']

    download_blob(
        BUCKET_NAME,
        DOWNLOAD_FILE_PATH,
        LOCAL_FILE_PATH
    )

    with open(LOCAL_FILE_PATH) as fIn:
        nb = nbformat.read(fIn, as_version=4)
        ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
        ep.preprocess(nb)
        with open(PROCESSED_FILE_PATH, 'w', encoding='utf-8') as fOut:
            nbformat.write(nb, fOut)
            upload_blob(
                BUCKET_NAME,
                PROCESSED_FILE_PATH,
                UPLOAD_FILE_PATH
            )
        # end of with fOut
    # end of with fIn
# end of main

if __name__ == "__main__":
    # execute only if run as a script
    main()

コンテナの Dockerfile は以下のようになりました。こちらも上記の Python スクリプトを動かすだけの簡単な内容です。

FROM tensorflow/tensorflow:latest-gpu-py3-jupyter
COPY requirements.txt /tf
COPY tf_exec.py /tf
RUN pip install --upgrade pip
RUN pip install -r /tf/requirements.txt
ENTRYPOINT python /tf/tf_exec.py

requirements.txt は、Cloud Storage の SDK だけです。

google-cloud-storage

以下コマンドでコンテナを作って Container Registry に上げます。

docker build --force-rm --tag=test:latest.
gcloud auth configure-docker
docker tag test:latest gcr.io/${PROJECT_ID}/${REPOSITORY_NAME}:latest
docker push gcr.io/${PROJECT_ID}/${REPOSITORY_NAME}:latest

コンテナを、preemptible GPU ノードで GKE ジョブとして実行する

このステップは少々苦労しました。以下のような課題がありました。

  • ジョブに GCP の権限を付与する必要があった。
  • CPU ノードも用意する必要があった。
  • ジョブが preemptible GPU ノードに配置されるように指定する必要があった。

ジョブに GCP の権限を付与する必要があった

今回実行するコンテナは Cloud Storage をアクセスしますので、専用のサービスアカウントを作成し、 Storage Object Admin のロールを付与しました。Container Registry もアクセスしますが、こちらは Cloud Storage の権限がそのまま適用されるとありますので、追加作業はありません。

このサービスアカウントを、GKE ノードを作成するときに指定します(後述)。

CPU ノードも用意する必要があった

Kubernetesではシステム管理のコンテナが常時活動しており、これらを実行しているインスタンスで preemption が発生すると、回復までにより多くの時間(5~10分ぐらい?)を要します。ダウンタイムを減らすには、常時起動の管理ノードと preemptible なノードの両方を用意します。管理ノードには小さな廉価なインスタンスを使用しました。

ジョブが preemptible GPU ノードに配置されるように指定する必要があった

Kubernetes は taint (忌避要件?)と toleration (忌避要件許容?)という2つの概念を用いてジョブのアサイン先を条件設定することができます。今回の場合は、以下の作戦を取ります。

  • Preemptible ノードに taint 「cloud.google.com/gke-preemptible="true":NoSchedule」を設定し、システム管理コンテナがスケジュールされないようにする。
  • ジョブスケジュールに以下の条件設定をする。

以上の諸要件を勘案し、まず GKE クラスタと管理ノードを作成します。

gcloud container clusters create @{TEST_ID} \
  --zone=us-west1-b \
  --machine-type=e2-small \
  --num-nodes=1 \
  --disk-size=10GB

次にこのクラスタに preemptible GPU ノードを追加します。

gcloud container node-pools create ppool \
  --zone=us-west1-b \
  --cluster=@{TEST_ID} \
  --num-nodes=1 \
  --machine-type=n1-standard-1 \
  --disk-size=50GB \
  --preemptible \
  --accelerator=type=nvidia-tesla-k80,count=1 \
  --node-taints=cloud.google.com/gke-preemptible="true":NoSchedule \
  --service-account=@{TEST_ID}@${PROJECT_ID}.iam.gserviceaccount.com

最後の4つのコマンドオプションに注意してください。上からそれぞれ以下のような意味があります。

  • Preemptible GPU インスタンス
  • NVIDIA Tesla K80 を1つ使用
  • Taint の設定
  • Cloud Storage へのアクセス権を付与したサービスアカウントをノードに設定

コンテナが起動したら、ノードインスタンスに NVIDIA ドライバをインストールする daemonSet を仕掛けます

kubectl apply -f daemonset-preloaded.yaml

以上で準備は完了です。以下のジョブを実行します。

apiVersion: batch/v1
kind: Job
metadata:
  name: ${TEST_ID}gpu
spec:
  template:
    spec:
      containers:
      - name: ${TEST_ID}
        image: gcr.io/${PROJECT_ID}/${REPOSITORY_NAME}:latest
        resources:
          limits:
            nvidia.com/gpu: 1
        env:
        - name: BUCKET_NAME
          value: ${BUCKET_NAME}
        - name: DOWNLOAD_FILE_PATH
          value: ${SOURCE_NOTEBOOK_PATH}
        - name: UPLOAD_FILE_PATH
          value: ${DESTINATION_NOTEBOOK_PATH}
      restartPolicy: Never
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: cloud.google.com/gke-preemptible
                operator: In
                values: 
                - "true"
      tolerations:
      - key: cloud.google.com/gke-preemptible
        operator: Equal
        value: "true"
        effect: NoSchedule
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
  backoffLimit: 0

以下の部分に注意してください。

resources:
    limits:
        nvidia.com/gpu: 1

この指定をしないと、コンテナから GPU が見えなくなってしまいます。

このバッチで実行した notebook の冒頭では、 TensorFlow から GPU が使用できる状態になっていることが確認できました。

f:id:masashi-sato-flect:20200131130247p:plain

ようやく当初目論見だった preemptible GPU ノードで Kubernetes ジョブ実行ができました。

Preemption からの復帰時間はどのぐらいか

Preemption は GCP 側が起こしてくるので意図して発生させることはできませんが、同等のイベントを発生させてテストすることができます

gcloud compute instances simulate-maintenance-event gke-${TEST_ID}-ppool-xxxx

実際のダウンタイムはどのくらいになるのでしょうか?筆者が今回調査の際に経験したダウンタイムは、最低2分、長いものでは10分ほどでした。結構長いですね・・・。運用については順次調整していきたいと思います。

最後までお読み下さり、ありがとうございました。

Einstein Discoveryで「Einsteinのアドバイス」をカスタム項目に実装するには

みなさんこんにちは。技術開発室の佐藤です。

Dreamforce'19のキーノートビデオを見ていると、Einstein関連機能がユーザにアドバイスする場面がしばしば登場します。例えば以下の動画では、Einsteinのおなじみの挿絵と共に、商談の見込み度数とその算定根拠がアドバイスされます。

https://www.youtube.com/watch?v=emxF6yuC9Q8&t=618

これらの機能は、Salesforce標準オブジェクトの結合関係に基づいて作成された機械学習モデルから導き出されたものと考えられます。標準オブジェクトとはつまり取引先(Account)とか商談(Opportunity)のことで、それらの関係は、例えば以下のようになっています。

https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_erd_majors.htm

Sales Cloudやその他の製品ではこれらの機械学習モデルが最初から有効になっており、入力されたデータを観察しているわけです。

一方で、カスタムオブジェクトや、標準オブジェクトに追加されたカスタム項目については、このような機能は実装されていません。しかしEinstein Discoveryを設定することで、開発者が意図した「Einsteinのアドバイス」を表示させることができます。今回はこの手順についてお話しさせていただきたいと思います。

Einstein Discoveryとは

Einstein Discoveryは、Einstein Analyticsの製品機能の一部です。本稿執筆時点では、以下のEinstein Analytics価格表の中の機能リストにEinstein Discoveryの名称があります。

https://www.salesforce.com/editions-pricing/einstein-analytics/

その機能はデータ分析と予測モデルの作成適用に大きく分類できますが、今回のお話では予測モデルの作成適用が主な対象となります。

ところでその「予測モデル」ですが、Einstein Discoveryでサポートしている予測モデルの種類は、現在時点では以下の2つです。(https://help.salesforce.com/articleView?id=bi_edd_model_about.htm)

  • Classification Use Case: logistic regression
  • Numeric Use Case: linear regression

え?たった2つ?と思われたかもしれません。この道の有名ライブラリScikit-learnのlinear_modelモジュールなどと比較すると少ないように思いますが、Einstein Discoveryは、最も基本的なミニマム構成を、ユーザにわかりやすく機能実装しているということなのでしょう。

シナリオを決める

ところで、そもそもカスタムオブジェクトのアドバイスって、何でしょうか?
いえいえ、予め決まっている話は何もありません。まずはこのシナリオから考えなければなりません。

動作検証ということで、以下のような、これ以上ない簡単なシナリオを考えてみます。

  • 入力値は Category ただひとつで、データ型は文字列で、値は A または B のみ。
  • 出力値はこれまたただひとつ SpeculatedValue で、データ型は数値。
  • 「Categoryが A だったら、SpeculatedValue は 4.0 付近に分布し、Categoryが B だったら、SpeculatedValue は 1.0 付近に分布する」という真理を仮に設定する。
  • Category が入力されたときに、その入力値が A だったら約 4.0 を、入力値が B だったら約 1.0 を予測値としてEinsteinにアドバイスしてもらう。

本来であれば、この「真理」は誰にもわからない秘密の法則で、多数のデータを用いて発見していくものですが、ここでははじめに設定します。なんとも回りくどい話で、かつミニマムですので全く味気がありません。しかしここまで自明なシナリオなら、意図したアドバイス(=予測結果)になっているかどうか簡単に確かめられるでしょう。ちょうどテストコードを書いて本来の機能コードの機能を確認していくような作業でしょうか。

レーニングデータを作る

次に、仮設の真理に基づくデータを作成します。

Einstein Discoveryでは最低400件のデータを必要とします。(https://help.salesforce.com/articleView?id=bi_edd_limits.htm)

今回は以下のようなデータを作成してみました。

date        category  value
2018-01-01  A         3.5435852645574193
2018-01-02  B         1.275152805839512
2018-01-03  A         5.40014361858529
2018-01-04  B         0.6804527791760495
2018-01-05  A         3.212071974642821
2018-01-06  B         0.02215664060029765
2018-01-07  A         2.6168577420923445
2018-01-08  B         1.5697355255078034
2018-01-09  A         3.738544793192011
(以下同様に、2年分、730件)

要するに category が A だったら value が 4.0 を中心に、B だったら 1.0 を中心に分布しているだけです。プロットすると以下のようになりました。まさに仮設した真理から作り出されたデータです。Einstein Discoveryの予測モデル作成機能はきっと容易にこの真理を見つけ出してくれるはずです。

f:id:masashi-sato-flect:20200109162618p:plain

Einstein Analyticsデータセットを作成

次にEinstein Analyticsにこのデータをインポートします。

Einstein Analyticsには無償の開発者エディションがありますので、今回はこれを利用しました。

https://developer.salesforce.com/promotions/orgs/analytics-de

ログイン後、以下のTrailheadの手順にならって作成したCSVをインポートします。

予測モデルの作成

ここからインポートしたデータをもとに予測モデルを作っていくわけですが、はじめにインポートしたデータセットの「Create Story」を選択します。

f:id:masashi-sato-flect:20200109162709p:plain

すると設定項目をいろいろ問い合わせる一連のダイアログが表示されます。

最初は Start an Einstein Discovery Story ですが、ここは以下のように設定します。

f:id:masashi-sato-flect:20200109162729p:plain

Story Goalと言われても困ってしまいますね。これはEinstein Discovery固有の概念と考えたほうが良いと思います。予測モデルの期待動作は言わずとしれた予測で、これ自体の評価は「正確かどうか」に尽きるでしょう。しかしここで言うStory Goalは、予測対象の数値を「どうしたいのか」という、予測モデル作成の背景に関する質問なのです。売上や利益なら「最大化したい」とか、故障率や返品率なら「最小化したい」という意図があると思いますが、その意図を入力します。

なぜこういう設定が必要なのでしょうか?それはEinstein Discoveryが「現状成果の構成要因は何か?もっと良くするにはどうすればよいのか?」を探るためのツールとして設計されており、予測モデルは「こうすればいいよ」というアドバイスを出力するために使われることになっているからです。

ここでは「Valueを最大化する」という目標を設定します。その他に、この予測モデルの名称と、Einstein Analytics上での保存先を設定します。

次は What type of story are you interested in? ですが、今回のゴールは予測値をアドバイスしてもらうことですので、 Insights & Predictions を選択します。

f:id:masashi-sato-flect:20200109162749p:plain

次は How would you like to select the fields for your story? ですが、ここでは Automated を選択します。

f:id:masashi-sato-flect:20200109162858p:plain

以上を入力すると「Story」が出来上がります。Einstein Discoveryでは、これがすなわち、予測モデルです。

f:id:masashi-sato-flect:20200109162920p:plain

どんな予測モデルを作ったの?

出来上がった予測モデルを観察してみましょう。クリックすると、以下のような画面になります。

f:id:masashi-sato-flect:20200109162940p:plain

機械学習に多少の覚えのある筆者でしたが、この画面を読み解くには、Einstein Discoveryの設計背景を少々洞察する必要がありました。この図はつまり、category の選択が、 value 最大化という目的に「大きく影響している」と言っているのです。もちろんそうなるようにデータを作ったのだから当然ですが、この「Story(=予測モデル)」、何を根拠に数値を出しているのでしょうか?

そのタネ明かしは、「R Code」の中にありました。

f:id:masashi-sato-flect:20200109163019p:plain

ポイントは以下の部分です。

converters <- list(
    `date` = converter.date(c("2018/01" 中略),
    `category` = converter.text(c("B", "A"), 中略)
)

predict <- function(data) {

    predictions <- (
        2.3825128790222143 +
        (data[[1]] == 0 & data[[2]] == 0) * 0.11883377753580698 +
        (data[[1]] == 0 & data[[2]] == 1) * 0.11883377753580698 +
        中略
        (data[[2]] == 0) * -1.4558662462697816 +
        (data[[2]] == 1) * 1.4458832575039133
    )

筆者はR言語には詳しくありませんが、上記のコードが以下のような処理を実行していることは容易に読み取れます。

  • 平均 2.3825128790222143
  • category が B なら、 -1.4558662462697816 (=~ 0.92)
  • category が A なら、 1.4458832575039133 (=~ 3.83)
  • あとは category と date によって、細かい値調整

まさに最初に仮設した真理をだいたい反映する内容になっていると言えます。 これこそが予測モデルの正体のようです。 Einstein Discoveryがやっていたことは、モデル作成という名の、係数調整だったのです。(言われてみれば、Linear Regressionのトレーニングとは、そういう作業にほかなりませんね。。。)

予測モデルをデプロイする

予測モデルはできました。次はデプロイ作業です。デプロイとはつまり、Salesforceのカスタムオブジェクトにこの予測モデルを仕掛け、どこかの項目に「A」や「B」と入力されたら、予測値を別の項目に設定するように仕掛ける作業ということになります。このためには、カスタムオブジェクトを定義する必要があります。

まず以下のような項目を持つカスタムオブジェクト Csv01__c を定義します。例によって確実な動作確認のための味気ない最低限の内容です。

Nane           Auto Number
Category__c    Text(16)
FinalValue__c  Number(9,9)

FinalValue__cという項目ですが、これはEinstein Discoveryに予測に対する実測値を伝えるための項目です。これについては後述します。

カスタムオブジェクトを定義したら、以下の「Deploy Model」をクリックして作業を開始します。

f:id:masashi-sato-flect:20200109163210p:plain

新規デプロイを選択し、適当な名前をつけます。

f:id:masashi-sato-flect:20200109163227p:plain

次に、予測モデルを適用するカスタムオブジェクトとして「Csv01__c」を指定します。

f:id:masashi-sato-flect:20200109163249p:plain

次に、予測モデル作成で使用したデータの項目と、カスタムオブジェクトの項目の対応関係を設定します。

(なお、ここで「date - Create Date」の対応関係が設定されているのはなぜかと思われたかもしれませんが、これは「最低2項目の対応関係が必要」というEinstein Discoveryの制約をかわすための措置ですので、ここでは気にしないでください。)

f:id:masashi-sato-flect:20200109163303p:plain

次は予測モデルの予測結果を設定する項目の指定です。Einstein DiscoveryではaiPredictionFieldと呼ばれる特別なメタデータ属性を設定した読み取り専用項目に予測結果を設定します。既存項目を指定することもできるようですが、ここでは項目名を入力してEinstein Discoveryに適当な新規項目を追加してもらうことにしましょう。

f:id:masashi-sato-flect:20200109163320p:plain

次は予測モデルを適用するレコード条件の設定ですが、ここでは「全レコードにひとつの予測モデルを適用」を指定します。

f:id:masashi-sato-flect:20200109163336p:plain

次に「ユーザが入力する、予測モデルの入力情報となる項目はどれか」を指定します。今回の場合は category の1項目だけです。

f:id:masashi-sato-flect:20200109163609p:plain

次に指定するのは、「実測値」です。ここでは、カスタムオブジェクト作成時に設定した項目 FinalValue__c を指定します。また、 0 を超える実測値が入力された場合に、実績値の入力が完了したと認識するように設定します。実測値を指定することにより、予実比較のレポートを作成することができます。これについては後述します。

f:id:masashi-sato-flect:20200109163628p:plain

ようやくすべての設定が完了しました。しばらく待つと、デプロイ完了のメッセージが表示されます。

f:id:masashi-sato-flect:20200109163654p:plain

Salesforce Platform側の準備

以上でEinstein Discovery側の準備は完了しましたので、次はSalesforce Platform側の作業です。

まずはじめに、Einstein Discoveryと、カスタムオブジェクトを操作するユーザが必要な項目にアクセスできるように項目レベルセキュリティを設定します。ここで注意ですが、Einstein Discoveryは「Analytics Cloud Integration User」というプロファイルですので、これに対しても項目レベルセキュリティを設定する必要があります。ここでは以下のように設定しました。

Field               System Administrator  Analytics Cloud Integration User
---------------------------------------------------------------------------
SpeculatedValue__c  read only             read only
FinalValue__c       read write            read only

ページレイアウトにも項目を追加しておきます。

「Einsteinのアドバイス」のUIを追加する

次は冒頭にご紹介したような「Einstienのアドバイス」を表示する画面上の導線設定です。これはカスタムオブジェクトのLightning Record Pagesを設定することによって行います。

Lightning Componentsから「Einstein Predictions」をドラッグしてオブジェクト詳細の右側にドロップし、このコンポーネントの設定「Prediction」に、作成した予測モデルの名称 csv01 SpeculatedValue を設定します。

f:id:masashi-sato-flect:20200109163719p:plain

実験してみる

以上で長い準備作業が完了しました。早速テストしてみましょう。カスタムオブジェクトを新規作成し、Categoryに「A」と入力して保存します。

f:id:masashi-sato-flect:20200109163737p:plain

するとEinsteinのアドバイスが表示されました。「予測値は3.95で、そのうちCategoryがAであることによる要因が1.45」と、前述のR言語のコードに書かれた係数値の通りの内容です。

f:id:masashi-sato-flect:20200109163752p:plain

なお、この段階でSpeculatedValueの表示更新が滞る現象が何度か観察されましたが、原因についてはわかりませんでした。APIでクエリしてみると、以下のように正しい値が取得できました。

f:id:masashi-sato-flect:20200109163811p:plain

次にCategoryをBに変更してみましょう。こうすると「予測値は1.01で、CategoryがBであることが要因であり、改善のためにはCategoryをAにすることが考えられる」という内容のアドバイスになりました。

f:id:masashi-sato-flect:20200109163825p:plain

実に長い道のりでしたが、ようやく当初の目標通りEinsteinのアドバイス機能をカスタム実装することができました。

実測値との比較

詳細は割愛しますが、カスタムオブジェクトのFinalValueを設定することで、実測値との比較も可能です。以下のようなダッシュボードを作成してくれます。

f:id:masashi-sato-flect:20200109163922p:plain

ここでは予測モデルが適用された2つのレコードについて、 SpeculatedValue に設定された Predicted Value と FinalValue に設定された Actual Value がプロットされ、予測が概ね正確に行われていることが観察できます。また一定期間運用すれば、 Accuracy Trend で予測精度の時系列変化を観察することもできるでしょう。

最後までお読みいただきありがとうございました。

AWS 認定 機械学習 – 専門知識に合格しました

新年明けまして、おめでとうございます。2020年最初のFlect Blog!本年もよろしくお願いします。

技術開発室エンジニアのオクダです。今回は昨年受験した「AWS 認定 機械学習 – 専門知識」について、受験レポートと試験勉強内容ついて解説していきたいと思います。

AWS 認定 機械学習 – 専門知識とは

AWS 認定 機械学習 – 専門知識の公式サイトでは、以下の内容が記載されています。

AWS 認定 機械学習 – 専門知識は、開発あるいはデータサイエンスの業務を行っている方を対象としています。この試験では、与えられたビジネス上の問題に対する機械学習ソリューションを設計、実装、デプロイ、維持する受験者の能力が検証されます。

認定によって検証される能力

  • 与えられたビジネスの問題に対し、適切な ML アプローチを選び、その理由を説明できる
  • ML ソリューションの実装に適した AWS のサービスを選択する
  • スケーラビリティ、コスト効率、信頼性、安全性に優れた ML ソリューションを設計し、実装する

推奨される知識と経験

  • AWS クラウドでの ML/深層学習ワークロードの開発、設計、実行における、1~2 年の経験
  • 基本的な ML アルゴリズムの基となる考えを表現する能力
  • 基本的なハイパーパラメータ最適化の実践経験
  • ML および深層学習フレームワークの使用経験
  • モデルトレーニングのベストプラクティスを実行する能力
  • デプロイと運用のベストプラクティスを実行する能力

試験の準備

実践的な経験ほど優れた準備はありません。関連する AWSレーニングコースやその他のリソースが多くあります。認定準備のために追加の知識とスキルを習得するのに役立ちます。認定試験で評価された能力の関連情報については、試験ガイドを確認してください。

aws.amazon.com

と記載されています。ふむふむといった感じです。

ことの始まり

弊社でも、機械学習案件が増えてきて、別に資格を取得していないと、仕事ができない職種(医師、弁護士、会計士、建築士)ではないが、 案件を受ける際に、資格を持っていると、お客様への信頼はより一層深まることは、間違いありません。 機械学習の資格と言っても色々存在します。例えば、

今回は、我々の仕事に直結している「AWS 認定 機械学習 – 専門知識」の受験体験とその勉強方法について解説していきます。

試験勉強(どういったことに取り組んできたか)

以前、弊社では週に1度2ヶ月くらいかけて、Sagemaker Builtin Algorithmをモブプログラミング形式でレビューしていきました。 実際にJupyterNotebookを起動して、Sagemaker のBuiltin Algorithmを動かしていくことで、理解を深めていきました。 Sagemaker Builtin Algorithmを使用するにあたり注意が必要なのは、CPU、GPU使用料が当然かかってくるので 指示通りGPU使って、何回も繰り返しトレーニングしたりすると、アッと言う間に高額請求されるので、気を付けてください。

2019年11月にも入り、機械学習の勉強しなきゃな〜、と思い、まずは、AWS機械学習についての公式ドキュメントを読んで、理解することにしました。

https://docs.aws.amazon.com/ja_jp/machine-learning/latest/dg/what-is-amazon-machine-learning.html

Amazon Machine Learning」は枯れたサービスではありますが、機械学習を勉強する資料としては役に立ちました。 左のカラムの「Amazon Machine Learning とは。」から「Amazon Machine Learning のリファレンス」まで、一通り読破しました。所用日数約1週間。 結構なボリュームで、各トピックに関して、詳しく書かれています。

など、モデルを作成する上で、気を付けなければいけないことなどが記載されており、機械学習に関して知識と理解が深まりました。

さて、自分なりに機械学習に対して理解も深まってきたので、模擬試験を受けることにしました。 AWS公式の無料の模擬試験は10題しかなく(解答付き)、有料の模擬試験も20題でこちらは解答がついていないので、 サードパーティの模擬試験を漁ることにしました。 以前、模擬試験を漁っていた時には、Udemyとか有料会員向けの模擬試験も対応されていませんでしたが、 2019年11月に入り、チラホラ「AWS 機械学習 対策試験問題」が公開されてきました。 そこでUdenyの他にも、インターネット上で見つけた模擬問題を解いていきました。

模擬問題の中にはどう考えても解答が間違えている問題もありましたが、問題集の粗探しをしながら、11月末の日程で「AWS 認定 機械学習 – 専門知識」認定試験を申し込みを行いました。

AWS 認定 機械学習 – 専門知識」受験レポート

11月末の試験当日、試験会場に行き、受付を行う。まず、試験会場で、

  • 試験中不正行為は行いません。
  • 試験中他の人に迷惑となるような行為を一切行いません。
  • 試験問題等、試験中に知り得た内容は口外しません。

等の同意書にサインをして、受付に提出。

身分証明として、顔写真付きのIDカードと、クレジットカード等の証明書を2点提出する。僕の場合は、運転免許証と在留カード(どちらも顔写真付き)

IDカードが確認できたら、写真付きでない証明書を返され、Webカメラで顔写真撮影される。撮られた写真はその場で確認される。

撮影後、電子端末に名前を記載する。記載中、電子端末に指が触れるとやり直しとなるので気を付ける。

署名後、荷物、腕時計、ポケットの中の物全て、所定のロッカーに入れる。

受付の人に「お手洗いも済み、準備ができましたら、お声かけください。」と言われるので「準備できました。」と伝えると、再度、腕時計をしていないか等、確認され、試験室に案内される。

試験室に持って入れるのは、顔写真付きIDカードとロッカーキーのみ。

受付の人が試験室の端末を立ち上げ、受験者の名前が合っているのを確認し、準備ができたら試験開始。試験時間170分。問題数65問。

日本語で、受験したが、問題文の日本語が怪しい場合は、画面左上に「English」ボタンがあるので、それを押すと原文を確認することができる。

Machine Learningの場合、AccurateとPrecisionがどう訳されるか、不安要素が多々あったので、問題文を見ながら、適宜原文を確認した。

試験時間100分ぐらいで、全問解答し、20分ぐらい見直しをして、見直し終了ボタンをクリック。

  • なぜ、この試験を受験しようと思いましたか?
  • 試験会場は快適でしたか?
  • 試験勉強期間はどれくらいでしたか?

等々聞かれるので、正直に回答し、試験終了ボタンをクリック。

試験終了確認画面が表示され、無事に「合格」という文字を確認できました。

試験室を退室し、受付に現在の時刻と試験終了しましたのサインをした後、ロッカーから自分の荷物を取り出し、ロッカーキーを受付に返却して試験会場をあとにした。

試験時間が午前中だったので、同日夜20時に試験結果をダウンロードして確認できるようになっていた。

試験結果がこちら

f:id:flect170:20200203094242p:plain
AWS 認定 機械学習 – 専門知識「認定資格証明書」

ふり返って、どうか

今回、本格的に試験勉強を始めてから、受験までについて、ご報告させていただきました。

期間が短かったこともあり、試験対策に重点を置いて勉強してきましたが、機械学習の資格を取得する上で

においては、AWSに限らず、他社のサービスでも共通した知識と経験が必要となるので、これから仕事で機械学習を利用されることを考えている人は、是非勉強すると良いと思います。

  • 開発および運用に関するベストプラクティスに従う能力

に関しては、AWSのサービスを利用した実装、テスト、結果分析方法についての知識と経験とその解決方法が問われるので、他のサービスを利用されるエンジニアには、あまり関係がありません。 AWSのサービスを利用して機械学習を利用される場合は「AWS 認定 アソシエイト」レベルの知識と実務経験が必要となってくるので、まずは、そちらの試験を受験されることをお勧めします。

最後までお読みいただきありがとうございました。皆様にとってOlympic Yearである2020年が良い年になることを願っています。

きゅうり画像で回帰問題を解いてみた

みなさんこんにちは。技術開発室の岡田です。

前回の投稿では、AWS re:invent2019のレポートをしました。 いやー、楽しいイベントだったなー。来年も行きたいなー(と書き続けたら行かせてもらえないだろうか。ちなみに増員はしてもらえそうな雰囲気だった。)

cloud.flect.co.jp

さて、今回から私の担当している機械学習に関する投稿に戻りますが、今回は「画像から回帰問題を解く」をテーマにしたいと思います。

はじめに

画像を用いた機械学習といえば分類問題(Classification)が一般的ですが、実は回帰問題(Regression)を解くこともできます。 有名なところでは「顔の画像から年齢を推定する」というものが有ります。こちらの記事で詳しく紹介されている方がおられますので、ご参照ください。

FLECTでも同様の技術を用いたソリューションを提供しています。 詳しくは述べられないのですが、ざっくり一例をご紹介しますと、下記の図はとある工業製品の摩耗状態を画像から判定するモデルになります。 この工業製品の摩耗状態はとある指標で定量的に10段階で計測できるのですが、図の横軸が計測した正しい摩耗状態、縦軸が画像から推定した摩耗状態となります。かなりしっかりと推定ができているように見えると思います。

f:id:Wok:20191223195757p:plain

上図ではテストサンプルの件数が多くわかりにくいですが、推定した摩耗状態と実際の摩耗状態の誤差をバイオリンチャートで表現すると次の図のようになっています。 縦軸が0を中心に摩耗状態の段階誤差の範囲を示しています。ほぼ1段階以内の誤差で推定することができています。 (自画自賛ですがすごいと思ってる!興味のある方はご連絡を。)

f:id:Wok:20191223195822p:plain

この回帰の技術に関連して、ちょっとした実験をしてみましたのでご紹介します。

実験の動機

まず、実験をしてみた動機からご説明します。

みなさんもGoogleなどで検索していただくとわかると思いますが、画像で回帰問題を解くことについてWeb上にはそれほど多くの情報がありません。大体が上記ブログと同様に年齢推定の話です。 FLECTでも回帰問題を扱うことになりブログで情報発信をしようと思ってその時にその理由がわかったのですが、業務上扱っているデータは当然あるのですがブログで扱えるような公のデータがないのです(私の観測範囲では)。 なので、実験的に何かをしてブログを書くというのが難しかったのではないかと考えています。

で、我々も同じ理由でだいぶ長い間ブログを書けずにいたのですが、あるとき、ぱっと、あることを思い出しました。 そういえば、昔、きゅうりの仕分け(ランクづけ)を画像分類でやっていた人がいたなぁ、と。

それからずっと、このきゅうりのランクを回帰で推定できないか?というのが頭から離れずにいたのですが、この年末幸運にも少し時間が空いたので早速実験をしてみた、ということになります。

ということで、以下、きゅうりのランクを回帰で推定する話をつらつらと書いていきます。 なお、今回用いたソースコード(notebook)は文末のgit repositoryに格納しておきます。

きゅうりの仕分けとデータセット

ご存知の方も多いと思いますが、自動車部品メーカーに務められていた方が退職して、Deep Learningを用いてきゅうりの仕分けをするシステムを作りました。 当時だいぶバズって、Googleのブログにも特集されています。詳細はこの記事を見ていただくのが良いかと思います。

cloudplatform-jp.googleblog.com

この仕分けシステムでは、2LからCまで、きゅうりを9つのランクに仕分けることができるそうです。 そして、この学習用のデータはなんとgitで公開されているのです(Creative Commons)。ありがたい!

github.com

今回は、この中でもprototype_2の学習用のデータを用いて実験を行いました。 なお、このprototype_2は上下側面の3方向からの撮影したデータとなっていますが、今回は上から撮影したもののみを用いることにしました。 また、画像の中には、手が写り込んでいたりする写真などが含まれていますが、これらは事前に取り除きました(OTHERラベルがついている)。

一応、分布を確認するとこんな感じに各クラス800枚前後でまんべんなく格納されています。ちなみに横軸の数値は2L〜Cまでのランクに対応づいています。縦軸は画像数。

f:id:Wok:20191220152417p:plain

なお、本データはCIFAR10の形式で格納されていますので、読み出し方はCIFAR10のサイトかgit repositoryを参考にしてください。

ネットワーク

今回はresnet v2を用いてやってみました。ポイントはFlatten()のあとにsoftmaxではなくreluを活性化関数とした全結合層を置くことです。 また、今回はCIFAR10と同じ画像なので、Inputのサイズは32x32x3になります。

    <略>
    x = AveragePooling2D(pool_size=8)(x)
    x = Flatten()(x)
    x = Dense(8, activation='relu')(x)
    outputs = Dense(1)(x)

モデル全体は次のような感じです。 なお、ある程度エイヤで作っているネットワークなので、チューニングの余地は有りますので、興味のある方は適当にいじってみるといいと思います。

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_8 (InputLayer)            (None, 32, 32, 3)    0                                            
__________________________________________________________________________________________________
conv2d_218 (Conv2D)             (None, 32, 32, 16)   448         input_8[0][0]                    
__________________________________________________________________________________________________
batch_normalization_197 (BatchN (None, 32, 32, 16)   64          conv2d_218[0][0]                 
__________________________________________________________________________________________________
activation_197 (Activation)     (None, 32, 32, 16)   0           batch_normalization_197[0][0]    
__________________________________________________________________________________________________
conv2d_219 (Conv2D)             (None, 32, 32, 16)   272         activation_197[0][0]             
__________________________________________________________________________________________________
batch_normalization_198 (BatchN (None, 32, 32, 16)   64          conv2d_219[0][0]                 
__________________________________________________________________________________________________
activation_198 (Activation)     (None, 32, 32, 16)   0           batch_normalization_198[0][0]    
__________________________________________________________________________________________________
conv2d_220 (Conv2D)             (None, 32, 32, 16)   2320        activation_198[0][0]             
__________________________________________________________________________________________________
batch_normalization_199 (BatchN (None, 32, 32, 16)   64          conv2d_220[0][0]                 
__________________________________________________________________________________________________
activation_199 (Activation)     (None, 32, 32, 16)   0           batch_normalization_199[0][0]    
__________________________________________________________________________________________________
conv2d_222 (Conv2D)             (None, 32, 32, 64)   1088        activation_197[0][0]             
__________________________________________________________________________________________________
conv2d_221 (Conv2D)             (None, 32, 32, 64)   1088        activation_199[0][0]             
__________________________________________________________________________________________________
add_64 (Add)                    (None, 32, 32, 64)   0           conv2d_222[0][0]                 
                                                                 conv2d_221[0][0]                 
__________________________________________________________________________________________________
batch_normalization_200 (BatchN (None, 32, 32, 64)   256         add_64[0][0]                     
__________________________________________________________________________________________________
activation_200 (Activation)     (None, 32, 32, 64)   0           batch_normalization_200[0][0]    
__________________________________________________________________________________________________
conv2d_223 (Conv2D)             (None, 32, 32, 16)   1040        activation_200[0][0]             
__________________________________________________________________________________________________
batch_normalization_201 (BatchN (None, 32, 32, 16)   64          conv2d_223[0][0]                 
__________________________________________________________________________________________________
activation_201 (Activation)     (None, 32, 32, 16)   0           batch_normalization_201[0][0]    
__________________________________________________________________________________________________
conv2d_224 (Conv2D)             (None, 32, 32, 16)   2320        activation_201[0][0]             
__________________________________________________________________________________________________
batch_normalization_202 (BatchN (None, 32, 32, 16)   64          conv2d_224[0][0]                 
__________________________________________________________________________________________________
activation_202 (Activation)     (None, 32, 32, 16)   0           batch_normalization_202[0][0]    
__________________________________________________________________________________________________
conv2d_225 (Conv2D)             (None, 32, 32, 64)   1088        activation_202[0][0]             
__________________________________________________________________________________________________
add_65 (Add)                    (None, 32, 32, 64)   0           add_64[0][0]                     
                                                                 conv2d_225[0][0]                 
__________________________________________________________________________________________________
batch_normalization_203 (BatchN (None, 32, 32, 64)   256         add_65[0][0]                     
__________________________________________________________________________________________________
activation_203 (Activation)     (None, 32, 32, 64)   0           batch_normalization_203[0][0]    
__________________________________________________________________________________________________
conv2d_226 (Conv2D)             (None, 32, 32, 16)   1040        activation_203[0][0]             
__________________________________________________________________________________________________
batch_normalization_204 (BatchN (None, 32, 32, 16)   64          conv2d_226[0][0]                 
__________________________________________________________________________________________________
activation_204 (Activation)     (None, 32, 32, 16)   0           batch_normalization_204[0][0]    
__________________________________________________________________________________________________
conv2d_227 (Conv2D)             (None, 32, 32, 16)   2320        activation_204[0][0]             
__________________________________________________________________________________________________
batch_normalization_205 (BatchN (None, 32, 32, 16)   64          conv2d_227[0][0]                 
__________________________________________________________________________________________________
activation_205 (Activation)     (None, 32, 32, 16)   0           batch_normalization_205[0][0]    
__________________________________________________________________________________________________
conv2d_228 (Conv2D)             (None, 32, 32, 64)   1088        activation_205[0][0]             
__________________________________________________________________________________________________
add_66 (Add)                    (None, 32, 32, 64)   0           add_65[0][0]                     
                                                                 conv2d_228[0][0]                 
__________________________________________________________________________________________________
batch_normalization_206 (BatchN (None, 32, 32, 64)   256         add_66[0][0]                     
__________________________________________________________________________________________________
activation_206 (Activation)     (None, 32, 32, 64)   0           batch_normalization_206[0][0]    
__________________________________________________________________________________________________
conv2d_229 (Conv2D)             (None, 16, 16, 64)   4160        activation_206[0][0]             
__________________________________________________________________________________________________
batch_normalization_207 (BatchN (None, 16, 16, 64)   256         conv2d_229[0][0]                 
__________________________________________________________________________________________________
activation_207 (Activation)     (None, 16, 16, 64)   0           batch_normalization_207[0][0]    
__________________________________________________________________________________________________
conv2d_230 (Conv2D)             (None, 16, 16, 64)   36928       activation_207[0][0]             
__________________________________________________________________________________________________
batch_normalization_208 (BatchN (None, 16, 16, 64)   256         conv2d_230[0][0]                 
__________________________________________________________________________________________________
activation_208 (Activation)     (None, 16, 16, 64)   0           batch_normalization_208[0][0]    
__________________________________________________________________________________________________
conv2d_232 (Conv2D)             (None, 16, 16, 128)  8320        add_66[0][0]                     
__________________________________________________________________________________________________
conv2d_231 (Conv2D)             (None, 16, 16, 128)  8320        activation_208[0][0]             
__________________________________________________________________________________________________
add_67 (Add)                    (None, 16, 16, 128)  0           conv2d_232[0][0]                 
                                                                 conv2d_231[0][0]                 
__________________________________________________________________________________________________
batch_normalization_209 (BatchN (None, 16, 16, 128)  512         add_67[0][0]                     
__________________________________________________________________________________________________
activation_209 (Activation)     (None, 16, 16, 128)  0           batch_normalization_209[0][0]    
__________________________________________________________________________________________________
conv2d_233 (Conv2D)             (None, 16, 16, 64)   8256        activation_209[0][0]             
__________________________________________________________________________________________________
batch_normalization_210 (BatchN (None, 16, 16, 64)   256         conv2d_233[0][0]                 
__________________________________________________________________________________________________
activation_210 (Activation)     (None, 16, 16, 64)   0           batch_normalization_210[0][0]    
__________________________________________________________________________________________________
conv2d_234 (Conv2D)             (None, 16, 16, 64)   36928       activation_210[0][0]             
__________________________________________________________________________________________________
batch_normalization_211 (BatchN (None, 16, 16, 64)   256         conv2d_234[0][0]                 
__________________________________________________________________________________________________
activation_211 (Activation)     (None, 16, 16, 64)   0           batch_normalization_211[0][0]    
__________________________________________________________________________________________________
conv2d_235 (Conv2D)             (None, 16, 16, 128)  8320        activation_211[0][0]             
__________________________________________________________________________________________________
add_68 (Add)                    (None, 16, 16, 128)  0           add_67[0][0]                     
                                                                 conv2d_235[0][0]                 
__________________________________________________________________________________________________
batch_normalization_212 (BatchN (None, 16, 16, 128)  512         add_68[0][0]                     
__________________________________________________________________________________________________
activation_212 (Activation)     (None, 16, 16, 128)  0           batch_normalization_212[0][0]    
__________________________________________________________________________________________________
conv2d_236 (Conv2D)             (None, 16, 16, 64)   8256        activation_212[0][0]             
__________________________________________________________________________________________________
batch_normalization_213 (BatchN (None, 16, 16, 64)   256         conv2d_236[0][0]                 
__________________________________________________________________________________________________
activation_213 (Activation)     (None, 16, 16, 64)   0           batch_normalization_213[0][0]    
__________________________________________________________________________________________________
conv2d_237 (Conv2D)             (None, 16, 16, 64)   36928       activation_213[0][0]             
__________________________________________________________________________________________________
batch_normalization_214 (BatchN (None, 16, 16, 64)   256         conv2d_237[0][0]                 
__________________________________________________________________________________________________
activation_214 (Activation)     (None, 16, 16, 64)   0           batch_normalization_214[0][0]    
__________________________________________________________________________________________________
conv2d_238 (Conv2D)             (None, 16, 16, 128)  8320        activation_214[0][0]             
__________________________________________________________________________________________________
add_69 (Add)                    (None, 16, 16, 128)  0           add_68[0][0]                     
                                                                 conv2d_238[0][0]                 
__________________________________________________________________________________________________
batch_normalization_215 (BatchN (None, 16, 16, 128)  512         add_69[0][0]                     
__________________________________________________________________________________________________
activation_215 (Activation)     (None, 16, 16, 128)  0           batch_normalization_215[0][0]    
__________________________________________________________________________________________________
conv2d_239 (Conv2D)             (None, 8, 8, 128)    16512       activation_215[0][0]             
__________________________________________________________________________________________________
batch_normalization_216 (BatchN (None, 8, 8, 128)    512         conv2d_239[0][0]                 
__________________________________________________________________________________________________
activation_216 (Activation)     (None, 8, 8, 128)    0           batch_normalization_216[0][0]    
__________________________________________________________________________________________________
conv2d_240 (Conv2D)             (None, 8, 8, 128)    147584      activation_216[0][0]             
__________________________________________________________________________________________________
batch_normalization_217 (BatchN (None, 8, 8, 128)    512         conv2d_240[0][0]                 
__________________________________________________________________________________________________
activation_217 (Activation)     (None, 8, 8, 128)    0           batch_normalization_217[0][0]    
__________________________________________________________________________________________________
conv2d_242 (Conv2D)             (None, 8, 8, 256)    33024       add_69[0][0]                     
__________________________________________________________________________________________________
conv2d_241 (Conv2D)             (None, 8, 8, 256)    33024       activation_217[0][0]             
__________________________________________________________________________________________________
add_70 (Add)                    (None, 8, 8, 256)    0           conv2d_242[0][0]                 
                                                                 conv2d_241[0][0]                 
__________________________________________________________________________________________________
batch_normalization_218 (BatchN (None, 8, 8, 256)    1024        add_70[0][0]                     
__________________________________________________________________________________________________
activation_218 (Activation)     (None, 8, 8, 256)    0           batch_normalization_218[0][0]    
__________________________________________________________________________________________________
conv2d_243 (Conv2D)             (None, 8, 8, 128)    32896       activation_218[0][0]             
__________________________________________________________________________________________________
batch_normalization_219 (BatchN (None, 8, 8, 128)    512         conv2d_243[0][0]                 
__________________________________________________________________________________________________
activation_219 (Activation)     (None, 8, 8, 128)    0           batch_normalization_219[0][0]    
__________________________________________________________________________________________________
conv2d_244 (Conv2D)             (None, 8, 8, 128)    147584      activation_219[0][0]             
__________________________________________________________________________________________________
batch_normalization_220 (BatchN (None, 8, 8, 128)    512         conv2d_244[0][0]                 
__________________________________________________________________________________________________
activation_220 (Activation)     (None, 8, 8, 128)    0           batch_normalization_220[0][0]    
__________________________________________________________________________________________________
conv2d_245 (Conv2D)             (None, 8, 8, 256)    33024       activation_220[0][0]             
__________________________________________________________________________________________________
add_71 (Add)                    (None, 8, 8, 256)    0           add_70[0][0]                     
                                                                 conv2d_245[0][0]                 
__________________________________________________________________________________________________
batch_normalization_221 (BatchN (None, 8, 8, 256)    1024        add_71[0][0]                     
__________________________________________________________________________________________________
activation_221 (Activation)     (None, 8, 8, 256)    0           batch_normalization_221[0][0]    
__________________________________________________________________________________________________
conv2d_246 (Conv2D)             (None, 8, 8, 128)    32896       activation_221[0][0]             
__________________________________________________________________________________________________
batch_normalization_222 (BatchN (None, 8, 8, 128)    512         conv2d_246[0][0]                 
__________________________________________________________________________________________________
activation_222 (Activation)     (None, 8, 8, 128)    0           batch_normalization_222[0][0]    
__________________________________________________________________________________________________
conv2d_247 (Conv2D)             (None, 8, 8, 128)    147584      activation_222[0][0]             
__________________________________________________________________________________________________
batch_normalization_223 (BatchN (None, 8, 8, 128)    512         conv2d_247[0][0]                 
__________________________________________________________________________________________________
activation_223 (Activation)     (None, 8, 8, 128)    0           batch_normalization_223[0][0]    
__________________________________________________________________________________________________
conv2d_248 (Conv2D)             (None, 8, 8, 256)    33024       activation_223[0][0]             
__________________________________________________________________________________________________
add_72 (Add)                    (None, 8, 8, 256)    0           add_71[0][0]                     
                                                                 conv2d_248[0][0]                 
__________________________________________________________________________________________________
batch_normalization_224 (BatchN (None, 8, 8, 256)    1024        add_72[0][0]                     
__________________________________________________________________________________________________
activation_224 (Activation)     (None, 8, 8, 256)    0           batch_normalization_224[0][0]    
__________________________________________________________________________________________________
average_pooling2d_8 (AveragePoo (None, 1, 1, 256)    0           activation_224[0][0]             
__________________________________________________________________________________________________
flatten_8 (Flatten)             (None, 256)          0           average_pooling2d_8[0][0]        
__________________________________________________________________________________________________
dense_15 (Dense)                (None, 8)            2056        flatten_8[0][0]                  
__________________________________________________________________________________________________
dense_16 (Dense)                (None, 1)            9           dense_15[0][0]                   
==================================================================================================
Total params: 848,497
Trainable params: 843,281
Non-trainable params: 5,216
__________________________________________________________________________________________________

レーニン

ネットワークを定義できたら、トレーニングを開始します。損失関数は今回はMSEにしています。 また、全サンプル数は7861で、これをTraining, Validation, Testでそれぞれ6:2:2に分割しています。 またこちらもエイヤですがエポック数は200にしています。トレーニングの結果は次の通り。 青線がlossでオレンジ線がval_lossです。大体130エポックで収束しているようですね。

f:id:Wok:20191220160258p:plain

評価

では、トレーニングしたモデルで評価してみましょう。 推定ランクをプロットした図です。なかなかうまく推定できているように見えます。 f:id:Wok:20191220160321p:plain

ただ、やはり、サンプル数が多いのでどの程度分散しているのかわかりにくいですね。 なので、こちらのバイオリンチャートを見ていただきましょう。 f:id:Wok:20191220160335p:plain

ご覧になっておわかりになると思いますが、ほぼ正しいランクの位置をてっぺんに正規分布していますね。 やや裾が重いところがあり、他のクラスと認識されてしまっているものが有るようですが、そこそこ正しく推定されたと見て良いと思います。 (作成者の方もおっしゃっていますが、人間がやっているのでランク付けもある程度えいやなところがあるらしいので。)

TensorFlowでディープラーニングによる『キュウリ』の仕分け | Workpiles

今回の場合は、各分類クラス間が離れていないため、人間がやっても“2LとL”や“LとBL”の判別は難しい(けっこう適当)だったりします。

といことで、今回の実験はひとまず成功したかなと思います。画像からの回帰、みなさんもチャレンジしてみてはいかがでしょうか。 結果をシェアしてもらえるとありがたいです。

しきい値調整

推定されたランクは浮動小数点となっていますが、実際のランクは整数値です。 この整数値に変換するためにしきい値を決める必要が有ります。 デフォルトで小数点第1位が5のところでしきい値にしてもいいですが、このしきい値をいい感じに調整してくれる方法も有ります。 今回は画像から回帰をすることろまでが主題となりますので、そこまでは踏み込みません。また機会があればご紹介します。

余談ですが、、、

resnet v2でクラス分類させたら、全然クラス分類のほうが精度が高かったです。 タスクによって解き方をしっかり使い分けることが重要です。この辺の話も機会があれば別途。

最後に

今回は、きゅうりの画像のランク付けを題材に、画像から回帰問題を解く方法についてご紹介しました。 最初にご紹介したとおり、画像から連続値を推定する技術は様々な分野に応用可能であり、FLECTでも実績があります。 ぜひご活用いただければありがたいと思います。

次回は、また別の技術をご紹介する予定です。ではでは。

リンク

github.com

HerokuでTensorFlowのAPIをホストした話

この記事は、Heroku Advent Calendar 201920日目の記事です。

19日目は@mttrsさんさんによる「Heroku Changelog 2019」でした。 21日目は@sizukutamagoさんです。

こんにちは、技術開発室の藤野です。 今回はHeroku上でTensorFlowの推論を行うWeb APIを作った話についてです。

はじめに

今回の話のあらましは、TensorFlowでディープラーニングのモデルを作ったので、Web APIとして使えるようにしたい、というものです。 APIはリクエストに対して推論結果を返すシンプルなもので、なるべく早く構築しデプロイや運用の手間は省きたいと思っていました。

これに対して、Herokuは下記の利点があります。

  • インフラ、ミドルウェアが用意されており、デプロイが容易
  • 運用のための仕組みがあらかじめ提供されている
  • CI/CDの機能を利用できる

一方で、ディープラーニングアルゴリズムGPU等を使って高速化することが一般的だと思います。 Herokuではこのようなアクセラレータは利用できず、CPUのみで推論処理を行うことになります。

そこで、今回はこのような推論処理を含むAPIをHeroku上で動かしたときのパフォーマンスはどのくらいなのか?を主眼に見ていきたいと思います。

続きを読む