Azure Machine Learning で自作モデルをデプロイする

# はじめに
この記事では Azure Machine Learning（以下、Azure ML）を利用したモデルの学習、そのモデルを推論サービスとしてデプロイする手順を紹介する。今回は Kaggleの [CommonLit Readability Prize](https://www.kaggle.com/c/commonlitreadabilityprize)という自然言語処理系のコンペを題材にして進めた。なお、 Azure ML上の作業の大部分は MicroSoftのドキュメントに沿って進めているため、手順があまり変わらない部分は Tipsのみの記述としている。また、基本的な流れは以下の 4ステップになる。
　1. 環境準備： Azure MLを利用するための準備
　2. データ準備：モデルを学習させるデータの準備
　3. 学習：モデルの作成
　4. デプロイ：作成したモデルをサービスとしてデプロイする


# 環境準備
## Azure ML ワークスペースの作成  
まず、Azure ML 上のリソースを管理する最上位リソースであるワークスペースを作成する。手順は[こちらの](https://docs.microsoft.com/ja-jp/azure/machine-learning/concept-workspace)ドキュメントを参照してほしい。ワークスペース作成時にリージョンを設定するが、リージョンごとに提供されているコンピューティングリソースが異なるので、事前に GPU利用数や型等を想定しておくと良い。リージョンとコンピューティングリソースの対応は[リージョン別の利用可能な製品]
(https://azure.microsoft.com/ja-jp/global-infrastructure/services/?products=virtual-machines)にてコンピューティング→仮想マシンと選択することで調べることができる。

## クォータ引き上げ申請  
利用したいコンピューティングマシンのクォータが制限されている場合がある。その場合はコンピューティングメニューの "クォータを表示" タブからクォータの要求を行う（[手順の参考リンク](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-manage-quotas))。 Basicサポートプランの場合、一日程度で承認のメールが届いた。

# データ準備
題材としているコンペでは以下の3ファイルが提供されている。  

* train.csv：学習データ
* test.csv：評価用データ
* sample_submit.csv ：提出ファイルのサンプル

その他、文章を分散表現に変換する際に事前学習済みモデルのファイルも利用するが、こちらは次節の学習で取り扱う。上記3ファイルを個別にAzure ML のデータセット機能で登録していく（[手順の参考リンク](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-connect-data-ui#create-datasets)）。今回はローカルからアップロードする方法で登録した。登録手順の途中、"フォルダのアップロード” 、”ファイルのアップロード” という選択肢があるが、同じ列構成のデータが複数ファイルある場合、フォルダごとアップロードすると手間が省ける。また、今回のデータは "excerpt" という列に文章が格納されている。文章が ",” で区切られる場合があり、この "," がCSVの区切りと認識され、デフォルトでは正しく登録できなかった。そのようなデータの場合は登録時の設定とプレビューにて　"データセットに複数行のデータが含まれています” にチェックを入れると正しく登録できる。

# 学習
## コンピューティングリソースの準備  
コンピューティングメニューで事前に利用したコンピューティングインスタンスを作成しておく（[手順の参考リンク](https://docs.microsoft.com/ja-jp/azure/machine-learning/concept-compute-instance)）。十分なクォータがない場合は環境準備にあるクォータ引き上げ申請を行う。

## Notebooks での学習
ここまでで環境、データの準備ができたので、Azure MLの Notebooks機能を利用してモデルを学習させる （[手順の参考リンク](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-run-jupyter-notebooks) ）。手元に学習スクリプトファイルがある場合は Notebooks機能のツールバーの "ファイルを追加" でアップロードする。また、事前学習モデルのファイル参照等、学習データ以外にもファイルが必要な場合がある。その際にも同様の手順でアップロードする。(フォルダごとアップロードもできる。)　以下は学習時のフォルダ構成の参考である。

```:【参考】フォルダ構成  
Users
└　username
　├　train.ipynb : 学習スクリプト
  └　pretrainFolder　: 学習データ以外の参照ファイル格納フォルダ
    ├　config.json
    ├　tokenizer_config.json
   　...
```

学習スクリプトから Azure ML上のリソース（ワークスペースや登録したデータセット等)にアクセスするために Azure　ML SDK を読み込む。SDK 自体は既に Notebooks環境にインストールされている。以下に SDKのインポートとワークスペースへの接続の例を示す。


<!-- なお、既定で選択される実行環境(docker image)は2021.7時点で以下であった。-->

```python
from azureml.core import Workspace
from azureml.core import Dataset

#id設定
subscription_id = '自身のサブスクリプションID'
resource_group  = 'ワークスペースが属するリソースグループID'
workspace_name  = 'ワークスペースの名前'
#ワークスペースへの接続
workspace = Workspace(subscription_id = subscription_id, resource_group = resource_group, workspace_name = workspace_name)
# データセットの取得
train_df = Dataset.get_by_name(workspace, name='データセット登録の時定義した名前')
# pandasへの変換
train_df = train_df.to_pandas_dataframe()
```

ここでデータセットを読み込むために azureml.core から Dataset というモジュールをインポートしている。pytorch を利用している方は torch.utils.data から Dataset をインポートすることもあるかと思うが、**名前が被る**のでリネーム等して回避しなければならない。また、データセットの読み込みだが、戻り値は Azure のデータセットオブジェクトなので、pandas等に変換して利用する（[Dataset詳細](https://docs.microsoft.com/ja-jp/python/api/azureml-core/azureml.core.dataset.dataset?view=azure-ml-py)）。

<!-- ### [Todo]ジョブ実行 -->

# デプロイ
今回はまずローカルにデプロイしデバックした後、クラウド上にデプロイする。以下の2ドキュメントを参考にデプロイを実行した。
[機械学習モデルを Azure にデプロイする](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-deploy-and-where?tabs=azcli)
[ローカルでのモデル デプロイを使用したトラブルシューティング](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-troubleshoot-deployment-local)

デプロイに必要なコンポーネントは以下である。事前準備を行った後、新規作成部分の紹介をする。
 　　1. モデル　・・・作成済み
 　　2. 推論ファイル (score.py) ：リクエスト処理を記述するファイル　　・・・**新規作成**
 　　3. 推論構成ファイル (inferenceconfig.json) ：推論実行環境を定義するファイル　　・・・**新規作成**
 　　4. デプロイ構成ファイル ( (local_)deploymentconfig.json) ：デプロイ先を定義するファイル　・・・**新規作成**

## ローカルへのデプロイ  

#### cliのインストール
先ず、ローカル環境に azure cli をインストールする。インストールは[こちら](https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli)を参考に実施した。次に、cli で Azure ML の機能を利用するために azure-cli-ml の拡張機能をインストールする。2021.7 時点で azure-cli-ml のバージョンは 1.0、2.0(プレビュー)があり、実行コマンドも多々異なっている。今回は 1.0の拡張機能をインストールして進めた。（[インストールの参考](https://docs.microsoft.com/ja-jp/azure/machine-learning/reference-azure-machine-learning-cli) ）
2.0(プレビュー)でデプロイしたい方はこちらのドキュメントが参考になる([cli ver2.0のデプロイ手順](https://docs.microsoft.com/ja-jp/azure/machine-learning/how-to-deploy-managed-online-endpoints)）。

#### モデルの登録
デプロイ時に Azure ML に登録されているモデルが利用されるので、事前にモデルを Azure ML に登録しておく必要がある。モデルの登録はローカルから cli で登録する方法と Azure ML のモデル機能から登録する方法がある。ここでは cli で登録する方法を示す。cli 利用時にはログイン等の初期設定を行う必要がある。その後、モデルの登録コマンドを実行する。引数の -n は登録するモデルの名前、-p はモデルファイル(もしくはフォルダ)のローカル上のパスである。パス指定でフォルダを選択すればフォルダ内のファイルをまとめて一つのモデルとして登録できる。複数モデルをアンサンブルして推論したい時はフォルダに複数モデルファイルを格納し、まとめて登録すると良い。また、推論時にモデルファイル以外にも事前学習済みモデルの関連ファイルを参照したい場合にも、それらのファイルをモデルと同じフォルダに格納して登録すると良い。今回のケースでは cross validation を5回に設定して学習を実施したので、モデルファイル × 5個と[Notebooksでの学習](## Notebooksでの学習)でも利用した pretrainFolder を models というフォルダーに格納しまとめて登録した。

```
#　cli 利用時の初期設定
az login
az account set -s <自身のサブスクリプションID>
#　モデル登録コマンド
az ml model register -n mymodel -p ./models
```

### 推論ファイルの用意
次に、モデルを Web サービスとしてデプロイする際に、リクエストの推論データを受け取り、推論を実施し、推論結果をレスポンスとして返す処理を書いた推論ファイルを用意する必要がある。推論ファイルには init と run の２つの関数を定義する必要がある。

* init()　　
サービスのデプロイ時に一度実行される。モデルの読み込みや CPU、GPU 等デバイスへの配置処理等を定義する。デプロイ時にAzure ML 上に登録されているどのモデルを使用するか指定するが、指定したモデルのパスは ”AZUREML_MODEL_DIR” という環境変数で取得できる。

* run(request) 
リクエストからデータを受け取り、モデルで推論処理を実行しレスポンスとして返す処理を定義しておく。引数はリクエストのオブジェクト。リクエストがくる度にこの関数が実行される。

その他、推論処理の補助関数もこの推論ファイルの中に定義しておく。以下は本ケースにおける推論ファイル (score.py) の参考である。

```python:score.py
import os
import json
import numpy as np
import pandas as pd
 
import torch.nn as nn
from torch.utils.data import Dataset
from torch.utils.data import DataLoader

from transformers import AutoTokenizer,AutoModel,AutoConfig

def init():
    ###########################
    # モデルの読み込み処理。複数モデルでアンサンブルする場合はこのブロックをモデル数分繰り返すことで扱える。
    ###########################
    global model_1
    # モデルのパス定義。"AZUREML_MODEL_DIR"で指定したモデルのルートパスを取得する。以降のパスはモデルを登録したときの
    # モデル構成による。
    model_path = os.path.join(
        os.getenv("AZUREML_MODEL_DIR"), "models/model_1.pth"
    )
    # モデルの読み込み。今回はCPUを利用。GPUを利用する場合はデバイスを定義し、model.to(DEVICE)で配置する。
    model_1 = MyModel()
    model_1.load_state_dict(torch.load(model_path,map_location=torch.device('cpu')))

    ###########################
    # 事前学習モデルの読み込み
    ###########################
    global tokenizer
    #事前学習済みモデルのパス読み込み
    ROBERTA_PATH = os.path.join(
        os.getenv("AZUREML_MODEL_DIR"), "models/pretrainFolder"
    )
    #事前学習済みモデルの読み込み
    tokenizer = AutoTokenizer.from_pretrained(ROBERTA_PATH)

def run(request):
    # データの読み込み。推論のためにpandasへ変換する。
    data = json.loads(request)["data"]
    data = pd.DataFrame.from_dict(data,orient='index').T
    # 推論データをモデルが扱えるように整形する。
    test_dataset = MyDataset(data, inference_only=True)
    test_loader = DataLoader(test_dataset, batch_size=16,drop_last=False, shuffle=False, num_workers=2)
    # 推論処理。複数モデルでアンサンブルする場合はこの一行を繰り返し、平均を取る等する。
    result = predict(model_1,test_loader)
    # 結果を定義した形に整形した上で返す。
    return result.tolist()

#以下補助関数。中身は割愛。
def Mymodel(nn.Module):
...
def MyDataset(Dataset):
...
def predict(model,dataloader):
...
```

### 推論構成の定義
Webサービスを構築する環境 ( Docker コンテナ)と利用する推論ファイルを json ファイルに記載する。Web サービスを展開するときは、sourceDirectory にあるすべてのファイルを圧縮しクラウドにアップロードされるので、モデル以外の参照ファイルを sourceDirectory に格納しておく方法もある。以下は本ケースの例である。./source_dir ディレクトリの score.py ファイルによってリクエストを処理することと project_environment 環境で指定した Python パッケージを Docker イメージで使用することを指定している。

```json:inferenceconfig.json
{
    "entryScript": "score.py",
    "sourceDirectory": "./source_dir",
    "environment": {
        "docker": {
            "arguments": [],
            "baseDockerfile": null,
            "baseImage": "mcr.microsoft.com/azureml/base:intelmpi2018.3-ubuntu16.04",
            "enabled": false,
            "sharedVolumes": true,
            "shmSize": null
        },
        "environmentVariables": {
            "EXAMPLE_ENV_VAR": "EXAMPLE_VALUE"
        },
        "name": "my-env",
        "python": {
            "baseCondaEnvironment": null,
            "condaDependencies": {
                "channels": [],
                "dependencies": [
                    "python=3.6.2",
                    {
                         "pip": [
                            "azureml-defaults",
                            "nltk",
                            "numpy==1.19.5",
                            "pandas==1.1.5",
                            "transformers==4.5.1",
                            "scikit-learn==0.24.1",
                            "torch==1.7.0"
                        ]
                    }
                 ],
                "name": "project_environment"
            },
            "condaDependenciesFile": null,
            "interpreterPath": "python",
            "userManagedDependencies": false
        },
        "version": "1"
    }
}
```

### デプロイ構成の定義
Web サービスを動作させるための CPU、メモリなどのコンピューティングリソースを定義する。ローカルで実行する際には以下のようにポート番号のみを指定する。

```json:local_deploymentconfig.json
{
    "computeType": "local",
    "port": 32267
}
```
### サービスのデプロイ
以下のコマンドで今まで登録したモデル、作成した推論構成を指定してサービスをデプロイする。 -m の引数はモデルの指定であり、モデル名:バージョンの形で指定する。注意として、サービスのデプロイ実行の前にローカルで Docker Engineを起動しておく必要がある。また、デプロイ時にモデルを Azure ML からローカルにダウンロードしたり、環境をビルドするので、完了までに数分かかる。 (私の環境はネットワークが弱いので数十分要した。)

```
az ml model deploy -n myservice -m mymodel:1 --overwrite --ic inferenceconfig.json --dc local_deploymentconfig.json
```

デプロイ後、下記スクリプトのように推論データを json 形式で定義し service.run を実行することでサービスを呼び出せる。

```python:localtest.py
# 初期設定ブロック
from azureml.core import Workspace
from azureml.core.webservice import LocalWebservice
import json

ws = Workspace(subscription_id = '自身のサブスクリプションID',
               resource_group = 'workspaceが属するリソースグループID',
               workspace_name = 'workspaceの名前')
 
service = LocalWebservice(workspace=ws, name="myservice")

# サービス呼び出しブロック
data = json.dumps({"data":{
       "id": "965e592c0",
       "url_legal": "https://www.africanstorybook.org/#",
       "license":"CC BY 4.0",
       "excerpt":"Milka and John are playing in the garden. Her little sister is playing too. Milka is ready to start classes next week and it will be her first term in school. In the morning, Milka gets     up early to take a bath."
}}
)
test_sample = bytes(data, encoding='utf8')
prediction = service.run(input_data=test_sample)
print(prediction) 
```
上記のサービス呼び出しスクリプトと service の get_logs() 関数 (もしくはlocaltest.py のサービス呼び出しブロックを print(service.get_logs()) 等に編集してスクリプトを実行) を利用してローカルにて score.py や inferenceconfig.json のデバックを行っていく。score.py をデバッグした後、service.reload()関数を利用すると score.py のみデプロイ環境に反映させることができる。この関数を利用することで毎回環境全体をデプロイする必要がなくなる。なお、この関数はローカルでのみ使用可能である。reload 関数を使用する際は以下の手順でデバッグを進めた。

* localtest.py の初期設定ブロックをインタラクティブモードで実行
* localtest.py を別ウインドウで実行
* service が定義されているウインドウでget_logs を実行しエラー確認
* score.py を修正し service.reload を実行

```
az ml service get-logs -n myservice
```

### サービスの削除
デバッグが完了したらローカルにデプロイしたサービスを削除しておく。-n でサービス名を指定する。

```
az ml service delete -n myservice
```

## クラウドへのデプロイ
ローカルでデバッグが完了したらクラウドにデプロイするためにデプロイ構成を作成する。設定の詳細は[こちら](https://docs.microsoft.com/ja-jp/azure/machine-learning/reference-azure-machine-learning-cli#deployment-configuration-schema)を参照。

```json:deploymentconfig.json
{    
    "computeType": "aci",
    "containerResourceRequirements":
    {
        "cpu": 0.5,
        "memoryInGB": 2.0
    },
    "authEnabled": true,
    "sslEnabled": false,
    "appInsightsEnabled": false
}        
```
クラウドへのデプロイはローカルのデプロイコマンドの -dc で指定するデプロイ構成ファイルを "local_deploymentconfig.json" から "deploymentconfig.json" に変えるのみで実行できる。

```
az ml model deploy -n myservice -m mymodel_tmp:1 --overwrite --ic inferenceconfig.json --dc deploymentconfig.json
```

サービスの呼び出しの参考スクリプトは以下になる、localtest.py との違いは WebService のモジュールが異なることと、認証情報を付与している点である。

```python
import requests
import json
from azureml.core import Webservice
from azureml.core import Workspace

ws = Workspace(subscription_id = '自身のサブスクリプションID',
               resource_group = 'workspaceが属するリソースグループID',
               workspace_name = 'workspaceの名前')

service = Webservice(workspace=ws, name="myservice")
scoring_uri = service.scoring_uri

# 権限情報取得
key, _ = service.get_keys()

# ヘッダーを設定する
headers = {"Content-Type": "application/json"}
headers["Authorization"] = f"Bearer {key}"
 
data = json.dumps({"data":{
     "id": "965e592c0",
     "url_legal": "https://www.africanstorybook.org/#",
    "license":"CC BY 4.0",
    "excerpt":"Milka and John are playing in the garden. Her little sister is playing too. Milka is ready to start classes next week and it will be her first term in school. In the morning, Milka gets up early to take a bath."
}}
)
resp = requests.post(scoring_uri, data=data, headers=headers)
print(resp.text)
```

# 終わりに
自製のモデルを使ってサービスをデプロイすることができた。MicroSoft公式の記事が充実しているので、記事に沿って実行すれば進めることができると思うが、推論処理（score.py）作成の試行錯誤の部分は参考になるのではないかと思っている。今後は pipeline 機能を利用したCI/CD 環境の構築や運用側の機能等も見ていきたい。
また、同じ題材を AWS で試行している[記事](https://qiita.com/yamazombie/items/b7cf47a01f037a69de5b)があるので、参考にしてもらいたい。