はじめに
レコメンドシステムのコンテンツベースフィルタリングを、TF-IDFを用いて実装してみます。前回の記事では、One-Hot Encodingを用いてアニメのジャンルデータから類似度を計算しています。良ければこちらもご覧ください。
今回は以下のような実装を行います。
- 映画の概要(文章)をTF-IDFでベクトル化
- コサイン類似度を使って類似度を計算
- 類似度の高いアイテムをレコメンド
scikit-learnを使って簡単に実装していきます。
TF-IDF
TF-IDFは前回One-Hot Encodingで行ったようなアイテムのベクトル化の手法の一つです。今回はアイテムの概要(文章のもの)をベクトル化するため、TF-IDFを使ってみます。
TF-IDFはTF(Term Frequency)とIDF(Inverse Document Frequency)の積で求められます。以下の式で表されますが、単語がレアなほどその文章の特徴を表す際にその単語の重要度が上がるというものです。
$$IDF = \log \frac{全文章数}{単語Xを含む文章数}$$$$TF = \frac{文章Aにおける単語Xの出現頻度}{文章Aにおける全単語の出現頻度の和}$$
$$TFIDF = TF \cdot IDF$$
こちらの記事がTF-IDFに関して詳しく書かれていたので紹介しておきます。
実装
今回はこちらのデータを使って実装します。まずはデータを確認します。
(カラムが多いので、今回使うカラムだけ取り出します。)
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()
データ数: 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を試すのが目的なので、処理が雑ですがご了承ください。
#欠損値の確認
movies.isnull().sum()
#欠損値のある行を削除
movies = movies.dropna(how='any')
print('データ数:',len(movies.id))
#欠損値
id 0
original_title 0
overview 954
dtype: int64
データ数: 44512
次は実際にTF-IDFでアイテムをベクトル化します。TF-IDFの計算はscikit-learnのTfidfVectorizerを使って簡単に実装します。
#overview(概要)のカラムをTF-IDFでベクトル化
from sklearn.feature_extraction.text import TfidfVectorizer
tf = TfidfVectorizer()
tfidf_matrix = tf.fit_transform(movies['overview'])
#作成した行列のサイズを確認
tfidf_matrix.shape
(44512, 76132)
映画のデータ数が44512で、出現する単語数が76132単語と確認できます。
今回は類似度行列もscikit-learnを使って求めます。コサイン類似度の説明は前回の記事で書いたので省略します。
#類似度行列を作成
from sklearn.metrics.pairwise import pairwise_distances
cosine_sim = 1 - pairwise_distances(tfidf_matrix, metric = 'cosine')
cosine_sim
対角成分が1の類似度行列ができます。
[[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)と類似度の高いアイテムを抽出します。
#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
['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を使うと便利です。
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)のコサイン類似度を求めると、上で計算した類似度行列の値と一致していることがわかります。
0.0306972031053245
最後に
映画の概要(文章)を特徴ベクトルにし、Toy Storyと類似度の高い映画Top10を求めてみました。Toy Story2,3やSmall Fryのように似ている映画もあるものの、The 40 Year Old VirginやAndy Kaufman Plays Carnegie Hallなどあまり似ていない映画もありました(おそらく登場人物の名前がAndyでヒットしたんだと思います・・・)。
映画や本の場合は純粋にジャンルで類似度を求めた方が精度が高いかもしれません。
今回TF-IDFを用いて文章の類似度を計算してみて、精度に関して詳しく勉強したいと感じました。