文章を分類するウェブアプリを作ろう

ラーメン食べてますか。
ラーメンに関するツイートについて札幌と福岡のどちらでツイートされたか判別するウェブアプリをデプロイしたので、「モデル編」「ウェブアプリ編」に分けて記録を残そうと思います。こちらは「ウェブアプリ編」です(モデル編)。
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周りの文献をまとめておきます


Photo by Hari Panicker on Unsplash