Nuxt.js + Responder（Python）で作る機械学習アプリケーション 

## 本記事でやること
Nuxt.jsとResponder（pythonのwebフレームワーク）を使って簡単な機械学習アプリケーションを作成する。
またこちらの記事は、2019年開催「技術書典6」で販売されていた[「Nuxt.jsとPythonでつくる『ぬるさくAIアプリ開発入門』」](https://techbookfest.org/event/tbf06/circle/40460002)を参考に作成させて頂きました。

- フロントエンド（Nuxt.js）
    - 入力データを受け付けるフォームを作成する
    - 入力された値を元にAPIを叩き、返り値を画面上に出力する
- バックエンド（Responder）
    - 入力データに対して（irisの多値分類）予測ラベルを返すAPIを作成する
- Docker化
    - docker-composeを用いてDocker内でアプリケーションを動かせる様にする

本記事でやらないことは以下になりますので他の記事を参照してください。

- irisのデータセットを使った予測モデルの作成


## 対象読者
- 機械学習を使ったアプリケーションを作成してみたい方
- Nuxt.js / Responderを触ってみたい方

## 成果物
- 以下の様な挙動のアプリケーションをNuxt.jsとResponderを用いて作成する。

![ezgif.com-optimize.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270991/2a1e433d-73b9-3f48-ddf4-362d0f112a8f.gif)


## アプリケーションのディレクトリ構成

```
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プロジェクトの作成

```bash
$ mkdir nuxt-responder-app
$ cd nuxt-responder-app
$ mkdir client
$ cd client
$ npx create-nuxt-app
```
（`npx`コマンドは`npm`にバンドルされているので`npx`単体でのインストールは不要です。）
`npm`のインストール（mac環境）に関しては、[こちら](https://qiita.com/PolarBear/items/62c0416492810b7ecf7c)の記事を参考にさせて頂きました。

上記`npx`コマンドを実行すると以下を聞かれます。今回はこの様な設定にしました。

```bash
? 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

```

上記設定が完了したら、試しにアプリケーションを起動します。

```bash
yarn run dev
```
`localhost:3000`にアクセスし、画面が表示されればひとまずプロジェクトの作成は完了です。


### 画面の共通部分ヘッダーとフッターを作成する

Nuxt.jsの場合、画面のレイアウトを修正する際には`layouts`配下のファイルを修正していきます。
各ディレクトリの役割は、[公式サイト](https://ja.nuxtjs.org/guide/directory-structure/)を参照してください。

```
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のファイルを呼んでいます。

```vuejs:default.vue

<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>

```

<img width="518" alt="スクリーンショット 2019-04-30 15.52.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270991/baa8eeb4-f98c-f73a-a927-b296851ce5e2.png">



### 入力を受け付けるフォームを作成する

画面に出力するコンテンツは、`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に代入する。（詳細は[公式サイト](https://jp.vuejs.org/v2/guide/forms.html)を参照してください）
- `label` : テキストフィールドの項目名を表示します。

```vuejs:FormText.vue
<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`の中身については後述します。）

```javascript:index.js

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`をインストールします。

```bash
yarn install --save axios
```

インストールが完了したら、`nuxt.config.js`の下記箇所に追記をします。

```javascript: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 `で定義している初期値（空文字）を代入しています。

```vuejs:FormText.vue
<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"`は必ず記載してください。

```python:api.py
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を返すようにしています。

```python:handlers.py
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()`は以下のように作成をしています。
予め作成しておいたモデルのファイルを読み込み、入力値を元に予測を実行しています。

```python:services.py
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にアクセスしても何も表示されません。

```Dockerfile:Dockerfile

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


```Dockerfile: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`ディレクトリをマウントすることやポートの設定は忘れずにやりましょう。

```yaml:docker-compose.yml
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に画面が表示されれば完了です。

```bash
$ docker-compose build
$ docker-compose up -d
```

画面が表示されるまでに時間がかかるので、以下のlogコマンドで確認しましょう。

```bash
$ 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アプリ開発入門』」](https://techbookfest.org/event/tbf06/circle/40460002)を参考に作成させて頂きました。現在2019年4月30日時点では、[BOOTH](https://norwegian-geek.booth.pm/items/1296418)にて電子書籍を販売をしていますので、より詳しい説明が欲しい方は購入することをオススメします。（Nuxt.jsやResponderについて、とても丁寧に書いてあります。）