鯖寒リターンズ。鯖の寒干しをあいつらから守ろうじゃないか feat ABEJA Platform

この投稿は、ABEJA Advent Calendar 2019 12 日目 の記事です。

この記事は、オランダで新鮮な魚の干物を作りたいプログラマーが、大切な干物を外敵から守るためだけのシステムを構築するために、まだプレビューの ABEJA Platform を使わせてもらって、関係者を引きづり回し(ている)記録です。執筆時点で、構築は途中でありこの話題は来年も続いていくであろうことを先にご報告しておきます。

前提として、筆者は ML に関して深い知識はほぼゼロです。 「ML(おもに教師データあり)周りのプロセスと、用語、そしてそれがなんとなく何するかはわかる」というレベルです。

ゴール

ざっくりいいうと、鳥(カモメとウミネコとカモ)を検知したら、鳥よけシステム(リボンをつけた棒)がぐるぐる回るシステムを作ります。

材料

  • ラズパイ
  • リボン
  • ラズパイ用カメラ
  • 推論エンドポイント

そして、今回執筆時点で構築途中(すいません)そして、ABEJA Advent Calendar ということで、推論エンドポイントができるまでを記載しています。

推論エンドポイントの作成

Step1: 鳥検知モデル

ベースとなるデータセットは、Caltech-UCSD Birds 200 を使います。このデータセットは AWS ブログ の DeepLens と SageMaker を使った鳥検知 サンプル で使用されていたデータです。このデータセットには、すでにアノテーションされたデータが含まれており、200種類の鳥の種類を検知できるようになっています。ただし、今回は、「カモ」「カモメ」「ウミネコ」を検知したいのと、他の鳥はなんでもかまわないので、画像データのみを拝借して自力でアノテーションします。

モデルを作成するとき、いきなり全てのデータを使ってモデルを作るのは時間もかかりますし、アノテーション作業が大変なので、一般にはまず小さなデータでモデルを作ってテストをしながら、意図した推論結果が得られるようにサイクルを回すようです。

  1. 母集団からデータをサンプリング
  2. サンプリングしたデータをアノテーション
  3. テスト用データセットを作成
  4. モデルをデプロイ
  5. テスト

テストして結果が思わしくない場合は、サンプリングするデータを増やす、アノテーションを見直す、違うデータをサンプリングしてみる、などして、推論結果をテストしていきます。

今回はまず、400 枚のデータを抽出、アノテーションして、推論結果がどのようになるか見てみます。

DataLake

まずは、アノテーションの元になる画像をDataLakeに登録します。まずは、画像の格納先となる「channel」を作成します。

チャンネルを作成できたら、Upload ボタンがあるので、GUIからアップロードします。

GUIからでは、単一のフォルダしか選択できないません。また、だからといって一つのフォルダに大量のデータを集めてアップロードすると失敗します。(私は 24000 枚をアップロードしようとしました)

今回利用したデータ Caltech-UCSD Birds は、鳥の種類ごとにフォルダが切られていて、GUIからでは手間がかかります。というところで、今回は ABEJA Platform CLI を使います。 CLI はフォルダの指定が可能です。以下の コマンドで 24000 枚 を 無事に アップロードできました。

find . -type d | xargs -I{} abeja datalake upload {} --recursive

アノテーション

画像がアップロードできたので、アノテーションを行います。

ABEJA Platform から Annotation を選択すると、Annotation ツール(Platform とは別のDashboard)が起動します。Annotation Projectを新規に作成して スキーマを定義します。スキーマは画像を分類するためのタグのようなイメージです。今回は、「カモ」と「カモメ(ウミネコ)」と「その他」くらいの分類で十分なので、スキーマはとてもシンプルです。

まずはアノテーションツールでプロジェクトを作成します。データ元としては、さきほど作成したチャンネルを指定します。目標データ数は400枚で アノテーションします。

プロジェクトが作成の際にスキーマを定義します。(これはいつでも変更できます)

スキーマができたら、アノテーションを行います。

タグを選んで…(otherを選択)

Boxで囲うと。。。

この作業 を 400 回 やります。(本番はあと 23600 枚 やらないと.. )

テスト用のモデルでチェック

アノテーションが終わったので、DataSet に書き出します。これは、Annotation ツールで 先ほどアノテーションしたプロジェクトのメニューから「DataSetを書き出し」を選択するだけです。書き出しが終わると、ABEJA Platform に DataSetが登録されます。

データセットができたので、推論モデルを作ります。ABEJA Platrofm 上で 簡単に モデルの作成とエンドポイントまで作成できます。

Job Definition を作成して、、、

Job Definition version を作成して、、、(Templateに 画像分類用の定義がすでに用意されているので、それを選択)

Job を実行します。(先ほどの Job Definition version と データセットを指定します)

モデルができました。

ジョブの実行が終了すると、ABEJA Platform は 推論エンドポイントも生成してくれます。Deployment の Services のところを見ると、エンドポイントを確認できます。

右のほうに「check」ボタンがあります。ここで、推論結果を確認できます。やってみましょう。

Score 0.5 で何も出ません。

0.36 でやっと Other と出てきました。

Score を 0.13 くらいまでして、ようやく Gull (カモメ / ウミネコ)と判断してきたようです。

うーん。まだまだです。Score値 0.5 くらいで、Gull と判定するくらいまで精度を上げたいところです。

2回目のサイクル

1回目で あまり良い結果が得られなかったので、データセットを増やしてみます。一気に 1000枚 くらいまで増やしたいと思うのですが、一人で頑張るには心が折れそうです。現実はたくさんのアノテーターがいて、複数人で頑張るのでしょうが、今回は、ただただカモメかカモかそれ以外を分類する、しかもただただ私の鯖の干物のためにアノテーションをお願いするのも気が引けてしまいます。

事前推論スクリプト

ABEJA Platform には、Pre Inference(事前推論) エンドポイントというものが用意されており、「元となる推論エンドポイントを利用して、他のデータにアノテーションをつける」ということができます。これがあれば、24000 枚のアノテーションを行うのもポジティブになれそうです。

Pre Inference Function

※ 現在 この機能は別途リクエストして、API実行用のCredentialを発行してもらう必要があります。

  1. Annotation ツールのタスク設定を呼び出す
  2. タスクに紐づいてるDataLakeのChannelから画像を取得する
  3. 画像を推論エンドポイント送って結果をえる
  4. 得た結果が期待するもの(例:Score: 0.5 で Glu と判定できていたら) だったら、Pre Inference API を呼び出して、アノテート したデータとして登録する

ほぼ無改造で使えるサンプルが先ほど紹介したサイトに掲載されてますので参考にしてください。


import os
import io
from urllib.parse import urljoin

from abeja.datalake import Client as DatalakeClient
import requests
from PIL import Image
import numpy as np
from chainercv.datasets import voc_bbox_label_names
from chainercv.links import SSD300

from chainercv.visualizations import vis_bbox


ANNOTATION_API = os.environ.get('ANNOTATION_API', 'https://annotation-tool.abeja.io')
ANNOTATION_ACCESS_USER_ID = os.environ.get('ANNOTATION_ACCESS_USER_ID')
ANNOTATION_ACCESS_TOKEN = os.environ.get('ANNOTATION_ACCESS_TOKEN')

ANNOTATION_ORGANIZATION_ID = os.environ.get('ANNOTATION_ORGANIZATION_ID')
ANNOTATION_PROJECT_ID = os.environ.get('ANNOTATION_PROJECT_ID')

headers = {
    'api-access-user-id': ANNOTATION_ACCESS_USER_ID,
    'api-access-token': ANNOTATION_ACCESS_TOKEN
}

FIXED_CATEGORY_ID = 0


def main():
    # ABEJA Platformの組織IDを取得します
    organization_url = urljoin(ANNOTATION_API, "/api/v1/organizations/{}".format(ANNOTATION_ORGANIZATION_ID))
    res = requests.get(organization_url, headers=headers)
    res.raise_for_status()
    platform_organization_id = res.json()['id']

    # ABEJA Platform上でアノテーション対象ファイルが格納されているChannelのIDを取得します
    organization_url = urljoin(ANNOTATION_API, "/api/v1/organizations/{}/projects/{}".format(ANNOTATION_ORGANIZATION_ID, ANNOTATION_PROJECT_ID))
    res = requests.get(organization_url, headers=headers)
    res.raise_for_status()
    platform_datalake_channel_id = res.json()['data_lake_channels'][0]['channel_id']

    # ABEJA PlatformのSDKを使用します
    client = DatalakeClient(organization_id=platform_organization_id)
    datalake_channel = client.get_channel(platform_datalake_channel_id)

    # wget https://github.com/yuyu2172/share-weights/releases/download/0.0.3/ssd300_voc0712_2017_06_06.npz
    pretrained_model = 'ssd300_voc0712_2017_06_06.npz'
    model = SSD300( n_fg_class=len(voc_bbox_label_names), pretrained_model=pretrained_model)

    task_url = urljoin(ANNOTATION_API, "/api/v1/organizations/{}/projects/{}/tasks/".format(ANNOTATION_ORGANIZATION_ID, ANNOTATION_PROJECT_ID))
    page = 1
    while True:
        res = requests.get(task_url, headers=headers, params={'page': page})
        res.raise_for_status()
        res_body = res.json()
        if len(res_body) == 0:
            break
        for task in res_body:
            metadata = task['metadata'][0]
            # タスクのアノテーション対象画像ファイルをダウンロードします
            file = datalake_channel.get_file(metadata['file_id'])
            img_io = io.BytesIO(file.get_content())
            img = np.array(Image.open(img_io))
            img = img.transpose(2, 0, 1)
            try:
                # 推論を行います
                bboxes, labels, scores = model.predict([img])
            except:
                continue
            information = []
            for bbox, label in zip(bboxes, labels):
                for b, l in zip(bbox, label):
                    y_min, x_min, y_max, x_max = tuple(b.tolist())
                    rect = [
                        x_min,
                        y_min,
                        x_max,
                        y_max
                    ]
                    information.append({
                        'rect': rect,
                        'classes': [
                            {
                                'id': int(l),
                                'name': voc_bbox_label_names[l],
                                'category_id': FIXED_CATEGORY_ID
                            }
                        ]
                    })
            if len(information) == 0:
                continue
            preinference_url = urljoin(task_url, "{}/preinferences".format(str(task['id'])))
            res = requests.post(preinference_url, json={'information': information}, headers=headers)
        page = page + 1

注 )24000 枚 するのに 約5日 かかりますので、結果は次の記事でご報告します。

さて、ここまでで、データのアップロードからモデルの作成、結果をチェックしてテストを回す一連のサイクルができました。やってみた感想としては、アノテーションがつらいの一言です。ML にかけるコストの大変さを垣間見かきがします。

ただ ABEJA Platform は、APICLISDK が しっかり目に用意されているようなので、スクリプト書きまくって Ops 回すというのはやりやすいのかなと思いました。AWS だと、たくさんのサービスを組み合わせて組み上げないといけないのですが、一つのPlatform で一連のフローを完結できるところのに 一日の長があるように思います。

ABEJA Advent Calendar の記事としてはここまでです。本来の目的である 鯖を監視する というところに到るまで、どんな壁があるのやら、それはまたおいおいということで。