doc2vec・scikit-learn・Django・Vue.js・Herokuで機械学習webアプリを作った備忘録

# はじめに
タイトルの通り、doc2vec・scikit-learn・Django・Vue.js・Herokuなどいろいろ駆使し、**機械学習Webアプリケーション**をフルスクラッチ実装した。

機械学習のモデリング手法、またはSPA構築のHowToはそれら単体では紹介されているが、それらをフルスクラッチで構成するノウハウは未だあまり出回っていないように思える。
まずは自分の備忘のため、そして特に作った機械学習モデルをWebアプリ化して公開してみたい人のため、アプリの構成や躓いたポイントなどを記事に残す。

# この記事のスコープ
**含む内容**

- Webアプリを実装するのに使用したライブラリ
- Webアプリのディレクトリ構成
- 機械学習モデルとDjangoの連携
- Vue.js、Django REST Frameworkを使用したSPA(Single Page Application)実装
- Herokuデプロイの設定Tips

**含まない内容**

- ライブラリ単体の(doc2vec, scikit-learn, ...)説明や、Web開発の基礎(フロントエンドやREST Framework)の説明。各種チュートリアルを参考にされたい。
- 自然言語処理・機械学習の理論の説明。筆者は未だ理解が浅く、理論に関する言及はなるべく控える。
- ソースコード。いつかGithubで公開するかも。
- アプリの精度に対する議論。そもそも文章から性格を判断すること自体ナンセンスだし、学習のためのジョークアプリという位置づけにしたい。


# 作ったWebアプリの紹介
**URL**
http://personalityestimator.herokuapp.com/

**サービスの説明**

 - 英文をInputとし、[MBTIメソッド](https://ja.wikipedia.org/wiki/MBTI)に基づいた16タイプの性格診断を行う。
 - 16タイプの指標は、2値をもつ4つの指標の乗算(2^4 = 16)で表される。文章を入力することで、それぞれの指標にどれだけ寄っているかをパーセンテージで算出することができる。
 - 試しに[トランプ大統領のスピーチ](https://www.rev.com/blog/transcripts/donald-trump-press-conference-on-coronavirus-testing-may-11)をInputすると**ENTP - The Debater(討論者)**と評価される。なかなかそれっぽい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591481/30e9eb75-8d53-31aa-ba87-89c7e2226dbd.png)

参考：MBTI 4指標（[MBTI協会サイト](https://www.mbti.or.jp/what/what1.php)より引用）

| 指標              |基準 | 
|:----------------:|:-----------------:|
| 興味関心の方向|外向(**E**xtroverts) vs 内向(**I**ntroverts) |
|ものの見方|感覚(**S**ensors) vs 直観(i **N**tuitives)|
|判断のしかた|思考(**T**hinkers) vs 感情(**F**eelers)|
|外界への接し方|判断的態度(**P**erceivers)・知覚的態度(**J**udgers)|

**システムの説明**

 - 教師データとしてKaggle公開のデータセット[「(MBTI) Myers-Briggs Personality Type Dataset」](https://www.kaggle.com/datasnaek/mbti-type)を使用している。レコードの単位は被験者単位で、カラムは性格診断結果(Type)と投稿した文章(Posts)の二つを持つ。
 - 学習は、投稿した文章(Posts)をdoc2vecで100次元ベクトル化し、それらに対して2値分類を行うモデル（ロジスティック回帰）を4つ実装している。

# 備忘録

## ランタイム・仮想環境

Pythonバージョンは`python-3.7.3`を使用。

仮想環境はanyenv + pyenv(インタプリタ切り替え) + virtualenv(パッケージ切り替え) + virtualenvwrapperを使用している。
尚、Anacondaは使用していない。当初入れていたがアプリ実装時はすべて削除している。（[condaとpip：混ぜるな危険](http://onoz000.hatenablog.com/entry/2018/02/11/142347))

## ブループリント

当Webアプリのブループリント（構成図）を説明する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591481/5dfa8fd1-484f-7655-a88a-59cd00f869c0.png)

| No.|コンポーネント |説明|
|:-------------------|:--------------------|:--------------------|
| ①|FE|**Vue.js**を採用。axiomライブラリを使用してAPIを非同期でコールし、返却値をレンダリングする。<br>CDN版を採用し、状態管理(vuex)やルーティング(vue-rooter)は実装していない。|
| ②|API|**Django REST Framework**を採用。フォームで入力した文章をリクエストし、モデルの予測結果をレスポンスする。|
| ③|DB|**SQ Lite**を採用。プロジェクト開始時に自動で付属するだけで、今回特にデータの参照・登録は行っていない。|
| ④|Model|**Gensim (doc2vec)**を単語のベクトル化、**Scikit-Learn**をモデリングに採用。モデルは各指標ごとに2値分類を行うモデルを4つ実装。|
| ⑤|Server|**Heroku**を採用。①〜④を丸ごと1つのAppとしてデプロイしている。|

## requirements.txt

仮想環境にインストールした各種ライブラリを説明する（google-api-coreとか、明らかに不要そうなものも入っている気がする・・・）。基本すべてpipでインストール可能。

```requirements.txt
boto3==1.12.36
botocore==1.15.36
cachetools==4.0.0
certifi==2019.11.28
chardet==3.0.4
dj-database-url==0.5.0
Django==2.2.5　# ①
django-heroku==0.3.1 # ②
djangorestframework==3.10.3 # ①
docutils==0.15.2
gensim==3.8.1 # ③
google-api-core==1.16.0
google-auth==1.13.1
google-cloud-core==1.3.0 
google-cloud-storage==1.27.0
google-resumable-media==0.5.0
googleapis-common-protos==1.51.0
gunicorn==20.0.4 # ②
idna==2.9
jmespath==0.9.5
joblib==0.14.1
numpy==1.18.1
pandas==1.0.1
protobuf==3.11.3
psycopg2==2.8.5
pyasn1==0.4.8
pyasn1-modules==0.2.8
python-dateutil==2.8.1
pytz==2019.3
requests==2.23.0
rsa==4.0
s3transfer==0.3.3
scikit-learn==0.22.2.post1 # ③
scipy==1.4.1
six==1.14.0
smart-open==1.10.0
sqlparse==0.3.1
urllib3==1.25.8
whitenoise==4.1.3
```

### ①：Django関係のライブラリ
`django`はWebフレームワーク、`djangorestframework`はその中でもREST API実装特化。両方ともpipでインストールする。

### ②：Heroku関係のライブラリ
djangoアプリをherokuにデプロイする際に必要となる。`django-heroku`をインストールすれば依存パッケージの`whitenoise`や`dj-database-url`も一緒にインストールされる。

### ③：機械学習関係のライブラリ
`gensim`, `scikit-learn`を導入する。モデルの学習はjupyterで別で学習しているが、モデルの予測処理には必要。他、`numpy`, `pandas`等も一緒に導入する。

## ディレクトリ構成

当Webアプリのディレクトリ構成を説明する。

```
personalityestimator
├── Procfile # ②
├── apiv1 # ①
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── clfs ③
│   │   ├── logreg_ie.pickle
│   │   ├── logreg_pj.pickle
│   │   ├── logreg_sn.pickle
│   │   ├── logreg_tf.pickle
│   │   └── model.pickle
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── config # ①
│   ├── __init__.py
│   ├── __pycache__
│   ├── local_settings.py # ④
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── get_random_secret_key.py # ④
├── manage.py
├── mbti # ①
│   ├── __init__.py
│   ├── __pycache__
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   ├── 0002_.....
│   │   ├── __init__.py
│   │   └── __pycache__
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── requirements.txt # ②
├── runtime.txt # ②
├── static
│   └── style.css
└── templates
    └── index.html
```

### ①：django プロジェクト・アプリケーション

指定ファイルは以下のコマンドで生成している。

```
(venv) $ django-admin startproject config .
(venv) $ python manage.py startapp mbti
(venv) $ python manage.py startapp apiv1
```
1プロジェクト、2アプリケーション(mbti, apiv1)で構成する。
プロジェクト開始時、`config . `と指定することで、ベースディレクトリと設定ディレクトリ(config)が同じ名前になることを回避している。アプリケーションはモデルのみを定義する`mbti`, REST API機能を有する`apiv1`の2つを作成している。
上記コマンド以外では、以下を手動で実行している。

 - `apiv1/serializers.py`を追加。REST Framework上でのデータの保持を行う役割を持つ。バリデーション等もこのファイルで定義する。
 - `static`、`templates`ディレクトリを追加し、静的ファイルやindex.htmlを格納する。`settings.py`も同ディレクトリを検索するように一部書き換える。


### ②：Herokuデプロイ設定
Herokuデプロイ時には、`Procfile`、`requirements.txt`、`runtime.txt`の3つを作成する。`Procfile`はアプリを動かす指示書の役割、`requirements.txt`は使用するライブラリをherokuに知らせる役割、`runtime.txt`は使用するPythonのバージョンを知らせる役割を持つ。

### ③：モデルの格納
学習済のgensim, doc2vecのモデルをpickle化し、`clfs`ディレクトリに格納している。
`model.pickle`は単語のベクトル化、`logreg_XX.pickle`はそれぞれの指標に対してロジスティック回帰で2値分類する。

### ④：秘密情報の設定
次節のはまったポイント③を参照。



## はまったポイント

### ①Procfileの設定によるHerokuデプロイエラー

**問題**
Herokuでデプロイには成功するが、WebアプリにアクセスするとApplication Errorが発生する。

**解決策**
Procfileのプロジェクト名の記載欄に、間違えてルートディレクトリ名を記載していたことが原因だった。
上記コマンド`startproject config .`の通り、このWebアプリの正しいプロジェクト名は`config`とである。一般的で紹介されているディレクトリ構成と異なっているためなかなか気づけなかった。

```procfile
（誤）web: gunicorn personalityestimator(ルートディレクトリ名).wsgi --log-file -

（正）web: gunicorn config(設定ファイル名).wsgi --log-file -
```

### ②index.html上のデリミタ「{{}}」がDjango, Vue.jsで重複する

**問題**
今回Vue.jsはCDN版を採用しているため、`index.html`に\<script\>タグで直接Vue.jsを書き込んでいる。
Djangoテンプレートの変数表示構文とVue.jsのMustache構文が両方ともデリミタ「{{}}」が使用されているため、そのままの設定ではバッティングする。

**解決策**
Vue.jsのデリミタを変更する。`Vue.options.delimiters`でデリミタを「#{}」に変更して衝突を回避する。

```index.html
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
...
    <title>PersonalityEstimator</title>

<!-- CSS -->
<link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<link href="{% static 'style.css' %}" rel="stylesheet">

<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<!-- axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app"> 
...
</div>
</body>

<script>
Vue.options.delimiters = ['#{', '}'] // ポイント

var app = new Vue({
    el: '#app',
    data: {
...
    },
    methods: {
...
})
</script>
```

### ③DjangoのSECRET_KEYをプロダクション環境に公開しないようにする

**問題** 
`settings.py`に初期値コメント「SECURITY WARNING: keep the secret key used in production secret!」の通り、`SECRET_KEY`はプロダクション環境からは見えないようにしたい。

**解決策**
`settings.py`から新たに`local_settings.py`を複製し、`SECRET_KEY`は`local_settings.py`のみに記載する。`settings.py`の`SECRET_KEY`は`local_settings.py`からインポートする。

```settings.py
import os
import django_heroku
from os import environ
from socket import gethostname

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
HOSTNAME = gethostname()

if 'local' in HOSTNAME:
    from . import local_settings
    SECRET_KEY = local_settings.SECRET_KEY
else:
    SECRET_KEY = environ['SECRET_KEY']

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', '.herokuapp.com']
...
```

`local_settings.py`は`.gitignore`に記載しgit管理されないようにする。
`DEBUG = False`に書き換えるのも忘れないように。

尚、上記ディレクトリ構成にあった`get_random_secret_key.py`は新しく`SECRET_KEY`を生成する際に使用した。一度うっかり秘密鍵をgit管理に上げてしまったので、鍵の再生成を行った。

```get_random_secret_key.py
from django.core.management.utils import get_random_secret_key

secret_key = get_random_secret_key()
text = 'SECRET_KEY = \'{0}\''.format(secret_key)
print(text)
```

### ④その他

 - `django-heroku`のインストール時に同梱される`whitenoise`は最新バージョンで5系になっている。最新の情報が載っている参考サイトでもほぼ4系を採用していたため、アンインストールした後再度4系をインストールした。
 - CSS。拡大・縮小による表示崩れやレスポンシブ対応の工夫をしている。親構造に`max-width`を指定するwrapperクラスをあちこちで指定している。

## まだ嵌ってるポイント・Todo
本アプリのWebアプリの残課題を記載する。解決策をご存知の方がいらっしゃればコメントをいただきたい。

 - **index.htmlに記載されている\<script\>をjsファイルに分離し、staticに格納したい。**main.jsファイルを読み込む手法も検証したが、どうやっても正常に読み込まれず断念。
 - **clsf（モデル）の格納場所を変えたい。**何となくapiv1の中ではなくstaticの中に置くべきな気がしているが、あるべきが何かを知りたい。
 - **apiv1/view.pyをリファクタリングしたい。**Typeの設定（"INTJ": "The Architect"など）は同ファイルにベタ書きで持っているので、モデルに移植してマスタテーブル化したい。が、2つのModelを参照してレスポンスする方法がよくわからず断念。
 - Modelの精度は上げたい。次は深層学習にチャレンジするつもり。

## 参考にしたサイト・書籍


### ①：（Web）doc2vec and logistic regression

**URL**
https://www.kaggle.com/wpncrh/doc2vec-and-logistic-regression

**説明**

当アプリで使用するデータセットを使用したKaggleのKernel。
`Doc2Vec`による単語のベクトル化や、前処理の方法はこのKernelを参考にしている。本アプリではこのKernelに実装されている前処理に加えて、URLや記号を正規表現で取り除いている。

### ②：（動画）Build a Machine Learning API with Django

**URL**
https://youtu.be/tDnAcbYROSI

**説明**
Django REST FrameworkとMachine Learningをかけ合わせたアプリ実装を全6回で説明している。範囲はモデル実装からDjangoアプリ作成、Herokuにデプロイするところまですべて。
モデルをPickle化してDjangoアプリ内に入れるアイデアはこの動画から着想を得た。一方、Django REST Frameworkの記法は若干怪しいように見受けられるので、Djangoの学習はチュートリアルに沿うことをおすすめする。後半のHerokuデプロイは観ていない。
全編インド人による英語の講義。

### ③：（書籍）現場で使えるDjangoの教科書

**URL**
現場で使える Django の教科書《基礎編》
https://booth.pm/ja/items/1308742

現場で使える Django REST Framework の教科書
https://booth.pm/ja/items/1559869

**説明**
Django及びDjango REST Frameworkを体系的に学ぶには最もおすすめ。
Webアプリのディレクトリ構成や、DjangoとVue.jsの連携、デリミタの重複回避はこの書籍を参考にした。特にDjangoとVue.jsの連携はサンプルプロジェクトがそのまま載っているので大変参考になる。

### ④（Web）『完全版』Djangoアプリをherokuにデプロイ！

**URL**
http://digital-tree.xyz/blogs/1169

**説明**

DjangoアプリケーションをHerokuにデプロイする手順が記載されている。
2019年9月の記事のため（2020年5月現在は）、比較的最新。

### ⑤（Web）Djangoでの"秘密にしたい値"の取り扱い - ティッシュ残り一枚

**URL**
https://tsukachu.hatenablog.com/entry/handling_secret_keys

**説明**

Heroku、Github等で秘密鍵を公開しないようにするための実装方法の記載がある。
嵌ったポイント③「DjangoのSECRET_KEYをプロダクション環境に公開しないようにする」は本記事を参考にしている。

### ⑥（書籍）1冊ですべて身につくHTML & CSSとWebデザイン入門講座

**URL**
https://www.amazon.co.jp/dp/4797398892/ref=cm_sw_em_r_mt_dp_U_Of2VEbSY2RKCE

**説明**
HTMLとCSSの説明。
アプリ構想時点でCSSが全く分からなかったのでこの書籍で1から学習した。スマホ・PC両対応したレスポンシブデザインを作るのにも参考にしている。

### ⑦その他
Python自体は[Pythonデータサイエンスハンドブック](https://www.oreilly.co.jp/books/9784873118413/)や、[みんなのPython 第4版](https://www.amazon.co.jp/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AEPython-%E7%AC%AC4%E7%89%88-%E6%9F%B4%E7%94%B0-%E6%B7%B3/dp/479738946X)で学習した。

# 終わりに

筆者はエンジニアではなく、趣味で一人コードを書いている。このアプリも空き時間を使って、構想から約半年かけて実装した。何でも調べれば出てくる時代にはなったが、その情報源を蓄積・公開している先人たちには感謝したい。この記事も、機械学習Webアプリを作りたい誰かのヒントになればよいなと思う。