Flask+Vue.jsでベイズ最適化アプリを作成してみた
0.はじめに
Flask + Vue.jsで簡単なベイズ最適化アプリを作成したので備忘録がてらまとめておきます。
1.つくりたいもの
以下のような、csv形式のテーブルデータ(property:目的変数、feature:説明変数、説明変数は2つ)を読み込んで目的変数を最大化する条件探索をベイズ最適化によって行うwebアプリケーションを作成します。
vue.jsで作成したフロントエンドで各変数の取りうる上下限値をフォームに入力、探索初期サンプルの入出力が記載されたcsv形式のデータを読み込みPlotボタンを押すとバックエンドで読み込んだデータを使用してベイズ最適化が行われ、獲得関数マップが表示されます。
また、獲得関数をもとに次の探索候補点(緑点)が表示されます。実験者は次の探索候補点の条件で再度データ取得を行い、取得したデータを追加し、再度アプリケーションを使用することで対話的に目的変数を最大化する条件の最適化が行えます。
初期サンプルデータ
No | property | feature_1 | feature_2 |
---|---|---|---|
sample_1 | -1.6901378073208400 | 6.17881511358717 | 2.686356032301740 |
sample_2 | -0.19462331210335700 | 3.716836009702280 | 3.4580994147680800 |
sample_3 | -0.05709174688868170 | 3.361108721385220 | 2.8771444893592500 |
sample_4 | -0.5455318144995780 | 5.307888614497350 | 2.1193557932191400 |
sample_5 | -1.8222159386202400 | 6.316813818713380 | 2.796088510660860 |
作成するアプリの出力
2.バックエンド
バックエンドではフロントエンドで入力したデータと説明変数の上下値を受け取り、ベイズ最適化を行います。
ベイズ最適化によって得られた獲得関数、次の探索候補点座標をjson形式に変換し、フロントエンドへ渡します。
app.py内に定義したエンドポイントをフロントから呼び出すことで上記のjson形式データを渡します。
ベイズ最適化関数はgpbo.py内に関数を定義します。
app.pyとgpbo.pyは同じ階層に配置しておきます。
2.1 事前準備
以下のライブラリをインストールしておきます。
pip install flask pip install flask_cors pip install pandas pip install numpy pip install scipy pip install scikit-learn
2.2 スクリプト
app.py
from flask import Flask, jsonify, request, send_file from flask_cors import CORS import pandas as pd import numpy as np from gpbo import gpbo app = Flask(__name__) cors = CORS(app, resources={r"/api/*": {"origins":"*"}} ) #エンドポイントの作成 @app.route('/api/v1.0/gpbo', methods=['POST']) def upload(): # フロントエンドで入力したCSVファイルを受け取る csv_file = request.files['file'] # ファイルをPandasのデータフレーム形式に変換する df = pd.read_csv(csv_file, index_col=0) print(df.head()) print('ファイルをアップロードしました') #探索の上下限の設定 lower_feature1 = int(request.form['Feature1_Lower']) upper_feature1 = int(request.form['Feature1_Upper']) lower_feature2 = int(request.form['Feature2_Lower']) upper_feature2 = int(request.form['Feature2_Upper']) # x, y の値域を指定(探索範囲内メッシュデータ、獲得関数マップを描画する目的) x_= np.linspace(lower_feature1, upper_feature1, 100) y_ = np.linspace(lower_feature2, upper_feature2, 100) X, Y = np.meshgrid(x_, y_) #ベイズ最適化により次の探索点を提案、獲得関数の値 z_, next_sample = gpbo(df, X, Y) #これまでのサンプルデータ、探索範囲内データ、次の探索点をjson形式で出力する。 data = {'x':df.iloc[:,1].values.tolist(), 'y':df.iloc[:,2].values.tolist(),'x_':x_.tolist(), 'y_':y_.tolist(), 'z_':z_.tolist(),'x_next':next_sample.iloc[:,0].values.tolist(), 'y_next':next_sample.iloc[:,1].values.tolist()} return jsonify(data) if __name__ == '__main__': app.run(debug=True)
gpbo.py
import pandas as pd import numpy as np from scipy.stats import norm from sklearn.model_selection import KFold, cross_val_predict from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import WhiteKernel, RBF, ConstantKernel, Matern, DotProduct from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error def gpbo(dataset, X, Y): acquisition_function = 'EI' fold_number = 4 # クロスバリデーションの fold 数 kernel_number = 2 # 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 relaxation = 0.01 # EI # データ分割 y = dataset.iloc[:, 0] # 目的変数 x = dataset.iloc[:, 1:] # 説明変数 #テストデータの設定 X_ = X.reshape(-1) Y_ = Y.reshape(-1) x_prediction = pd.DataFrame() x_prediction['feature_1'] = X_ x_prediction['feature_2'] = Y_ autoscaled_x_prediction = (x_prediction - x.mean()) / x.std() # カーネル 11 種類 kernels = [ConstantKernel() * DotProduct() + WhiteKernel(), ConstantKernel() * RBF() + WhiteKernel(), ConstantKernel() * RBF() + WhiteKernel() + ConstantKernel() * DotProduct(), ConstantKernel() * RBF(np.ones(x.shape[1])) + WhiteKernel(), ConstantKernel() * RBF(np.ones(x.shape[1])) + WhiteKernel() + ConstantKernel() * DotProduct(), ConstantKernel() * Matern(nu=1.5) + WhiteKernel(), ConstantKernel() * Matern(nu=1.5) + WhiteKernel() + ConstantKernel() * DotProduct(), ConstantKernel() * Matern(nu=0.5) + WhiteKernel(), ConstantKernel() * Matern(nu=0.5) + WhiteKernel() + ConstantKernel() * DotProduct(), ConstantKernel() * Matern(nu=2.5) + WhiteKernel(), ConstantKernel() * Matern(nu=2.5) + WhiteKernel() + ConstantKernel() * DotProduct()] # オートスケーリング autoscaled_y = (y - y.mean()) / y.std() autoscaled_x = (x - x.mean()) / x.std() autoscaled_x_prediction = (x_prediction - x.mean()) / x.std() # モデル構築 selected_kernel = kernels[kernel_number] model = GaussianProcessRegressor(alpha=0, kernel=selected_kernel) model.fit(autoscaled_x, autoscaled_y) # モデル構築 # 予測 estimated_y_prediction, estimated_y_prediction_std = model.predict(autoscaled_x_prediction, return_std=True) estimated_y_prediction = estimated_y_prediction * y.std() + y.mean() estimated_y_prediction_std = estimated_y_prediction_std * y.std() # 獲得関数の計算 acquisition_function_prediction = (estimated_y_prediction - max(y) - relaxation * y.std()) * norm.cdf((estimated_y_prediction - max(y) - relaxation * y.std()) / estimated_y_prediction_std) + estimated_y_prediction_std * norm.pdf((estimated_y_prediction - max(y) - relaxation * y.std()) / estimated_y_prediction_std) acquisition_function_prediction[estimated_y_prediction_std <= 0] = 0 estimated_y_prediction = pd.DataFrame(estimated_y_prediction, x_prediction.index, columns=['estimated_y']) estimated_y_prediction_std = pd.DataFrame(estimated_y_prediction_std, x_prediction.index, columns=['std_of_estimated_y']) acquisition_function_prediction = pd.DataFrame(acquisition_function_prediction, index=x_prediction.index, columns=['acquisition_function']) # 次のサンプル next_sample = x_prediction.loc[acquisition_function_prediction.idxmax()] return acquisition_function_prediction.values.reshape(X.shape[0], Y.shape[0]), next_sample
2.3 実行
以下のコマンドを実行して、バックエンドサーバーを立ち上げます。
python app.py
すると、http://localhost:5000
にバックエンドサーバーが立ち上がります。
3.フロントエンド
Vue.js(Vue-CLI)を使用してフロントエンドを作成します。 csvデータとフォームに入力した説明変数の上下値をバックエンドに渡し、バックエンドでベイズ最適化が行われた戻り値を受け取り、コンター図を描画する機能を実装します。
3.1 事前準備
node.jsをインストールしておく必要があります。node.jsのインストール
インストール後、npmコマンドが実行できることを確認してください。
以下のコマンドを実行してVueをインストールします。
npm install -g @vue/cli
vueの新しいプロジェクトを作成します。
npm init webpack my-project
(Y / n)を押して選択
矢印キーで選択してEnter
? Project name my-project ? Project description A Vue.js project ? Author ? Vue build standalone ? Install vue-router? Yes ? Use ESLint to lint your code? Yes ? Pick an ESLint preset Standard ? Set up unit tests Yes ? Pick a test runner jest ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recommended) npm
質問が終わるとプロジェクトの作成が始まります。
終了すると、実行ディレクトリ/プロジェクト名
配下にファイルが作成されました。
ファイルが作成されたことを確認し、
cd my-project npm run dev
上記のコマンドを実行すると、vueアプリケーションがhttp://localhost:8080
に立ち上がります。
アクセスして確認してください。
今回のアプロケーションではflaskバックエンドapiとhttpクライアントのaxiosによって通信を行うので、axiosをインストールします。
npm install axios --save
また、flaskバックエンドapiで受け取ったデータを用いてフロントエンドで獲得関数マップの描画を行うため、plotly.jsをインストールします。
npm install plotly.js
3.2 ディレクトリ構造
3.1でvueプロジェクトを作成すると以下のディレクトリが作成されています。
. ├── README.md ├── build ├── config ├── index.html ├── node_modules ├── package-lock.json ├── package.json ├── src └── static
今回は./srcディレクトリ内のcomponentsディレクトリ内にFigure.vueを追加し、Figure.vueをflaskバックエンドapiを受け取って、獲得関数マップを表示するフロントエンドして機能させます。
また、index.jsにFigure.vueのルーティングを追加します。
. ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Figure.vue │ ├── HelloWorld.vue | ├── main.js └── router └── index.js
3.3 スクリプト
Figure.vue
<template> <div> <form> <div> <label for="feature1_upper">Feature1_Upper:</label> <input type="text" id="feature1_upper" v-model="feature1_upper"> </div> <div> <label for="feature1_lower">Feature1_Lower:</label> <input type="text" id="feature1_lower" v-model="feature1_lower"> </div> <div> <label for="feature2_upper">Feature2_Upper:</label> <input type="text" id="feature2_upper" v-model="feature2_upper"> </div> <div> <label for="feature2_lower">Feature2_Lower:</label> <input type="text" id="feature2_lower" v-model="feature2_lower"> </div> </form> <input type="file" ref="fileInput" @change="onFileSelected"> <button @click="drawPlot" :disabled="!dataLoaded">Plot</button> <div ref="plot" class="plot"></div> </div> </template> <script> import axios from 'axios' import Plotly from 'plotly.js' export default { data () { return { feature1_upper: '', feature1_lower: '', feature2_upper: '', feature2_lower: '', x_: [], y_: [], z_: [], x: [], y: [], x_next: [], y_next: [], dataLoaded: false } }, methods: { onFileSelected (event) { const file = event.target.files[0] const formData = new FormData() formData.append('file', file) formData.append('Feature1_Upper', this.feature1_upper) formData.append('Feature1_Lower', this.feature1_lower) formData.append('Feature2_Upper', this.feature2_upper) formData.append('Feature2_Lower', this.feature2_lower) axios.post('http://127.0.0.1:5000/api/v1.0/gpbo', formData) .then(response => { this.x = response.data.x this.y = response.data.y this.x_ = response.data.x_ this.y_ = response.data.y_ this.z_ = response.data.z_ this.x_next = response.data.x_next this.y_next = response.data.y_next this.dataLoaded = true }) .catch(error => { console.log(error) }) }, drawPlot () { const trace = { x: this.x_, y: this.y_, z: this.z_, type: 'contour' } const scatterTrace = { x: this.x, y: this.y, mode: 'markers', type: 'scatter', marker: { color: 'orange', // 色をzに設定する size: 10 // サイズを10に設定する } } const scatterTrace2 = { x: this.x_next, y: this.y_next, mode: 'markers', type: 'scatter', marker: { color: 'green', // 色をzに設定する size: 10 // サイズを10に設定する } } const layout = { title: 'Acquisition function map', xaxis: { title: 'Feature1' }, yaxis: { title: 'Feature2' } } Plotly.newPlot(this.$refs.plot, [trace, scatterTrace, scatterTrace2], layout) } } } </script> <style> .plot { width: 600px; height: 600px; margin: auto; } </style>
※バックエンドのローカルアドレスをhttp://localhost:5000
とするとエラーが出てしまいました。
index.js
import Vue from 'vue' import Router from 'vue-router' import Figure from '@/components/Figure' Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/figure', name: 'Figure', component: Figure } ] })
3.4 実行
npm run dev
を実行するとフロントエンドサーバーがhttp://localhost:8080
に立ち上がります。
今回作成したベイズ最適化アプリはhttp://localhost:8080/figure
に立ち上がります。
4.使用例
下記のような解が既知である問題に対して、今回作成したベイズ最適化アプリを使って目的変数が最大となる解の探索を行なってみます。
出力関数は以下です。
def f(x,y): return np.cos(x) + np.cos(y)+ np.cos(x+y)+ np.cos(x-y)
初期サンプル(オレンジの点)を10点とり、ベイズ最適化を実行し、獲得関数をアプリで可視化、次の探索点で評価(上式に代入)、テーブルデータに追加を繰り返します。
1回目
2回目
3回目
4回目
作成したベイズ最適化アプリを使用することで獲得関数マップを確認しつつ、8回の試行でおおよそ目的変数が最大となる2変数の組み合わせを見つけられました。
5.参考
https://github.com/hkaneko1985/python_doe_kspub
Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - フロントエンド編】
Vue.js(vue-cli)とFlaskを使って簡易アプリを作成する【前半 - バックエンド編】
Flaskを勉強するのに下記の書籍を参考にしました。