レコメンドシステムの実装〜TF-IDFを使って映画の概要から類似度を求めてみました〜

# はじめに
レコメンドシステムのコンテンツベースフィルタリングを、TF-IDFを用いて実装してみます。<a href="https://qiita.com/k-oto/items/65c8b42d0229f7bee19f">前回の記事</a>では、One-Hot Encodingを用いてアニメのジャンルデータから類似度を計算しています。良ければこちらもご覧ください。
<br>
今回は以下のような実装を行います。

1. 映画の概要（文章）をTF-IDFでベクトル化
2. コサイン類似度を使って類似度を計算
3. 類似度の高いアイテムをレコメンド

scikit-learnを使って簡単に実装していきます。


# TF-IDF
TF-IDFは<a href="https://qiita.com/k-oto/items/65c8b42d0229f7bee19f">前回</a>One-Hot Encodingで行ったようなアイテムのベクトル化の手法の一つです。今回はアイテムの概要（文章のもの）をベクトル化するため、TF-IDFを使ってみます。

TF-IDFは<b>TF（Term Frequency）</b>と<b>IDF（Inverse Document Frequency）</b>の積で求められます。以下の式で表されますが、単語がレアなほどその文章の特徴を表す際にその単語の重要度が上がるというものです。
$$IDF = \log \frac{全文章数}{単語Xを含む文章数}$$$$TF = \frac{文章Aにおける単語Xの出現頻度}{文章Aにおける全単語の出現頻度の和}$$
$$TFIDF = TF \cdot IDF$$

<a href="https://gotutiyan.hatenablog.com/entry/2020/09/10/181919">こちらの記事</a>がTF-IDFに関して詳しく書かれていたので紹介しておきます。

# 実装
今回は<a href="https://www.kaggle.com/rounakbanik/the-movies-dataset?select=movies_metadata.csv">こちらのデータ</a>を使って実装します。まずはデータを確認します。
（カラムが多いので、今回使うカラムだけ取り出します。）

```python:コード
import pandas as pd
import numpy as np

#データの読み込み
movies = pd.read_csv("movies_metadata.csv")

#必要なカラムだけ取り出す
movies = movies[['id', 'original_title', 'overview']]

#データの長さを確認
print('データ数:',len(movies.id))

movies.head()
```
```python:実行結果
データ数: 45466

	id	  original_title	               overview
0	862	    Toy Story	                Led by Woody, Andys toys live happily in his ...
1	8844	Jumanji    	                When siblings Judy and Peter discover an encha...  
2	15602	Grumpier Old Men	        A family wedding reignites the ancient feud be...  
3	31357	Waiting to Exhale	        Cheated on, mistreated and stepped on, the wom...
4	11862	Father of the Bride Part II	Just when George Banks has recovered from his ...
```
overviewカラム（概要）に欠損値があるので、欠損値を含む行を削除します。今回はTF-IDFを試すのが目的なので、処理が雑ですがご了承ください。

```python:コード
#欠損値の確認
movies.isnull().sum()

#欠損値のある行を削除
movies = movies.dropna(how='any')
print('データ数：',len(movies.id))
```

```python:実行結果
#欠損値
id                  0
original_title      0
overview          954
dtype: int64


データ数: 44512
```

次は実際にTF-IDFでアイテムをベクトル化します。TF-IDFの計算はscikit-learnのTfidfVectorizerを使って簡単に実装します。

```python:コード
#overview（概要）のカラムをTF-IDFでベクトル化
from sklearn.feature_extraction.text import TfidfVectorizer

tf = TfidfVectorizer()
tfidf_matrix = tf.fit_transform(movies['overview'])

#作成した行列のサイズを確認
tfidf_matrix.shape
```

```python:実行結果
(44512, 76132)
```

映画のデータ数が44512で、出現する単語数が76132単語と確認できます。
今回は類似度行列もscikit-learnを使って求めます。コサイン類似度の説明は<a href="https://qiita.com/k-oto/items/65c8b42d0229f7bee19f">前回の記事</a>で書いたので省略します。

```python:コード
#類似度行列を作成
from sklearn.metrics.pairwise import pairwise_distances

cosine_sim = 1 - pairwise_distances(tfidf_matrix, metric = 'cosine')
cosine_sim
```

対角成分が1の類似度行列ができます。

```python:実行結果
[[1.        , 0.0306972 , 0.01283222, ..., 0.00942304, 0.03492933, 0.01737238],
 [0.0306972 , 1.        , 0.05674315, ..., 0.00944854, 0.06448034, 0.03307954],
 [0.01283222, 0.05674315, 1.        , ..., 0.01271578, 0.05854697, 0.02767532],
 ...,
 [0.00942304, 0.00944854, 0.01271578, ..., 1.        , 0.02566797, 0.01480498],
 [0.03492933, 0.06448034, 0.05854697, ..., 0.02566797, 1.        , 0.0590926 ],
 [0.01737238, 0.03307954, 0.02767532, ..., 0.01480498, 0.0590926 , 1.        ]]
```

このままだとどの映画がどの行にあるか分かりづらいので、indexと映画のidの対応表をキーバリュー型で作成します。そして、映画のidから類似度の高いアイテムを検索します。ここでは、Toy Story（id:862）と類似度の高いアイテムを抽出します。

```python:コード
#indexと映画のidの対応表を作成
itemindex = dict()
for num, item_id in enumerate(movies.id):
    itemindex[item_id] = num

#映画のid（Toy Storyなので862）を指定してindexを検索、row_numに格納
row_num = itemindex['862']

#類似度の高いアイテム（トップ10）の行数をtop10_indexに格納
top10_index = np.argsort(sim_mat[row_num])[::-1][1:11]
top10_index

#top10_indexに格納したindexと対応する映画のidを検索
rec_id = list()
for search_index in top10_index:
    for id, index in itemindex.items():
        if index == search_index:
            rec_id.append(id)
rec_id
```
```python:実行結果
['10193', '863', '6957', '82424', '92848', '181801', '364123', '250434', '42816', '355984']
```

このidの映画は以下の通りです。

       |id    |	original_title                             |overview                           
|----  | ---- |-------------------------------------       |---------------------------------------
|2997  |863   |Toy Story 2                                 |Andy heads off to Cowboy Camp, leaving his toy...
|8327  |42816 |The Champ                                   |Dink Purcell loves his alcoholic father, ex-he...
|10301 |6957  |	The 40 Year Old Virgin                     |Andy Stitzer has a pleasant life with a nice a...
|15348 |10193 |Toy Story 3                                 |Woody, Buzz, and the rest of Andy's toys haven...
|23843 |92848 |Andy Hardy's Blonde Trouble                 |Andy is going to Wainwright College as did his...
|24523 |82424 |Small Fry                                   |A fast food restaurant mini variant of Buzz fo...
|29202 |181801|Hot Splash                                  |Matt and Woody's summer in Cocoa Beach is goin...
|38476 |250434|Superstar: The Life and Times of Andy Warhol|Documentary portrait of Andy Warhol.
|42721 |355984|Andy Peters: Exclamation Mark Question Point|Exclamation Mark Question Point is the debut s...
|43427 |364123 |Andy Kaufman Plays Carnegie Hall           |Andy Kaufman's legendary sold-out Carnegie Hal...

# 補足
ちなみに2つのアイテム間の類似度を求めるだけなら、scikit-learnのcosine_similarityを使うと便利です。

```python:コード
from sklearn.metrics.pairwise import cosine_similarity
print(cosine_similarity(tfidf_matrix[0:1], tfidf_matrix)[0,1])
```

上で求めたTF-IDFの処理をした行列で0行目と1行目（Toy StoryとJumanji）のコサイン類似度を求めると、上で計算した類似度行列の値と一致していることがわかります。

```python:実行結果
0.0306972031053245
```

# 最後に
映画の概要（文章）を特徴ベクトルにし、Toy Storyと類似度の高い映画Top10を求めてみました。Toy Story2,3やSmall Fryのように似ている映画もあるものの、The 40 Year Old VirginやAndy Kaufman Plays Carnegie Hallなどあまり似ていない映画もありました（おそらく登場人物の名前がAndyでヒットしたんだと思います・・・）。
映画や本の場合は純粋にジャンルで類似度を求めた方が精度が高いかもしれません。

今回TF-IDFを用いて文章の類似度を計算してみて、精度に関して詳しく勉強したいと感じました。