Azure Machine LearningをAzure DevOps連携してデプロイ自動化する[MLOps]

Azure MLのチュートリアルの中であまり触れられていないMLOpsの部分について調査と検証をしてみました。Azure DevOps上で幾度となくエラーに当たりましたが、何とかデプロイまでSuccessさせることができました。その知見を残しておきたいと思います。

 ※何度かジョブを実行していますが、同じ条件でもたまにDeploy failed(404:ResourceNotFound)になることがあり不安定な部分があります。まだ原因を調査できていない状況で恐縮です。

追記1：デプロイ処理の順を["imageの作成"⇒"既存サービスの削除"⇒"デプロイ"]だったものを、["既存サービスの削除"⇒"imageの作成"⇒"デプロイ"]としたら安定しました。その場合、サービスが削除されている時間が長くなるのでリクエスト受付不可の時間が伸びてしまいます。これが正しいのかは不明です。

追記2：明示的にimageを作成する処理を挟まずmodel.deploy()を利用することで問題なく実行できました。


# 作成するデプロイフロー
#### Azure Machine Learning上で試行錯誤して精度の良いモデルを作成しWorkspaceに登録すると、それをトリガにしてACI(Azure Container Instance)にデプロイする
Azure Machine Learningに不慣れな方に説明をしておくと、Azure MLではデータセット・実験・モデル・デプロイしたサービスなどをWorkspaceの中に保持しています。これらを保持していることにより、データやモデルの管理や、学習の監視などがやりやすくなります。AzureML SDKの部分で学習コストが多少必要ですが、それを十分超えるメリットがあると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/2c668a84-999d-aba0-48cf-71659523bfa0.png)

# 本題
では早速設定に入っていきます。今回はモデルの学習部分は手作業でやることを想定し、納得したモデルができた段階でWorkspaceに登録するという状況を想定します。githubへのpushをトリガにすることでコードを変更した段階で学習させ、今保持している最良のモデルより良いモデルができたら登録する、といったようなフローも実現できるかと思います。しかしデータサイエンティストとしては手元で学習をさせることがほとんどだと思いますので、モデルの登録までは手作業で行う方が自然です。

## スクリプトの用意
Azure DevOpsを使い始める前に、Azure Machine Learningへアクセスするための拡張機能をインストールします。
https://marketplace.visualstudio.com/items?itemName=ms-air-aiagility.vss-services-azureml
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/40cf984c-b96a-5369-54d4-a8d96beb274c.png)

続いて、Azure DevOpsのReposに置くファイルを用意します。

AgentをUbuntu上で動かす際のPython実行環境を作成するシェルスクリプト
(私はここのバージョンが古かったせいでエラーになり、ハマりました、要注意です)

```bash:install_requirements.sh
#!/bin/bash
python --version
pip install azure-cli==2.9.1
pip install azureml-sdk==1.10.1
pip install --upgrade azureml-sdk[cli]
```

デプロイするモデルを乗せるACIの環境設定ファイルです。
(実はここも適当にやっていたらハマりました。AutoMLのモデルを使っている場合はしっかりとautomlも入れましょう。AzureMLでは学習環境をEnvironmentとして登録できるのでそれをファイルとして書き出すことで確実に実行環境を用意することができます。)

```yml:env.yml
name: project_environment
dependencies:
  # The python interpreter version.
  # Currently Azure ML only supports 3.5.2 and later.
- python=3.6.2

- pip:
    # Required packages for AzureML execution, history, and data preparation.
  - azureml-defaults

- scikit-learn
channels:
- anaconda
- conda-forge
```

Azure CLIに実行してもらうデプロイ用のスクリプトです。AzureMLのWorkspaceへの接続とモデルの特定、サービスとしてのデプロイが含まれています。

```python:deployToAci.py
from azureml.core import Workspace
from azureml.core.authentication import InteractiveLoginAuthentication
from azureml.core.image import ContainerImage, Image
from azureml.core.model import Model
from azureml.core.webservice import Webservice, AciWebservice
from azureml.core.model import InferenceConfig

# AzureML Serviceとの接続
cli_auth = InteractiveLoginAuthentication()
ws = Workspace.get(
    name="azml-demows",
    subscription_id="18dfd41f-e90c-471f-afe5-2ec193403788",
    resource_group="demo",
    auth=cli_auth
)

service_name = 'diabetes-service-deploytest'

# モデルの読み込み
model=Model(ws, 'diabetes_model')

# すでに同名のサービスがあるか確認し、あれば削除する
svcs = [svc for svc in Webservice.list(ws) if svc.name==service_name]
if len(svcs) == 1:
    print('Deleting prior {} deployment'.format(service_name))
    svcs[0].delete()

# # イメージの作成->ref.追記2：明示的に作成する処理を挟むと毎度時間がかかる-------------------------------
# image_config = ContainerImage.image_configuration(execution_script="scoring.py", 
#                                 runtime="python", 
#                                 conda_file="env.yml")
# image = Image.create(ws, service_name, [model], image_config)
# image.wait_for_creation(show_output=True)

## イメージのデプロイ->ref.追記2
# aciconfig = AciWebservice.deploy_configuration(cpu_cores=1, 
#                                             memory_gb=1, 
#                                             description='autodeploy_by_azuredevops')
# service = Webservice.deploy_from_image(workspace=ws, 
#                                     image=image, 
#                                     name=service_name, 
#                                     deployment_config=aciconfig)
#---------------------------------------------------------------------------------------------------------

# 推論環境の設定
inference_config = InferenceConfig(
    runtime='python',
    source_directory='.',
    entry_script='scoring.py',
    conda_file='env.yml')
# デプロイ処理
deploy_config = AciWebservice.deploy_configuration(cpu_cores=1, memory_gb=1)
service = model.deploy(ws, service_name, [model], inference_config = inference_config, deployment_config=deploy_config)
service.wait_for_deployment(show_output=True)
print(service.scoring_uri)
```
デプロイしたモデルをロードし、RESTサービスとしてオンデマンドでリクエストを受け付けるスコアリング用のスクリプトを用意します。今回は入力に対してnot-diabetic/diabeticのどちらかを返すようになっています。

```python:scoring.py
import json
import joblib
import numpy as np
from azureml.core.model import Model

# Called when the service is loaded
# 最初に一度だけ呼ばれる
def init():
    global model
    # Get the path to the deployed model file and load it
    model_path = Model.get_model_path('diabetes_model')
    model = joblib.load(model_path)

# Called when a request is received
# リクエストが来るたびに呼ばれる
def run(raw_data):
    # Get the input data as a numpy array
    data = np.array(json.loads(raw_data)['data'])
    # Get a prediction from the model
    predictions = model.predict(data)
    # Get the corresponding classname for each prediction (0 or 1)
    classnames = ['not-diabetic', 'diabetic']
    predicted_classes = []
    for prediction in predictions:
        predicted_classes.append(classnames[prediction])
    # Return the predictions as JSON
    return json.dumps(predicted_classes)
```

これで必要なスクリプトは揃ったのでAzure DevOpsの設定に入っていきます。

## Azure DevOps上でデプロイのフローを作る
まずは、Azure Machine Learningのワークスペースを接続します。

[Service Connections]->[New service connection]->[Azure Resource Manager]を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/35946b87-ce40-bb20-e029-69dfdc1db4d2.png)

[Sevice Principal]->[Machine Learing Workspace]と進み、対象のワークスペースを選択します。拡張機能を入れることでMachine Learningが選択可能になります。
![sc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/43c3e119-16c3-8c8d-e073-434d359cda7f.png)

続いてReleaseパイプラインを作成します。１からジョブを作成するので[Empty job]を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/7d94df7f-f2d1-dc89-5fc5-ca00e0229d4d.png)

Artifactとして利用するモデルを選択します。[Service Endpoint]は先ほど作成したconnection名が選択できるはずです。[Model Names]では利用するモデル名を選択します。モデルのバージョン選択は[Latest version for the specified models]に設定することで登録した最新のモデルを利用することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/664cf558-e57f-a9dd-4cb3-7b480ab23c69.png)

雷のマークから、Continuous deployment triggerをEnabledにしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/2cd825ea-fb0e-0c66-ceb7-b8c631e40de0.png)

さらに、Azure Reposに上げたコードも用いるためArtifactとして登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/7feb31cf-e632-c0b2-9e37-3fa347c740a1.png)

続いてAgentの設定に入ります。[Stage 1]のジョブを開き実行環境としてUbuntu 18.04を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/4327cd05-5a68-b4ff-77a3-ddd5cfed0c67.png)

[Agent job]の右側+ボタンから処理を追加していきます。

[Use Python version]でPython 3.6を指定します。現時点でAzure MLはデフォルトで3.6系を使っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/1d50fe6d-91ea-6b38-e55a-42dbc622eebe.png)

[Bash]モジュールを追加し、動かすスクリプトの場所を指定します。BrowseするとReposを参照できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/cb6403fc-d721-7504-d361-da79fc03d667.png)

続いて[Azure CLI]モジュールを追加します。[Task version]を*1にするようにしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/e3ace908-ee25-9f8b-f498-8ba55106c3f3.png)

[Azure Subscription]には作成したService Connection名を入れます。通常のサブスクリプション名も入力できますがService Connectionを使うことで権限の範囲をAzure Machine Learningに制限することができます。

[inline Script]には`python ./deployToAci.py`を入力します。Azure CLIがpythonスクリプトを実行してくれます。[Working Directory]にReposのディレクトリを指定することを忘れないように注意してください。これを忘れると`deployToAci.py`を見つけることができずにエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/442ca14f-efa2-a1e1-5b02-60fc2b988d6c.png)

これでモデルの登録をトリガとしたMLOpsパイプラインが定義できました。ArtifactのML modelにおいてContinuous deploymentをenabledにしたことによって、新しいML model Artifactが追加されることをトリガにパイプラインが実行されるように構成されています。

実行すると以下のようにジョブが流れ、もしエラーが生じた場合はログを見ながらその都度対応していくようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205918/b6eb0075-370c-567b-b30b-79e23e118ca2.png)

# おわりに
以上でモデルの登録をトリガとしたMLOpsの実装を終わります。最初にも述べましたが実際のところこちらは私の環境では不安定で、たまにResourceNotFoundエラーが生じており、現在調査中です。

MLOpsを利用する利点としては、Azure MLでのモデル作成作業とデプロイ作業を分離することによって、データサイエンティストとアプリケーションエンジニア、インフラエンジニアの作業を分離できることだと思っています(Azure MLのノートブック上から直接デプロイすることもできます)。

今回は簡単なデプロイのみ行いましたが、MLOpsパイプラインの中でACIへのデプロイ後にスモークテストを行うこともでき、テスト後にAKSへデプロイという流れがとれます。この領域はどちらかというとApp&Infraエンジニアの領域という気もするのでこのあたりをデータサイエンティストの作業から切り離せることはメリットだと感じます。

私もまだまだ勉強中の身なので、頑張ります、、、汗