【Azure】AzureAutoMLをFunctionsから実行してみた【AutoML】

前回まででAutoMLをコード上から実行することが確認できています。
今回はこれをAzureFunction上から実行してみたいと思います。

この記事では細かい実行内容にはあまり触れず、実装方法をメインに解説していきたいと思います。
実行内容につては[前回の記事](https://qiita.com/K-Nomu3/items/e0a708094e2086988dd9)を参考にしてみてください。

# 実行環境

||バージョン|
|:--:|:--:|
|windows|10|
|python|Python 3.7.9|

# リソース作成
まずはリソースを作成していきます。AzureMLのワークスペースの作成については終わっているものとして考えます。
こちらについては[この記事](https://qiita.com/K-Nomu3/items/6b9c89b958380ea67b64)で記載しているので参考にしてみてください。

最初にFunctionsのリソースを作成します。
ローカルからライブラリをアップロードしたいので、今回はVSCodeの拡張機能を利用してリソースの作成を行っていきます。
VSCodeに[こちら](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions)をインストールしてください。

インストールが終わるとツールバーにアイコンが表示されるのでクリックしてウィンドウを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/e0baf81f-af9c-af1a-205c-5e9a9e69310e.png)
開いたら画像右上にある`＋`をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/74145701-8a59-438c-de11-9c4a146f66fe.png)

すると画面上部に`Create FunctionApp in Azure...`という項目が出るので選択しましょう。
次に利用するサブスクリプションを選択します。
続いて関数名を作成します。これは一意の名前である必要がある点に注意しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/9a65bcf0-a71c-ad6d-9628-97492b370e73.png)
次にpythonのバージョン選択です今回は3.7を利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/e9acee37-cf0a-5175-45c4-9a558a01ca43.png)
最後にリソースのロケーションを選択します。今回はEastUSにしてます。

こちらで関数の作成は完了です。
また、`RESOURCE`タブの特定リソースの中にある`FunctionApp`を右クリックして`(Advanced)`のついている項目を選択することで、詳細設定を行いつつリソースを作成できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/8526cc0b-3a83-219f-b4d9-d14e569ae65c.png)

# 権限の設定
次にAutoMLを実行させるための権限設定を行っていきます。
まず作成した関数に権限を設定したいので、`関数アプリ`の項目を開きタブ内の`ID`を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/b79f9988-e210-1c33-fc12-38e13d8cc231.png)
開いたページの`状態`の項目がオフになっているのでオンにします。
これでマネージド ID に対して権限を付与できるようになります。

それではワークスペースの権限を付与しましょう。
AzureMLを開き、`IAM`の項目を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/28d3ac21-7d1d-9e07-bfcd-1865067d24c1.png)


画面上部の`+追加`をクリックし、`ロールの割り当ての追加`を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/8388e54b-8e4b-1f3e-c694-525ddeb5766e.png)

開いたら、所有者を選択し、メンバーを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/ec4ed8ee-37e0-a476-dba7-6ac9f846a3f1.png)
マネージドIDを選択してメンバーを選択するをクリックします。

画面右にタブが出てくるので、マネージドIDの項目で`すべてのシステム割り当てマネージドID`を選びます。
ここで出てくるAzureMLとFunctionsを選択し、下部の`選択`ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/c41cef60-dc66-32f8-c10d-bc8094cdd345.png)

その後レビューと割り当てをクリックして設定を終わります。
同様にリソースグループとFunciotnsにも権限の設定をします。
こちらは同様の操作になるため割愛します。

# ローカルに関数を作成
デプロイをする関数をローカルに作成していきます。
Azureタブの`WORKSPACE`の右にある`＋`をクリックして`CreateFunction`を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/a09ea267-7703-08eb-c133-9cd0ac846cc5.png)

画面上部に設定が出てくるのでこれらを設定します。
まずトリガーの設定です。今回は`HTTPtriger`を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/ff8b39d2-3696-4ebc-dccd-441a94ae8c06.png)

次に関数名を付けます。
最後に権限のレベルです。今回は`Anonymous`を設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/b40dc9fe-e584-e097-b015-a6aa57aee1e7.png)

作成が終わるとフォルダ内にファイルがいくつか生成されています。この中の__init__.pyが実行される関数になります。

# 利用するライブラリの設定
今回で一番詰まったところです。
まず、windowsマシンを利用している場合、モジュールのインストールはlinuxの仮想環境等を作成して行うようにしましょう。
私はVagrantでUbuntを立ててライブラリを入れました。
次に、フォルダに対してライブラリをインストールします。
インストール先は`.python_packages\lib\site-packages`にしましょう。

```powershell
pip install azure-functions azureml-core azureml-sdk[automl] -t .python_packages\lib\site-packages
```

続いて一部のモジュールを削除していきます。
これはバージョンの問題やPython3.7では必要ないものがありエラーになってしまうために行います。

- `dataclasses`とついているフォルダ、および`dataclasses.py`の削除
- `cryptography`とついているフォルダの削除

また、`cryptography`のインストールについてはローカルでのインストールではだめでしたが、デプロイによるインストールだと問題がなかったため、requirements.txtに入れておきましょう。

```text: requirements.txt
cryptography
```

AzureFunctionsでは一時ファイルは`/tmp/`フォルダにのみ作成可能で、それ以外のディレクトリはReadOnryとなりエラーになってしまいます。
AutoMLの関数では一時ファイルやLogの出力を行っているのでこれらを修正していきます。

まず`set_log_file`と記述されている個所を修正していきます。
logについては出力しなくていいと思ったのでコメントアウトで対応しました。

`.python_packages\lib\site-packages\azureml\automl\core\automl_base_settings.py` 756行目
`.python_packages\lib\site-packages\azureml\automl\core\_logging\log_server.py`　267,8行目
`.python_packages\lib\site-packages\azureml\automl\core\shared\log_server.py`　12行

次に一時ファイルの出力先です。こちらは生成する必要があるのでパスを変更しました。

76行目付近
```python: .python_packages\lib\site-packages\azureml\_base_sdk_common\common.py
# FILE LOCATIONS
if 'win32' in sys.platform:
    # USER_PATH = os.path.expanduser('~')
    # TODO Rename CREDENTIALS_PATH since there aren't credentials there anymore.
    # CREDENTIALS_PATH = os.path.join(USER_PATH, ".azureml")
    CREDENTIALS_PATH = "/tmp/.azureml"
else:
    # USER_PATH = os.path.join(os.getenv('HOME'), '.config')
    # CREDENTIALS_PATH = os.path.join(os.getenv('HOME'), '.azureml')
    CREDENTIALS_PATH = "/tmp/.azureml"
```

56行目付近
```python: .python_packages\lib\site-packages\azureml\train\automl\_experiment_drivers\experiment_driver.py
# Expand out the path because os.makedirs can't handle '..' properly
# aml_config_path = os.path.abspath(os.path.join(self.experiment_state.automl_settings.path, '.azureml'))
aml_config_path = "/tmp/.azureml"
os.makedirs(aml_config_path, exist_ok=True)
```

これでライブラリについては一通り完了です。

# 関数の設定
関数のランタイムのバージョンを変更する必要があったので修正します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/e4f354f5-c050-fe93-00fa-ba26f79e34b0.png)

関数の「構成」タブにある`FUNCTIONS_EXTENSION_VERSION`の値を`~3`に変更します。
また、vscodeからデプロイする場合は`.vscode/settings.json`にある`azureFunctions.projectRuntime`の値も~3にしておきましょう。

```json:.vscode/settings.json
{
    "azureFunctions.deploySubpath": ".",
    "azureFunctions.scmDoBuildDuringDeployment": true,
    "azureFunctions.pythonVenv": "venv",
    "azureFunctions.projectLanguage": "Python",
    "azureFunctions.projectRuntime": "~3",
    "debug.internalConsoleOptions": "neverOpen"
}
```

# デプロイするコード
実際にモデルを作成するコードになります。
`__init__.py`に記述することで動作します。
いくつかコードを紹介するので、それぞれ別の関数に設定したり、組み合わせて利用することをお勧めします。

## モデル作成
一つ目はモデルの作成を開始するコードになります。
クラスタやデータストアの作成、データの配置はすでにできていると想定しています。

```python
import json
import logging
import azure.functions as func
from azureml.core import VERSION
from azureml.core.experiment import Experiment
from azureml.core import Workspace, Datastore, Dataset
from azureml.core.compute import ComputeTarget
from azureml.train.automl import AutoMLConfig

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.basicConfig(level=logging.ERROR)
    ws = Workspace(
        subscription_id="<サブスクリプションID>",
        resource_group="<リソースグループ名>",
        workspace_name="<ワークスペース名>"
    )

    # 使用するクラスターの準備
    cluster_name = "<作成済みクラスター名>"
    cluster = ComputeTarget(workspace=ws, name=cluster_name)
    # データの登録
    datastore_name = '<登録済みデータストア名>'
    datastore = Datastore.get(ws, datastore_name)
    datastore_paths = [(datastore, '<データストア内のファイルパス>')]
    titanic_dataset = Dataset.Tabular.from_delimited_files(path=datastore_paths)


    # AutoMLの実行準備
    automl_classifier=AutoMLConfig(
        task='classification',
        iterations=5,
        compute_target=cluster,
        primary_metric='AUC_weighted',
        experiment_timeout_minutes=60,
        blocked_models=['XGBoostClassifier'],
        training_data=titanic_dataset,
        test_size=0.3,
        label_column_name="Survived",
        n_cross_validations=2)


    # AutoMLの実行
    experiment_name = '<実験名>'
    experiment = Experiment(ws, experiment_name)
    run = experiment.submit(automl_classifier)

    return func.HttpResponse(json.dumps({"job_id": run.id}))

```

## モデル作成の実行状況の取得
モデル作成の実行状況を取得します。
これを利用することでモデルの作成終了まで待機したりできると思います。
POSTのときに受け取るデータとしてjobのIDと実験名を受け取るようにしてます。

```python
import json
import logging
import azure.functions as func
from azureml.core.experiment import Experiment
from azureml.core import Workspace, Run

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.basicConfig(level=logging.INFO)

    job_id = req.get_json()['job_id']
    experiment_name = req.get_json()['experiment_name']

    ws = Workspace(
        subscription_id="<サブスクリプションID>",
        resource_group="<リソースグループ名>",
        workspace_name="<ワークスペース名>"
    )

    experiment = Experiment(ws, experiment_name)

    return func.HttpResponse(json.dumps({"job_status": Run(experiment, job_id).get_status()}))
```

## テスト結果の取得
一つ目に提示したモデルではテストを行うように設定をしています。
なのでテスト結果を受け取る関数を用意しました。
先ほどと同様に、POSTでjobIDと実験名を取得します。

```python
import logging
import json

import azure.functions as func
import pandas as pd
from azureml.core import Workspace
from azureml.train.automl.run import AutoMLRun

def main(req: func.HttpRequest) -> func.HttpResponse:

    job_id = req.get_json()['job_id']
    experiment_name = req.get_json()['experiment_name']

    ws = Workspace(
        subscription_id="<サブスクリプションID>",
        resource_group="<リソースグループ名>",
        workspace_name="<ワークスペース名>"
    )

    experiment = ws.experiments[experiment_name]
    automl_run = AutoMLRun(experiment, run_id = job_id)

    # テスト結果の取得
    best_model, _ = automl_run.get_output()

    testexe = best_model.get_children(tags={"mlflow.source.name" : "model_test.py"})
    rundata = list(testexe)[0]
    rundata.download_file("predictions/predictions.csv", output_file_path="/tmp/predictions.csv")
    return func.HttpResponse(json.dumps({"testdata": pd.read_csv("/tmp/predictions.csv").to_csv(index=False)}))
```

特定の実行データを取得後、最高値のモデルを取得します。その後、そのモデルの子実行のうち、特定のタグを持つ実行を取得します。
これがテストを行ってる実行になるので、その実行データからテスト結果をダウンロードする、といった流れになります。

## モデルの登録
作成したモデルを登録するための関数を作成します。
デプロイ時に登録が必要になるので、デプロイの準備として用意しました。
POSTの入力データには先ほどのものに加えてモデル名を追加しました。

```python
import logging
import json

import azure.functions as func
from azureml.core import Workspace
from azureml.train.automl.run import AutoMLRun

def main(req: func.HttpRequest) -> func.HttpResponse:

    job_id = req.get_json()['job_id']
    experiment_name = req.get_json()['experiment_name']
    model_name = req.get_json()['model_name']

    ws = Workspace(
        subscription_id="<サブスクリプションID>",
        resource_group="<リソースグループ名>",
        workspace_name="<ワークスペース名>"
    )

    experiment = ws.experiments[experiment_name]
    automl_run = AutoMLRun(experiment, run_id=job_id)

    model = automl_run.register_model(model_name=model_name)
    return func.HttpResponse(json.dumps({"status": "success"}))

```

## モデルのデプロイ
登録したモデルをデプロイします。
エントリポイントに利用するpythonファイルが必要なのでコード上で生成し、それを設定しています。
また、POST入力には上記に加え予測対象のカラム名を追加しています。


```python
from html import entities
import logging
import json
import io

import azure.functions as func
from azureml.core import Workspace
from azureml.core.model import Model
from azureml.core.webservice import AciWebservice
from azureml.core.model import InferenceConfig


def main(req: func.HttpRequest) -> func.HttpResponse:
    job_id = req.get_json()['job_id']
    experiment_name = req.get_json()['experiment_name']
    model_name = req.get_json()['model_name']
    target = req.get_json()['target']

    ws = Workspace(
        subscription_id="<サブスクリプションID>",
        resource_group="<リソースグループ名>",
        workspace_name="<ワークスペース名>"
    )

    entry_pint = f"""
import io
import json
import joblib
import pandas as pd
from azureml.core.model import Model

def init():
    global model
    model_path = Model.get_model_path('{model_name}')
    model = joblib.load(model_path)

def run(raw_data):
    df = pd.read_csv(io.StringIO(json.loads(raw_data)['data']))
    predictions = model.predict(df)
    df["{target}"] = predictions
    return df.to_csv(index=False)
    """
    with open("/tmp/score.py", "w") as f:
        f.write(entry_pint)
    model = ws.models[model_name]
    inference_config = InferenceConfig(entry_script="/tmp/score.py")
    deployment_config = AciWebservice.deploy_configuration(cpu_cores = 1, memory_gb = 1)
    service_name = f"{model_name}service"
    service = Model.deploy(ws, service_name, [model], inference_config, deployment_config)
    return func.HttpResponse(json.dumps({"status": "success", "service_state":service.state}))

```

また、デプロイ完了したモデルのエンドポイントがコードから取得したいこともあるとおもいます。
そちらについてのコードも併せて載せておきます。
Functionsの形式にはなっていないのでここを参考に利用するようにしてください。

```python 
from azureml.core import Workspace, Webservice
from azureml.train.automl.run import AutoMLRun

ws = Workspace(
    subscription_id="<サブスクリプションID>",
    resource_group="<リソースグループ名>",
    workspace_name="<ワークスペース名>"
)


service = Webservice(ws, "<サービス名>")
print(service.serialize()["scoringUri"])

```

`service.serialize()`によってデプロイした情報を取得し、その中にあるエンドポイントのデータを持ってきている形になります。

## エンドポイントから予測を行う

こちらはAutoMLを実行するわけではありませんが、一連の流れとして載せておこうと思います。
発行したエンドポイントを利用して予測を行う関数になります。
内容のほとんどはAzureML上にあるサンプルコードと同様です。Functionsの形式に合わせて修正した形になります。
また、POSTの入力としてエンドポイントと予測用データ(CSV)を受け取ります。
これにより汎用的に利用できるようになっています。

```python 
import logging

import azure.functions as func
import urllib.request
import json
import os
import ssl

def allowSelfSignedHttps(allowed):
    # bypass the server certificate verification on client side
    if allowed and not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None):
        ssl._create_default_https_context = ssl._create_unverified_context


def main(req: func.HttpRequest) -> func.HttpResponse:
    predict_data = req.get_json()['predict_data']
    endpoint = req.get_json()['endpoint']

    allowSelfSignedHttps(True)
    data = {
        "data": predict_data
    }
    body = str.encode(json.dumps(data))

    url = endpoint
    api_key = ''
    headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key)}
    req = urllib.request.Request(url, body, headers)
    try:
        response = urllib.request.urlopen(req)
        result = response.read().decode()
        return func.HttpResponse(json.dumps({"status": "success", "predict_data":result}))
    except urllib.error.HTTPError as error:
        return func.HttpResponse(json.dumps({"status": "error", "errorcode":error.code, "error_info":error.info()}))
```

# まとめ
今回はAzureFunctionsからAutoMLを実行してみました。
モジュール関係でかなりてこずってしまったのでもしかすると断念した方もいるかもしれません。
モジュールに手を加えることでFunctionsから実行でき、これにより様々なところでモデル作成を行えるようになると思います。

AutoML自体とても簡単に良いモデルを作成できるので、FuncionsでAPIを作成しモデルと共存した開発を行っていきたいですね。