今回使用したモデル
こちらの日本語用Sentence-BERTモデル(バージョン2)です。
参考記事
主にこちらの記事を参考にさせていただきました。
内容
映画レビューを文ベクトル化し、特定の文章との類似度を計算できるようにしました。
今回は「ゴッドファーザー」「スパイダーマン」「映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲」の3本の映画を対象に、Yahoo映画から★5のレビューを3件ずつ取得しています(手動で)。
取得したレビューを文ベクトル化し、さらに3件のレビューの平均ベクトルを計算しています。この平均ベクトルが、それぞれの映画の平均的なレビューのベクトルである、と考えています。
環境
Google Colabを使用しています。
実際のコード
ライブラリインストール
まずはBERTとpytorch等をインストールします。
!pip install -q transformers==4.7.0 fugashi ipadic
from transformers import BertJapaneseTokenizer, BertModel
import torch
モデルの定義
SentenceBertJapaneseクラスを定義します。これはこちらで紹介されていたクラスほぼそのままですが、batch_encode_plus
の部分を少し修正しています。
class SentenceBertJapanese:
def __init__(self, model_name_or_path, device=None):
self.tokenizer = BertJapaneseTokenizer.from_pretrained(model_name_or_path)
self.model = BertModel.from_pretrained(model_name_or_path)
self.model.eval()
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
self.device = torch.device(device)
self.model.to(device)
def _mean_pooling(self, model_output, attention_mask):
token_embeddings = model_output[0] #First element of model_output contains all token embeddings
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)
@torch.no_grad()
def encode(self, sentences, batch_size=8):
all_embeddings = []
iterator = range(0, len(sentences), batch_size)
for batch_idx in iterator:
batch = sentences[batch_idx:batch_idx + batch_size]
# encoded_input = self.tokenizer.batch_encode_plus(batch, padding="longest",
# truncation=True, return_tensors="pt").to(self.device)
encoded_input = self.tokenizer.batch_encode_plus(batch, padding="max_length", max_length=512,
truncation=True, return_tensors="pt").to(self.device)
model_output = self.model(**encoded_input)
sentence_embeddings = self._mean_pooling(model_output, encoded_input["attention_mask"]).to('cpu')
all_embeddings.extend(sentence_embeddings)
# return torch.stack(all_embeddings).numpy()
return torch.stack(all_embeddings)
事前学習モデルのロード
日本語事前学習モデルをロードします。
model = SentenceBertJapanese("sonoisa/sentence-bert-base-ja-mean-tokens")
コサイン類似度を計算する関数を定義
後ほど使うので先に定義しておきます。
import numpy as np
def cos_sim(v1, v2):
return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
レビューの読み込み
Yahoo映画のレビューから、役立ち度順に★5のレビューを拝借してきます。みなさんかなり気合の入ったレビューで文章が非常に長いので、ここでは省略して記載します。
movie_a = "ゴッドファーザー"
movie_a_comment_1 = "在米イタリア人による,..........皮肉な話だ。"
movie_a_comment_2 = "華やかな音楽も届かない書斎では、.....感じるからだろう。"
movie_a_comment_3 = "まず、こういう映画を毛嫌いする方もいると思いますし、.....と個人的に思っています。"
movie_a_vectors = model.encode([movie_a_comment_1, movie_a_comment_2, movie_a_comment_3])
movie_a_avg_vector = (movie_a_vectors[0].numpy() + movie_a_vectors[1].numpy() + movie_a_vectors[2].numpy()) / 3
movie_b = "スパイダーマン"
movie_b_comment_1 = "映画「スパイダーマン」は、他のアメコミ映画とは明らか....のは俺だけではあるまい。"
movie_b_comment_2 = "この映画の魅力…それは.....過ぎないのだから。"
movie_b_comment_3 = "※注意。....、いやなんとなく。"
movie_b_vectors = model.encode([movie_b_comment_1, movie_b_comment_2, movie_b_comment_3])
movie_b_avg_vector = (movie_b_vectors[0].numpy() + movie_b_vectors[1].numpy() + movie_b_vectors[2].numpy()) / 3
movie_c = "映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲"
movie_c_comment_1 = "子供向けじゃあない....育った大人なんて、ゾっとする。"
movie_c_comment_2 = "今まで観なくて....大事なのかもしれませんね"
movie_c_comment_3 = "ノスタルジーって、....笑えて泣けた。心から!"
movie_c_vectors = model.encode([movie_c_comment_1, movie_c_comment_2, movie_c_comment_3])
movie_c_avg_vector = (movie_c_vectors[0].numpy() + movie_c_vectors[1].numpy() + movie_c_vectors[2].numpy()) / 3
類似度を比較する
適当に文章を作って、その文章との類似度を比較してみます。まずは「笑って泣ける映画です」という文章に対して、どの映画のレビューが最も類似度が高いか?を比較します。
comment = model.encode(["笑って泣ける映画です"])
print(f'vs{movie_a}_平均:{cos_sim(movie_a_avg_vector, comment[0])}')
print(f'vs{movie_a}_コメント1:{cos_sim(movie_a_vectors[0].numpy(), comment[0])}')
print(f'vs{movie_a}_コメント2:{cos_sim(movie_a_vectors[1].numpy(), comment[0])}')
print(f'vs{movie_a}_コメント3:{cos_sim(movie_a_vectors[2].numpy(), comment[0])}')
print(f'vs{movie_b}_平均:{cos_sim(movie_b_avg_vector, comment[0])}')
print(f'vs{movie_b}_コメント1:{cos_sim(movie_b_vectors[0].numpy(), comment[0])}')
print(f'vs{movie_b}_コメント2:{cos_sim(movie_b_vectors[1].numpy(), comment[0])}')
print(f'vs{movie_b}_コメント3:{cos_sim(movie_b_vectors[2].numpy(), comment[0])}')
print(f'vs{movie_c}_平均:{cos_sim(movie_c_avg_vector, comment[0])}')
print(f'vs{movie_c}_コメント1:{cos_sim(movie_c_vectors[0].numpy(), comment[0])}')
print(f'vs{movie_c}_コメント2:{cos_sim(movie_c_vectors[1].numpy(), comment[0])}')
print(f'vs{movie_c}_コメント3:{cos_sim(movie_c_vectors[2].numpy(), comment[0])}')
このようになりました。_平均
というのが各映画平均ベクトルです。笑って泣ける映画として名高い「映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲」が最も高い数値が出ていますね。感覚的にも十分あっています。
vsゴッドファーザー_平均:0.445037305355072
vsゴッドファーザー_コメント1:0.33053019642829895
vsゴッドファーザー_コメント2:0.4115988314151764
vsゴッドファーザー_コメント3:0.4690192937850952
vsスパイダーマン_平均:0.49370622634887695
vsスパイダーマン_コメント1:0.4734647274017334
vsスパイダーマン_コメント2:0.42756545543670654
vsスパイダーマン_コメント3:0.49704039096832275
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_平均:0.582426905632019
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント1:0.4933982193470001
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント2:0.61888587474823
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント3:0.5201992988586426
他の文章も試してみます。
「スーパーヒーローが出てきて敵を退治する姿がかっこいい。」という文章で類似度を計算してみます。
comment = model.encode(["スーパーヒーローが出てきて敵を退治する姿がかっこいい。"])
やはりスパイダーマンのスコアが高いですね!
vsゴッドファーザー_平均:0.4695703089237213
vsゴッドファーザー_コメント1:0.31775474548339844
vsゴッドファーザー_コメント2:0.5198161602020264
vsゴッドファーザー_コメント3:0.4389423429965973
vsスパイダーマン_平均:0.5631412267684937
vsスパイダーマン_コメント1:0.5113078355789185
vsスパイダーマン_コメント2:0.5612474083900452
vsスパイダーマン_コメント3:0.5188660621643066
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_平均:0.3811109960079193
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント1:0.3106297552585602
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント2:0.4306870698928833
vs映画クレヨンしんちゃん 嵐を呼ぶモーレツ!オトナ帝国の逆襲_コメント3:0.3270125687122345
感想
類似度自体に大きな差はついていませんが、大小は精度よく計算ができているようです。
ランキング作成などに使えそうです。