モデルハンドラー関数 FOR 18.10

本頁では、 18.10 のイメージで利用できるハンドラー関数について説明します。 19.04 以降のイメージで利用できるハンドラー関数については、こちらを参照ください。

モデルハンドラー関数はモデルを実行する際に呼び出されるコード内の関数です。ハンドラー関数は以下の構文構造を使用します。

def handler(input_iter, context):
    for line in input_iter:
        ...
        yield ret_line
  • input_iter : 入力データがこのパラメータに格納されます。このパラメータは通常、 Python のイテレータです。
  • context : 実行時のメタデータなどがこのパラメータに格納されます。

ハンドラーの引数 

入力データ

ハンドラー関数の第一引数には入力データがイテレータとして渡されます。 イテレータに含まれるデータの形式は MIME Type 毎に異なります。

JSON

入力された JSON はイテレータの最初の要素に dictまたはlist型として渡されます。イテレータの長さは1です。

以下の MIME Type に対応しています。

  • application/json

画像

入力された画像はイテレータの最初の要素に numpy.ndarray 型として渡されます。イテレータの長さは1です。 また、 numpy.ndarray の色の順番は RGB となります。

以下の MIME Type に対応しています。

  • image/jpeg
  • image/png

動画

現在、動画はトリガー、実行でのみサポートされています。

入力された動画は、フレーム画像を含む以下の dict を返すイテレータに変換されハンドラー関数に渡されます。

{
  "image": `numpy.ndarray`,
  "pos_frames": `int`,
  "pos_msec": `int`
}
キー 説明
image numpy.ndarray RGB 順の画像の配列
pos_frames int 0 から始まる動画のインデックス
pos_msec int 動画の現在の位置となる秒数 (単位: ミリ秒)

以下の MIME Type に対応しています。

  • video/avi
  • video/x-matroska
  • video/quicktime
  • video/mp4

コンテキストデータ

ハンドラー関数の第二引数には以下のプロパティをコンテキスト情報として持つ変数が渡されます。

プロパティ名 説明
datatypes ABEJA Platform 上で返却値として処理可能な型を含んだモジュール
exceptions ABEJA Platform 上で例外として処理可能な型を含んだモジュール
http_method 実行された HTTP メソッド
http_headers HTTP リクエスト・ヘッダーを含む辞書。ヘッダー名をキーとし、すべて小文字に変換されています。Authorization ヘッダーを含む一部のヘッダーは削除されます
parameters 実行時のメタ情報やその他のパラメータ

ハンドラーの環境変数

ハンドラー関数では以下の環境変数を使用することが可能です。

環境変数名 説明
ABEJA_ORGANIZATION_ID オーガニゼーションの ID です
ABEJA_MODEL_ID モデルの ID です
ABEJA_MODEL_VERSION_ID モデルバージョンの ID です
ABEJA_DEPLOYMENT_ID デプロイメントの ID です
ABEJA_SERVICE_ID サービスの ID です
HANDLER ハンドラーのパスです
TRAINING_JOB_DEFINITION_NAME 学習ジョブ定義名です
TRAINING_JOB_ID 学習ジョブの ID です
ABEJA_TRAINING_RESULT_DIR 学習ジョブの結果が格納されるディレクトリへの相対パスです

学習結果のモデルハンドラーでの使用

ABEJA Platform では学習の結果を元にモデルバージョンを作成することができます。

学習時の出力をモデルハンドラー関数で使用するためには、学習時にABEJA_TRAINING_RESULT_DIR環境変数で指定されるディレクトリに格納する必要があります。 学習時に出力した結果は、モデルハンドラー関数実行時にABEJA_TRAINING_RESULT_DIR環境変数に格納されたディレクトリに展開されます。

ハンドラーの返り値と例外

返り値

データを出力するためには、コンテキストデータのdatatypesに含まれるResponse型のデータをジェネレータとして返却します。

context.datatypes.Response([返却するデータ], metadata=[('Content-Type', 返却するMIME Type)])

Responseの第一引数には返却するデータを配列で渡します。 metadataはレスポンス時のヘッダーとして使用されます。

以下は返却するデータと MIME Type の例です。

MIME Type 返却するデータの型
application/json dictまたはlist
image/jpeg bytes
image/png bytes

ABEJA Platform ではapplication/jsonをデフォルトの MIME Type として扱うため、JSON をレスポンスとしたい場合はResponse型を使用せず dictまたはlistを返却することも可能です。

例外

ABEJA Platform が定義する例外をハンドラー関数内で投げることで例外の種類に対応する処理が行われます。

定義された以下の例外クラスが、コンテキストデータの exceptionsプロパティに含まれます。

  • context.exceptions.InvalidInput
  • context.exceptions.ModelError

モデルハンドラー関数の実装例

以下はモデルハンドラー関数の簡単な実装例です。

CLI のrun-localコマンドを使うことで動作を確認することができます。

JSON を入力し JSON を返却する

入力された JSON に対し簡単な計算を行い、JSON のレスポンスを返す例です。

入力データを含む イテレータinput_iterから入力となるデータを以下のように取得します。

JSON 出力時には、 出力したいdictまたはlist型のデータを含むcontext.datatypes.Response型を返却するジェネレータを実装します。その際 metadataに以下のように Content-Typeapplication/jsonとなるように返却します。

def handler(input_iter, context):
    for line in input_iter:
        total_without_tax = line['total_without_tax']
        total_without_tax = int(total_without_tax)
        total = total_without_tax * 1.08
        yield context.datatypes.Response([{'total': total}], metadata=[('Content-Type', 'application/json')])

また、 JSON をレスポンスとする場合、Response型を返却せず以下のように省略して書いた場合も上記と同じレスポンスを返します。

def handler(input_iter, context):
    for line in input_iter:
        total_without_tax = line['total_without_tax']
        total_without_tax = int(total_without_tax)
        total = total_without_tax * 1.08
        yield {'total': total}

run-localコマンドの実行結果です。 request.jsonを入力とした出力結果が表示されます。

$ abeja model run-local --image abeja/all-cpu:18.10 --handler main:handler --input request.json --quiet
{
    "total": 108.0
}

画像を入力とし画像を返却する

入力された画像を左右反転して返す例です。

JSON と同様に入力データを含むイテレータinput_iterから入力となるデータを以下のように取得します。

反転した入力画像をbytes型としてcontext.datatypes.Response型に含め返却するジェネレータを実装しています。この例では 出力が JPEG 画像であるため metadataに以下のように Content-Typeimage/jpegとして設定します。

import cv2

def handler(input_iter, context):
    for img in input_iter:
        flipped_image = cv2.flip(img, 1)
        _, encoded_flipped_image = cv2.imencode('.jpeg', cv2.cvtColor(flipped_image, cv2.COLOR_BGR2RGB))

        yield context.datatypes.Response(
            [encoded_flipped_image.tostring()],
            metadata=[('Content-Type', 'image/jpeg')]
        )

run-localコマンドの実行結果です。 入力であるcat.jpegを反転した画像が出力されます。

$ abeja model run-local --image abeja/all-cpu:18.10 --handler main:handler --input cat.jpeg --quiet > flipped_cat.jpeg
$ open flipped_cat.jpeg

学習結果を使用し画像から JSON を返却する

学習の結果として出力された HDF5 ファイルを用いたモデルの推論結果を JSON で返却します。

ABEJA_TRAINING_RESULT_DIRに展開される学習結果から HDF5 ファイルを読み込み推論を行います。 この際、学習時に出力ディレクトリに保存した HDF5 ファイルと同じファイル名を指定する必要があります。

import os
from keras.models import load_model
from PIL import Image

model = load_model(os.path.join(os.environ.get('ABEJA_TRAINING_RESULT_DIR', '.'), 'model.h5'))

def handler(input_iter, ctx):
    for img in input_iter:
        img = Image.fromarray(img)

        # PREPROCESS INPUT HERE
        ...

        result = model.predict(img)

        # CONVERT RESULT FOR RESPONSE
        ...

        yield {"result": result}

run-localコマンドの実行結果です。 入力であるcat.jpegを入力とした推論結果が返されます。

$ abeja model run-local --image abeja/all-cpu:18.10 --handler main:handler --input cat.jpeg
{
    "result": [
        { "label":"cat", "probability":"0.85" },
        { "label":"dog", "probability":"0.15" }
    ]
}

不正な JSON 入力を例外として通知する

context.exceptions.InvalidInputを例外として投げることで不正な入力であることをリクエスト実行者に通知します。 この場合、入力された JSON のtotal_without_taxにデータが存在しない場合を不正な入力としています。

def handler(iter, context):
    for line in iter:
        if 'total_without_tax' not in line:
            raise context.exceptions.InvalidInput('total_without_tax not in the request')
        total_without_tax = line['total_without_tax']
        total_without_tax = int(total_without_tax)
        total = total_without_tax * 1.08
        yield {'total': total}

run-localコマンドの実行結果です。 不正な入力に対しエラーが出力されていることが確認できます。

$ abeja model run-local --image abeja/all-cpu:18.10 --handler tax:handler --input request.json --quiet
[error] failed to send request : 400 Client Error: BAD REQUEST for url: http://localhost:49873/

 ------ Local Server Error ------
{"log_id": "de614348-365b-446f-99c2-44f58d57eefd", "log_level": "INFO", "timestamp": "2018-05-10T01:27:15.416427+00:00", "source": "model:run.run.203", "requester_id": "-", "message": "start executing model. version:0.9.7", "exc_info": null}
{"log_id": "ef77beac-5384-442e-b345-fde112a2952b", "log_level": "INFO", "timestamp": "2018-05-10T01:27:15.416955+00:00", "source": "model:run.run.218", "requester_id": "-", "message": "start installing packages from requirements.txt", "exc_info": null}
{"log_id": "8024e5b7-c4da-4298-8e13-7fbace8c986d", "log_level": "INFO", "timestamp": "2018-05-10T01:27:15.417698+00:00", "source": "model:run.run.224", "requester_id": "-", "message": "requirements.txt not found, skipping", "exc_info": null}
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
{"log_id": "1e730acd-4b6b-4f24-98b5-0c874c122a11", "log_level": "INFO", "timestamp": "2018-05-10T01:27:16.031969+00:00", "source": "model:http_view.endpoint.91", "requester_id": "__requester-id__", "message": "start request", "exc_info": null}
{"log_id": "d6d12c55-57e1-4ca1-bf31-9e6a1b7cb34a", "log_level": "INFO", "timestamp": "2018-05-10T01:27:16.032966+00:00", "source": "model:http_view.endpoint.132", "requester_id": "__requester-id__", "message": "UserException::InvalidInput total_without_tax not in the request", "exc_info": null}
172.17.0.1 - - [10/May/2018 01:27:16] "POST / HTTP/1.1" 400 -