はじめに
レコメンドシステムのうち、アイテムの特徴のみでレコメンドを行うコンテンツベースフィルタリングを実装してみようと思います。(レコメンドシステムの種類に関してはこちらの記事をご覧ください。)
コンテンツベースフィルタリング
コンテンツベースフィルタリングとは、アイテムの特徴を元に推薦を行う手法です。ユーザーの閲覧/購入履歴のアイテムと類似性の高いアイテムを計算し、提示します。
実際に以下のような流れで実装します。
- アイテムの特徴ベクトルを抽出
- 類似度の計算
- 類似度の高いアイテムをレコメンド
アイテムのベクトル化
類似度を計算するために、まずはアイテムの特徴(単語や文章)を特徴ベクトルに変換します。ベクトル化の方法はOne-Hot EncodingやTF-IDFなどいくつかありますが、今回はアイテムの特徴が単語のデータを使用するのでOne-Hot表現にします。
類似度の計算
アイテムのベクトル化が行えたら次は類似度の計算です。類似度の計算もいくつか方法がありますが、今回はよく用いられるコサイン類似度で計算します。
$$
\cos({x}, {y}) = \frac{{x} \cdot {y}}{|{x}| |{y}|}
$$
例えばアイテム$x,y$の類似度を計算する場合、$x$と$y$の特徴ベクトルを以下とすると、
$$x=(0,1,1,0,0,1,0,1)$$$$y=(0,1,0,0,0,1,0,0)$$コサイン類似度はこのように計算できます。
$$x \cdot y=1+1=2$$$$|{x}|=\sqrt{1+1+1+1}=2$$$$|{y}|=\sqrt{1+1+1}=\sqrt{3}$$$$cos(x,y)=\frac{2}{|{2}| |{\sqrt{3}}|}=0.57735$$
実装
それでは実際にkaggleのコンペのデータを使用して実装してみます。まずはデータを確認します。(今回はgenreカラムでアイテムの特徴を計算するため、typeやratingカラムは見やすいように削除します。カラムを消さずに実行しても問題ないです。)
import pandas as pd
import numpy as np
#データの読み込み
anime_data = pd.read_csv("anime.csv")
#データの長さを確認
print("データ数:", len(anime_data.anime_id))
#使わないカラムを削除
anime_data = anime_data.drop(columns = ['type', 'episodes', 'rating', 'members'])
#データの中身を確認
anime_data.head()
データは以下の通りです。
データ数: 12294
anime_id name genre
0 32281 Kimi no Na wa. Drama, Romance, School, Supernatural
1 5114 Fullmetal Alchemist: Brotherhood Action, Adventure, Drama, Fantasy, Magic, Mili...
2 28977 Gintama Action, Comedy, Historical, Parody, Samurai, S...
3 9253 Steins;Gate SciFi, Thriller
4 9969 Gintama039; Action, Comedy, Historical, Parody, Samurai, S...
次にアイテムのベクトル化を行います。今回はジャンルのデータが単語で入っているので、One-Hot Encodingで特徴ベクトルにします。anime_dataのgenreカラムにはジャンル名がカンマ区切りで入っているので、以下のコードでジャンル名のカラムを作成します。genre_colにgenreを追加していきますが、この時set()を使っているので重複する要素は取り除かれます。
genres = anime_data['genre'].map(lambda x: x.split(',')).to_list()
genre_col = list()
for i in genres:
genre_col.extend(i)
genre_col = list(set(genre_col))
#ジャンル名のカラムの長さを確認
print(len(genre_col)
#ジャンル名のカラムの長さ
83
作成したジャンル名のカラムを使い、ジャンル要素のOne-Hot表現にします。row_listで各行をリスト化し、rowsに追加していきます。最後にDataFrameを作成し、genre_dfに格納します。
#One-Hot Encoding
rows = list()
for index, row in enumerate(genres):
row_list = np.array([0] * len(genre_col))
index_list = [genre_col.index(item) for item in row]
row_list[index_list] = 1
rows.append(list(row_list))
genre_df = pd.DataFrame(rows, columns = genre_col)
one_hot_data = pd.concat([anime_data, genre_df], axis= 1)
分かりやすいようにアニメのidや名前を結合したone_hot_dataを出力するとこんな感じです。
anime_id | name | Kids | Game | Psychological | Fantasy | Space | School | |
---|---|---|---|---|---|---|---|---|
0 | 32281 | Kimi no Na wa. | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 5114 | Fullmetal Alchemist | 0 | 0 | 1 | 0 | 0 | 0 |
2 | 28977 | Gintama | 0 | 0 | 0 | 0 | 0 | 0 |
(12294×86) |
この特徴ベクトルを使って、コサイン類似度の計算をします。以下のコードで類似度行列を作成します。
#one-hot表現の部分で配列を作成
item_vectors = np.array(one_hot_data[genre_col])
#行毎のベクトルノルム
norm = np.matrix(np.linalg.norm(item_vectors, axis=1))
#コサイン類似度の式を使って類似度行列を作成
sim_mat = np.array(np.dot(item_vectors, item_vectors.T)/np.dot(norm.T, norm))
このsim_matのままだと何行目がどのアニメか分かりづらいので、anime_idとindexの対応表をキーバリュー型で作成します。
itemindex = dict()
for num, item_id in enumerate(one_hot_data.anime_id):
itemindex[item_id] = num
itemindex
{32281: 0,
5114: 1,
28977: 2,
9253: 3,
9969: 4,
32935: 5,
11061: 6,
実際に類似度行列から類似度の高いアイテムを取り出してみます。ここでは『君の名は(anime_id:32281)』と類似度の高いアイテムを10個表示させます。
#anime_idを指定してindexを検索、row_numに格納
row_num = itemindex[32281]
#類似度行列のrow_numの列でのトップ10を抽出
top10_index = np.argsort(sim_mat[row_num])[::-1][1:11]
top10_index
array([6394, 5805, 208, 1959, 504, 1494, 2300, 1201, 5127, 1436])
top10_indexのindexと対応するanime_idを検索します。
rec_id = list()
for search_index in top10_index:
for anime_id, index in itemindex.items():
if index == search_index:
rec_id.append(anime_id)
rec_id
[546, 547, 28725, 713, 6351, 20903, 12175, 10067, 1607, 8481]
得られたanime_idから類似度の高いアイテムを表示させてみます。
anime_data.query("anime_id == [546, 547, 28725, 713, 6351, 20903, 12175, 10067, 1607, 8481] ")
anime_id | name | genre | |
---|---|---|---|
208 | 28725 | Kokoro ga Sakebitagatterunda. | Drama, Romance, School |
504 | 6351 | Clannad: After Story - Mou Hitotsu no Sekai | Drama, Romance, School |
1201 | 10067 | Angel Beats!: Another Epilogue | Drama, School, Supernatural |
1436 | 8481 | "Bungaku Shoujo" Memoire | Drama, Romance, School |
1494 | 20903 | Harmonie | Drama, School, Supernatural |
1959 | 713 | Air Movie | Drama, Romance, Supernatural |
2300 | 12175 | Koi to Senkyo to Chocolate | Drama, Romance, School |
5127 | 1607 | Venus Versus Virus | Drama, Romance, Supernatural |
5805 | 547 | Wind: A Breath of Heart OVA | Drama, Romance, School, Supernatural |
6394 | 546 | Wind: A Breath of Heart (TV) | Drama, Romance, School, Supernatural |
補足
One-Hot Encodingを今回は自力で行いましたが、Category Encodersを使うとより簡単にできます。こちらの記事が分かりやすかったので紹介させていただきます。
#最後に
アイテムのベクトル化と類似度の計算は、他の方法やコードの書き方があると思うのでまた別のも試してみようと思います。今回はこちらの記事を参考にさせていただきました。レコメンド以外にも分かりやすい記事が多かったです。