LoginSignup
4
3

More than 3 years have passed since last update.

レコメンドシステム(コンテンツベースフィルタリング)を実装してみた

Last updated at Posted at 2021-01-04

はじめに

レコメンドシステムのうち、アイテムの特徴のみでレコメンドを行うコンテンツベースフィルタリングを実装してみようと思います。(レコメンドシステムの種類に関してはこちらの記事をご覧ください。)

コンテンツベースフィルタリング

コンテンツベースフィルタリングとは、アイテムの特徴を元に推薦を行う手法です。ユーザーの閲覧/購入履歴のアイテムと類似性の高いアイテムを計算し、提示します。


実際に以下のような流れで実装します。

  1. アイテムの特徴ベクトルを抽出
  2. 類似度の計算
  3. 類似度の高いアイテムをレコメンド

アイテムのベクトル化

類似度を計算するために、まずはアイテムの特徴(単語や文章)を特徴ベクトルに変換します。ベクトル化の方法は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を使うとより簡単にできます。こちらの記事が分かりやすかったので紹介させていただきます。

最後に

アイテムのベクトル化と類似度の計算は、他の方法やコードの書き方があると思うのでまた別のも試してみようと思います。今回はこちらの記事を参考にさせていただきました。レコメンド以外にも分かりやすい記事が多かったです。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3