1ヶ月でできる！？仕様書なしから機械学習予測システムを Web サービスにして納品

12月も終わりに近づいてきましたね．
今年も残り少し頑張って乗り切りましょう！
というわけで[DSLアドベントカレンダー](https://qiita.com/advent-calendar/2019/muroran-it-dsl)21日目です．

今日は私 @y_k と同期の @liseos_x140 でお送りします！

本日は，研究室のメンバー数人でとある案件でWebアプリを作成することがあったのでそちらの紹介していきます．
モデル設計を考えて，サービス構成を勉強してから提供開始まで1ヶ月(×3.5人)でできるらしいです\:thinking_face:

うちの研究室はブラックじゃないのに，**案件がブラック**かもしれなかった話


## 仕様
* いい感じの（研究成果の）予測システムがほしい（ガチ）

**クライアントから本当にこう来ました．**
どうすればいいねん…

<small>~~(研究成果がこの時点で大したものがなかったお話は省略)~~</small>

## 要件
いい感じのシステムをつくるために，予測とその提供のために最低限必要な項目を並べました．(ここが一番大変だったかもしれない)

* スクレイピングを毎日1回，自動で実行する
* クライアントのクライアントで使われている**ネ申エクセルをパース**してデータベースに登録する機能
* 予測には時間がかかるので，非同期処理をいれる
    * ポップアップとかが出ないと不安なようです
* **開発期間は短い**(勉強込みで1ヶ月ちょっと)ので手間がかからない仕様
* 機械学習モデルは**現在進行形で研究・開発中**なので差し替えしやすい構成にする


## システム構成
今回製作したシステムの構成を紹介します．
Web/API サーバとWorker, Schedular の組み合わせは Redash の構成を大いに参考にしました\:pray:

### Web/API サーバ
Python で実装しています．
Web フレームワークは responder を用いました．
タスクの発行・管理にはジョブキューフレームワークの Celery を用いました．
WebUI は Vue.js で SPA として構築しました．

```
プロジェクトルート
├── Dockerfile
├── Pipfile
├── Pipfile.lock
├── README.md
├── bin
│   └── docker-entrypoint
├── celerybeat-schedule.db
├── client  # WebUI のメインディレクトリ
│   ├── README.md
│   ├── app  # ここに Web クライアントのソースコードが入っている
│   └── dist  # コンパイル後のものはここに格納される
├── docker-compose.override.yml
├── docker-compose.yml
├── docs
├── makefile
├── manager.py
├── notebooks
├── package.json
├── predict
├── server  # Web/API server のメインディレクトリ
│   ├── README.md
│   ├── __init__.py
│   ├── app.py # メインのスクリプト
│   ├── cli  # docopt を使った cli が定義されている
│   ├── handlers  # route の定義と処理
│   ├── models  # モデルクラス
│   ├── settings  # 設定用
│   ├── tasks  # Celery のタスク定義
│   ├── utils  # 共通モジュールの初期化や app への組み込み
│   ├── worker.py  # Celery
│   └── wsgi.py  # app を呼び出し
├── setup.cfg
├── setup.py
├── templates
├── webpack.config.js
└── yarn.lock
```

#### ドキュメント生成
sphinx で Web/API サーバのコードのドキュメントを生成，API のドキュメントは swagger で生成できる環境を構築しました．
responder × swagger は responder の標準機能で簡単にできるように見えて少し大きなプロジェクトでやるには responder v1 系では辛かったです．
responder v2 ではその辺りの構成が改善されているのでそのうち記事にします．

なお，環境は整えたのに結局**少人数・超短期開発**のためドキュメントをしっかり記述する余裕はなかった…
**API のドキュメントはホワイトボードが最新＆正確**というオチに（API を叩くコードには @y_k の確認が必要な始末）。

### Worker, Schedular
ジョブの非同期実行や定期実行のために Celery を導入しました．
この Celery は Python のジョブキューフレームワークの一つです．
ジョブキューフレームワークは他にもいくつかありますが，ドキュメントがあり，随時発行されるジョブも定期実行のジョブも扱えるため，Celery を選択しました（以降，ジョブは Celery のドキュメントやスクリプトなどに合わせて，タスクと呼びます）．

タスクの管理と実行結果の保存には Redis を用いました．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59163/2a18143b-f35c-cb35-96c0-de226e39fbb3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59163/03c46713-c20e-0087-af93-295a5ee6d7f1.png)


#### 定期実行タスク
Celery の beat 機能を用いました．
このプロジェクトでは1日1回，外部のサービスのAPIやWebページからデータを取得し，データベースに記録します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59163/e6b29f21-89ce-551d-2139-bd961a780fe0.png)


### Predict API サーバ
機械学習モデルで予測した結果を Web サーバにレスポンスを返すための API サーバを構築．Web アプリケーションフレームワーク として， Python の flask を使用．

Web サーバから送られてきた JSON をパース，その値をもとに MySQL サーバにクエリを発行し，予測に用いるデータセットを取得．それをもとに予め学習しておいたモデルを用いて予測結果を出力， Web サーバに送り返します．

モデルは予測のたびに読み込んでいると応答待ち時間が長くなって大変ストレスなので，常にモデルを読み込んでおいてリクエストが来た時に予測だけを行えるような仕様になっています．おそらく RESTfulAPI になっているはずです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59163/c1feb023-4a86-5b68-d026-21e4bf3f47ec.png)


## デプロイ
開発段階から Docker 上で動くように作っているため，開発環境で Docker image を build しています．
生成された image は GitLab の container registry に push して，本番環境で pull しています．


また，Web/API サーバと Predict API サーバはそれぞれ別の Docker image として build しています．
Predict API サーバ側では Web/API サーバのコードを Python のモジュールとしていれるために少し工夫しているため，makefile を作成して make コマンドで build できるようにしました．
（ORM 系のスクリプトとかの都合で Predict API サーバ側でも Web/API サーバのスクリプトが必要なんですよね…）

```makefile:makefile
IMAGE_SERVER=<サーバ IP>:<サーバ Port>
IMAGE_REPO=<ユーザ名>/<レポジトリ名>

# build
build: bweb bpred
bweb:
	docker build -t $(IMAGE_SERVER)/$(IMAGE_REPO) .
bpred:
	docker build -t $(IMAGE_SERVER)/$(IMAGE_REPO):predict -f ./predict/Dockerfile .

# push images
push:
	docker push $(IMAGE_SERVER)/$(IMAGE_REPO)
```

これにより，`make build` で build できて `make push` でサーバに push できます\:clap:


## 別途入れた OSS
* Redash  
  オープンソースの BI ツール．
  SQL の下書き，データの確認などに用いた．
* Sentury  
  オープンソースのクロスプラットフォームアプリケーションモニタリングとエラーレポートツール．
  エラーの監視，Slack への通知に利用．
* Grafana  
  オープンソースの様々なデータベースで使える解析・モニタリングツール．
  docker container からのログを fluentd に送り influxdb にためて，Grafana 上で確認できるような環境を構築．
  現状としては指定した時間のログを確認できるようにしてある．


## コードについて
このシステムなのですが，もとはいえば共同研究へ予測システムを提供するために作成したシステムで，中身は見られないこともあり雑なプログラムを書いてしまっていたりしました．無事デプロイが終わりしばらく経った頃，こんなことを聞きました．

「システムが複雑で来年から管理できる人がいないから外注して保守管理しやすいようにしてもらうわ」

...なるほど\:thinking_face:

ということで，これからリファクタリングが待ち受けています．ですのでそれが終わり次第どこかでソースコードを共有できればと思います．しかも，このシステムを元にシステム制作の発注をしなければいけないので今度は我々が仕様書を書かなければいけないみたいです．仕様書もらえなかったのに...\:innocent:

## 最後に

「1ヶ月でなんとかなった！」
と思いきや仕様書がなかったが故にデプロイ後に

「xxxの機能足りないから追加でお願いね〜」
...あっ，なるほどですね\:eyes:

**仕様書は予め作ってもらいましょう！**

あと，研究と並行してそれをシステムに乗せるのはやめた方が良い！


## 参考文献
- [Redash helps you make sense of your data](https://redash.io/)
- [Python responder 入門のために… 下調べ - Qiita](https://qiita.com/y_k/items/6e4da3bef5c8d8bd2730)
- [Homepage | Celery: Distributed Task Queue](http://www.celeryproject.org/)
- [Application Monitoring and Error Tracking Software | Sentry](https://sentry.io/welcome/)
- [Grafana documentation | Grafana Labs](https://grafana.com/docs/grafana/latest/)
- [Python(Flask) でサクッと 機械学習 API を作る - Qiita](https://qiita.com/fam_taro/items/1464c42324f15d7b8223)
- [Extending Flask-RESTful](https://flask-restful.readthedocs.io/en/latest/extending.html)

---

**2020/01/19 追記**
フロントエンド開発のために実際に導入したライブラリなどをまとめた記事を別で書きましたので，見ていただけると幸いです．
[【フロントエンド編】1ヶ月でできる！？仕様書なしから機械学習予測システムを Web サービスにして納品](http://www.yk-lab.net/archives/%e3%80%90%e3%83%95%e3%83%ad%e3%83%b3%e3%83%88%e3%82%a8%e3%83%b3%e3%83%89%e7%b7%a8%e3%80%911%e3%83%b6%e6%9c%88%e3%81%a7%e3%81%a7%e3%81%8d%e3%82%8b%ef%bc%81%ef%bc%9f%e4%bb%95%e6%a7%98%e6%9b%b8%e3%81%aa/)