PyTorchでBERT日本語Pretrainedモデルを使って文章ベクトルだすまで

# はじめに

BERTが何なのかという説明はありません(できません(T_T))。
とりあえずbert使って出力をだすとこまでやってみた！という記事です。

# やったことまとめ

- pytorch から [BERT日本語Pretrainedモデル](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB) を扱える環境構築 
- [pytorch-pretrained-BERT](https://github.com/huggingface/pytorch-pretrained-BERT)のexampleにある`extract_features.py`を使って各トークンのベクトルを出す
- [Embedding Projector](http://projector.tensorflow.org/)を使って可視化


# 環境
- macOS High Sierra 10.13.6
- python 3.6.5

# 環境構築!
PyTorchで[BERT日本語Pretrainedモデル](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB)を使えるようにしていく。

## とりあえずpytorch のインストール

公式サイト通りを参考に。
- https://pytorch.org/get-started/locally/

## 次はpythonからJuman++を使えるように

<details><summary>クリックで展開します。</summary><div>
### JUMAN++のインストール
形態素解析器JUMAN++をインストールします。

```
$ brew install jumanpp
```

インストールできたか確認。

```
$ jumanpp -v
JUMAN++ 1.02 
```

### KNP のインストール

JUMAN++をpythonから利用するには、PyKNPが必要になります。
PyKNPの導入には構文解析器KNPが必要なため、先にKNPをインストールします。

```
$ brew tap uetchy/nlp
$ brew install knp
```

インストールできたかの確認。

```
% echo "jumanppとknpをいれました" | jumanpp | knp                    [10:42:51]
# S-ID:1 KNP:4.19-CF1.1 DATE:2019/06/05 SCORE:-27.38333
jumanppと<P>──┐　　　　　<体言>
        knpを<P>─PARA─────┐　<体言>
                      いれました<用言:動><格解析結果:ガ/-;ヲ/jumanpp;ヲ/knp;ニ/-;ノ/->
EOS
```


> KNPって何?
> [愛するKNPの使い方を紹介する](https://qiita.com/Kensuke-Mitsuzawa/items/ca6967f0ce07a978f358)

### PyKNP のインストール
> 形態素解析器JUMAN++(JUMAN)と構文解析器KNPのPythonバインディング (Python2系と3系の両方に対応)
> https://github.com/ku-nlp/pyknp

公式サイトの方法でインストールしていきます。

```
$ pip install pyknp
```

> 公式サイトのpyknpを用いたプログラムです。

>```python
># coding: utf-8
>from __future__ import unicode_literals  # It is not necessary when you use >python3.
>from pyknp import Juman
>
>jumanpp = Juman()  # default is JUMAN++: Juman(jumanpp=True). if you use JUMAN, >use Juman(jumanpp=False)
>result = jumanpp.analysis("下鴨神社の参道は暗かった。")
>for mrph in result.mrph_list():  # 各形態素にアクセス
>    print("見出し:%s, 読み:%s, 原形:%s, 品詞:%s, 品詞細分類:%s, 活用型:%s, 活用形:%s, 意>味情報:%s, 代表表記:%s" \
>          % (mrph.midasi, mrph.yomi, mrph.genkei, mrph.hinsi, mrph.bunrui, >mrph.katuyou1, mrph.katuyou2, mrph.imis,
>             mrph.repname))
>```

</div>




## BERT日本語Pretrainedモデル
公式サイトよりダウンロードしてきます。
- http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB

## pytorch-pretrained-BERT

PyTorchベースのBERTライブラリになります。

- https://github.com/huggingface/pytorch-pretrained-BERT


`tokenization.py`の修正が必要になります。
> 注意: --do_lower_case False オプションをつけてください。これをつけないと、濁点が落ちてしまいます。また、tokenization.pyの以下の行をコメントアウトしてください。これを行わないと漢字が全て一文字単位になってしまいます。
> [黒橋・河原研究室](http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB)


```:/path/to/your/site-packages/pytorch_pretrained_bert/tokenization.py
# text = self._tokenize_chinese_chars(text)
```
修正しましょう。

```
$ pip list | grep bert
pytorch-pretrained-bert 0.6.2
% pip show pytorch-pretrained-bert | grep Location
Location: /path/to/your/site-packages
$ vim /path/to/your/site-packages/pytorch_pretrained_bert/tokenization.py
```

# 文章ベクトルだしてみる

[pytorch-pretrained-BERT](https://github.com/huggingface/pytorch-pretrained-BERT)のexampleにある`extract_features.py`を使って
文ベクトルを抽出していこうと思います。

## 準備
### extract_features.pyのダウンロード
`extract_features.py`を持ってくる。

```
curl https://raw.githubusercontent.com/huggingface/transformers/v0.6.2/examples/extract_features.py -O
```

**少しプログラムを変更します。**
`tokenizer`にJuman++で分かち書きしたものを渡します。

プログラムの一部のみ掲載しています。


```python3:extract_features.py
def convert_examples_to_features(examples, seq_length, tokenizer):
    """Loads a data file into a list of `InputFeature`s."""

    from pyknp import Juman
    jm = Juman()

    features = []
    for (ex_index, example) in enumerate(examples):
        jm_result_a = jm.analysis(example.text_a)
        tokens_a = tokenizer.tokenize(" ".join([m.midasi for m in jm_result_a.mrph_list()]))
        # tokens_a = tokenizer.tokenize(example.text_a)

        tokens_b = None
        if example.text_b:
            jm_result_b = jm.analysis(example.text_b)
            tokens_b = tokenizer.tokenize(" ".join([m.midasi for m in jm_result_b.mrph_list()]))
            # tokens_b = tokenizer.tokenize(example.text_b)
```
 
### 入力するテキストの準備

改行区切りのテキストファイルを準備します。

```:input.txt
サッカーがしたいな
シュート上手ですね
オムライスが好き
いちご美味しい
今日は良い天気だ！
昨日は雨だった
```

## 実行してみる

### extract_features.py の引数
今回使用するものだけ

|引数名|説明|
|:-:|:-:|
|input_file|改行区切りのtxtファイル|
|output_file|出力ファイル名(json)|
|bert_model|`pytorch_model.bin`, `bert_config.json`, `vocab.txt`へのpath|
|layers|何層目の隠れ層から抽出するか(defaule: -1,-2,-3,-4)|
|do_lawer_case|tokenを小文字化する|

google-research/bertのextract_features.pyから少しだけ変わっている

### 実行

```shell

python extract_features.py \                                                                                                                                                             
--input_file=input.txt \
--output_file=output.json \
--bert_model=/path/to/your/Japanese_L-12_H-768_A-12_E-30_BPE/ \
--layers -2
```
--do_lawer_case のオプションはつけることで`True`となります。`False`にしたいので書いていません。

隠れ層は最後から２つ目を指定しています。(12 or 24中の)
なんで２つ目なんだろう...↓

> **Why not the last hidden layer? Why second-to-last?**
> The last layer is too closed to the target functions (i.e. masked language model and next sentence prediction) during pre-training, therefore may be biased to those targets. If you question about this argument and want to use the last hidden layer anyway, please feel free to set pooling_layer=-1.
> [bert-as-serviseのよくある質問より](https://bert-as-service.readthedocs.io/en/latest/section/faq.html#why-not-use-the-hidden-state-of-the-first-token-as-default-strategy-i-e-the-cls)

### 出力されるjsonファイル

>```json:output.json
{
    "linex_index": 入力された文章が何行目のものか(0ベース),
    "features": [
        {
            "token": 対応するトークン,
            "layers": [
                {
                    "index": レイヤーの番号,
                    "values": 対応する表現ベクトル
                }
            ]
        }
    ]
}
>```
> [BERTの日本語事前学習済みモデルでテキスト埋め込みをやってみる](https://dev.classmethod.jp/machine-learning/bert-text-embedding/)

# 出力結果の可視化

768次元あるベクトルを人がみて理解するのは難しいと思うので、
次元削減を行い、なんとなく見える形にしていきます。

今回は[Embedding Projector](http://projector.tensorflow.org/)というサイトを使い、
PCA(主成分分析)を行って、データを２次元で表示してみます。

## tsvファイルへ書き出し
`tsvファイル`でのロードを要求されるため、
`jsonファイル`から必要なトークンのベクトルのみを`tsvファイル`に書き出していきます。

```python3:
import json
import csv

TARGET_LAYER = -2
TARGET_TOKEN = '[CLS]'

input_file = open('./output.json', 'r')
output_tsv = open('./output.tsv', 'w')
tsv_writer = csv.writer(output_tsv, delimiter="\t")

for line in input_file:
    json_dict = json.loads(line)
    for feature in json_dict['features']:
        if feature['token'] != TARGET_TOKEN:
            continue
        for layer in feature['layers']:
            if layer['index'] == TARGET_LAYER:
                tsv_writer.writerow(layer['values'])


input_file.close()
output_tsv.close()
```
## 読み込み!
[Embedding Projector](http://projector.tensorflow.org/)

`ベクトルのファイル`: output.tsv
`metadata`: input_txt(文章を改行区切りしたもの)
でロードさせると結果が表示されると思います。

<img width="1221" alt="スクリーンショット 2019-06-11 14.04.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260131/3f5081d4-c856-4e51-d14c-f72dbdf29edf.png">

人だったらこうやって分類するな、という感じになっている気がします。

# 最後に

BERTが何なのか深く理解はできていませんが、
とりあえず環境構築から出力をだすところまで出来ました(T_T)！

間違っているところや、もっとこうしたほうが良いよ！みたいなことがあれば
ご指摘いただけると幸いです。


# 参考
- [Pytorchで日本語のbert学習済みモデルを動かすまで](https://qiita.com/masaki_sfc/items/1564cf9122db7ed47096)
- [pipでインストールしたパッケージの場所を調べる](https://qiita.com/t-fuku/items/83c721ed7107ffe5d8ff)
- [BERTの日本語事前学習済みモデルでテキスト埋め込みをやってみる](https://dev.classmethod.jp/machine-learning/bert-text-embedding/)
- [愛するKNPの使い方を紹介する](https://qiita.com/Kensuke-Mitsuzawa/items/ca6967f0ce07a978f358)
- https://bert-as-service.readthedocs.io/en/latest/section/faq.html#why-not-use-the-hidden-state-of-the-first-token-as-default-strategy-i-e-the-cls