LoginSignup
4
4

More than 3 years have passed since last update.

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

Posted at

はじめに

機械学習で生成した学習モデルを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のデータフレームとして読み込んでいます。

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
train_df = pd.read_csv('titanic/train.csv')
test_df = pd.read_csv('titanic/test.csv')
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
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データを連結しています。

all_df = pd.concat((train_df.loc[:, 'Pclass' : 'Embarked'], test_df.loc[:, 'Pclass' : 'Embarked']))
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の値が欠損しているので、平均値と最頻値で埋めています。

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])
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
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で数値に置き換えています。

cat_features = ['Sex', 'Embarked']

for col in cat_features:
    lbl = LabelEncoder()
    all_df[col] = lbl.fit_transform(list(all_df[col].values))
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は、欠損値が多いので削除します。

all_df = all_df.drop(columns = ['Name', 'Ticket', 'Cabin'])
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を分けることができます。

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サーバーを作ることを目的としているので、パラメーターなどはほぼデフォルト値で学習しています。

y = train_df['Survived']
X_train, X_test, y_train, y_test = train_test_split(train, y, random_state = 0)
import xgboost as xgb
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サーバーのフォルダにコピーするなりして使用します。

from sklearn.externals import joblib
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で仮想環境をアクティブな状態にします。

mkdir titanic_api
cd titanic_api
conda create -n titanictenv
conda activate titanictenv

FlaskでAPIを開発する

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

ターミナルに必要なフォルダとファイルを生成する。

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

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などを使用すると便利です。

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'
       }
     ]
  }))
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()
titanic_app.py
from api import app

if __name__ == '__main__':
  app.run()

curlでAPIサーバーにアクセスする。

先ほどのコードを書いたら、python titanic_app.py でサーバーを起動します。うまく起動できたら、別のターミナルを開いて、次のようなcurlコマンドで通信テストを行います。通信が成功していれば、以下のようなデータを返します。

curl http://127.0.0.1:5000/api/users
{
  "users": [
    {
      "id": 1, 
      "name": "John"
    }
  ]
}

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

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

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データに対して、小数点1以下の値が返ってきたら成功です。これで、機械学習で生成した学習モデルをAPIサーバーにすることができました。

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は事前にインストールしてある必要があります。

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ファイルは、インターフェイスの部分は

タグ内のinputタグなどで、データの入力フォームを作ります。その入力データをjavascriptで受け取って、整形し、JSON形式に変換して、Ajax通信でPOSTしています。通信が成功したら、APIサーバーからの予測値を受け取り、それをtextareaタグのエリア内に表示するという処理をしています。
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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4