Flaskでツイート自動生成Webアプリ作ってみた

#動機
LSTMで文書の自動生成を行う簡易なWebアプリを作成しました。
業務で自然言語処理を行うっていますが、趣味で何か目に見える楽しいものを作りたいなと考え作成に至りました。

## 1. 環境
* Docker
* Python3.6
* Flask
* PyTorch

githubレポジトリは[こちら](https://github.com/abeken1011/abetter)

## 2. Dockerfile

```Dockerfile:Dockerfile
FROM ubuntu:16.04

RUN apt-get update \
    && apt-get install -y mecab \
    && apt-get install -y libmecab-dev \
    && apt-get install -y mecab-ipadic-utf8\
    && apt-get install -y git\
    && apt-get install -y make\
    && apt-get install -y curl\
    && apt-get install -y xz-utils\
    && apt-get install -y file\
    && apt-get install -y sudo\
    && apt-get install -y wget

RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git\
    && cd mecab-ipadic-neologd\
    && bin/install-mecab-ipadic-neologd -n -y

RUN apt-get install -y software-properties-common vim
RUN add-apt-repository ppa:jonathonf/python-3.6
RUN apt-get update

RUN apt-get install -y build-essential python3.6 python3.6-dev python3-pip python3.6-venv
RUN python3.6 -m pip install pip --upgrade

RUN pip install flask
RUN pip install numpy
RUN pip install pandas
RUN pip install sklearn
RUN pip install gensim
RUN pip install mecab-python3
RUN pip install cloudpickle
RUN pip install torch

RUN export LC_ALL=C.UTF-8
RUN export LANG=C.UTF-8

EXPOSE 5000
```

MeCab Neologd, その他必要なライブラリのインストールやFlaskに必要な環境変数の設定、ポート開放など行っています。

```bash:
$cd path/to/directory
$docker build -t flask-app:1.0 .
```

Dockerコンテナを起動する際の注意はポートを5000で開放することです。Flaskのデフォルト開放ポートが5000のためホストとコンテナで5000ポートを接続しました。

```bash:
$docker run -it -p 5000:5000 -v path/to/directory:/flask-app flask-app:1.0
```

Dockerコンテナの起動を完了しました。

## 3. FlaskでWebアプリ作成
```python:app.py
import cloudpickle
import torch
import LSTM_model
from flask import Flask, render_template, request

app=Flask(__name__)
app.debug = True

@app.route("/", methods=["GET"])
def index():
    return render_template("index.html", message="最初のメッセージを入力してね！")

@app.route("/", methods=["POST"])
def form():
    with open('data/model.pkl', 'rb') as f:
        model = cloudpickle.load(f)
    field=request.form["field"]
    maked_words = LSTM_model.generate_seq(model, start_phase=field, length=20)
    return render_template("index.html", message=maked_words)

if __name__ == '__main__':
    app.run(host='0.0.0.0')
```
GET, POSTで入力した文章を推論、出力を行なっています。
私がハマったポイントとしてはVMでのポート開放です。

```python:
app.run(host='0.0.0.0')
```
ここでhost="0.0.0.0"としておかないとアクセスできずにハマるためご注意を（あまりFlaskの仕様に慣れていませんでした）

```html:index.html
<!doctype html>
<html lang="ja">
  <head>
    <title>tweet_maker</title>
    <meta charset='utf-8'>
  </head>
  <body>
    <h1>tweet_maker</h1>
    <p>{{message}}</p>
    <div>
      <form method="post" action="/">
        <input type="text" name="field">
        <input type="submit">
      </form>
    </div>
  </body>
</html>
```

```python:LSTM_model.py
import re
import pickle
import torch
import torch.nn as nn
import MeCab

class SequenceGenerationNet(nn.Module):
  def __init__(self, num_embeddings, embedding_dim=50, hidden_size=50, num_layers=1, dropout=0.2):
    super().__init__()
    self.emb=nn.Embedding(num_embeddings, embedding_dim)
    self.lstm=nn.LSTM(embedding_dim, hidden_size, num_layers, batch_first=True, dropout=dropout)

    self.linear=nn.Linear(hidden_size, num_embeddings)

  def forward(self, x, h0=None):
    x=self.emb(x)
    x, h =self.lstm(x, h0)
    x=self.linear(x)
    return x, h

def make_wakati(sentence):
  tagger = MeCab.Tagger("-Owakati -d /usr/lib/mecab/dic/mecab-ipadic-neologd")
  sentence = sentence.replace(",\n", " ")
  # MeCabで分かち書き
  sentence = tagger.parse(sentence)
  # 半角全角英数字除去
  sentence = re.sub(r'[0-9０-９]+', "0", sentence)
  sentence.translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))
  # 記号もろもろ除去
  sentence = re.sub(r'[\．_－―─！＠＃＄％＾＆\-‐|\\＊\“（）＿■×+α※÷⇒—●★☆〇◎◆▼◇△□(：〜～＋=)／*&^%$#@!~`){}［］…\[\]\"\'\”\’:;<>?＜＞〔〕〈〉？、。・,\./『』【】「」→←○《》≪≫\n\u3000]+', "", sentence)
  # スペースで区切って形態素の配列へ
  wakati = sentence.split(" ")
  # 空の要素は削除
  wakati = list(filter(("").__ne__, wakati))
  return wakati

def sentence2index(sentences):
  wakati = make_wakati(sentences)
  with open("data/w2i.pkl", "rb")as data:
    word2index = pickle.load(data)
  id_stc = [word2index[i] for i in make_wakati(sentences)]
  return id_stc

def generate_seq(net, start_phase="私は", length=200, temperature=0.8, device="cpu"):
  net.eval()
  result=[]

  start_tensor=torch.tensor(
      sentence2index(start_phase), dtype=torch.int64
  ).to(device)

  x0=start_tensor.unsqueeze(0)
  o, h=net(x0)
  out_dist=o[:, -1].view(-1).exp()
  top_i=torch.multinomial(out_dist, 1)[0]
  result.append(top_i)

  for i in range(length):
    inp=torch.tensor([[top_i]], dtype=torch.int64)
    inp=inp.to(device)
    o, h=net(inp, h)
    out_dist=o.view(-1).exp()
    top_i=torch.multinomial(out_dist, 1)[0]
    result.append(top_i)
  with open("data/i2w.pkl", "rb")as data:
    index2word = pickle.load(data)
  res = "".join([index2word[int(i.to("cpu").numpy())] for i in result])
  return start_phase+ res
```
LSTMの推論はこちらで行なっています。学習データはgithubにアップしているためそちらを参照ください。コード中のi2wは単語とインデックスの関係を表すpickleファイルです。

## 4. 動かしてみる
```bash:
flask run --host 0.0.0.0
```
上記コードを入力後のURLにアクセスすると動作します。

<img width="306" alt="スクリーンショット 2019-10-08 16.45.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413339/417fdcd4-e9b0-476c-823a-ee8138f80b5d.png">
実際の画面はこんな感じです。興味ある方は是非git cloneして遊んで見てください。


## 参考にさせていただいた記事
こちらの記事を参考にさせていただきました。
https://qiita.com/oreyutarover/items/909d614ca3b48d2c9e16