bert-as-serviceで作るGPU無しのBERTサーバー

# はじめに

文を投げるとBERTで文ベクトルに変換して返してくれるサーバーを作ります。日本語も英語も利用したいので両方対応させます。

## BERTとは
Googleが2018年の終わり頃に発表した自然言語処理の事前学習済みモデルです。複数のNLPタスクでstate-of-the-artを叩き出し、その凄まじい性能でNLP界隈で話題になりました。

ただモデルのサイズも凄まじ過ぎて、一番性能の高いBERTは安GPUではFine Tuningはおろか推論すらOut of memoryで動きません。目の前に自然言語処理の最先端があるのにそれを使えないのは悔しい限りです。

## 目的
そこそこのCPUと大容量のメモリを積んだちょっと強い民生向けコンピューター（**GPU非搭載**）上でBERTを動かし、文ベクトルを出力させます。

データを前処理的にBERTの文ベクトルに変換することで、強力な計算リソースが無い状況でもBERTの恩恵を受けられるようにします。

# BERTサーバー構築

## 前提

この記事に登場するソフトウェアは特に記載が無い限り2019年4月27日時点での最新版を使用しています。

クラウドでも基本的に同様の設定で問題ないと思いますが、オンプレミス環境で行った作業を基に書いた記事ですので、オンプレミス環境を前提とします。

## 環境
- CPU: Core i7 6700K
- メモリ: 64GB
- OS: Ubuntu 18.04 LTS (Desktop)

## OS準備
まず最低限作業ができるようにOSと設定を整えます。

Ubuntu 18.04 LTSのISOイメージをダウンロードし、CDやUSBメモリを経由してコンピューターにインストールします。

今回は僕が研究室から居なくなった後、新人管理者が保守・管理をするかもしれないことを考慮して、ハードルを下げるために[日本語デスクトップ版のUbuntu](https://www.ubuntulinux.jp/News/ubuntu1804-ja-remix)を採用しました。


インストールが完了したらまずは更新を行います。

```bash

sudo apt-get update && sudo apt-get -y upgrade
```

続いてディレクトリ構造を英語に変更します。日本語版を使うときのみやっておいた方が良い設定ですので、英語版を使っている場合は不要です。

```
LANG=C xdg-user-dirs-gtk-update
```

必要なソフトウェアのインストールに進みます。

```
sudo apt install -y git vim tmux openssh-server vsftpd
```

vsftpdはデフォルトで書き込み無効になっているので設定を変更します。

```
sudo vim /etc/vsftpd.conf
```

下記を追記します。

```
write_enable=YES
```

続いてファイアウォールを設定します。FTPとSSHが使用するポート、bert-as-serviceが使用するポートを開けておきます。

```
sudo ufw default DENY
sudo ufw allow 20:22/tcp
sudo ufw allow 5555
sudo ufw allow 5556
sudo ufw allow 5557
sudo ufw allow 5558
sudo ufw enable
```

ここまで設定を行えば、後はSSH越しに設定できます。

## Anacondaのインストール

Pythonの環境については色々と流派や好みがあり~~宗教戦争の様相を呈してい~~ますが、ここではAnacondaを使用してPython環境を整えます。

```
wget https://repo.anaconda.com/archive/Anaconda3-2019.03-Linux-x86_64.sh
bash Anaconda3-2019.03-Linux-x86_64.sh
```

色々と出てきますが基本的にデフォルトで構いません。

最後にパスを通します。

```
echo 'export PATH=/home/<USERNAME>/anaconda3/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```

<USERNAME>は環境ごとに異なると思うので、使用しているユーザー名を入れます。デフォルトの場所以外にAnacondaをインストールした場合はインストール先を指定してください。

## Python仮想環境の構築とパッケージのインストール

既にパスを通しているのでPythonを使用できる状態になっています。このときに呼ばれるPythonはAnacondaのBase環境です。このBase環境に色々とインストールしても良いのですが、今後bert-as-serviceの仕様が変更になったりバージョンの変更が難しくなっても面倒なので、仮想環境にインストールします。

```
conda create -n bert python=3.6 anaconda
```

仮想環境に入ります。

```
source activate bert
```

必要なパッケージをインストールします。

```
pip install tensorflow
pip install bert-serving-server
pip install bert-serving-client
```

## BERTの準備

BERTの学習済みモデルをダウンロードします。
今回は[日本語Wikipediaで学習したBERTの事前学習済みモデル](https://yoheikikuta.github.io/bert-japanese/)とGoogleが発表した[オリジナルのBERT](https://github.com/google-research/bert)の事前学習済みモデルを使えるようにします。

ホームディレクトリにBERT用のディレクトリを作っておきます。

```
cd ~
mkdir bert
```

### BERT（日本語）の準備

日本語BERTを納めるためのディレクトリを作ります。

```
cd bert
mkdir bert-jp
cd bert-jp
```

ダウンロードを行います。サイズが大きいファイルをGoogleドライブからダウンロードする時は一工夫必要です。

```
curl -sc /tmp/cookie "https://drive.google.com/uc?export=download&id=1F4b_u-5zzqabA6OfLxDkLh0lzqVIEZuN" > /dev/null
CODE="$(awk '/_warning_/ {print $NF}' /tmp/cookie)"  
curl -Lb /tmp/cookie "https://drive.google.com/uc?export=download&confirm=${CODE}&id=1F4b_u-5zzqabA6OfLxDkLh0lzqVIEZuN" -o bert_model.ckpt.data-00000-of-00001
```

このコマンドは「[curlやwgetで公開済みGoogle Driveデータをダウンロードする](https://qiita.com/namakemono/items/c963e75e0af3f7eed732)」を参考にしました。

その他のファイルも必要になるのでダウンロードしておきます。

```
wget "https://drive.google.com/uc?export=download&id=1uzPpW38LcS4YS431GgdG0Hsj4gNgE5X1" -O wiki-ja.vocab
wget "https://drive.google.com/uc?export=download&id=1jjZmgSo8C9xMIos8cUMhqJfNbyyqR0MY" -O wiki-ja.model
wget "https://drive.google.com/uc?export=download&id=1LB00MDQJjb-xLmgBMhdQE3wKDOLjgum-" -O bert_model.ckpt.index
wget "https://drive.google.com/uc?export=download&id=1V9TIUn5wc-mB_wabYiz9ikvLsscONOKB" -O bert_model.ckpt.meta
wget "https://drive.google.com/uc?export=download&id=11V3dT_xJUXsZRuDK1kXiXJRBSEHGl3In" -O graph.pbtxt
```

もしファイルのIDが変わっていてダウンロードできない場合は、改めて[日本語Wikipediaで学習したBERTの事前学習済みモデル](https://yoheikikuta.github.io/bert-japanese/)からダウンロード先のURLからIDを確認してください。

続いてSentencePieceの未知語の記号を書き換えます。

```
cut -f1 wiki-ja.vocab | sed -e "1 s/<unk>/[UNK]/g" > vocab.txt
```

なお、wiki-ja.modelは今後外部のサーバーで使用することになるので、取り出しておくと便利です。

続いて設定ファイルを作成します。

```
vim bert_config.json
```

```json:bert_config.json
{
    "attention_probs_dropout_prob" : 0.1,
    "hidden_act" : "gelu",
    "hidden_dropout_prob" : 0.1,
    "hidden_size" : 768,
    "initializer_range" : 0.02,
    "intermediate_size" : 3072,
    "max_position_embeddings" : 512,
    "num_attention_heads" : 12,
    "num_hidden_layers" : 12,
    "type_vocab_size" : 2,
    "vocab_size" : 32000
}
```

日本語BERTの準備の手順は「[bert-as-serviceを使って日本語BERTの文エンベディング計算サーバーを作る](https://qiita.com/shimaokasonse/items/97d971cd4a65eee43735)」を参考にしました。

### BERT（オリジナル）の準備

まずは日本語BERT同様ディレクトリを作ります。

```
cd ~/bert
mkdir bert-origin
cd bert-origin
```

事前学習済みモデルをダウンロード・解凍します。今回はBERT-Large( Uncased: 24-layer, 1024-hidden, 16-heads, 340M parameters)を選択しています。

```
wget https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-24_H-1024_A-16.zip
unzip uncased_L-24_H-1024_A-16.zip
```

こちらは既に必要なものは揃っているのでこれで完了です。

## 動作確認

bert-as-serviceを動かす準備が整いました。動作確認を行います。

```
bert-serving-start -model_dir ~/bert/bert-jp -num_worker=2 -cpu
```

別のPython実行環境を持つマシンから以下のテストコードを実行します。wiki-ja.modelが必要なので、SFTPを使って転送するかダウンロードし直して、テストコードの実行環境に置いてください。

実行環境に必要なソフトをインストールします。（一部いらないものも混じってるかも）

```
pip install tensorflow
pip install bert-serving-server
pip install bert-serving-client
pip install sentencepiece
```

テストコードを実行します。

```python
from bert_serving.client import BertClient
bc_jp = BertClient(ip='<BERTサーバーのIP>')

import sentencepiece as spm
s = spm.SentencePieceProcessor()
s.Load('wiki-ja.model')
 
def parse(text):
    text = text.lower()
    return s.EncodeAsPieces(text)
 
texts = ["BERTのテスト","覚悟はいいか？オレはできてる"]

parsed_texts = list(map(parse,texts))
 
print(bc_jp.encode(parsed_texts,is_tokenized=True))
```

きちんと文ベクトルが表示されればOKです。

このテストコードは「[bert-as-serviceを使って日本語BERTの文エンベディング計算サーバーを作る](https://qiita.com/shimaokasonse/items/97d971cd4a65eee43735)」を参考にしました。

続いてオリジナルのBERTも試します。まずBERTサーバーで日本語BERTのbert-as-serviceをCTRL+Cで停止し、以下のコマンドを実行します。

```
bert-serving-start -model_dir ~/bert/bert-origin/uncased_L-24_H-1024_A-16 -num_worker=2 -cpu -port_in 5557 -port_out 5558
```

文を受け取るポートを5557、文ベクトルを返すポートを5558としています。

テストコードを実行します。

```python
from bert_serving.client import BertClient
bc_jp = BertClient(ip='<BERTサーバーのIP>')

parsed_texts = [["Are","you","ready","?","I'm","ready"]]
print(bc_en.encode(parsed_texts,is_tokenized=True))
```

文ベクトルが表示されればこちらもOKです。

## bert-as-serviceのサービス化と自動起動

起動確認が取れたら、サーバーの起動と同時にbert-as-serviceが立ち上がるようにサービス化を行います。

まず日本語BERT用の起動スクリプトを作成します。

```
cd ~
vim run_bas_jp.sh
```

```bash:run_bas_jp.sh
#!/bin/bash

. /home/<USERNAME>/anaconda3/bin/activate bert
nohup /home/<USERNAME>/anaconda3/envs/baas/bin/bert-serving-start -model_dir /home/<USERNAME>/bert/bert-jp -num_worker=2 -cpu
```

Python仮想環境に入り、bert-as-serviceをワーカー数2個、CPUで動かすという内容です。<USERNAME>はそれぞれの環境に合わせて書き換えてください。

続いてオリジナルBERT用の起動スクリプトを作成します。

```
vim run_bas_origin.sh
```

```bash:run_bas_origin.sh
#!/bin/bash

. /home/<USERNAME>/anaconda3/bin/activate bert
nohup /home/<USERNAME>/anaconda3/envs/baas/bin/bert-serving-start -model_dir /home/<USERNAME>/bert/bert-origin/uncased_L-24_H-1024_A-16 -num_worker=2 -cpu -port_in 5557 -port_out 5558
```
Python仮想環境に入り、bert-as-serviceをワーカー数2個、CPUで動かし、受信ポートを5557、送信ポートを5558に変更するという内容です。<USERNAME>はそれぞれの環境に合わせて書き換えてください。

スクリプトに実行権限を持たせます。

```
sudo chmod +x run_bert_jp.sh
sudo chmod +x run_bert_origin.sh
```

続いてサービスの登録を行います。

```
cd /lib/systemd/system
```

ここでサービスファイルを作成します。

```
sudo vim bas-jp.service
```

```bas-jp.service
[Unit]
Description=BERT-as-a-Service with Japanese BERT model

[Service]
Type=simple
ExecStart=/home/<USERNAME>/run_bas_jp.sh
Restart=no

[Install]
WantedBy=multi-user.target
```

```
sudo vim bas-origin.service
```

```bas-origin.service
[Unit]
Description=BERT-as-a-Service with Original BERT model

[Service]
Type=simple
ExecStart=/home/<USERNAME>/run_bas_origin.sh
Restart=no

[Install]
WantedBy=multi-user.target                 
```

例によって<USERNAMES>は自分の環境に合わせて変更してください。

続いてサービスを有効化します。

```
sudo systemctl enable bas-jp
sudo systemctl enable bas-origin
```

サービスを起動します。

```
sudo systemctl start bas-jp
sudo systemctl start bas-origin
```

上記コマンドのstartをstatusに変更すれば状態確認を行うことができます。

# おわりに

~~**Titan RTXを2台NVLinkで束ねたGPUメモリ48GBのGPUマシンが欲しい！（本末転倒）**~~

文を投げるとBERTで文ベクトルに変換して返してくれるサーバーができました。強力なGPUが無くてもメモリさえ十分に積んでいるマシンがあれば自然言語処理の最先端に至れます。

データの前処理の段階で文ベクトルに変換して以後使い回すようにする運用になりそうです。

サービスに組み込む場合は、今回はセットアップしていませんがbert-as-serviceにはHTTPリクエストで同様の処理を行う機能が提供されているので、そちらを利用すれば簡単にBERTを組み込めそうです。

# 参考文献

参考にさせていただきました。貴重な知見をありがとうございます。

https://yoheikikuta.github.io/bert-japanese/
https://bert-as-service.readthedocs.io/en/latest/index.html
https://github.com/google-research/bert
https://qiita.com/namakemono/items/c963e75e0af3f7eed732
https://qiita.com/nwing/items/f2a595389f39206235e8
https://www.usagi1975.com/080720181203/
https://twitter.com/ElectMemo/status/1119028790137462784?s=20