諸々

信号処理・機械学習に興味を持っているエンジニア見習いのメモです。技術的なことから趣味のことを書いていきます。

素人がBERT触ってみた

はじめに

NLPド素人の私が、少しBERTに興味が湧いたので動かしてみました。(最近物忘れが酷く、備忘録代わりです...)

今回は日本語版BERTを使って、似たような意味合いの文の文ベクトル・単語ベクトルを算出して確認していきます。

コードはこちら

この記事の最後に、最近Bandai Namco Research様が公開されたDistilBERTを動かしてみた結果も記載しております。ただ、こちら動かしかたが悪かったのか期待する結果が得られませんでした...(もし何か間違っているようでしたら、教えていただけると幸いです)

実施環境

  • MacOS Mojave 10.14.5
  • Anaconda3-5.0.0

環境構築

$conda create -n {env_name} python=3.7
$source activate {env_name}
$pip install torch torchvision
$pip install transformers==2.4.1

source activateのときにPyenv関係で引っかかる場合は、pyenv which activateactivateのパスを確認して、それを叩くするようにすればOK。

transformers==2.4.1は、バージョン指定しないとインストールできなかったため、公式リポジトリのIssueで書いてあったバージョン(2.4.1)を指定しています。

日本語版BERTをとりあえず動かす

transformersの公式サンプルに沿って、文書を推論させてみます。

まずは事前準備として、必要な関数を作ります。

import torch
import numpy as np
from transformers.tokenization_bert_japanese import BertJapaneseTokenizer
from transformers import BertModel

# 日本語トークナイザ
tokenizer = BertJapaneseTokenizer.from_pretrained('bert-base-japanese')
# 事前学習済みBert
model = BertModel.from_pretrained('bert-base-japanese')

def cos(x1, x2):
    """
    引数の2つベクトルの内積を計算する
    """
    return np.dot(x1, x2) / (np.linalg.norm(x1) * np.linalg.norm(x2))

def create_vec(text):
    """ 入力されたテキストを推論する """
    input_batch = [text]

    # トークナイズでテキストを分割する
    encoded_data = tokenizer.batch_encode_plus(
        input_batch, pad_to_max_length=True, add_special_tokens=True)

    # 単語ベクトルをIDに変換する
    input_ids = torch.tensor(encoded_data["input_ids"])

    # 推論
    with torch.no_grad():
        outputs = model(input_ids)

    # 最終層の情報と、分割したトークンを返す
    last_hidden_states = outputs[0]
    return last_hidden_states.detach().numpy(), tokenizer.convert_ids_to_tokens(input_ids[0].tolist())

準備ができたら、実際に試してみます!

text = "私は明日から自宅勤務です。腰を傷めないようにお高い椅子を買おうと思います"
last_hidden_states, token = create_vec(text)
print(last_hidden_sates.shape)
> (1, 27, 765)

ちゃんと計算してくれました!(バッチサイズ, 文字数, 特徴数)みたいな感じで返してくれるみたいですね。 (文字数には、[CLS], [SEP]が文頭と文末に追加されているっぽい。)

last_hidden_states[:, 0, :]  # [CLS]のベクトル
last_hidden_states[:, -1, :] # [SEP]のベクトル

文ベクトルを比較してみる

次に、文ベクトルがどうなるかを試してみようかと思います。

ここで比較する文書は、

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンで椅子を買ってリモートワーク環境を作る"

この3つです!

text1とtext2, text1とtext3, text2とtext3の文書ベクトルを比較してみます。

個人的希望では、text1とtext2の類似度がtext1とtext3より大きくなってくれるとうれしいなぁと思います。

ここでの文ベクトルは、

https://qiita.com/ichiroex/items/6e305a5d5bed7d715c2f

こちらを参考にさせて頂いて、[CLS]のベクトルを使います!

具体的には、

# batch_size=3として計算できるが、今回はわけて計算しています。
vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
vec3, _ = create_vec(text3)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, 0, :], vec3[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, 0, :], vec2[0, 0, :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.72820
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.71819
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.68089

text1とtext2の類似度が一番高い結果になりました!

「IT企業ではないアマゾン...」といった否定形のように書けば、「南米のアマゾン...」と同じ意味だと捉えれくれたのでしょうか?(そこまで類似度に差はないけれど...)

ただ、text2とtext3の類似度は他に比べ低くなっているので、そのあたりはちゃんと認識してくれてるっぽい...?

text3の文章をちょこっと変えたら、類似度逆転しました...この辺りはまだまだ難しいのでしょうかね?

ちなみに、全然関係ない文との関係は?

上ではアマゾン系統の文章を使いましたが、全く関係ない文章との比較も試しました!

text1 = "アマゾンで椅子を買ってリモートワーク環境を作る"
text2 = "アマゾンのクラウドを利用してシステムを構築する"
except_text = "明日の晩御飯は豚の生姜焼きともやし炒めにする"

vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
ex_vec, _ = create_vec(except_text)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], except_text[:5], cos(vec1[0, 0, :], ex_vec[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(except_text[:5], text2[:5], cos(ex_vec[0, 0, :], vec2[0, 0, :])))

> 「アマゾンで...」と「アマゾンの...」の文ベクトル類似度:0.81488
> 「アマゾンで...」と「明日の晩御...」の文ベクトル類似度:0.67456
> 「明日の晩御...」と「アマゾンの...」の文ベクトル類似度:0.65609

割とはっきり結果が出るもんですねー。

特定単語ベクトルを比較してみる

お次は、文章の中の特定の単語のベクトルに違いが出るのか試してみます!

ここで使う文章も、

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンのクラウドサービスをよく利用している"

この3つです!

そして、特定の単語は「アマゾン」にしてみます。個人的には、text1とtext2が一番類似度高くなってほしいですね!

vec1, token1 = create_vec(text1)
vec2, token2 = create_vec(text2)
vec3, token3 = create_vec(text3)

# 対象の単語
TARGET = "アマゾン"

# 単語ベクトルの類似度を出力
# TARGETのインデックスを取得し、それに対応するベクトルで計算する
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, token1.index(TARGET), :], vec2[0, token2.index(TARGET), :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, token1.index(TARGET), :], vec3[0, token3.index(TARGET), :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, token3.index(TARGET), :], vec2[0, token2.index(TARGET), :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.65961
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.71137
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.68076

うーん...難しかったのでしょうか、なかなか期待通りにはなってくれないです。

でも、ただただWord2Vec使うよりかは断然BERT使ったほうが良さげですね!

おわりに

超初心者がBERTを単純に動かしてみました!

Word2Vecに比べると、ちゃんと文章を理解してくれているのかなぁといった結果が見て取れました。

案件で少し使ってみようかと検討していたので、今回試すことができてよかったです!

余談:DistilBERTも少し動かしてみた

公式クイックスタート読んで環境構築して、以下のようなコードを動かしてみました。

間違いなどありましたら、ご指摘お願いいたします。

from transformers import AutoModel, AutoTokenizer, AutoConfig
import torch
import numpy as np

tokenizer = AutoTokenizer.from_pretrained('bert-base-japanese-whole-word-masking')
model = AutoModel.from_pretrained("./")

def cos(x1, x2):
    return np.dot(x1, x2) / (np.linalg.norm(x1) * np.linalg.norm(x2))

def create_vec(text):
    input_batch = [text]
    encoded_data = tokenizer.batch_encode_plus(
        input_batch, pad_to_max_length=True, add_special_tokens=True)
    input_ids = torch.tensor(encoded_data["input_ids"])
    with torch.no_grad():
        outputs = model(input_ids)
    last_hidden_states = outputs[0]
    return last_hidden_states.detach().numpy(), outputs[1]

text1 = "IT企業ではないアマゾンにはよく釣りに行く"
text2 = "南米のアマゾン川は危険な生き物がたくさんいる"
text3 = "アマゾンで椅子を買ってリモートワーク環境を作る"

vec1, _ = create_vec(text1)
vec2, _ = create_vec(text2)
vec3, _ = create_vec(text3)

# 文ベクトルの類似度を出力
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text2[:5], cos(vec1[0, 0, :], vec2[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text1[:5], text3[:5], cos(vec1[0, 0, :], vec3[0, 0, :])))
print("「{}...」と「{}...」の文ベクトル類似度:{:.5f}".format(text3[:5], text2[:5], cos(vec3[0, 0, :], vec2[0, 0, :])))

> 「IT企業で...」と「南米のアマ...」の文ベクトル類似度:0.99377
> 「IT企業で...」と「アマゾンで...」の文ベクトル類似度:0.99441
> 「アマゾンで...」と「南米のアマ...」の文ベクトル類似度:0.99102

BERTに比べると類似度に差異があまりないですね...(やっぱりどこかミスってるのか?)