BERTで文章のベクトル表現を得るための環境構築紹介

<img width="705" alt="Screen Shot 2019-04-12 at 22.23.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104478/9fcd0bc4-179e-7263-8d54-eac4121f0164.png">


BERTが様々な自然言語処理タスクでSOTAを達成し、コミュニティを賑わせたことは記憶に新しいと思います。
同時にBERTの事前学習には時間がかかることが知られていますが、ありがたいことに本家Googleや有志の方々が事前学習を行ったBERTモデルを配布しています。
本記事ではそんなBERTの恩恵を得るべく、表題のように事前学習済みのBERTを通して文章のベクトル表現を得るための環境作りを紹介します。
私が業務で試した範囲だとBERTのベクトル表現に変更しただけで全てのタスク、特に教師データが少ないタスクでは精度が大きく向上しました。
今までWord2vecやGloVe,fastTextしか知らなかった方は是非試してみてください。

ちなみに本記事で紹介する手法の代案として[bert-as-service](https://github.com/hanxiao/bert-as-service)などもありますが、デプロイが複雑になりやすいことと、何かしらの例外が発生すると固まってしまう問題があるようで、現状では本番環境を見据えるとこちらの方がおすすめに見受けられます。

筆者の環境はUbuntu18.04となります。
もしGPU周りで問題がある場合は、CUDAが推奨していないGPUを使用していないかどうか、ドライバが適切にインストールできているかどうかをチェックしてみてください。

# GPU計算環境を整える
1. [Dockerのインストール](https://docs.docker.com/install/)
2. [CUDA9.0](https://developer.nvidia.com/cuda-toolkit-archive)のインストール
  - ディストリビューションにもよるので、恐縮ですが別途調べながらインストールしてください
  - 配布するtensorflowは1.12.0なので、CUDA9.0を選択してください
3. [nvidia-dockerのインストール](https://github.com/NVIDIA/nvidia-docker)
3. tensorflow-gpu-py3のDockerイメージをPull

```
docker pull tensorflow/tensorflow:latest-gpu-py3
```

4. GPUで計算できるか確認

```bash

docker run --runtime=nvidia -it --rm tensorflow/tensorflow:latest-gpu-py3 python -c "import tensorflow as tf; tf.enable_eager_execution(); print(tf.reduce_sum(tf.random_normal([1000, 1000])))"
```

下のスクショのようにGPUに関する記載があり、`tf.Tensor(-1375.6091, shape=(), dtype=float32)`といったような結果が返ってきたら無事にGPUで計算できる環境が整ったことになります。

<img width="1216" alt="Screen Shot 2019-04-10 at 19.07.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/104478/3c819116-4773-40bf-ba2b-826fc5f36c7b.png">


# プロジェクトの作成とJupyterNotebookの起動

1. プロジェクトの作成

```bash

git clone git@github.com:Jah524/try-bert.git && cd try-bert
```

2. DockerイメージのBuildとRun

時間かかるのでどうぞ気長に。

```bash

docker build -t try-bert .
docker run --runtime=nvidia -p 8888:8888 -v "$(pwd):/notebooks/workspace" -it try-bert
```

3. Jupyter Notebookの起動

今回のサンプルではパスワードを設定していないため、下記のコマンドを実行した結果提示されるtokenを含んだurlをブラウザに入力してください。

```bash

./start-notebook.sh
```

# BERTモデルの準備

Googleが提供している本家BERTモデルは単語分割が特殊なため、今回は日本語wikipediaの文章を対象としてsentencepieceで単語分割を行うBERTモデルを使用します。
次のページを一読してからモデルをダウンロードしてきてください。

[BERT with SentencePiece を日本語 Wikipedia で学習してモデルを公開しました](https://yoheikikuta.github.io/bert-japanese/)

`model/`に移動し、CUI環境の人は以下のスクリプトを実行すると必要なデータがダウンロードできます。

ダウンロードには15分程度かかります。

```bash

./download-model.sh
```

# BERTから文のベクトル表現を取得する

ここからの結果は次のリンク先のnotebookでも確認できます。

https://github.com/Jah524/try-bert/blob/master/try-bert.ipynb

## BERTを読み込むスクリプト

```python

from keras_bert import load_trained_model_from_checkpoint

config_path = './model/bert_config.json'
checkpoint_path = './model/model.ckpt-1400000'

bert = load_trained_model_from_checkpoint(config_path, checkpoint_path)
bert.summary()
```

モデルのファイル名には気をつけてください。
大きなモデルなので読み込みに多少の時間がかかります。
以下のようにBERTのモデル構造が出力されたら無事に読み込めています。

<img width="1085" alt="Screen Shot 2019-04-11 at 0.02.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/104478/1acad41f-ef46-53e7-0505-0e61a8797bc7.png">

## BERTを使うスクリプト

[SentencePiece + 日本語WikipediaのBERTモデルをKeras BERTで利用する](https://qiita.com/hideki/items/411c7531354fede9ade1)を参考にさせていただきました。

```python

import sentencepiece as spm
import numpy as np

spp = spm.SentencePieceProcessor()
spp.Load('./model/wiki-ja.model')

def texts2matrix(texts):
    maxlen = 512 # from BERT config
    common_seg_input = np.zeros((len(texts), maxlen), dtype = np.float32)
    matrix = np.zeros((len(texts), maxlen), dtype = np.float32)
    for i, text in enumerate(texts):
        tok = [w for w in spp.encode_as_pieces(text.replace(" ", ""))]
        if tok == [] or len(tok) > maxlen:
            print("skip processing", tok)
        else:
            tokens = []
            tokens.append('[CLS]')
            tokens.extend(tok)
            tokens.append('[SEP]')
            for t, token in enumerate(tokens):
                try:
                    matrix[i, t] = spp.piece_to_id(token)
                except:
                    print(token+"is unknown")
                    matrix[i, t] = spp.piece_to_id('<unk>')
    return bert.predict([matrix, common_seg_input])[:,0] # embedding of [CLS]
```

`[:,0]`でそれぞれの文章のベクトル表現となる`[CLS]`シンボルを参照するようにしています。
文章の分類タスクなどはこの例でよいのですが、系列ラベリングなどのタスクの場合は全ての系列における結果を取得してください。
後は上記で作成した`texts2tokens`に文章のリストを与えれば、それぞれのベクトル表現が得られます。

```python

spp.encode_as_pieces("隣の客はよく柿食う客だ")
#=> ['▁', '隣の', '客', 'は', 'よく', '柿', '食', 'う', '客', 'だ']
```

```python

texts2matrix(["隣の客はよく柿食う客だ"])
#=> array([[-3.41196954e-01,  1.03131640e+00,  1.53629273e-01,
#        -4.40728009e-01,  1.48483515e-02,  8.22932497e-02,
#        -2.90441006e-01,  7.41560638e-01,  9.66181457e-01, ...
```

# 最後に

ベクトル表現さえできてしまえば分類タスクでもクラスタリングでもできるので、ここからは皆さんのタスクに合わせて色々試して見てください。
例えば分類タスクで、もしkerasであれば以下のように全結合で中間層を1枚追加するだけでも十分な性能を発揮してくれます。

```python

model = Sequential([
    Dense(state_size, input_dim=768),# 768はBERTから得られたベクトルの次元数
    Dense(len(class_maps), activation=tf.nn.softmax),
])
```

今回は特徴ベクトル(feature vector)を得る方法でしたが、当然fine-tuningによる手法もあります。
一方でfine-tuningは計算コストがかかるため、タスクをさくっと試すのであれば本記事の方法が適しているように思えます。

本記事がお役にたったようであれば幸いです。