Amazon SageMakerとAzure MLにおける機械学習モデルのサービング技術比較: AWS/Azureが提供するコンテナイメージを利用する場合

初版: 2021年6月7日
著者: 橋本恭佑、柿田将幸, 株式会社 日立製作所

## はじめに
本投稿では、学習済みの機械学習モデルをAWS/Azureが提供するコンテナイメージを用いてサービングする方法を紹介します。
Amazon SageMakerとAzure Machine Learning (Azure ML)で実際にサービングを試して分かった比較結果を解説します。

今回の検証では、AWS/Azureが提供するコンテナイメージをコンテナ基盤にデプロイするときに、必要なライブラリの追加またはバージョン変更を行い、作成済みの前処理・モデル・後処理を追加する方法(下記表1の赤枠部分)を紹介します。

表1: オンプレミス環境で学習させた機械学習モデルをAWSおよびMicrosoft Azure上でサービングする際のサービス一覧(2021年2月現在)
![fig11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592367/f642cac9-a186-676f-b41d-2d27806b0025.png)


### 投稿一覧
* [AWSとMicrosoft Azureにおける機械学習モデルのサービング技術の概要](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33)
* Amazon SageMakerとAzure MLにおける機械学習モデルのサービング技術比較: AWS/Azureが提供するコンテナイメージを利用する場合・・・本投稿
* [Amazon SageMakerとAzure MLにおける機械学習モデルのサービング技術比較: 自作コンテナイメージを利用する場合](https://qiita.com/kyosuke_hashimoto/items/91ffa607d1f37a2d0885)
* [AWS FargateとAzure Container Instancesにおける機械学習モデルのサービング技術比較](https://qiita.com/kyosuke_hashimoto/items/eb6dc3aa781d4191a8b6)

## AWSとAzureにおけるパターン1の実現技術の違い

パターン1を実現する場合の手順を図1に、パターン1を実現する場合に利用する技術をAWSとAzureで比較した結果を表2に示します。
Amazon SageMakerとAzure MLの双方で、機械学習モデルのサービングに必要なサービスが提供されておりますが、AWSには手順2を実現する手段がないことや、モデルのアップロード先が双方のパブリッククラウドで異なるなどの差があります。本稿ではこの手順に沿ってパターン1が実現可能かを実機検証した結果を紹介します。

![fig31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/592367/df91f3a4-16bf-5609-68d5-0f23dbcf9997.png)

図1: パブリッククラウドにおいてパターン1を実現する場合の手順

表2: AWSとAzureでパターン1を実現する場合に利用する技術の比較

| 項番 | 手順 | AWSで手順を実現する技術 | Microsoft Azureで手順を実現する技術  | 
| --- | --- | --- | --- | 
|  1   | コンテナイメージを指定する | AWSが用意しているコンテナイメージの中から推論に必要最低限のライブラリと、機械学習モデルの用途に合うライブラリが入ったコンテナイメージをAmazon SageMakerのノートブックインスタンス上で指定する | Azureが用意しているコンテナイメージの中から推論に必要最低限のライブラリがインストールされたコンテナイメージをAzure MLのノートブックインスタンス上で暗黙的に指定する | 
|  2   | 不足するライブラリがあれば追加する | 対応技術なし(Amazon SageMakerのノートブックインスタンス上でライブラリを追加できない)  | Azure MLのノートブックインスタンス上でライブラリを追加する  | 
|  3   | モデルをアップロードし、前処理・モデル・後処理を呼び出す推論コードを書き換える | Amazon S3にモデルをアップロードし、Amazon SageMakerのノートブックインスタンス上で推論コードを書き換える | Azure MLのノートブックインスタンス上にモデルをアップロードし、推論コードを書き換える  | 
|  4   | コンテナイメージ・モデル・推論コード・コンテナ基盤を指定しコンテナを立ち上げる | Amazon SageMakerのノートブックインスタンス上でコンテナイメージ・モデル・推論コード・コンテナ基盤を指定する | Azure MLのノートブックインスタンス上でコンテナイメージ・モデル・推論コード・コンテナ基盤を指定する  | 


### 本検証の事前準備

本投稿では、以前の投稿の[検証シナリオ](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33#%E3%82%B5%E3%83%BC%E3%83%93%E3%83%B3%E3%82%B0%E6%A4%9C%E8%A8%BC%E3%81%AE%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA)で作成した下記ファイルを利用しますので、事前に用意してください。

* 学習済みの手書き文字認識モデル: [`model.joblib`](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33#%E3%82%B5%E3%83%BC%E3%83%93%E3%83%B3%E3%82%B0%E6%A4%9C%E8%A8%BC%E3%81%AE%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA)
* 前処理・モデル利用・後処理を記載したスクリプト: [`api_server.py`](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33#%E3%82%B5%E3%83%BC%E3%83%93%E3%83%B3%E3%82%B0%E6%A4%9C%E8%A8%BC%E3%81%AE%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA)

## Amazon SageMakerでサービングする場合

学習済みの手書き文字認識モデルを、AWSが提供するコンテナイメージでサービングできるか検証しました。

Amazon SageMakerのノートブックインスタンスを起動して、作成した機械学習モデルをノートブックインスタンスへアップロードします。
それから、下記コードをノートブックインスタンス上のJupyter Notebookから実行すると、ノートブックインスタンスからS3ストレージへ機械学習モデルをアップロードできます。下記コードを利用せずに機械学習モデルを直接S3ストレージへアップロードしても問題ありません。

```python
from sagemaker import get_execution_role
from sagemaker.sklearn.model import SKLearnModel
import sagemaker

sagemaker_session = sagemaker.Session()
role = get_execution_role()

# ノートブックインスタンスを起動したときにアタッチされるS3ストレージのimportディレクトリへ、
# 機械学習モデルをアップロードする
uploaded_model = sagemaker_session.upload_data(path='model.tar.gz', key_prefix='import')
print(uploaded_model)

# 実行後、機械学習モデルがアップロードされたディレクトリ名を確認できる。例えば下記など。
# 's3://sagemaker-ap-northeast-1-aws_account_id/import/model.tar.gz'
```

次に、[前回の投稿](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33)に示した推論用のソースコード(api_server.py)を、SageMakerで実行可能な形式に書き換えます。下記に書き換え後のソースコード(entry.py)を示します。

```python:entry.py
import os
import joblib
from sklearn.neural_network import MLPClassifier
import numpy as np

import sys
from six import BytesIO
from PIL import Image

def model_fn(model_dir):
    clf = joblib.load(os.path.join(model_dir, "model.joblib"))
    return clf

def _npy_loads(data):
    """
    Deserializes npy-formatted bytes into a numpy array
    """
    stream = BytesIO(data)
    return np.load(stream)

def input_fn(request_body, request_content_type):
    if request_content_type == 'application/x-npy':
        return _npy_loads(request_body)
    elif request_content_type == "image/jpeg":
        image_number = np.array(Image.open(request_body).convert('L'))
        array = [image_number.reshape(RESHAPED)]
        return array
    else:
        raise ValueError('Content type must be application/x-npy')

def predict_fn(input_data, model):
    prediction = model.predict(input_data)
    return prediction[0]
```

ここでポイントとなるのは以下の2点です。
1. 機械学習モデルのロード、入力データの変換、推論をmodel_fn, input_fn, predict_fnと各々で別の関数に分けて書きます。前処理はinput_fn、後処理はpredict_fnへ記入することで、それぞれ前処理と後処理を推論コンテナへ実装できます。元のapi_server.pyから書き換える必要があります。
2. input_fn関数の引数に、入力データ(request_bodey)だけでなく、入力データで受け付けるデータの形式(request_content_type)をとります。そして、入力データで受け付けるデータの形式(たとえばapplication/x-npyやimage/jpeg)ごとに前処理を書きます。こうすることで、サービングした後の機械学習モデルが複数の形式の入力データを受け付けることが可能となります。

次に、AWSが提供するコンテナイメージの中から、今回の機械学習モデルの用途に合ったコンテナイメージを指定します。AWSではApache MXNet、tensorflow、scikit-learnなど複数の機械学習向けライブラリについて、推論環境に利用できるコンテナイメージを提供しています。今回はオンプレミス環境で機械学習モデルを作成するときに用いたライブラリである、scikit-learnがインストールされたサービング用コンテナイメージを利用します。前処置・後処理・モデルを内包するscikit-learnの入った推論用コンテナをデプロイするには、ノートブックインスタンス上のJupyter Notebookから下記のコードを実行し、上記の推論用コード(entry.py)・モデルのファイル・scikit-learnのバージョンを指定します。

```python
from sagemaker.sklearn.model import SKLearnModel

# SKLearnModelのコンテナイメージを利用して、推論用コードを内包させる
sklearn_model = SKLearnModel(model_data='s3://sagemaker-ap-northeast-1-aws_account_id/import/model.tar.gz',
                             role=role,
                             entry_point="./entry.py",
                             framework_version="0.20.0")

# 推論用コードを内包させたコンテナイメージをデプロイする
predictor = sklearn_model.deploy(instance_type="ml.c4.xlarge", initial_instance_count=1)
```

これでサービング処理の記述は完了です。推論コンテナのデプロイが成功するかを確認します。

```
# CloudWatchから確認できるログ
Traceback (most recent call last):
  File "/miniconda3/lib/python3.7/site-packages/sagemaker_containers/_modules.py", line 258, in import_module
    module = importlib.import_module(name)
  File "/miniconda3/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/miniconda3/lib/python3.7/site-packages/fail_entry.py", line 7, in <module>
    from PIL import Image
ModuleNotFoundError: No module named 'PIL'
```

上記の実行結果より、AWSが提供するコンテナイメージには画像の前処理に用いるPillowのライブラリが含まれず、処理が完了していないことがわかります。エラーを解消するために、Pillowと同等の機能を持つ画像処理ライブラリ(例えばscikit-image)を利用可能か試しましたが、2021年2月現在、AWSの提供するscikit-learn用イメージコンテナには、scikit-imageなどの画像処理ライブラリが含まれていませんでした。

また、コンテナイメージをコンテナ基盤にデプロイするときに、pip, condaといったパッケージ管理ツールやAWSが提供するAPIを用いて必要なライブラリ(今回の場合は例えばpillowなど)を追加できるかを調査しましたが、2021年2⽉現在、そのような機能は提供されていませんでした。

以上の検証から、AWSにおいて「パブリッククラウドが提供するコンテナイメージをコンテナ基盤にデプロイするときに、ライブラリの追加またはバージョン変更して、前処理・モデル・後処理を追加する」ことはできないことがわかりました。

## Azure MLでサービングする場合

学習済みの手書き文字認識モデルを、Azureが提供するコンテナイメージでサービングできるか検証しました。

Azure Machine Learningのノートブックインスタンスを起動し、ノートブックインスタンスへ作成した機械学習モデルをアップロードします。アップロードはOSSのJupyterと同様に、ブラウザからアップロード可能でした。

次に、[前回の投稿](https://qiita.com/kyosuke_hashimoto/items/cf2aaef31115f2ceca33)に示した推論用のソースコード(api_server.py)を、Azure Machine Learningで実行可能な形式に書き換えます。下記に書き換え後のソースコード(score.py)を示します。

```python:score.py
import os
import joblib
import numpy as np
import json
from PIL import Image
from io import BytesIO
import base64

# 推論開始時に呼び出される関数
def init():
    global model

    # モデルのPATHを指定
    model_path = os.path.join(os.getenv('AZUREML_MODEL_DIR'), 'model3.joblib')
    # モデルをロード
    model = joblib.load(model_path)


# 推論リクエストがあった場合の処理を定義
def run(raw_data):
    try:
        # JSONリクエストのtext propertyを抽出
        data = json.loads(raw_data)['data']
        img = base64.b64decode(data) # base64に変換された画像データを元のバイナリデータに変換
        img = BytesIO(img) # _io.BytesIO pillowで扱えるように変換

        RESHAPED = 784
        image_number = np.array(Image.open(img).convert('L'))
        array = [image_number.reshape(RESHAPED)]
        prediction = model.predict(array) # このpredictionの型はarray型
        return str(prediction[0]) # array型がJSONでシリアライズ不可のためstr型に変換

    except Exception as e:
        error = str(e)
        print('array: {0}'.format(array))
        return json.dumps({"error": error})

```

ここでポイントとなるのは以下の2点です。
1.	機械学習モデルのロードと、入力データの変換・推論をinit, runの2つの関数に分けて書きます。Amazon SageMakerにおける書き換え時と比べて、前処理や後処理を厳密に分ける必要がないといえます。
2.	Azure MLの仕様上、run関数の入力データ(raw_data)および出力データの型はJSONのみです。したがって、今回の様に画像を扱う場合は、JSON化された入力データに含まれる画像のデータを読み込む前処理が必要です。同様に、推論結果もJSONでシリアライズ可能な形式で出力する必要があります。

次に、推論に必要最低限のライブラリが入ったコンテナイメージをノートブックインスタンス上で暗黙的に指定し、推論用のスクリプト(score.py)と、推論に利用するライブラリ群を紐づけた、前処理・後処理・ライブラリに関連するコンフィグ(inference_config)のオブジェクトを生成します。
Azure Machine Learningのノートブックインスタンス上のJupyter Notebookで下記のコードを実行します。

```python
from azureml.core import Workspace
from azureml.core.model import Model

from azureml.core import Workspace, Environment

ws = Workspace.from_config()

from azureml.core.model import InferenceConfig
from azureml.core.environment import Environment
from azureml.core.conda_dependencies import CondaDependencies

# 推論に必要最低限のライブラリが入ったコンテナイメージを暗黙的に指定する
myenv = Environment(name="oss_center")
conda_dep = CondaDependencies()

# 推論に利用するライブラリ群を定義
conda_dep.add_conda_package("numpy")
conda_dep.add_conda_package("scikit-learn")
conda_dep.add_conda_package("pillow")
conda_dep.add_conda_package("joblib")

myenv.python.conda_dependencies=conda_dep

# 推論用のスクリプトと推論に利用するOSS群を紐づけた、前処理・後処理・ライブラリに関連するコンフィグ(inference_config)のオブジェクトを生成
inference_config = InferenceConfig(entry_script="score.py", environment=myenv)
```

最後に、推論のためのコンテナのスペックのコンフィグ(deployment_config)のオブジェクトを生成し、上記で生成した、前処理・後処理・ライブラリに関連するコンフィグ(inference_config)と紐づけて、推論のためのコンテナをデプロイします。
Azure Machine Learningのノートブックインスタンス上のJupyter notebookから下記のコードを実行すると、推論のためのコンテナをAzure Container Instanceで起動できます。

```python
from azureml.core.webservice import AciWebservice, Webservice

# 推論のためのコンテナのスペックのコンフィグのオブジェクトを生成
deployment_config = AciWebservice.deploy_configuration(cpu_cores = 1, memory_gb = 1)

model = Model(ws, name='hash-mnist')

# 前処理・後処理・ライブラリに関連するコンフィグ(inference_config)とdeploymet_configを紐づけて、推論のためのコンテナをデプロイ
service = Model.deploy(ws, 'hash-mnist-first', [model], inference_config, deployment_config)

service.wait_for_deployment(True)
print(service.state)
print("scoring URI: " + service.scoring_uri)
# 下記の様な出力が得られることを確認する
# Running............................................................................
# Succeeded
# ACI service creation operation finished, operation "Succeeded"
# Healthy
# scoring URI: http://7032b584-13d9-4bcb-a096-9fe0d9dc4941.japaneast.azurecontainer.io/score

```

これでサービング完了です。推論が成功するかを確認します。SageMakerの場合と違って、画像送信時に画像をJSONで送信可能な型に変換する必要があることに注意してください。

```python
import requests
from PIL import Image
import json
import base64
from io import BytesIO

img = Image.open("x_test_50.jpeg")

# PillowImageをbytesに変換してさらにbase64に変換
buffered = BytesIO()
img.save(buffered, format="JPEG")
img_byte = buffered.getvalue() # bytes
img_base64 = base64.b64encode(img_byte) # base64でエンコードされたbytes ※strではない

# まだbytesなのでjson.dumpsするためにstrに変換(jsonの要素はbytes型に対応していないため)
img_str = img_base64.decode('utf-8') # str

files = {
    "data":img_str
    } 

response = requests.post(
    'http://7032b584-13d9-4bcb-a096-9fe0d9dc4941.japaneast.azurecontainer.io/score',
    json.dumps(files),
    headers={'Content-Type': 'application/json'})
pprint.pprint(response.json())
# ‘6’と返答が返ってくることを確認する

```

これで動作検証が完了し、サービングが成功したことを確認できました。以上の検証結果から、Azure Machine Learningでは「パブリッククラウドが提供するコンテナイメージをコンテナ基盤にデプロイするときに、ライブラリの追加またはバージョン変更して、前処理・モデル・後処理を追加する」が可能であることを確認できました。

## 検証結果の考察
パブリッククラウドの提供する汎用のコンテナイメージに推論に必要なライブラリが含まれない場合も、後からライブラリの追加が可能であることから、「パブリッククラウドが提供するコンテナイメージをコンテナ基盤にデプロイするときに、ライブラリの追加またはバージョン変更して、前処理・モデル・後処理を追加する」パターンの実現手段はAzure Machine Learningに限られることがわかりました。この手法は、初回デプロイ時に追加で開発する項目が最も少なく、モデルを学習させてからサービング完了までのリードタイムを短くできるため、顧客にすぐに推論システムを提供する必要あるユースケースに最も適しています。

## おわりに

次回の投稿において、パブリッククラウドが提供するコンテナイメージを利用せず、オンプレミス環境で必要なライブラリを含んだコンテナイメージを作成して、コンテナ基盤へデプロイするときに、前処理・モデル・後処理を追加する手法について、Amazon SageMakerとAzure Machine Learningで実証した結果を解説します。