文章を分類するウェブアプリを作ろう
ラーメン食べてますか。
ラーメンに関するツイートについて札幌と福岡のどちらでツイートされたか判別するウェブアプリをデプロイしたので、「モデル編」「ウェブアプリ編」に分けて記録を残そうと思います。こちらは「ウェブアプリ編」です(モデル編)。
FastAPIもGoogle App Engineもわかってないところが大量にあってお恥ずかしい限りですが、とりあえず動くものができたので良しとします。
利用した技術とサービス
- バックエンドの言語:python
- ディープラーニングライブラリ:pytorch, torchtext
- 自然言語処理モデル:双方向LSTM
- ウェブフレームワーク:FastAPI
- デプロイ:Google App Engine, Docker, Git
最終的にはGit(GitHub)に上げてからGoogle App Engineにデプロイするのですが、ファイル一覧はこんな感じです。ramen_classificationがレポジトリです。lstm.py、ramen_model.pt、LABEL.dill、TEXT.dillについては前回説明しました。
ramen_classification
├─static/
│ ├─css/
│ └─layout.css
│ └─images/
│ └─deepblue-logo.png
├─templates/
│ ├─evaluation.html
│ ├─footer.html
│ ├─header.html
│ ├─index.html
│ └─layout.html
├─Dockerfile
├─LABEL.dill
├─TEXT.dill
├─app.yaml
├─docker-compose.yml
├─lstm.py
├─main.py
├─ramen_model.pt
├─requirements.txt
└─run.py
モデル周り
run.py
今どきっぽいのでFastAPIで書いていきます。別に非同期処理が使いたかったとかそういう理由は特にないです。参考にできる資料が少なかったのでちょっと後悔しましたが、初心者でも自動ドキュメント生成の強力さはしみじみと感じました。むしろ初心者こそお世話になるかもしれない。細かい説明は他のFastAPI記事に譲りますがAPIドキュメントを勝手に作ってくれて、ここで関数のテストができます。
from main import app
import uvicorn
if __name__ == '__main__':
uvicorn.run(app, host="127.0.0.1", port=8080)
main.py
メインの関数はevaluationです。まずawait request.form() でフォームから文章を受け取ります。非同期処理が使いたいとか訳ではないとか言いつつも、文章を受け取るまでぼーっと待ってて欲しくないのでasync defで定義します(参考)。
フォームから文章を受け取ったら、lstm_model.pyに記述しているLSTMモデル(関数名:lstm)に渡します。結果、推定に使ったテキスト、推定されたラベル、自信の度合いが返ってきます。
あとは静的ファイルをまとめて場所を指定したり、ルーティングの準備をしたりなんだりします。
from fastapi import FastAPI, Request, File
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from lstm import lstm_model
# FastAPIの用意
app = FastAPI()
# staticフォルダ下の静的ファイルを呼び出すために必要
app.mount("/static", StaticFiles(directory="static"), name="static")
# templates配下に格納したindex.htmlをrenderするために必要
templates = Jinja2Templates(directory="templates")
def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
async def evaluation(request: Request):
if request.method == 'GET':
return templates.TemplateResponse('evaluation.html',
{"request": request, "pred_text": pred_text, "pred_label": pred_label,
"pred_conf": pred_conf, "tweet": tweet})
if request.method == 'POST':
data = await request.form()
tweet = data.get('tweet')
pred_text, pred_label, pred_conf = lstm_model(tweet)
return templates.TemplateResponse("evaluation.html",
{"request": request, "pred_text": pred_text, "pred_label": pred_label,
"pred_conf": pred_conf, "tweet": tweet})
# FastAPIのルーティング用関数
app.add_api_route('/', index)
app.add_api_route('/evaluation', evaluation, methods=['GET', 'POST'])
requirements.txt
pipdeptreeで依存関係を調べて、必要なパッケージをぽちぽち列挙していきます。多分もっと楽な方法があるでしょう。
あとはfastapiとかpython-multipartとかデプロイに必要なものを追加します。
fastapi == 0.63.0
uvicorn == 0.13.4
gunicorn == 20.0.4
jinja2 == 2.11.2
aiofiles == 0.6.0
python-multipart == 0.0.5
pandas == 1.1.5
janome == 0.4.1
numpy == 1.19.5
torchtext == 0.3.1
requests == 2.25.1
chardet == 4.0.0
certifi == 2020.12.5
idna == 2.10
urllib3 == 1.26.2
torch == 1.7.1
dataclasses == 0.6
future == 0.16.0
typing-extensions == 3.7.4.3
tqdm == 4.55.1
dill == 0.3.3
Google app engineの設定
app.yaml
フレキシブル環境にすることが大事です。スタンダード環境ではpytorchとtorchtextが使えません。ただしフレキシブル環境には無料枠がないので注意しましょう。
マシンタイプは適切な設定がよくわかっていませんが、Hello worldの設定のままでは動かなかったので、人の設定を参考にもう少し増設しました。
#
service: takahashi
runtime: custom
env: flex
runtime_config:
python_version: 3
resources:
cpu: 2
memory_gb: 4
disk_size_gb: 50
volumes:
- name: ramdisk1
volume_type: tmpfs
size_gb: 1
automatic_scaling:
min_num_instances: 1
max_num_instances: 8
Docker
runtimeをcustomにするとDockerからしかデプロイすることができなくなるので(参考)、Dockerに詰めていきます。
Dockerfile
FROM gcr.io/google_appengine/python
RUN virtualenv /env -p python3.7
# python3の環境をactivate
ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH
WORKDIR /app
COPY . ./
# コンテナ内で必要なパッケージをインストール
RUN apt-get update && apt-get install -y libsm6 libxext6 libxrender-dev
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
EXPOSE 8000
CMD uvicorn run:app --host 0.0.0.0 --port $PORT --reload
docker-compose.yml
version: "3.0"
services:
# FastAPI
api:
container_name: "api"
build: .
restart: always
tty: true
ports:
- 8080:8080
HTMLとCSS
layout.html
CSSへのリンクは、最初 href="{{ url_for('static', path='/css/layout.css') }}" みたいに記述していたのですが、これだとhttpに接続してしまうのでCSSが表示されなくなります(参考)。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="/static/css/layout.css">
<title>text classification app</title>
</head>
<body>
{% include "header.html" %}
<div class="wrapper">
{% block body %}
{% endblock %}
</div>
{% include "footer.html" %}
</body>
</html>
index.html
{% extends "layout.html" %}
{% block body %}
<div class="main">
<div class="contact-form">
<form action="/evaluation" method="post">
<textarea name="tweet"></textarea><br>
<input class="contact-submit" type="submit" value="ツイートする"><br>
<p class="ramen">使い方:ラーメンに関するツイートを入れると札幌か福岡のどちらで呟かれた勝手に判定します(実際にはツイートされませんので安心してください)</p>
<p class="ramen">中身:札幌と福岡で呟かれた「ラーメン」が含まれるツイートを8000件ほどLSTMモデルに学習させています</p>
</form>
</div>
</div>
{% endblock %}
残りのページとCSSは割愛します。playgroundのページから覗いてみてください。特に難しいことはしてないです。
デプロイ
作ったものを全部Gitにpushします(参考)。できたらGoogle App Engineの上部からGoogle Cloud Shellを呼び出します(参考)。プロジェクトの作成、支払い情報の登録等は済ませておいてください。
この一番左のそれっぽいアイコンです。
git cloneしようとするとGitHubのユーザーネームとパスワードを求められるので入力します。ちなみに二段階認証している場合はパスワードの代わりに
パーソナルアクセストークンを作ってそれを入れる必要があります(参考)。
あとはぽちぽちコマンドを打ちます。先にGAEのチュートリアルでHello worldアプリを作って慣れるといいと思います。
git clone https://github.com/deepblue-ts/XXX
(ユーザーネーム聞かれる)
XXX
(パスワード聞かれる)
XXX
cd ramen_classification
gcloud app deploy
(構成が表示されてこれでいいか聞かれる)
y
(20分ほど待つ……)
gcloud app browse -s takahashi
できた!!!
たまに「エラー: The zone 'XXX' does not have enough resources available to fulfill the request. Try a different zone, or try again later.」というエラーが出て止まりますが(リージョン: asia-northeast1 2021年1月現在)、警告通りしばらく待って、もう一度最初からデプロイしてみてください。
参考文献
FastAPI周りの文献をまとめておきます
- FastAPI公式チュートリアル
丁寧なことで有名なので読むといいです。いろいろ書かれすぎていてどこに何が書いてあるかよくわからなくなるので、先に下記のような文献を読んで、そこには書かれていないが知りたいことを探す辞書のように使うのも手です。 - 【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】
FastAPIのチュートリアルとしてすごく手厚いです。コードに数箇所間違ってるところがあります。 - Python / FastAPI でつくる WebAPI 入門 研修コースに参加してみた
コンパクトにまとまっていてわかりやすかったです。 - FastAPIとTensorflowで簡単な画像認識APIを作ってみた
特にお世話になったQiita - あとはこんな感じでGitHubを検索しまくりました。感謝。
Photo by Hari Panicker on Unsplash