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

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

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

def handler(request, context):
    ...
    return response
  • request : 入力データがこのパラメータに格納されます。このパラメータは dict であり、また file-like object のように振る舞います。
  • context : 実行時のメタデータなどがこのパラメータに格納されます。

ハンドラーの引数 

入力データ

ハンドラー関数の第一引数には入力データが渡されます。 入力データに含まれるデータの形式は Content-Type 毎に異なります。

multipart/form-data 以外

入力データからは以下の要素を取得できます。

キー 内容
http_method Request HTTP Method
content_type Content-Type
headers Request Header。ヘッダー名及び値を含む dict のリストになっていて、ヘッダー名はすべて小文字に変換されています。 Authorization ヘッダーを含む一部のヘッダーは削除されます
read() リクエスト本文

以下のようにして、各値を取得できます。

>>> request['http_method']
'POST'
>>> request['content_type']
'application/json'
>>> pprint.pprint(request['headers'])
[{'key': 'content-type', 'values': ['application/json']},
 {'key': 'accept-encoding': 'values': ['gzip, deflate, br']}, ...]
>>> request.read()
b'{"foo": "bar"}'

リクエスト本文( GET ならリクエストパラメーター、POST PUT ならリクエストボディ)は、常にバイナリデータとして読み出されます。

multipart/form-data

入力データからは以下の要素を取得できます。

キー 内容
http_method Request HTTP Method
content_type Content-Type
headers Request Header。ヘッダー名及び値を含む dict のリストになっていて、ヘッダー名はすべて小文字に変換されています。 Authorization ヘッダーを含む一部のヘッダーは削除されます
contents Contents

Contents は 各 part 毎の Content の配列です。 各 Content からは以下の要素を取得できます。

キー 内容
content_type Content-Type
form_name フォーム名
file_name ファイル名
read() コンテンツ本文

以下のようにして、各値を取得できます。

>>> request['http_method']
'POST'
>>> request['content_type']
'multipart/form-data'
>>> contents = request['contents']
>>> contents[0]['content_type']
'application/json'
>>> contents[0]['form_name']
'file1'
>>> contents[0].read()
b'{"foo": "bar"}'
>>> contents[1]['content_type']
'image/jpeg'
>>> contents[1]['file_name']
'cat.jpg'
...

コンテキストデータ

ハンドラー関数の第二引数には実行時のメタデータなどが渡される予定です。 現在、この変数は空の dict です。

ハンドラーの環境変数

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

環境変数名 説明
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 環境変数に格納されたディレクトリに展開されます。

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

返り値

データを出力するためには、以下の要素を含む dict を返却します。

キー 内容
status_code HTTP Status Code
content_type Content-Type
content レスポンス本文
metadata レスポンスヘッダーで返却したい要素の dict

status_code を指定しない場合は、デフォルト値として http.HTTPStatus.OK が使用されます。 content_type を指定しない場合は、デフォルト値として text/plain が使用されます。

例えば以下のように返却します。

import http

def handler(request, context):

    # some inference process...

    content = {
        'transaction_id': 1234567890,
        'category_id': 10,
        'predictions': predictions,
    }
    return {
        'status_code': http.HTTPStatus.OK,
        'content_type': 'application/json; charset=utf8',
        'content': content
    }

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

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

JSON を入力し JSON を返却する

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

入力データから入力となるデータを以下のように取得します。

import json
import http

def handler(request, context):
    req_data = json.load(request)
    total_without_tax = req_data['total_without_tax']
    total_without_tax = int(total_without_tax)
    total = total_without_tax * 1.08

    return {
        'status_code': http.HTTPStatus.OK,
        'content_type': 'application/json; charset=utf8',
        'content': {'total': total}
    }

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

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

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

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

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

反転した入力画像を bytes 型として content に含め返却しています。この例では 出力が JPEG 画像であるため content_type に以下のように image/jpeg を設定します。

import cv2
import http
import numpy as np
from io import BytesIO
from PIL import Image

def handler(request, context):
    data = request.read()
    imgfile = BytesIO(data)
    img_pil = Image.open(imgfile)
    img_numpy = np.asarray(img_pil)
    flipped_array = cv2.flip(img_numpy, 1)
    flipped_image = Image.fromarray(flipped_array)
    retBytes = BytesIO()
    flipped_image.save(retBytes, format='JPEG', quality=95)
    retBytes.seek(0)

    return {
        'status_code': http.HTTPStatus.OK,
        'content_type': 'image/jpeg',
        'content': retBytes
    }

また、画像処理ライブラリとして Pillow を利用しているので、 requirements.txt (または Pipfile ) で Pillow のインストールを指定します。

requirements.txt の場合

Pillow==6.0.0

Pipfile の場合

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
Pillow = "==6.0.0"

[requires]
python_version = "3.6"

run-local コマンドの実行結果です。入力である cat.jpg を反転した画像が flipped_cat.jpg として出力されます。 ( --quiet オプションを指定しないと、ログも出力されてしまうため JPEG ファイルにならないので、ご注意ください)

$ abeja model run-local -h main:handler -i abeja/all-cpu:19.04 --input cat.jpg --quiet > flipped_cat.jpg

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

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

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

import os
import numpy as np
from io import BytesIO
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(request, ctx):
    data = request.read()
    imgfile = BytesIO(data)
    img_pil = Image.open(imgfile)
    img_numpy = np.asarray(img_pil)

    ...

    result = model.predict(img)

    # CONVERT RESULT FOR RESPONSE
    ...

    return {
        'status_code': http.HTTPStatus.OK,
        'content_type': 'application/json; charset=utf8',
        'content': result
    }

run-local コマンドの実行結果です。入力である cat.jpg を入力とした推論結果が返されます。 (環境変数 ABEJA_TRAINING_RESULT_DIR には、初期値として abejainc_training_result が設定されるので、指定しない場合は <カレントディレクトリ>/abejainc_training_result/ 配下から HDF5 ファイルを検索することに注意してください)

$ abeja model run-local -h main:handler -i abeja/all-cpu:19.04 --input cat.jpg --environment ABEJA_TRAINING_RESULT_DIR:. --quiet
{
    "result": [
        { "label":"cat", "probability":"0.85" },
        { "label":"dog", "probability":"0.15" }
    ]
}

不正な JSON 入力を通知する

status_code を設定することで不正な入力であることをリクエスト実行者に通知します。 この場合、入力された JSON の total_without_tax にデータが存在しない場合を不正な入力としています。

import json
import http

def handler(request, context):
    req_data = json.load(request)
    if 'total_without_tax' not in req_data:
        return {
            'status_code': http.HTTPStaus.BAD_REQUEST,
            'content_type': 'application/json; charset=utf8',
            'content': {'message': 'total_without_tax is need'}
        }

    ...

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

$ abeja model run-local --image abeja/all-cpu:19.04 --handler main:handler --input request.json --quiet
[error] failed to send request : 400 Client Error: Bad Request for url: http://localhost:62910/

 ------ Local Server Error ------
{"level":"info","msg":"proxy version: [0.4.1] start run.","time":"2019-05-23T09:29:06Z"}
{"level":"debug","msg":"call handleSignal","time":"2019-05-23T09:29:06Z"}
{"level":"debug","msg":"handleSignal: create channel for signal","time":"2019-05-23T09:29:06Z"}
{"level":"debug","msg":"runtime bootstrapping yet...","time":"2019-05-23T09:29:06Z"}
{"address":":5000","level":"debug","msg":"start listen.","time":"2019-05-23T09:29:06Z"}
{"level":"debug","msg":"handleSignal: channel for signal created","time":"2019-05-23T09:29:06Z"}
{"level":"debug","msg":"waiting signal...","time":"2019-05-23T09:29:06Z"}
{"log_id": "b94bba13-7046-4697-8417-8ab2f5020387", "log_level": "INFO", "timestamp": "2019-05-23T09:29:07.194826+00:00", "source": "runtime:run.main.26", "requester_id": "-", "message": "start executing model with abeja-runtime-python36 (version: 0.1.4)", "exc_info": null}
INFO: requirements.txt/Pipfile not found, skip installing dependencies process
INFO: Loading user model handler with main:handler...
{"level":"debug","msg":"runtime started","time":"2019-05-23T09:29:07Z"}
{"level":"debug","msg":"response body length = 157","time":"2019-05-23T09:29:08Z"}
{"level":"debug","msg":"response body = {\"content_type\": \"application/json; charset=utf8\", \"metadata\": {\"x-abeja-sys-meta-runtime-version\": \"0.1.4\"}, \"path\": \"/tmp/tmpgl8iizco\", \"status_code\": 400}","time":"2019-05-23T09:29:08Z"}