BERTって「あめ(飴)」と「あめ(雨)」の違いがわかるのか

## 背景
Word2Vecは以前使用したことがありしたが、文脈を考慮しない表現であるらい。

「明日の天気予報はあめだ。」
「駄菓子屋であめを買った。」

上のような2つの文章があった時、
意味的には「雨」と「飴」で意味が異なるが、同じ単語であるため同じもの認識する。

そんな中BERTが文脈を考慮した表現が可能であると言うことで、
上の「あめ」を違う意味として捉えているのかを確認してみたくなった。

## 計画
### 対象データ
[こちら](https://qiita.com/norihitoishida/items/85150cfacc1f75f552f3)の記事のELMoの実装を参考に以下のようなコーパスを使ってチェックする。

* 雨
    * 「明日の天気予報はあめだ。」
    * 「今日の朝はあめだったので、犬の散歩には行きませんでした。」
    * 「梅雨になったので、毎日あめが降っている。」
* 飴
    * 「駄菓子屋であめを買った。」
    * 「あめを舐めながら作業をする。」
    * 「すっぱいあめは苦手だ。」
    * 「明日の天気予報はあめだ。」

### BERTモデル
BERTは[huggingface/transformers](https://github.com/huggingface/transformers)を使って分散表現を獲得する。

## 実装と結果

大きく以下の工程で実装を行った。

1. 単語分割
2. 単語に番号を振る
3. モデルの入力形式に変換(テンソル化)
4. モデル準備と入力
5. 出力間(雨と雨、雨と飴、飴と飴)の類似度を算出

```python
import torch
import numpy as np
from transformers import BertJapaneseTokenizer, BertForMaskedLM

tokenizer = BertJapaneseTokenizer.from_pretrained('bert-base-japanese-whole-word-masking')

def tokenize(text):
    return tokenizer.tokenize(text)

def word_to_index(text):
    return tokenizer.convert_tokens_to_ids(text)

def to_tensor(tokens):
    return torch.tensor([tokens])


def cos_sim(vec1, vec2):
    x = vec1.detach().numpy()
    y = vec2.detach().numpy()

    x_l2_norm = np.linalg.norm(x, ord=2)
    y_l2_norm = np.linalg.norm(y, ord=2)
    xy = np.dot(x,y)

    return xy / (x_l2_norm * y_l2_norm)


if __name__ == "__main__":
    d_rainy_01 = "[CLS]明日の天気予報はあめだ。[SEP]"
    d_rainy_02 = "[CLS]今日の朝はあめだったので、犬の散歩には行きませんでした。[SEP]"
    d_rainy_03 = "[CLS]梅雨になったので、毎日あめが降っている。[SEP]"
    d_candy_01 = "[CLS]駄菓子屋であめを買った。[SEP]"
    d_candy_02 = "[CLS]あめを舐めながら作業をする。[SEP]"
    d_candy_03 = "[CLS]すっぱいあめは苦手だ。[SEP]"

    # 1. 単語分割
    tokenize_rainy_01 = tokenize(d_rainy_01)
    tokenize_rainy_02 = tokenize(d_rainy_02)
    tokenize_rainy_03 = tokenize(d_rainy_03)
    tokenize_candy_01 = tokenize(d_candy_01)
    tokenize_candy_02 = tokenize(d_candy_02)
    tokenize_candy_03 = tokenize(d_candy_03)

    # 2. 単語に番号を振る
    indexes_rainy_01 = to_vocabulary(tokenize_rainy_01)
    indexes_rainy_02 = to_vocabulary(tokenize_rainy_02)
    indexes_rainy_03 = to_vocabulary(tokenize_rainy_03)
    indexes_candy_01 = to_vocabulary(tokenize_candy_01)
    indexes_candy_02 = to_vocabulary(tokenize_candy_02)
    indexes_candy_03 = to_vocabulary(tokenize_candy_03)

    # 3. モデルの入力形式に変換(テンソル化)
    tensor_rainy_01 = to_tensor(indexes_rainy_01)
    tensor_rainy_02 = to_tensor(indexes_rainy_02)
    tensor_rainy_03 = to_tensor(indexes_rainy_03)
    tensor_candy_01 = to_tensor(indexes_candy_01)
    tensor_candy_02 = to_tensor(indexes_candy_02)
    tensor_candy_03 = to_tensor(indexes_candy_03)

    # 4. モデル準備と入力
    bert = BertForMaskedLM.from_pretrained('bert-base-japanese-whole-word-masking')
    bert.eval()

    index_rainy_01 = tokenize_rainy_01.index('あめ')
    index_rainy_02 = tokenize_rainy_02.index('あめ')
    index_rainy_03 = tokenize_rainy_03.index('あめ')
    index_candy_01 = tokenize_candy_01.index('あめ')
    index_candy_02 = tokenize_candy_02.index('あめ')
    index_candy_03 = tokenize_candy_03.index('あめ')
    vec_rainy_01 = bert(tensor_rainy_01)[0][0][index_rainy_01]
    vec_rainy_02 = bert(tensor_rainy_02)[0][0][index_rainy_02]
    vec_rainy_03 = bert(tensor_rainy_03)[0][0][index_rainy_03]
    vec_candy_01 = bert(tensor_candy_01)[0][0][index_candy_01]
    vec_candy_02 = bert(tensor_candy_02)[0][0][index_candy_02]
    vec_candy_03 = bert(tensor_candy_03)[0][0][index_candy_03]

    # 5. 出力間(雨と雨、雨と飴、飴と飴)の類似度を算出
    print("雨_01 and 雨_02 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_01, vec_rainy_02)))
    print("雨_01 and 雨_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_01, vec_rainy_03)))
    print("雨_02 and 雨_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_02, vec_rainy_03)))
    print("-"*30)


    print("雨_01 and 飴_01 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_01, vec_candy_01)))
    print("雨_01 and 飴_02 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_01, vec_candy_02)))
    print("雨_01 and 飴_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_01, vec_candy_03)))
    print("-"*30)

    print("雨_02 and 飴_01 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_02, vec_candy_01)))
    print("雨_02 and 飴_02 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_02, vec_candy_02)))
    print("雨_02 and 飴_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_02, vec_candy_03)))
    print("-"*30)

    print("雨_03 and 飴_01 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_03, vec_candy_01)))
    print("雨_03 and 飴_02 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_03, vec_candy_02)))
    print("雨_03 and 飴_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_rainy_03, vec_candy_03)))
    print("-"*30)

    print("飴_01 and 飴_02 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_candy_01, vec_candy_02)))
    print("飴_01 and 飴_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_candy_01, vec_candy_03)))
    print("飴_02 and 飴_03 : 「あめ」 の cos類似度 : {:.2f}".format(cos_sim(vec_candy_02, vec_candy_03)))
```

結果をまとめると、

|          | rainy_01 | rainy_02 | rainy_03 | candy_01 | candy_02 | candy_03 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| rainy_01 | *        | 0.79     | 0.88     | 0.83     | 0.83     | 0.83     |
| rainy_02 | *        | *        | 0.79     | 0.77     | 0.75     | 0.77     |
| rainy_03 | *        | *        | *        | 0.87     | 0.89     | 0.84     |
| candy_01 | *        | *        | *        | *        | 0.93     | 0.90     |
| candy_02 | *        | *        | *        | *        | *        | 0.90     |
| candy_03 | *        | *        | *        | *        | *        | *        |

* 同じ意味同士の類似度の平均値は、*0.865*
* 異なる位置同士の類似度の平均値は、*0.820*

一応、雨と飴の意味を異なるものとして捉えています(?)
でも、期待より値が離れなかったのは何故なんでしょう？

## おまけ
2020年3月にNICTがPre-trainedモデルを公開していたので、bert-base-japanese-whole-word-maskingと比較してみました。
NICTのモデルを使用して同じ処理で類似度を出した結果が以下の通り、

|          | rainy_01 | rainy_02 | rainy_03 | candy_01 | candy_02 | candy_03 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| rainy_01 | *        | 0.83     | 0.82     | 0.86     | 0.82     | 0.85     |
| rainy_02 | *        | *        | 0.88     | 0.87     | 0.79     | 0.84     |
| rainy_03 | *        | *        | *        | 0.84     | 0.80     | 0.86     |
| candy_01 | *        | *        | *        | *        | 0.82     | 0.85     |
| candy_02 | *        | *        | *        | *        | *        | 0.81     |
| candy_03 | *        | *        | *        | *        | *        | *        |

* 同じ意味同士の類似度の平均値は、*0.835*
* 異なる位置同士の類似度の平均値は、*0.837*

|         | bert-base-japanese-whole-word-masking | NICT  |
| ------- | ------------------------------------- | ----- |
| 同じ意味同士  | 0.865                                 | 0.835 |
| 異なる意味同士 | 0.820                                 | 0.837 |

## おわりに
期待とは異なる結果となってしまった…
このやり方があっているかどうかもわからないので、わかる方教えて欲しいです！

## 参考
* https://qiita.com/norihitoishida/items/85150cfacc1f75f552f3
* https://github.com/huggingface/transformers