【Azure】モデルを作成・デプロイ・再学習をしてみる【MLStudio classic】

# わたし、公式ドキュメントわかんないです。
MLStudio classicでモデル作成やデプロイは何回かしてきてます。
今回、再学習させてみたいなと思って公式ドキュメントを読んでみたのですがよくわかんなくてあっちっこっち行ったり来たり。
どうにかできるようになったので忘れないように記事にします。
また、初めて使う人向けに書いてるつもりなので細かいことは省いているのでゆるしてください。

# 大まかな流れ
1. 学習実験を作る
2. 予測実験を作る
3. 予測実験のデプロイ
4. 再学習をデプロイ
5. 再学習を行う
6. 再学習結果を予測実験に反映する
7. 反映したかをテストする

です。

# 動作環境
途中でPythonを使うので環境とimportしたものを書いておきます。
- python3.7
- blobStorage

|名前|
|:--:|
|urllib|
|azure-storage-blob|

# 利用するデータについて
それではまず今回使うデータです。
再学習できたのかどうかが知りたいので単調すぎるデータを使います。
下に学習用データ、テスト用データ、リモデル用データを書いておきます。

```train1.csv
id,target
1,1
2,1
3,1
4,1
5,1
```
```remodel0.csv
id,target
1,0
2,0
3,0
4,0
5,0
```

作成したモデルではどんな数を入れても必ず1が返ってくるモデルを期待します。
なのでテストデータを入れてもすべて１で返ってくるはずです。
すべてが0であると学習することで0が返ってくることを期待します。

モデルとしていいモデルでは全くないので、あくまでテスト用として使います。

# 学習実験を作成しましょう
それではモデルを作成します。
MLStudioにログインしたら、`EXPERIMENTS`を選択して画面下の`NEW`をクリックしましょう
![Web services - Microsoft Azure Machine Learning Studio (classic) - Google Chrome 2020_01_21 17_12_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/f3d7b118-5002-c49b-9bfe-c1e9007026bc.png)

そしたら`Blank Experiment`をクリックしましょう。
これで予測モデルの作成準備ができました。

では実際にモデルを作っていきます。
この検索窓から必要なブロックを検索して追加していくだけです。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/3363df16-a26e-1709-3cde-c2a87af6f597.png)

今回は二項(一応)なのでtwo classで検索してできたたボックスを配置します。今回は`Two-Class Boosted Decision Tree`を利用しました。
そのほかのブロックも同様に検索して画像のように配置しましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/693d44af-d526-5c1b-9593-69f04d313231.png)

次に各ブロックの設定をしていきます。ブロックをクリックすると
ページ右側に設定項目が出てきます。
アルゴリズムブロックではパラメータの設定ができます。今回はそのままでいいでしょう。

続いてimport Dataです。ここでは学習に用いるデータの指定をします。
自身のblobユーザー名、Key、ファイルパスを記述しましょう。
今回のデータはフォーマットはcsvなのですが、ファイルにはヘッダーがあるため、`File has header row`にチェックを入れましょう。

最後にTrain Modelです。こちらでは学習するターゲットのカラム名を指定してあげます。
今回は`target`を学習したいので、`Launch column selector`で`target`を記入してあげます。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/9d822224-ff93-e1cd-d385-a1f1df843a46.png)

ここまで終わったなら、ブロック同士を線でつないでみましょう。
Train Modelは左右反対には線でつなげないので気をつけましょう。
下のように配置できたら画面下部の`RUN`をクリックして実行してみましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/f5561905-cecc-8239-59ce-2cfae5e1f51a.png)

ボックスすべてにチェックマークがついたら完了です。
途中で止まってしまった場合、どこかに間違いがあるはずです。
blobのファイル名が違ったりするかもしれないですね(体験談)

全部にチェックが付いたらモデルの完成です！

# 予測実験を作る
さてさて、では先ほど作ったモデルを使ってデータを投げると答えを返してくれるものを作りましょう。
先ほどの`RUN`の隣にある`SET UP WEB SERVICE`から`Predictiv web Service`をクリックしましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/a8cd8a66-6ffb-d4bd-a3ba-af93d6a65bb3.png)

するとボックスがうようよ動いて下のようになります。
検索窓から`Export Data`を持ってきて、blobのユーザー名などを書きます。
ここで書いたパスにテスト結果が出力されることになります。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/b4399923-a6c5-be67-e612-5b1a8676b145.png)

もう一度`RUN`してみましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/8fb871fe-4b3c-355e-9452-52644b20d566.png)

先ほどのようにチェックが付いたのなら完了です。
完了したなら`DEPLOY WEB SERVICE`をクリックしましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/5b4185c1-4cf1-5827-c8f1-0d978a6b9b84.png)
しばらくすると画面が変わり、APIKEYなどが表示されます。
`REQUEST/RESPONSE`をクリックするとAPIが表示されるので、それを利用すると予測することができます。
このページで簡単なテストをすることもできます。
じっさにに青色のTESTボタンをクリックしてidに3、targetに1を入れて実験してみましょう。
しばらくするとページ下部に結果が返ってきます。
カラム名、カラムの型、値がリストで返ってきます。

```python:return
Result: {"Results":{"output1":{"type":"table","value":{"ColumnNames":["id","target","Scored Labels","Scored Probabilities"],"ColumnTypes":["Int32","Int32","Int32","Double"],"Values":[["3","1","1","0.142857149243355"]]}}}}
```
`Scored Labels`が結果なので、この場合は１が返ってきてるので期待通りです。


# 再学習をデプロイするよ
では`View latest`をクリックして先ほどの画面に戻りましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/5118bf05-233c-b070-43de-4edf4d002547.png)

そしたら`Training expepriment`タブに移動して、`Web service input`および`Web service output`を追加して`RUN`します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/6f982355-b6ce-fa77-7bc6-6797d2e100e0.png)

そしたら下の`SET UP WEB SERVICE`から`Deploy Web Service`をクリックしましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/f0d5dbfc-1caa-963c-cd27-02a5429a7765.png)

これでデプロイは完了です。
APIKeyとAPIのURLは以降で必要になってきます。
APIKeyは画面に映っているものです。URLは`BATCH EXECTION`をクリックすると出てきます。
利用するAPIのURLは`?api-version…`より前、つまり`job`で終わるURLを使います。
私はここでつまずいた。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/5b2dff7f-f8ef-00a8-4e78-28f39a1909fa.png)

# 再学習してみよう！
それでは再学習してみます。
再学習自体はpythonで行いました。C#とかもサンプルであるのですが、ところどころ変えないと動かないかもしれないです。
(python3.7ではそうでした。)

まず、実際に使うプログラムです。

```python:retrain.py
# python 3.7 なのでurllib2を変更
import urllib
import urllib.request
import json
import time
from azure.storage.blob import *

def printHttpError(httpError):
    print(f"The request failed with status code: {str(httpError.code)}")
    print(json.loads(httpError.read()))
    return


def processResults(result):
    results = result["Results"]
    for outputName in results:
        result_blob_location = results[outputName]
        sas_token = result_blob_location["SasBlobToken"]
        base_url = result_blob_location["BaseLocation"]
        relative_url = result_blob_location["RelativeLocation"]
        print(f"The results for {outputName} are available at the following Azure Storage location:")
        print(f"BaseLocation: {base_url}")
        print(f"RelativeLocation: {relative_url}")
        print(f"SasBlobToken: {sas_token}")
    return



def uploadFileToBlob(input_file, input_blob_name, storage_container_name, storage_account_name, storage_account_key):
    #BlobServiceってのがないっぽいので変更
    blob_service = BlockBlobService(account_name=storage_account_name, account_key=storage_account_key)
    print("Uploading the input to blob storage...")
    blob_service.create_blob_from_path(storage_container_name, input_blob_name, input_file)

def invokeBatchExecutionService():

    storage_account_name = "blobのユーザー名"
    storage_account_key = "blobのキー"
    storage_container_name = "blobのコンテナ名"
    connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={storage_account_key}"
    api_key = "再学習のAPIKey"
    url = "APIのURL"



    uploadFileToBlob("アップロードするファイルパス",
                     "アップロードした後のファイルパス", 
                     storage_container_name, storage_account_name, storage_account_key)

    payload =  {
        "Inputs": {
            "input1": { 
                "ConnectionString": connection_string, 
                "RelativeLocation": f"/{storage_container_name}/リモデル対象のblobのファイルパス" 
            },
        },
        "Outputs": {
            "output1": { 
                "ConnectionString": connection_string, 
                "RelativeLocation": f"/{storage_container_name}/リモデル結果のblobのファイルパス.ilearner" 
            },
        },
        "GlobalParameters": {
        }
    }

    body = str.encode(json.dumps(payload))
    headers = { "Content-Type":"application/json", "Authorization":("Bearer " + api_key)}
    print("Submitting the job...")

    req = urllib.request.Request(url + "?api-version=2.0", body, headers)
    response = urllib.request.urlopen(req)

    result = response.read()
    job_id = result[1:-1] 
    # job_idがstrじゃないよって怒られたので変換する
    job_id=job_id.decode('utf-8')
    print(f"Job ID: {job_id}")

    print("Starting the job...")    
    headers = {"Authorization":("Bearer " + api_key)}
    req = urllib.request.Request(f"{url}/{job_id}/start?api-version=2.0", headers=headers, method="POST")
    response = urllib.request.urlopen(req)

    url2 = url + "/" + job_id + "?api-version=2.0"

    while True:
        print("Checking the job status...")
        req = urllib.request.Request(url2, headers = { "Authorization":("Bearer " + api_key) })

        response = urllib.request.urlopen(req)

        result = json.loads(response.read())
        status = result["StatusCode"]
        if (status == 0 or status == "NotStarted"):
            print(f"Job: {job_id} not yet started...")
        elif (status == 1 or status == "Running"):
            print(f"Job: {job_id} running...")
        elif (status == 2 or status == "Failed"):
            print(f"Job: {job_id} failed!")
            print("Error details: " + result["Details"])
            break
        elif (status == 3 or status == "Cancelled"):
            print(f"Job: {job_id} cancelled!")
            break
        elif (status == 4 or status == "Finished"):
            print(f"Job: {job_id} finished!")
        
            processResults(result)
            break
        time.sleep(1) # wait one second
    return

invokeBatchExecutionService()

```
ほとんどサンプル通りなのですが、一部変更しています。(コメントで書いてます。)
URLやKey、PATHなどは各々の環境に合わせて書き換えてくださいね。
そしたらデータを用意して実行してみましょう。

```python:console
>python remodel.py

Uploading the input to blob storage...
Submitting the job...
Job ID: ID
Starting the job...
Checking the job status...
JobID not yet started...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID running...
Checking the job status...
JobID finished!
The results for output1 are available at the following Azure Storage location:
BaseLocation: URL
RelativeLocation: PATH
SasBlobToken: KEY
```
上記のように結果が返ってくるはずです。
下の三つは以降で利用します。

# 再学習の反映
さて、リモデルの実行はしましたが、まだ反映はされていません。
この画面から`New Web Services Experience`を開いてください。
この時ページ上部のモデル名の後ろに`[predictive exp.]`がついていない場合は`Experiment created on … [Predictive Exp.]`をクリックして移動しましょう。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/b60e7ae0-0ae0-2bc9-3cbc-0c9cb8889dfe.png)

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/61f25e52-ea97-7580-3cce-232da2a11c09.png)

既存のものを上書きしてもいいと思いますがたいていは残しておくと思います。
なので新しいエンドポイントを作成します。
この画面から左ボタンを押して…
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/7fb379bd-acaa-1aaf-233d-e47ea4a9d022.png)
(矢印が雑になったのは気にたら負けです。)
`+NEW`をクリックしてエンドポイントに利用する名前を付けて保存します。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/a276e524-3762-2075-cf93-61f35c524f6f.png)

作成されたエンドポイント名をクリックすると先ほどのような画面に移動するので`Consume`タブを開きましょう。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/42f8f6ba-2d26-7b31-904c-5cfbf67cd927.png)

開くといろんなKEYやらURLが出てきます。
今回利用するのは`Primary Key`と`Patchです。`

これらを使って以下のコードを実行します。

```python:update.py
import urllib
import urllib.request
import json 

data =  {
            "Resources": [
                {
                    "Name": "モデルの名前",
                    "Location": 
			            {
                            "BaseLocation": "結果のURL",
                            "RelativeLocation": "結果のPATH",
                            "SasBlobToken": "結果のKEY"
			            }
                }
            ]
        }

body = str.encode(json.dumps(data))

url = "Patchの値"
api_key = "PrimaryKeyの値" # Replace this with the API key for the web service
headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key)}

req = urllib.request.Request(url, body, headers)
req.get_method = lambda: 'PATCH'
response = urllib.request.urlopen(req)
result = response.read()
print(result) 
```

こんな感じです。
先ほどのページのPatchの下にあるAPIhelpを開くとサンプルがあるので、大体はその通りに書くといいと思います。
では、実行してみましょう。

`b''`が返ってきます。
なるほど？
でもこれでいいみたいです。
最後にこれをデプロイします。
デプロイ手順は

# 再学習できたかな？
ページ上部の`Test`タブを開いてテストしてみましょう。
もしかするとリロードが必要かもしれないです。
id=3,target=1です。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395106/ea50462d-d67d-9b3a-49a8-8e53af162551.png)
ちゃんと0になってるみたいです。
よかった…

# まとめ
どうにか再学習したモデルをデプロイすることができました。
皆さんも利用しているモデルの精度が下がってきたかな？と思ったら、再学習して、モデルを継続利用しましょう！