機械学習で学習モデルを生成・保存し、APIサーバーにしてブラウザーからJSONで通信するまでの手順

# はじめに
機械学習で生成した学習モデルをAPIサーバーにして、ブラウザーからJSON通信で、データを送って予測値を返すということをやりました。この機械学習によるAPIサーバーは、主に三つのプログラムによって実装されています。最初に、XGBoostで機械学習を行い、学習モデルを生成し、保存します。次にFlaskで学習モデルのAPIサーバーを実装します。最後に、HTMLファイルでフォームタグを書き、フォームタグから得たデータをjavascriptのAjaxでJSON通信を行えるようにします。この三つのプログラムによって、ブラウザーからAPIサーバーにデータを送って予測値を返すというものを作ることができます。

# このプログラムを実行するために必要な環境
Anaconda、XGBoost、joblib、Flask、flask-corsなどのライブラリがインストールされている。

# 主なプロセス
この機械学習によるAPI通信は以下のプロセスを追っていくと実装することができます。

-  機械学習で学習モデルを作る
-  flaskでAPIサーバーを作る
-  ブラウザーからAPI通信する

# 機械学習で学習モデルを作る
ここでは、XGBoostを使って学習モデルを生成します。学習データはKaggleのタイタニックのデータセットを使用しています。

## データセットの前処理
XGBoostで機械学習を行う前に、幾つかの前処理を行っています。Kaggleのタイタニックのデータセットはtrainとtestデータに分けてあるので、前処理をまとめて行うためにconcatで連結しています。前処理としては、欠損値の処理、カテゴリカルなデータを数値に置き換える、不必要な特徴量の削除などです。

### ライブラリとデータセットの読み込み
前処理に必要なライブラリを読み込みます。また、データセットをpandasのデータフレームとして読み込んでいます。

```python
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
```

```python
train_df = pd.read_csv('titanic/train.csv')
test_df = pd.read_csv('titanic/test.csv')
```

```python
train_df.head()
```

||PassengerId|Survived|Pclass|Name|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:-:|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|0|1|0|3|Braund, Mr. Owen Harris|male|22.0|1|0|A/5 21171|7.2500|NaN|S|
|1|2|1|1|Cumings, Mrs. John Bradley (Florence Briggs Th...|female|38.0|1|0|PC 17599|71.2833|C85|C|
|2|3|1|3|Heikkinen, Miss. Laina|female|26.0|0|0|STON/O2. 3101282|7.9250|NaN|S|
|3|4|1|1|Futrelle, Mrs. Jacques Heath (Lily May Peel)|female|35.0|1|0|113803|53.1000|C123|S|
|4|5|0|3|Allen, Mr. William Henry|male|35.0|0|0|373450|8.0500|NaN|S|

```python
test_df.head()
```

||PassengerId|Pclass|Name|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:-:|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|0|892|3|Kelly, Mr. James|male|34.5|0|0|330911|7.8292|NaN|Q|
|1|893|3|Wilkes, Mrs. James (Ellen Needs)|female|47.0|1|0|363272|7.0000|NaN|S|
|2|894|2|Myles, Mr. Thomas Francis|male|62.0|0|0|240276|9.6875|NaN|Q|
|3|895|3|Wirz, Mr. Albert|male|27.0|0|0|315154|8.6625|NaN|S|
|4|896|3|Hirvonen, Mrs. Alexander (Helga E Lindqvist)|female|22.0|1|1|3101298|12.2875|NaN|S|

### データセットの連結
まとめて前処理を行いたいので、concatでtrainデータと、testデータを連結しています。

```python
all_df = pd.concat((train_df.loc[:, 'Pclass' : 'Embarked'], test_df.loc[:, 'Pclass' : 'Embarked']))
```

```python
all_df.info()
```

```
<class 'pandas.core.frame.DataFrame'>  
Int64Index: 1309 entries, 0 to 417  
Data columns (total 10 columns):  
Pclass      1309 non-null int64  
Name        1309 non-null object  
Sex         1309 non-null object  
Age         1046 non-null float64  
SibSp       1309 non-null int64  
Parch       1309 non-null int64  
Ticket      1309 non-null object  
Fare        1308 non-null float64  
Cabin       295 non-null object  
Embarked    1307 non-null object  
dtypes: float64(2), int64(3), object(5)  
memory usage: 112.5+ KB  
```

### 欠損値の処理
Age、Fare、Embarkedの値が欠損しているので、平均値と最頻値で埋めています。

```python
all_df['Age'] = all_df['Age'].fillna(all_df['Age'].mean())
all_df['Fare'] = all_df['Fare'].fillna(all_df['Fare'].mean())
all_df['Embarked'] = all_df['Embarked'].fillna(all_df['Embarked'].mode()[0])
```

```python
all_df.info()
```

```
<class 'pandas.core.frame.DataFrame'>  
Int64Index: 1309 entries, 0 to 417  
Data columns (total 10 columns):  
Pclass      1309 non-null int64  
Name        1309 non-null object  
Sex         1309 non-null object  
Age         1309 non-null float64  
SibSp       1309 non-null int64  
Parch       1309 non-null int64  
Ticket      1309 non-null object  
Fare        1309 non-null float64  
Cabin       295 non-null object  
Embarked    1309 non-null object  
dtypes: float64(2), int64(3), object(5)  
memory usage: 112.5+ KB
```

```python
all_df.head()
```

||Pclass|Name|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:-:|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|0|3|Braund, Mr. Owen Harris|male|22.0|1|0|A/5 21171|7.2500|NaN|S|
|1|1|Cumings, Mrs. John Bradley (Florence Briggs Th...|female|38.0|1|0|PC 17599|71.2833|C85|C|
|2|3|Heikkinen, Miss. Laina|female|26.0|0|0|STON/O2. 3101282|7.9250|NaN|S|
|3|1|Futrelle, Mrs. Jacques Heath (Lily May Peel)|female|35.0|1|0|113803|53.1000|C123|S|
|4|3|Allen, Mr. William Henry|male|35.0|0|0|373450|8.0500|NaN|S|


### カテゴリカルなデータを数値に置き換える
SexとEmbarkedは、カテゴリカルなデータなので、LabelEncoderで数値に置き換えています。

```python
cat_features = ['Sex', 'Embarked']

for col in cat_features:
    lbl = LabelEncoder()
    all_df[col] = lbl.fit_transform(list(all_df[col].values))
```

```python
all_df.head()
```

||Pclass|Name|Sex|Age|SibSp|Parch|Ticket|Fare|Cabin|Embarked|
|:-:|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|0|3|Braund, Mr. Owen Harris|1|22.0|1|0|A/5 21171|7.2500|NaN|2|
|1|1|Cumings, Mrs. John Bradley (Florence Briggs Th...|0|38.0|1|0|PC 17599|71.2833|C85|0|
|2|3|Heikkinen, Miss. Laina|0|26.0|0|0|STON/O2. 3101282|7.9250|NaN|2|
|3|1|Futrelle, Mrs. Jacques Heath (Lily May Peel)|0|35.0|1|0|113803|53.1000|C123|2|
|4|3|Allen, Mr. William Henry|1|35.0|0|0|373450|8.0500|NaN|2|

### 不要な特徴量を削除する
Name、Ticketは、カテゴリカルで、ユニークな値なので、削除します。また、Cabinは、欠損値が多いので削除します。

```python
all_df = all_df.drop(columns = ['Name', 'Ticket', 'Cabin'])
```

```python
all_df.head()
```

||Pclass|Sex|Age|SibSp|Parch|Fare|Embarked|
|:-:|:--|:--|:--|:--|:--|:--|:--|
|0|3|1|22.0|1|0|7.2500|2|
|1|1|0|38.0|1|0|71.2833|0|
|2|3|0|26.0|0|0|7.9250|2|
|3|1|0|35.0|1|0|53.1000|2|
|4|3|1|35.0|0|0|8.0500|2|

### trainとtestを元のように分ける
trainとtestを連結していたので、学習データとなるようにtrainとtestを分けます。train_dfのshapeの値を使用することでtrainとtestを分けることができます。

```python
train = all_df[:train_df.shape[0]]
test = all_df[train_df.shape[0]:]
train.info()
```

```
<class 'pandas.core.frame.DataFrame'>  
Int64Index: 891 entries, 0 to 890  
Data columns (total 7 columns):  
Pclass      891 non-null int64  
Sex         891 non-null int64  
Age         891 non-null float64  
SibSp       891 non-null int64  
Parch       891 non-null int64  
Fare        891 non-null float64  
Embarked    891 non-null int64  
dtypes: float64(2), int64(5)  
memory usage: 55.7 KB  
```

## XGBoostで学習モデルを生成する
一通り、前処理が終わったので、XGBoostで機械学習を行っていきます。今回は学習モデルの精度を上げるというよりも、学習モデルを使ってAPIサーバーを作ることを目的としているので、パラメーターなどはほぼデフォルト値で学習しています。

```python
y = train_df['Survived']
```

```python
X_train, X_test, y_train, y_test = train_test_split(train, y, random_state = 0)
```

```python
import xgboost as xgb
```

```python
params = {
    "objective": "binary:logistic",
    "eval_metric": "auc",
    "eta": 0.1,
    "max_depth": 6,
    "subsample": 1,
    "colsample_bytree": 1,
    "silent": 1
}

dtrain = xgb.DMatrix(X_train, label = y_train)
dtest = xgb.DMatrix(X_test, label = y_test)

model = xgb.train(params = params,
                 dtrain = dtrain,
                 num_boost_round = 100,
                 early_stopping_rounds = 10,
                 evals = [(dtest, 'test')])
```

```
[0]	test-auc:0.886905  
Will train until test-auc hasn't improved in 10 rounds.  
[1]	test-auc:0.89624  
[2]	test-auc:0.893243  
[3]	test-auc:0.889603  
[4]	test-auc:0.892857  
[5]	test-auc:0.886005  
[6]	test-auc:0.890673  
[7]	test-auc:0.894741  
[8]	test-auc:0.889603  
[9]	test-auc:0.888832  
[10]	test-auc:0.889431  
[11]	test-auc:0.89153  
Stopping. Best iteration:  
[1]	test-auc:0.89624  
```

## joblibで学習モデルを保存する
機械学習を行った学習モデルを保存する方法はいくつかありますが、ここではjoblibを使用してpklファイルとして保存しています。保存したpklファイルは指定した場所に保存されているので、この後のAPIサーバーのフォルダにコピーするなりして使用します。

```python
from sklearn.externals import joblib
```

```python
joblib.dump(model, 'titanic_model.pkl')
```
['titanic_model.pkl']


# FlaskでAPIサーバーを作る
ここでは、先ほど機械学習で生成した学習モデルをAPIサーバーにするということをやっています。APIサーバー開発にはPythonのマイクロサービス系のフレームワークである、Flaskを使用しています。開発の流れとしては、condaで仮想環境を構築し、簡易的なAPIサーバーをテストして、そこにXGBoostで作った学習モデルを載せるという流れになります。

## condaで仮想環境を構築する
仮想環境は、Anacondaのcondaを使用します。ターミナルでアプリ開発用のフォルダ（ここでは、titanic_api）を作り、そのフォルダ内に移動します。そしたらconda createで仮想環境を生成し、conda activateで仮想環境をアクティブな状態にします。

```terminal
mkdir titanic_api
cd titanic_api
```

```terminal
conda create -n titanictenv
conda activate titanictenv
```

## FlaskでAPIを開発する

FlaskでAPIサーバーを開発するために、最初に簡易的なAPIサーバーを作ってテストしてみます。先ほど作ったフォルダ内に以下のようなフォルダとファイルを作ります。ファイルにはそれぞれ以下のようなコードを書いて、APIサーバーを起動して、curlから通信できれば簡易的なAPIサーバーテストの成功です。

### ターミナルに必要なフォルダとファイルを生成する。
以下のような階層になるようにフォルダとファイルを作ります。空ファイルを作るならtouchコマンドなどを使用すると便利です。

```terminal
titanic_api
├── api
│   ├── __init__.py
│   └── views
│       └── user.py
├── titanic_app.py
└── titanic_model.pkl
```
### 作成したファイルにコードを書く

先ほど作成したファイルに以下のようにコードを書きます。簡易的なAPIサーバーをテストするために必要なファイルは、api/views/user.py、api/__init__.py、titanic_app.pyの三つです。ターミナルで書く場合はvim、GUIで書く場合はAtomなどを使用すると便利です。

```python:api/views/user.py
from flask import Blueprint, request, make_response, jsonify

# ルーティング設定
user_router = Blueprint('user_router', __name__)

# パスとHTTPメソッドを指定
@user_router.route('/users', methods=['GET'])
def get_user_list():

  return make_response(jsonify({
    'users': [
       {
         'id': 1,
         'name': 'John'
       }
     ]
  }))
```

```python:api/__init__.py
from flask import Flask, make_response, jsonify
from .views.user import user_router

def create_app():

  app = Flask(__name__)
  app.register_blueprint(user_router, url_prefix='/api')

  return app

app = create_app()
```

```python:titanic_app.py
from api import app

if __name__ == '__main__':
  app.run()
```
### curlでAPIサーバーにアクセスする。
先ほどのコードを書いたら、python titanic_app.py でサーバーを起動します。うまく起動できたら、別のターミナルを開いて、次のようなcurlコマンドで通信テストを行います。通信が成功していれば、以下のようなデータを返します。

```terminal
curl http://127.0.0.1:5000/api/users
```

```terminal
{
  "users": [
    {
      "id": 1, 
      "name": "John"
    }
  ]
}
```

## xgboostのモデルをAPIサーバーにする

先ほどの簡易的なAPIサーバーの起動ファイルである、titanic_app.pyを以下のように書き換えます。この時に学習モデルをtitanic_api直下に保存している必要があります。

```python:titanic_app.py
import json

from flask import Flask
from flask import request
from flask import abort

import pandas as pd
from sklearn.externals import joblib
import xgboost as xgb

model = joblib.load("titanic_model.pkl")

app = Flask(__name__)

# Get headers for payload
headers = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

@app.route('/titanic', methods=['POST'])
def titanic():
    if not request.json:
        abort(400)
    payload = request.json['data']
    values = [float(i) for i in payload.split(',')]
    data1 = pd.DataFrame([values], columns=headers, dtype=float)
    predict = model.predict(xgb.DMatrix(data1))
    return json.dumps(str(predict[0]))


if __name__ == "__main__":
    app.run(debug=True, port=5000)
```

## curlでAPI通信テストをする

コードを書き換えたら、改めて、python titanic_app.py で、APIサーバーを起動します。APIサーバーが起動したら、以下のようにcurlコマンドで通信テストをしています。送ったJSONデータに対して、小数点１以下の値が返ってきたら成功です。これで、機械学習で生成した学習モデルをAPIサーバーにすることができました。

```terminal
curl http://localhost:5000/titanic -s -X POST -H "Content-Type: application/json" -d '{"data": "3, 1, 22.0, 1, 0, 7.2500, 2"}'
```

# ブラウザーからAPI通信する
最後に、ターミナルからのcurlコマンドを使った通信だけでなく、ブラウザーから値を入力したら予測値を返すというものを作ってい行きます。ここで、行うことは先ほど作ったAPIサーバーをAjax通信ができるようにすることと、HTMLファイルでブラウザーからAPIサーバーに通信できるようにすることです。

## Ajax通信でAPIサーバーにjsonをPOSTできるようにする
ここでは、HTMLからjavascriptのAjaxで通信できるようにするために、Flaskで書いた、APIサーバーの起動ファイルを以下のように追記する必要があります。追記するのは、flask_corsというライブラリとそれに関連するコードです。flask_corsは事前にインストールしてある必要があります。

```python:titanic_app.py
import json

from flask import Flask
from flask import request
from flask import abort
from flask_cors import CORS #追加する

import pandas as pd
from sklearn.externals import joblib
import xgboost as xgb

model = joblib.load("titanic_model.pkl")

app = Flask(__name__)

# 追加
@app.after_request
def after_request(response):
  response.headers.add('Access-Control-Allow-Origin', '*')
  response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  return response
# ↑ここまでを追加

# Get headers for payload
headers = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

@app.route('/titanic', methods=['POST'])
def titanic():
    if not request.json:
        abort(400)
    payload = request.json['data']
    values = [float(i) for i in payload.split(',')]
    data1 = pd.DataFrame([values], columns=headers, dtype=float)
    predict = model.predict(xgb.DMatrix(data1))
    return json.dumps(str(predict[0]))


if __name__ == "__main__":
    app.run(debug=True, port=5000)
```

## HTMLファイルからPOSTでJSONデータを送信する
HTMLファイルは、インターフェイスの部分は<body>タグ内のinputタグなどで、データの入力フォームを作ります。その入力データをjavascriptで受け取って、整形し、JSON形式に変換して、Ajax通信でPOSTしています。通信が成功したら、APIサーバーからの予測値を受け取り、それをtextareaタグのエリア内に表示するという処理をしています。

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HTMLファイルからPOSTでJSONデータを送信する</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>

<script type="text/javascript">
    $(function(){
        $("#response").html("Response Values");

        $("#button").click( function(){
            var url = $("#url_post").val();
            var feature1 =
                $("#value1").val() + "," +
                $("#value2").val() + "," +
                $("#value3").val() + "," +
                $("#value4").val() + "," +
                $("#value5").val() + "," +
                $("#value6").val() + "," +
                $("#value7").val();

            var JSONdata = {
                    data: feature1
                };

            alert(JSON.stringify(JSONdata));

            $.ajax({
                type: 'POST',
                url: url,
                data: JSON.stringify(JSONdata),
                contentType: 'application/JSON',
                dataType: 'JSON',
                scriptCharset: 'utf-8',
                success : function(data) {

                    // Success
                    alert("success");
                    alert(JSON.stringify(JSONdata));
                    $("#response").html(JSON.stringify(data));
                },
                error : function(data) {

                    // Error
                    alert("error");
                    alert(JSON.stringify(JSONdata));
                    $("#response").html(JSON.stringify(data));
                }
            });
        })
    })
</script>

</head>
<body>
    <h1>HTMLファイルからPOSTでJSONデータを送信する</h1>
    <p>URL: <input type="text" id="url_post" name="url" size="100" value="http://localhost:5000/titanic"></p>
    <p>Pclass: <input type="number" id="value1" size="30" value=3></p>
    <p>Sex: <input type="number" id="value2" size="30" value=1></p>
    <p>Age: <input type="number" id="value3" size="30" value="22.0"></p>
    <p>SibSp: <input type="number" id="value4" size="30" value="1"></p>
    <p>Parch: <input type="number" id="value5" size="30" value="0"></p>
    <p>Fare: <input type="number" id="value6" size="30" value="7.2500"></p>
    <p>Embarked: <input type="number" id="value7" size="30" value="2"></p>
    <p><button id="button" type="button">submit</button></p>
    <textarea id="response" cols=120 rows=10 disabled></textarea>
</body>
</html>
```

参考
condaで仮想環境を構築する：https://code-graffiti.com/how-to-build-a-virtual-environment-with-conda/
flaskでAPIを開発する：https://swallow-incubate.com/archives/blog/20190819
xgboostのモデルをAPIサーバーにする：https://towardsdatascience.com/publishing-machine-learning-api-with-python-flask-98be46fb2440
Ajax通信でAPIサーバーにjsonをPOSTできるようにする：https://www.hands-lab.com/tech/entry/3716.html
HTMLファイルからPOSTでJSONデータを送信する：https://qiita.com/kidatti/items/21cc5c5154dbbb1aa27f