LoginSignup
1
1

More than 3 years have passed since last update.

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

Posted at

背景

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

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

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

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

計画

対象データ

こちらの記事のELMoの実装を参考に以下のようなコーパスを使ってチェックする。

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

BERTモデル

BERTはhuggingface/transformersを使って分散表現を獲得する。

実装と結果

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

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

おわりに

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

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1