Congnitive Service(Custom Vision API）をAzure Functionから呼び出してみる

## Congnitive Service(Custom Vision API）をAzure Function Appから呼び出してみる

## 最初に。
AIの民主化を目標にMicrosort Azureでは学習済モデルを利用したAPIサービスが提供されています。複雑なロジックを駆使してシステムを構築するまでもなくAPIを呼べばjson形式で返却される時代になりました。また事前に用意した独自データを元に学習モデルを簡単に作成するサービスも揃ってきました。
今回は2018年10月に参加した以下のハンズオンセミナーのまとめを投稿してみました。
いづれも[Cognitive Services(Computer Vision API)](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)を使って、画像認識／音声認識／自然言語の解析など提供されているAPIでどのようなことができるか、またどのように実装していくかについて説明頂きました。

- 2018年10月12日　「Cognitive Serviceを使って画像認識してみよう」
- 2018年10月16日　「Cognitive ServiceでAIアプリを作ろう」

## Computer Vision APIについて
一言でいうと、WebAPIを介して簡単に利用できるMicrosoftのAIサービスです。
ネットワークや訓練データ、学習ロジックやリクエストを受け付け返却するマイクロサービスも不要です。
利用できる分野は、視覚認識(Vision)、音声認識(Speech)、言語理解(language)、知識(knowledge)、検索(search)に分類され、
例えば、音声→言葉に変換、静止画・動画→被写体の特徴を判定、画像文字→文字を識別しテキスト化等と言った、本来複雑な実装が手軽に利用可能です。

#### 基本原則
では他のクラウドサービスとどのような違いがあるか言うと、以下のような基本原則があるようです。
- 公平性（偏見を防ぎながら効率を最大化）
- 説明責任（アルゴリズムに説明責任が課せられる）
- 透明性（透明性が求められる）
- 倫理・道徳（人類を支援しプライバシーを尊重する設計）

但し、FaceAPI利用時女性の顔が判定より少し若くなうようにできているとも聞いています
（公平性。尊厳を損なわない方を優先したのかもしれません。）

## 概要
上述の技術の内、比較的新しいサービスのCustom Vison APIを使って画像を判定し、取得した結果をAzure table Storageに格納するAPIを作成してみたいと思います。

## 実装方法
「Cognitive ServiceでAIアプリを作ろう」で予定していたAzure Functionsの使用が時間的に叶わなかったので今回利用していみたいと思います。
処理の流れは以下のようになります。

### ユースケース
1.ユーザがhtmlから直接Azure Blob Storageへアップロード
2.Azure StorageへアップロードされたことをAzure Functionsが検知（←trigger）
3.Azure FunctionsでComputer VisionのAPIを起動
4.上記3.の結果(json)を受け取り、その内容の一部をAzure Table Storageへ書き込む

### 実装イメージ
単純ですが上記ユースケースのイメージが以下となります。

![Art182.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/e1027ee3-50ce-305a-0f69-3cef228c65be.jpeg)

## 下準備(Vision APIでのGUIによる設定～訓練画像データ投入)
#### プロジェクト作成
アカウント作成後、[custom vision](https://www.customvision.ai/)にてプロジェクトを作成
![Art113.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/1b5893a3-b5bd-baf2-ff61-e2f5fcbafda0.jpeg)

寿司ネタの判定を踏襲しました。
![Art114.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/318716d8-5bdb-e6f2-ef34-1a862eaf7870.jpeg)

#### メニューの"Trainging Images"から訓練対象の画像をアップロード
![Art123.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/22d50cba-600e-116c-74d3-51b9012e8afc.jpeg)

#### タグに予測対象の名前を設定（予測APIのレスポンスがこの単位で配列に格納され返却されます）
同様に「まぐろ」「サーモン」「たまご」の画像を学習させます
![Art116.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/9ff2ce65-b416-f7c9-4256-37d6f7aefc0b.jpeg)

#### メニューから"Train"ボタンを押下→学習開始
![Art125.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/f646311b-908c-5661-fae9-7051505bd5b3.jpeg)

#### メニューの"Quick Test"テスト実施（ブラウザへの画像ドラッグか画像URLの指定で判定が可能です）
![Art120.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/2178e45d-190c-37fd-3592-50daa4f4716a.jpeg)

90%の確率でたまごと判定。
![Art121.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/89992926-8477-a4fd-cb68-1b0032b2d41c.jpeg)


#### メニューの"Predictions"→"View Endpoint"選択
![Art199.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/abef71e7-43b7-87aa-a7f0-0f0c945881ca.jpeg)

#### 表示されたウィンドウのURLと赤字のPrediction-keyを取得
![Art200.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/64f96d8e-db95-ef7d-f6bf-5cd1dd0e6b65.jpeg)

#### PostmanでAPI動作確認
headerにcontent/typeと先程のPrediction-keyを設定
![Art201.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/47bfa6a8-30cb-0116-6d6e-0438eff11f83.jpeg)

bodyに適当なマグロ画像URLを指定してpostでsend。マグロの確率99.8....を取得できました。
bodyはjsonで{"Url": "https://xxxxxxx.com/images_file/small_images/s005_361.jpg"}
の形式で送信します。
![Art204.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/447c9d75-00b0-7e43-8fdb-9f76811e9cf8.jpeg)

#### custom visionからのレスポンス
custom visionでタグ付けした単位で確率が設定されている

```javascript:レスポンス

{
    "id": "750eb2ca-4949-42e7-9b08-238c2a63486c",
    "project": "9facba3c-aaaf-49ed-9eff-31132a803889",
    "iteration": "0f253301-7b7e-433c-a0a3-e701695f169b",
    "created": "2018-11-02T01:17:23.3497946Z",
    "predictions": [
        {
            "probability": 0.999815047,
            "tagId": "ae2ebf89-a080-4ff2-b2ac-7f08c339999f",
            "tagName": "まぐろ"
        },
        {
            "probability": 0.00018419986,
            "tagId": "86cb23f2-860f-4b13-9267-615eac94a739",
            "tagName": "サーモン"
        },
        {
            "probability": 7.23571361e-7,
            "tagId": "bde416ca-543a-4bf8-8159-9e99b2554f0c",
            "tagName": "たまご"
        }
    ]
}

```

## 下準備(Azure StorageでBlobコンテナとtable作成～cors設定)
### ◆Azure Storage
#### テーブル作成
ストレージアカウントを作成後→「テーブル」リンク押下→任意の名称（function Appのバインド時のテーブル名と一致）でテーブル作成（テーブルと言えどスキーマの考慮は特段必要なし）
![Art194.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/91cdc37d-05c9-b909-af38-79436e4a75fc.jpeg)
![atr201.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/627637b0-10d5-75c8-3e72-0ecf06859547.jpeg)


#### Blobコンテナ作成
ストレージアカウントを作成後→「Blob」リンク押下→任意の名称（function Appのトリガー時のパス名のコンテナ部と一致）でコンテナ作成
![Art194.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/b3f1d7b5-e762-2855-c451-3d10684210cf.jpeg)
![Art195.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/2da06103-b47a-554d-3b1b-ec99e516366f.jpeg)

#### Azure Storage cors設定
![Art198.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/502bf9de-7d0a-3575-eb6e-5b8c35cbe613.jpeg)
アップロードする側のサイトのドメイン[Azure Storageアップローダ](https://authinazuread.azurewebsites.net/storage.html)を許可

## 下準備(Function App初期設定～trigger、bindの設定)
### ◆Function App
#### パッケージインストール

プラットフォーム機能→開発ツール→
![Art184.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/f141bb09-7b91-cb95-07f9-ca07fa0b0305.jpeg)

スクロールしてkudoを選択
![Art190.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/7542cc9c-b713-8e3f-73d9-685c98895d20.jpeg)

表示した画面でDebug Console→CMDまたはPowerShell選択
![Art186.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/c08e22cc-cc6c-6431-cef5-cfa15259fcc6.jpeg)

site/wwwrootまで移動して npm install xxxxx を実行
![Art188.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/4dd718c0-174c-3c9b-be5a-2e6f64c662e3.jpeg)

#### 統合→トリガー設定
![Art192.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/da84e320-f5a8-6e90-30ae-0959900b3fc6.jpeg)
パスにトリガーとするstorageのコンテナパスを記述。拡張子でフィルタがかけれます
上記の例だと任意のファイルがtest-container001コンテナへのアップロードを契機に当ファンクションが起動します

#### 統合→出力（バインド）設定
![Art193.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/5ede6020-c23d-7984-524b-3cd34b8075c1.jpeg)
Azure Table Storageで作成したテーブルのテーブル名を「テーブル パラメーター名」で指定した変数名でfunction内で操作します。

## 実装(Azure Function App)
#### javascriptにて実装
下準備に時間はかかったもののトリガーやテーブルへの書き込みなど面倒なコーディングはなく30行程度の実装内容となりました。

```javascript:index.js

// 使用したことのないisomorphic-fetchを使用
var fetch = require('isomorphic-fetch');

module.exports = async function(context, trigger,myBlob) {
    //console.log(context)では出力されない。context.log(context);でログ出力
    var imgUri = context.bindingData.uri;
    // customvisionのAPIのURL
    var visionUri = "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.0/Prediction/9fxxXX3c-XXXaf-4XXd-9eXf-3XXXXXX889/url?iterationId=0f253301-7b7e-433c-a0a3-e701695f169b";
    var invocationId = context.bindingData.invocationId;
    // ヘッダに以下の二つの指定が必要、またボディにトリガーとして取得した予測対象の画像URLを指定
    var options = {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Prediction-Key": "4d51exxxxxxxxx9bexxxxxxxxx398da9"  // customvisionのAPIキー
        },
        body: JSON.stringify({"Url": imgUri}),
    };
    
    await fetch(visionUri, options)
    .then(function(res) {
    	return res.json();
    }).then(function(json) {
        var ret = "";
        for(var val in json.predictions){
            ret = ret + json.predictions[val].tagName +":" + json.predictions[val].probability +", ";
        }
        context.log(ret);
        context.bindings.outputTable = {
        "partitionKey": "sushi20181101",
        "rowKey":invocationId,
        "result":ret
        };
        context.done();
    });
    
};
```

```javascript:function.json

{
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test-container001/{name}",
      "connection": "AzureWebJobsStorage"
    },
    {
      "type": "table",
      "name": "outputTable",
      "tableName": "outTable",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ]
}
};
``` 


## 結果
#### 画面から画像をアップロード
[Azure Storageアップローダ](https://authinazuread.azurewebsites.net/storage.html)を利用しました。ちょっと試すには重宝します。
アップロード画面を作成するjs↓も配布されているようです。
[Javascriptを使用してAzure Blobにアップロード](https://azure.microsoft.com/en-us/blog/new-azure-storage-javascript-client-library-for-browsers-preview/)

![not_salmon1result.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/5394c2a8-b54f-5cff-b8a6-930db2984620.jpeg)

ネットで捕まえた以下の画像を3回投入
![img00001.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/52920547-6914-b189-927c-7fc203fdd95f.jpeg)

画像のアップロードをトリガーにして画像を判定し結果がテーブルに格納されているのがわかります。
タグ付けされたサーモン、マグロはかなりの精度で判別してくれています。パンダの画像が70%の確率でまぐろと判定されたものが残念。
![Art196.jpg](https://qiita-image-store.s3.amazonaws.com/0/151893/7867f227-71eb-2562-dca0-d8cb0a36e0a0.jpeg)


## まとめ
画像のアップロードをトリガーとしてFunctionを起動すること、Functionの終了時に結果をテーブルに格納すること、また何より画像認識に関する学習と予測についてもGUI操作で簡単に実装できてしまいました。