BERT+FastAPIを用いた感情分類器をコンテナ化する

# なんの記事？
BERTを用いた日本語感情分類器を作製しました。

https://github.com/izuna385/Japanese-BERT-Sentiment-Analyzer

# 実装モデル
[過去の記事](https://qiita.com/izuna385/items/a004982f76256cab1b2d)でも書いたような、非常に単純なテキスト分類モデルです。

今回は3値分類のモデル（Positive/Negative/Neutral）を作製しました。

`probs` で1文が各クラスに相当する確率が返却されます。

```python

'''
Model classes
'''
import torch
import pdb
import torch.nn as nn
from allennlp.modules.seq2vec_encoders import Seq2VecEncoder, PytorchSeq2VecWrapper
from allennlp.models import Model
from overrides import overrides
from allennlp.training.metrics import CategoricalAccuracy, BooleanAccuracy
from torch.nn.functional import softmax

class PosNegClassifier(Model):
    def __init__(self, args,
                 mention_encoder: Seq2VecEncoder,
                 num_label: int,
                 vocab):
        super().__init__(vocab)
        self.args = args
        self.mention_encoder = mention_encoder
        self.accuracy = BooleanAccuracy()
        self.BCEWloss = nn.BCEWithLogitsLoss()
        self.accuracy = CategoricalAccuracy()
        self.loss = nn.CrossEntropyLoss()
        self.linear_for_classify = nn.Linear(self.mention_encoder.get_output_dim(), num_label)

    def forward(self, context,
                mention_uniq_id: torch.Tensor = None,
                label: torch.Tensor = None):
        emb = self.mention_encoder(context)
        scores = self.linear_for_classify(emb)
        probs = softmax(scores, dim=1)
        output = {}
        if label is not None:
            loss = self.loss(scores, label)
            self.accuracy(probs, label)
            output['loss'] = loss
            output['logits'] = scores
            output['mention_uniq_id'] = mention_uniq_id

        output['encoded_embeddings'] = emb
        output['probs'] = probs
        return output

    @overrides
    def get_metrics(self, reset: bool = False):
        return {"accuracy": self.accuracy.get_metric(reset)}
```

# FastAPIを用いたコンテナ化
今回はこのモデルをコンテナの中に送り、コンテナを叩いてレスポンスが返却されるようにします。

予めFastAPIを起動する為に必要なDockerfileを作製します。

今回はcondaベースで作製しました。

```Docker:Dockerfile

FROM continuumio/anaconda3:2019.03
RUN apt-get update && apt-get install -y \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    git \
    build-essential
RUN pip install --upgrade pip && \
    pip install autopep8
ARG project_dir=/projects/
WORKDIR $project_dir
ADD requirements.txt .
RUN pip install -r requirements.txt
COPY . $project_dir
CMD ["uvicorn","app:app","--reload", "--host", "0.0.0.0" ,"--port","8000", "--log-level", "trace"]
```
モデル自体はgpu上で訓練しますが、その後cpuに移しているので、cuda関連のパッケージは不要です。

このイメージをビルドして、コンテナを建てます。
`$ docker build -t jsa:latest .`
`$ docker run -d -itd -p 8000:8000 jsa`

ここまででコンテナを建てることが出来ました。

# 実際の解析結果
コンテナに向かってcurlしてみます。

```
$ curl -X 'POST' 'http://localhost:8000/sentiment/' -H 'accept: application/json' \
       -H 'Content-Type: application/json' \
       -d '{
            "sentence": "今日はいい天気"
           }'

>> {"probs":
        {
         "neutral":0.8089876174926758,
         "negative":0.015650086104869843,
         "positive":0.17536230385303497
         }
    }
```
それぞれのクラスの確率が返却されました。

# 課題点
何個か挙げます。

## コンテナの軽量化
今回はconda+gpu上でモデルを訓練し、それをコンテナに送り込んでいます。加えてallennlp周辺のパッケージまで含めて、約3GB程度のイメージとなっています。

## 学習データのクロール
今回使用したデータは携帯電話に関するtwitter上の反応にアノテーションを行ったものです。多様なPositive/Negative/Neutral文に対応するためには、携帯ドメイン以外のツイートのクロールが考えられます。

# ソースコード

https://github.com/izuna385/Japanese-BERT-Sentiment-Analyzer