Azure ML StudioとFlask連携によるWebアプリケーションサンプル

機械学習や組合最適化の知識をブラッシュアップすべく、python・scikit-learn・Azure MLStudioを触っていますが、Azure MLStudioのモデルを実用っぽいアプリにしたいと考え、Azure MLStudioでモデルを作って、それをWebAppsアプリからREST APIを通じて呼び出してみました。

※2019/06/22  無料版のAzure MLStudioではクラシックWebAPIしか利用できません。今までは
  課金される標準版で動かしていましたが、もったいないので無料版をクラシックWebAPIで呼ぶように
  書き換え、そのソースも追加しました。Azureの無償/有償については、[こちら](https://qiita.com/vye03702/items/c980645dbfb59f67c4c8)で説明しています。

###[demo→12](https://sl-vin.azurewebsites.net/)
※7/12環境設定を拡張してライブラリを追加する方法で環境を再構築
#手順
1.Azure ML Studioでモデルを作成しAPIとして公開する。

2.入力項目の選択枝を得る為に、サンプルデータをダウンロードしてExcelで開いて｢データ⇒重複の削除｣で値を得ました。

3.Azure ML Studioでモデルに接続するPythonのクライアントアプリが生成されるので、それをローカルのAnaconda(Jupyter Notebook)にコピーして、必要な修正を施す。

4.ローカルのFlask実行環境で入力画面を作成し、3のソースをベースにサーバーサイドアプリを作成する。(今回私私自身は、4で試行錯誤してしまいましたが3の段階で、APIへ渡すjsonデータ・戻ってきたjsonデータの扱いをきちんとしてから3に進んだ方が効率的だったなと思いました。)

5.Azure WebAppsに移行する。

といった手順で行いました。

#Azure ML Studioでモデルを作成
これについては、下記サイトをそのまま踏襲して、「米国国勢調査局提供の、成人収入に関する二項分類データセット」を用いて、条件を入力して年収が5万ドル以下/大なりを判定するモデルを作成しました。Webアプリに利用したのは、ARM対応版のWebサービスで、下記例はサンプルソースC#ですが、Pythonを利用します。
[Azure Machine Learningを始めよう（1）- MLStudioの使い方 ](https://news.mynavi.jp/article/zeroazure-10/)

#Anaconda(Jupyter Notebook)上で必要な修正を施す
3のサンプルソースを動かすだけであれば、api_keyを修正するだけで動きます。ただ戻ってきたjsonデータのハンドリングもこの段階でしていた方が良いです。ハンドリングは下記リンクのMicrosoft Docsの｢Azure Machine Learning Studio｣の｢API Management を使用して Azure Machine Learning Studio Web サービスを管理する｣のサンプルソースが参考になりました。
[BES エンドポイントのテスト](https://docs.microsoft.com/ja-jp/azure/machine-learning/studio/manage-web-service-endpoints-using-api-management)

```python:MLから戻り値の処理のサンプル
    body = str.encode(json.dumps(data))
    url = 'https://japaneast.serv～割愛～'
    api_key = '764McF0w85～割愛～'
    headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key)}  #Bearer=無記名
    req = urllib.request.Request(url, body, headers)
    try:
        response = urllib.request.urlopen(req)
        result = response.read()
        print(result)
    except urllib.error.HTTPError as error:
        print("The request failed with status code: " + str(error.code))
        print(error.info())
        print(json.loads(error.read().decode("utf8", 'ignore')))
    result_dic = json.loads(result)  #json形式を辞書型に変換
    results = result_dic["Results"]  #"Results"の値を取り出して
    for outputName in results:       #複数の値が返る場合"output1"・"output2"・・のインデックスが付いて戻る     
        result_blob_location = results[outputName]
        out_sl  = result_blob_location[0]['Scored Labels']
```
forで回しているとこは一行しかなければ｢out_sl  = result_dic["Results"]["output1"][0]['Scored Labels']｣で良いです。私は[0]が必要な事に気づかずハマリました。

#ローカルのFlask実行環境で入力画面とサーバーサイドアプリを作成
これもhtmlから渡したjsonをサーバサイドで受け取って、MLに渡すjsonの情報を付けて渡すところでなかなかうまくいきませんでした。結局サンプルの形式のまま値だけrequest.json['キー']で置き換えるようにしていますが、htmlの時点でMLに渡すjson形式にしてしまえば良いのかもしれません。

```html:クライアントソースazureML.html
<!--*****************************************************************
*  azureMLサンプル:米国国勢調査提供の収入のサンプルの訓練結果にrestで接続
*  
*  2019/03/24
*****************************************************************-->
{% extends "base.html" %}
{% block body %}


<div class="card" >
<div class="card-header" style="height:50px; font-size:1.5rem; ">
    <b>azure ML webアプリサンプル</b>
</div>
<div class="card-body">
	<div class="row">
		<div class="col-md-2 mb-1">年齢</div>      <!--col-md-1：mdはPCのMIDDLEサイズ。mb-1はマージン(空白)をボトム(下)に設定-->
		<div class="col-md-3 mb-1" >
			<select class="form-control input-lg mb-1" id="age">
				<option>20</option>
				<option>25</option>
				<option>30</option>
				<option>35</option>
				<option>40</option>
				<option>45</option>				
				<option>50</option>
				<option>55</option>				
				<option>60</option>
				<option>65</option>				
				<option>70</option>
				<option>75</option>				
				<option>80</option>
			</select>
		</div>
		<div class="col-md-2 mb-1">ワーククラス</div>
		<div class="col-md-5 mb-1" >
			<select class="form-control input-lg mb-1" id="workclass">
				<option value="Federal-gov">Federal-gov</option>
				<option value="Local-gov">Local-gov</option>
				<option value="Never-worked">Never-worked</option>
				<option value="Private">Private</option>
				<option value="Self-emp-inc">Self-emp-inc</option>
				<option value="Self-emp-not-inc">Self-emp-not-inc</option>
				<option value="State-gov">State-gov</option>
				<option value="Without-pay">Without-pay</option>
				<option value="Without-pay">Without-pay</option>
			</select>
		</div>
	</div>
	  
	<div class="row">
		<div class="col-md-2 mb-1">卒業</div>
		<div class="col-md-2 mb-1" >
			<select class="form-control input-lg mb-1" id="education-num">
				<option value="1">Preschool</option>
				<option value="2">1st-4th</option>
				<option value="3">5th-6th</option>
				<option value="4">7th-8th</option>
				<option value="5">9th</option>
				<option value="6">10th</option>
				<option value="7">11th</option>
				<option value="8">12th</option>
				<option value="9">HS-grad</option>
				<option value="10">Some-college</option>
				<option value="11">Assoc-voc</option>
				<option value="12">Assoc-acdm</option>
				<option value="13">Bachelors</option>
				<option value="14">Masters</option>
				<option value="15">Prof-school</option>
				<option value="16">Doctorate</option>
			</select>
		</div>
		<div class="col-md-1 mb-1">結婚</div>
		<div class="col-md-3 mb-1" >
			<select class="form-control input-lg mb-1" id="marital-status">
				<option value="Divorced">Divorced</option>
				<option value="Married-AF-spouse">Married-AF-spouse</option>
				<option value="Married-civ-spouse">Married-civ-spouse</option>
				<option value="Married-spouse-absent">Married-spouse-absent</option>
				<option value="Never-married">Never-married</option>
				<option value="Separated">Separated</option>
				<option value="Widowed">Widowed</option>
			</select>
		</div>
		<div class="col-md-1 mb-1">関係</div>
		<div class="col-md-3 mb-1" >
			<select class="form-control input-lg mb-1" id="relationship">
				<option value="Husband">Husband</option>
				<option value="Not-in-family">Not-in-family</option>
				<option value="Other-relative">Other-relative</option>
				<option value="Own-child">Own-child</option>
				<option value="Unmarried">Unmarried</option>
				<option value="Wife">Wife</option>
			</select>
		</div>
	</div>

	<div class="row">
		<div class="col-md-2 mb-1">人種</div>
		<div class="col-md-2 mb-1" >
			<select class="form-control input-lg mb-1" id="race">
				<option value="Amer-Indian-Eskimo">Amer-Indian-Eskimo</option>
				<option value="Asian-Pac-Islander">Asian-Pac-Islander</option>
				<option value="Black">Black</option>
				<option value="Other">Other</option>
				<option value="White">White</option>
			</select>
		</div>
		<div class="col-md-2 mb-1">性別</div>
		<div class="col-md-2 mb-1" >
			<select class="form-control input-lg mb-1" id="sex">
				<option value="Male">Male</option>
				<option value="Female">Female</option>
			</select>
		</div>
	 </div>

	<div class="row">
		<div class="col-md-2 mb-1">キャピタルゲイン</div>
		<div class="col-md-2 mb-1" >
			<input  type="text" class="form-control input-lg" id="capital-gain" value=0 >
		</div>

		<div class="col-md-2 mb-1">キャピタルロス</div>
		<div class="col-md-2 mb-1" >
			<input  type="text" class="form-control input-lg" id="capital-loss" value=0 >
		</div>
	</div>

	<div class="row">
		<div class="col-md-2 mb-1">労働時間</div>
		<div class="col-md-2 mb-1" >
			<input  type="text" class="form-control input-lg" id="hours-per-week" value=40 >
		</div>

		<div class="col-md-1 mb-1">国</div>
		<div class="col-md-3 mb-1" >
			<select class="form-control input-lg mb-1" id="native-country">
 				<option value="Cambodia">Cambodia</option>
  				<option value="Canada">Canada</option>
  				<option value="China">China</option>
  				<option value="Columbia">Columbia</option>
  				<option value="Cuba">Cuba</option>
 				<option value="Dominican-Republic">Dominican-Republic</option>
 				<option value="Ecuador">Ecuador</option>
  				<option value="El-Salvador">El-Salvador</option>
  				<option value="England">England</option>
  				<option value="France">France</option>
  				<option value="Germany">Germany</option>
 				<option value="Japan">Japan</option>
  				<option value="Laos">Laos</option>
  				<option value="Mexico">Mexico</option>
  				<option value="United-States">United-States</option>
  				<option value="Vietnam">Vietnam</option>
  				<option value="Yugoslavia">Yugoslavia</option>
			</select>
		</div>
		
		<div class="col-md-3 mb-1" >
			<button class="btn btn-default" id="button" style="width: 100px; padding: 5px;">Load Data</button>
		</div>
	</div>

</div>  <!--class="card-body"の終端-->
</div>  <!--class="card"の終端-->

<script type="text/javascript">
"use strict";


var send_data = {};

$("#button").click(function() {


	send_data = JSON.stringify({
	"age":$("#age").val(),
	"workclass":$("#workclass").val(),
	"education":" ",
	"education-num":$("#education-num").val(),
	"marital-status":$("#marital-status").val(),
	"relationship":$("#relationship").val(),
	"race":$("#race").val(),
	"sex":$("#sex").val(),
	"capital-gain":$("#capital-gain").val(),
	"capital-loss":$("#capital-loss").val(),
	"hours-per-week":$("#hours-per-week").val(),
	"native-country":$("#native-country").val()
	});
	
	$.ajax({
		type:'POST',
		url:'/azureML_cal',
		data:send_data,
		contentType:'application/json',
		success:function(data) {
			alert("結果 = " + data );},
		error: function(data) {
			alert('error!!!' + JSON.stringify(data) );
		}
	});
	return false;
});

</script>

{% endblock %}


```
```Python:サーバサイドソースmain.py


import os
import sys
import platform

sys.path.append('D:/home/site/wwwroot/Lib/site-packages')
from flask import Flask, render_template, request, jsonify, send_file, make_response, send_from_directory, request, jsonify, session, redirect, url_for
import json

import sqlite3
import werkzeug
import datetime
import time
import urllib.request

#************************************** 
#  ルーティング
#************************************** 
	
#azureML機械学習
@application.route('/azureML',methods=['POST'])
def azureML():   
    return render_template('azureML.html')

#**************************************
#azureML機械学習結果返信
#**************************************
@application.route('/azureML_cal', methods=['POST'])    
def azureML_cal():

    data = {
            "Inputs": {
                    "input1":
                    [
                        {
                                'age': request.json['age'],
                                'workclass': request.json['workclass'],   
                                'education': " ",   
                                'education-num': request.json['education-num'],   
                                'marital-status': request.json['marital-status'],   
                                'relationship': request.json['relationship'],   
                                'race': request.json['race'],   
                                'sex': request.json['sex'],   
                                'capital-gain': request.json['capital-gain'],   
                                'capital-loss': request.json['capital-loss'],   
                                'hours-per-week': request.json['hours-per-week'],   
                                'native-country': request.json['native-country'],   
                        }
                    ],
            },

        "GlobalParameters":  {
        }
    }

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

    url = 'https://japaneast.services.azureml.net/workspaces/fe89906902004c4a9eaaf4281e150～略～'

    api_key = '764McF0w85eyeYk8kM7pzrCm～略～' # Replace this with the API key for the web service

    headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key)}  #Bearer=無記名

    req = urllib.request.Request(url, body, headers)

    try:
        response = urllib.request.urlopen(req)
        result = response.read()
        print('★★★★★ok★★★★★')
        print(result)
    except urllib.error.HTTPError as error:

        print('★★★★★error★★★★★')
        print("The request failed with status code: " + str(error.code))
        print(error.info())
        print(json.loads(error.read().decode("utf8", 'ignore')))

    result_dic = json.loads(result)
    results = result_dic["Results"]
    for outputName in results:
        result_blob_location = results[outputName]
        out_sl  = result_blob_location[0]['Scored Labels']
    out_sl  = result_dic["Results"]["output1"][0]['Scored Labels']
    if out_sl is None:
        out_sl = "計算不可"
    return out_sl
```
一旦判ってしまえば、MLでいろいろ試したモデルを簡単に別サーバから呼び出せるので、マイクロソフトもMLのこういう使い方を想定しているのだと思いました。
AZUREの設定については下記を参照下さい。
[tabulatorを使って、データベースの更新にトライ(python版）](https://qiita.com/vye03702/items
/88800e38caaf38641409)

#クラシックAPI版
リクエストとレスポンスのjsonの形式がARM版と異なります。
また無料ワークスペースは米国中南部リージョンで作成されるので、そちらに向いています。

```Python:サーバサイドソースmain.py
#**************************************
#12.azureML機械学習結果返信の関数
#**************************************
@application.route('/azureML_cal', methods=['POST'])    
def azureML_cal():
    data = {
          "Inputs": {
              "input1": {
                  "ColumnNames": [
                    "age",
                    "workclass",
                    "education-num",
                    "occupation",
                    "race",
                    "sex",
                    "capital-gain",
                    "capital-loss",
                    "hours-per-week",
                    "native-country"
              ],
              "Values": [
                    [
                      request.json['age'],
                      request.json['workclass'],
                      request.json['education-num'],
                      request.json['occupation'],
                      request.json['race'],
                      request.json['sex'],
                      request.json['capital-gain'],
                      request.json['capital-loss'],
                      request.json['hours-per-week'],
                      request.json['native-country']
                    ],
              ]
            }
          },
          "GlobalParameters": {}
        }    

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

    if request.json['alg'] == "1":
        url = "https://ussouthcentral.services.azureml.net/workspaces/以下省略～"
        api_key = "M4pihqQXhoYR以下省略～" 
    elif request.json['alg'] == "2":
        url = "https://ussouthcentral.services.azureml.net/workspaces/以下省略～"
        api_key = "DuoTIGAEQwF以下省略～"
    else:
        url = "https://ussouthcentral.services.azureml.net/workspaces/以下省略～"
        api_key = "pFYdUvutM+以下省略～"
        
    headers = {'Content-Type':'application/json', 'Authorization':('Bearer '+ api_key)}  #Bearer=無記名
    req = urllib.request.Request(url, body, headers)

    try:
        response = urllib.request.urlopen(req)
        result = response.read()
        result_dic  = json.loads(result.decode('utf-8'))  
        log_out('azureML return OK /n' + json.dumps(result_dic , indent=2))
        out_sl  = result_dic["Results"]["output1"]["value"]["Values"][0][10]
    except urllib.error.HTTPError as error:
        log_out("The request failed with status code: " + str(error.code))
        return "error"

    return out_sl

--------------------------------------------------------
【azureML.htmlについては以下の部分だけ変えました】

<div class="col-md-2 mb-1">アルゴリズム</div>
<div class="col-md-4 mb-1" >
	<select class="form-control input-lg mb-1" id="alg">
	  <option value="1">DecisionForest</option>
	  <option value="2">DecisionJungle</option>
	  <option value="3">SVN</option>
        </select>
</div>	

```