はじめに
機械学習で生成した学習モデルを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などを使用すると便利です。
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'
}
]
}))
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()
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直下に保存している必要があります。
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は事前にインストールしてある必要があります。
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タグのエリア内に表示するという処理をしています。<!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