背景
Word2Vecは以前使用したことがありしたが、文脈を考慮しない表現であるらい。
「明日の天気予報はあめだ。」
「駄菓子屋であめを買った。」
上のような2つの文章があった時、
意味的には「雨」と「飴」で意味が異なるが、同じ単語であるため同じもの認識する。
そんな中BERTが文脈を考慮した表現が可能であると言うことで、
上の「あめ」を違う意味として捉えているのかを確認してみたくなった。
計画
対象データ
こちらの記事のELMoの実装を参考に以下のようなコーパスを使ってチェックする。
- 雨
- 「明日の天気予報はあめだ。」
- 「今日の朝はあめだったので、犬の散歩には行きませんでした。」
- 「梅雨になったので、毎日あめが降っている。」
- 飴
- 「駄菓子屋であめを買った。」
- 「あめを舐めながら作業をする。」
- 「すっぱいあめは苦手だ。」
- 「明日の天気予報はあめだ。」
BERTモデル
BERTはhuggingface/transformersを使って分散表現を獲得する。
実装と結果
大きく以下の工程で実装を行った。
- 単語分割
- 単語に番号を振る
- モデルの入力形式に変換(テンソル化)
- モデル準備と入力
- 出力間(雨と雨、雨と飴、飴と飴)の類似度を算出
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 |
おわりに
期待とは異なる結果となってしまった…
このやり方があっているかどうかもわからないので、わかる方教えて欲しいです!