sentence-bertを使って映画レビューの類似度を比較してみた

# 今回使用したモデル
こちらの日本語用Sentence-BERTモデル（バージョン2）です。

https://huggingface.co/sonoisa/sentence-bert-base-ja-mean-tokens-v2


# 参考記事

主にこちらの記事を参考にさせていただきました。

https://qiita.com/sonoisa/items/1df94d0a98cd4f209051

# 内容
映画レビューを文ベクトル化し、特定の文章との類似度を計算できるようにしました。
今回は「ゴッドファーザー」「スパイダーマン」「映画クレヨンしんちゃん　嵐を呼ぶモーレツ！オトナ帝国の逆襲」の3本の映画を対象に、Yahoo映画から★5のレビューを3件ずつ取得しています（手動で）。
取得したレビューを文ベクトル化し、さらに3件のレビューの平均ベクトルを計算しています。この平均ベクトルが、それぞれの映画の平均的なレビューのベクトルである、と考えています。

# 環境
Google Colabを使用しています。

# 実際のコード

### ライブラリインストール

まずはBERTとpytorch等をインストールします。

```python
!pip install -q transformers==4.7.0 fugashi ipadic
from transformers import BertJapaneseTokenizer, BertModel
import torch
```

### モデルの定義

SentenceBertJapaneseクラスを定義します。これは[こちら](https://qiita.com/sonoisa/items/1df94d0a98cd4f209051)で紹介されていたクラスほぼそのままですが、`batch_encode_plus`の部分を少し修正しています。

```python
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)
```

### 事前学習モデルのロード

日本語事前学習モデルをロードします。

```python
model = SentenceBertJapanese("sonoisa/sentence-bert-base-ja-mean-tokens")
```

### コサイン類似度を計算する関数を定義

後ほど使うので先に定義しておきます。

```python
import numpy as np

def cos_sim(v1, v2):
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
```

### レビューの読み込み

Yahoo映画のレビューから、役立ち度順に★5のレビューを拝借してきます。みなさんかなり気合の入ったレビューで文章が非常に長いので、ここでは省略して記載します。

```python
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
```

### 類似度を比較する

適当に文章を作って、その文章との類似度を比較してみます。まずは「笑って泣ける映画です」という文章に対して、どの映画のレビューが最も類似度が高いか？を比較します。

```python
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])}')
```

このようになりました。`_平均`というのが各映画平均ベクトルです。笑って泣ける映画として名高い「映画クレヨンしんちゃん　嵐を呼ぶモーレツ！オトナ帝国の逆襲」が最も高い数値が出ていますね。感覚的にも十分あっています。

```bash
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
```

他の文章も試してみます。
「スーパーヒーローが出てきて敵を退治する姿がかっこいい。」という文章で類似度を計算してみます。

```bash
comment = model.encode(["スーパーヒーローが出てきて敵を退治する姿がかっこいい。"])
```

やはりスパイダーマンのスコアが高いですね！

```bash
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
```

# 感想

類似度自体に大きな差はついていませんが、大小は精度よく計算ができているようです。
ランキング作成などに使えそうです。