本記事でやること
Nuxt.jsとResponder(pythonのwebフレームワーク)を使って簡単な機械学習アプリケーションを作成する。
またこちらの記事は、2019年開催「技術書典6」で販売されていた「Nuxt.jsとPythonでつくる『ぬるさくAIアプリ開発入門』」を参考に作成させて頂きました。
- フロントエンド(Nuxt.js)
- 入力データを受け付けるフォームを作成する
- 入力された値を元にAPIを叩き、返り値を画面上に出力する
- バックエンド(Responder)
- 入力データに対して(irisの多値分類)予測ラベルを返すAPIを作成する
- Docker化
- docker-composeを用いてDocker内でアプリケーションを動かせる様にする
本記事でやらないことは以下になりますので他の記事を参照してください。
- irisのデータセットを使った予測モデルの作成
対象読者
- 機械学習を使ったアプリケーションを作成してみたい方
- Nuxt.js / Responderを触ってみたい方
成果物
- 以下の様な挙動のアプリケーションをNuxt.jsとResponderを用いて作成する。
アプリケーションのディレクトリ構成
nuxt-responder-app
├── client ## Nuxt.jsのソースコード格納
│ ├── Dockerfile
│ ├── README.md
│ ├── assets
│ ├── components
│ ├── layouts
│ ├── middleware
│ ├── node_modules
│ ├── nuxt.config.js
│ ├── package.json
│ ├── pages
│ ├── plugins
│ ├── static
│ ├── store
│ └── yarn.lock
├── docker-compose.yml
└── server ## pythonファイルを格納
├── Dockerfile
├── __pycache__
├── api.py
├── handlers.py
├── services.py
├── static
└── templates
フロントエンドの作成(Nuxt.js)
Nuxtプロジェクトの作成
$ mkdir nuxt-responder-app
$ cd nuxt-responder-app
$ mkdir client
$ cd client
$ npx create-nuxt-app
(npx
コマンドはnpm
にバンドルされているのでnpx
単体でのインストールは不要です。)
npm
のインストール(mac環境)に関しては、こちらの記事を参考にさせて頂きました。
上記npx
コマンドを実行すると以下を聞かれます。今回はこの様な設定にしました。
? Project name client
? Project description My fabulous Nuxt.js project
? Use a custom server framework none
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Use a custom UI framework vuetify
? Use a custom test framework none
? Choose rendering mode Universal
? Author name your-name
? Choose a package manager yarn
To get started:
yarn run dev
To build & start for production:
yarn run build
yarn start
上記設定が完了したら、試しにアプリケーションを起動します。
yarn run dev
localhost:3000
にアクセスし、画面が表示されればひとまずプロジェクトの作成は完了です。
画面の共通部分ヘッダーとフッターを作成する
Nuxt.jsの場合、画面のレイアウトを修正する際にはlayouts
配下のファイルを修正していきます。
各ディレクトリの役割は、公式サイトを参照してください。
client
├── Dockerfile
├── README.md
├── assets
├── components
├── layouts <- 画面共通部分のディレクトリ
│ ├── README.md
│ └── default.vue
├── middleware
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
├── store
└── yarn.lock
以下の様に、template
内に <v-toolbar>
と<v-footer>
を定義すれば簡単にヘッダーとフッターをいい感じに表示してくれます。
また、次で作成するフォームなどのコンテンツは<v-content>
内からcomponetsのファイルを呼んでいます。
<template>
<v-app>
<v-toolbar
app
color="primary"
class="white--text"
>
<v-toolbar-side-icon class="white--text"/>
<v-toolbar-title>Title</v-toolbar-title>
</v-toolbar>
<v-content>
<v-container>
<nuxt/>
</v-container>
</v-content>
<v-footer
app
>
<span>Footer</span>
</v-footer>
</v-app>
</template>
入力を受け付けるフォームを作成する
画面に出力するコンテンツは、components
以下に作成していきます。
(コンテンツをパーツ毎にファイルを分けて作成できるところがvue.js / Nuxt.jsの良いところなのでしょうか?個人的にはそう感じました。)
今回は、簡単なフォームを作成するだけなのでFormText.vue
の1つのファイルだけ新しく作成します。
client
├── Dockerfile
├── README.md
├── assets
├── components <- 画面のコンテンツのディレクトリ
│ ├── FormText.vue <- フォーム及び返り値を出力するファイル
│ └── README.md
├── layouts
├── middleware
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
├── store
└── yarn.lock
今回は、irisのラベルを予測するモデルを使うので入力値はSepal Length
とSepal Width
、Petal Length
、Petal Width
の4つになります。なので、4つの値を受け付けるフォームを作成していきます。
基本的には<form>
の中に、<v-text-field>
を作成すればokです。非常に簡単です。
また、<v-text-field>
タグ?の値ですが、
-
v-model
: テキストフィールドに入力された値をstore/index.js
で定義した初期値state.sepalLengthに代入する。(詳細は公式サイトを参照してください) -
label
: テキストフィールドの項目名を表示します。
<template>
<form>
<div>
<v-text-field
v-model="$store.state.sepalLength"
label="Sepal Length"
></v-text-field>
<v-text-field
v-model="$store.state.sepalWidth"
label="Sepal Width"
></v-text-field>
<v-text-field
v-model="$store.state.petalLength"
label="Petal Length"
></v-text-field>
<v-text-field
v-model="$store.state.petalWidth"
label="Petal Width"
></v-text-field>
</div>
</form>
</template>
<script>
export default {
name: "FormText"
}
</script>
<style scoped>
</style>
store/index.js
のinitialState
箇所で入力値である、sepalLength
とsepalWidth
、petalLength
、petalWidth
の初期値を定義します。
(mutation
の中身については後述します。)
export const strict = false;
const initialState = {
sepalLength: '',
sepalWidth: '',
petalLength: '',
petalWidth: '',
predictLabel: ''
};
export const state = () => Object.assign({}, initialState);
export const mutations ={
setPredictLabel (state, predictLabel) {
state.predictLabel = predictLabel
},
clear(state) {
for (let key in state) {
if (initialState.hasOwnProperty(key)) {
state[key] = initialState[key];
}
}
}
};
入力された値を元にAPIを叩き、返り値を画面上に出力する
前述した通り、フォームに値が入力されるとそれぞれ初期値が更新されます。(v-model
の箇所を参照してください)
その入力値を保持し、APIを叩きにいきますがその前にaxios
をインストールします。
yarn install --save axios
インストールが完了したら、nuxt.config.js
の下記箇所に追記をします。
modules: [
'@nuxtjs/axios'
],
axios: {
proxy:true,
},
これで、APIを叩きにいく準備が整いましたのでComponetsのFormText.vue
にAPIを叩く箇所を追記します。
<div class="predict">
の中にsubmit
とclear
というボタンを追加しました。
このボタンの役割は、以下の通りです。
-
submit
: 入力された値を元に予測ラベルを返すAPIを叩く -
clear
: 入力された値を全て消す。(初期値に戻す)
<script>
の中に、submit
とclear
という名前のメソッドを追加しています。
submit
メソッドは、見てわかる通りaxios
のpostメソッドを使って、入力値を元にAPI(localhost:5042/api/predict
)にアクセスし、返り値をstore/index.js
で定義していたpredictLabel
に代入します。
また、代入する際は、store/index.js
のmutation
で定義しているsetPredictLabel
のメソッドを実行しています。
返り値の予測ラベルは、<h1>
タグ内を見て頂ければわかる通り、値が存在すれば(v-if
)表示するようにしています。
clear
メソッドは、store/index.js
のmutation
で定義しているclear
メソッドを実行しています。
mutation
で定義しているclear
メソッドは、それぞれの入力値の項目に対してinitialState
で定義している初期値(空文字)を代入しています。
<template>
<form>
<div>
<v-text-field
v-model="$store.state.sepalLength"
label="Sepal Length"
></v-text-field>
<v-text-field
v-model="$store.state.sepalWidth"
label="Sepal Width"
></v-text-field>
<v-text-field
v-model="$store.state.petalLength"
label="Petal Length"
></v-text-field>
<v-text-field
v-model="$store.state.petalWidth"
label="Petal Width"
></v-text-field>
</div>
<div class="predict">
// クリックした時にsubmit / clearメソッドを実行する
<v-btn @click="submit">submit</v-btn>
<v-btn @click="clear">clear</v-btn>
<h1 v-if="$store.state.predictLabel">Predicted label is {{ $store.state.predictLabel }}</h1>
</div>
</form>
</template>
<script>
// APIを叩くためのaxiosライブラリをimportする
import axios from "axios";
export default {
name: "FormText",
methods :{
// 入力値を初期値に戻すメソッド
clear (){
this.$store.commit('clear')
},
// APIを叩くメソッド
submit() {
axios.post("http://localhost:5042/api/predict", {
sepal_length: this.$store.state.sepalLength,
sepal_width: this.$store.state.sepalWidth,
petal_length: this.$store.state.petalLength,
petal_width: this.$store.state.petalWidth
}).then((response) => {this.$store.commit('setPredictLabel', response.data.result)})
}
}
}
</script>
<style scoped>
.predict {
text-align: center;
}
</style>
これでフロントエンドは整いましたので、予測ラベルを返すAPIを作成します。
バックエンド(Responder)
今回作成するファイル及び役割は以下のようになります。
.server
├── jupyter_notebook
│ ├── iris.ipynb <- モデルを作成したjupyter notebook
│ └── iris.sav <- 作成したモデルファイル
├── api.py <- ルーティングの定義
├── handlers.py <- postされた時の挙動を書く
└── services.py <- postされたデータを使って予測しラベルを返す
api.py
のファイルは、以下の通りに記載をしました。
Responder
特有の書き方ですが、ルーティングは、api.add_route()
で定義をしています。
(localhost:5042/api/predict
にアクセスがあれば、PredictionResource
を実行する)
最終的にDocker内で動作させることを考えているので、address="0.0.0.0"
は必ず記載してください。
import responder
from handlers import PredictionResource
api = responder.API(
cors=True,
allowed_hosts=["*"],
cors_params={"allow_origins": "*",
"allow_methods": "*",
"allow_headers": "*"
})
api.add_route('/api/predict', PredictionResource)
if __name__ == '__main__':
api.run(address="0.0.0.0", debug=True)
ルーティングで定義した、PredictionResource
クラス(handlers.py
)は、以下の通りです。
こちらもResponder
特有の書き方ですが、on_post
メソッドでPOSTされた時のメソッドを書くことが可能です。
今回は、POSTされた時の入力値を受け取り、PredictionService.predict()
を実行します。
また、on_post
は、result(予測ラベル)とstatusを返すようにしています。
from services import PredictionService
class PredictionResource:
@staticmethod
async def on_post(req, resp):
data = await req.media()
res = PredictionService.predict(data)
resp.media = {
"status": True,
"result": res
}
PredictionService.predict()
は以下のように作成をしています。
予め作成しておいたモデルのファイルを読み込み、入力値を元に予測を実行しています。
import os
import pickle
import numpy as np
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_DIR = os.path.join(BASE_DIR, "jupyter_notebook")
MODEL_FILE = os.path.join(MODEL_DIR, "iris.sav")
class PredictionService:
model = pickle.load(open(MODEL_FILE, mode="rb"))
@staticmethod
def predict(data):
"""
:param data:
{'sepal_length': '1', 'sepal_width': '1', 'petal_length': '1', 'petal_width': '1'}
:return: one of label_names
"""
label_dict = {0: "setosa", 1: "versicolor", 2: "virginica"}
label_names = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
x = np.array([[data[name] for name in label_names]])
predict_num = PredictionService.model.predict(x)
predict_label = label_dict[predict_num[0]]
return predict_label
Responder
特有の書き方はありますが、予測ラベルを返すAPIの中身は特に難しいことはしていません。
これで、バックエンドも完了です。
Docker化
- フロントエンド(Nuxt.js)のDockerfile
今回は、既にnode.jsが入っているLinuxOs (alpine)のイメージを使います。
気をつけることとしては、環境変数ENVにHOST 0.0.0.0
を定義してください。そうしないと、localhost:3000にアクセスしても何も表示されません。
FROM node:10.14-alpine
RUN mkdir /app
WORKDIR /app
COPY ./package.json ./yarn.lock ./
RUN yarn install
RUN yarn run webpack
COPY . .
ENV HOST 0.0.0.0
RUN yarn run build
CMD ["yarn", "run", "dev"]
- バックエンド(Responder)のDockerfile
FROM python:3.6.3
RUN printf "deb http://archive.debian.org/debian/ jessie main\ndeb-src http://archive.debian.org/debian/ jessie main\ndeb http://security.debian.org jessie/updates main\ndeb-src http://security.debian.org jessie/updates main" > /etc/apt/sources.list
RUN apt-get update
RUN pip install --upgrade pip
RUN pip install --upgrade pip setuptools
RUN pip install responder==1.3.0 numpy==1.14.5 scikit-learn==0.20.3
WORKDIR /app
COPY . .
- docker-composeファイル
特に難しいことはやっていませんが、/app
ディレクトリをマウントすることやポートの設定は忘れずにやりましょう。
version: '3'
services:
app:
build: ./client/.
volumes:
- ./client/:/app
ports:
- "3000:3000"
command: ash -c "yarn install && yarn run dev"
api:
build: ./responder/.
volumes:
- ./responder/:/app
ports:
- "5042:5042"
command: /bin/sh -c "python api.py"
以下のコマンドを実行し、localhost:3000に画面が表示されれば完了です。
$ docker-compose build
$ docker-compose up -d
画面が表示されるまでに時間がかかるので、以下のlogコマンドで確認しましょう。
$ docker-compose logs
Attaching to nuxtapiapp_app_1, nuxtapiapp_api_1
app_1 | yarn install v1.12.3
app_1 | [1/4] Resolving packages...
app_1 | success Already up-to-date.
app_1 | Done in 0.92s.
app_1 | yarn run v1.12.3
app_1 | $ nuxt
app_1 | 06:05:02 ℹ Listening on: http://172.22.0.3:3000/
app_1 | 06:05:12 ℹ Preparing project for development
app_1 | 06:05:12 ℹ Initial build may take a while
app_1 | 06:05:12 ✔ Builder initialized
app_1 | 06:05:12 ✔ Nuxt files generated
app_1 | webpackbar 06:05:18 ℹ Compiling Client
app_1 | webpackbar 06:05:18 ℹ Compiling Server
api_1 | INFO: Started server process [5]
api_1 | INFO: Waiting for application startup.
api_1 | INFO: Uvicorn running on http://0.0.0.0:5042 (Press CTRL+C to quit)
終わりに
Vue.js / Nuxt.jsに関しては、知識ゼロの段階から始めたのでほとんどわかっていません。とりあえず動くものを作ったという感じです。なので、間違っている箇所などあればコメントや編集リクエストを頂ければ幸いです。
また、こちらの記事は、2019年開催「技術書典6」で販売されていた「Nuxt.jsとPythonでつくる『ぬるさくAIアプリ開発入門』」を参考に作成させて頂きました。現在2019年4月30日時点では、BOOTHにて電子書籍を販売をしていますので、より詳しい説明が欲しい方は購入することをオススメします。(Nuxt.jsやResponderについて、とても丁寧に書いてあります。)