BPR vs. SVD vs. WARPの比較
ここでは、BPRやWARPを手軽に試すことができるライブラリlightFMを用いて、前回紹介したSVDと結果を定性的に比較したいと思います。
まずはlightfmをインストールします。また、前回に引き続き、データセット読み込みのためにirspackを用います。
pip install irspack==0.1.18
pip install lightfm==1.16
前回と同じく、Movielens1Mのデータを疎行列に変換します。
from irspack import df_to_sparse
from irspack.dataset import MovieLens1MDataManager
from lightfm import LightFM
import numpy as np
from sklearn.utils.extmath import randomized_svd
data_manager = MovieLens1MDataManager()
rating_df = data_manager.read_interaction()
R, user_ids, item_ids = df_to_sparse(rating_df, "userId", "movieId")
movie_df = data_manager.read_item_info()
接触履歴の行列Rに対して、まずはBPRによる行列分解を実行します。
以下、パラメータの値は後に紹介する検証手続きで決定されています。
bpr = LightFM(
no_components=152,
item_alpha=8.173e-08,
user_alpha=6.137e-05,
loss='bpr',
random_state=0
)
bpr.fit(R, epochs=20)
続いてWARPによる行列分解も実行します。
warp = LightFM(
no_components=227,
item_alpha=2.314e-4,
user_alpha=1.970e-4,
loss='warp',
random_state=0
)
warp.fit(R, epochs=30)
最後に、前回と同じくrandomized_svdによる行列分解も実行します。
svd_item_vectors = randomized_svd(R, n_components=33, random_state=0)[2]
3通りの行列分解はそれぞれ異なった考え方・誤差関数に基づく行列分解の手法であり、ベクトルの次元数も大きく異なりますが、大まかには同じ結果を与えます。このことを、ある映画に似たベクトルを持つ映画はどれか、ということを計算して定性的に確認してみましょう。
vectors = {
'bpr': bpr.get_item_representations()[1],
'warp': warp.get_item_representations()[1],
# SVDの出力は転置が必要
'svd': svd_item_vectors.T
}
def show_similar_items(item_id=None, n_items=5):
if item_id is None:
item_id = np.random.choice(item_ids)
print(f'Query item: {movie_df.title.loc[item_id]}')
for algorithm_type, vector in vectors.items():
# それぞれの映画ベクトルのノルムを計算
vector_norms = (vector ** 2).sum(axis=1) ** .5
# クエリとなる映画のベクトルを取得
movie_index = item_ids.get_loc(item_id)
query_vector = vector[movie_index]
# コサイン類似度を計算
query_vector /= (query_vector ** 2).sum() ** .5
cosine_similarity = vector.dot(query_vector) / vector_norms
# 自分自身は表示されないようにする
cosine_similarity[movie_index] = -np.inf
# Top `n_items` 類似アイテムを表示
print(f'{algorithm_type}:')
recommended_item_ids = item_ids[cosine_similarity.argsort()[::-1][:n_items]]
print(list(movie_df.reindex(recommended_item_ids).title))
上記コードでは、ベクトル\(\vec{v}_1, \vec{v}_2\)の類似度をコサイン類似度 \(\mathrm{sim}(\vec{v_1}, \vec{v_2}) : = \vec{v}_1\cdot \vec{v}_2 / (||\vec{v_1}|| \ ||\vec{v}_2||)\)で定義しています。
試しに、item_id=1に対する結果を見てみましょう。
show_similar_items(item_id=1)
# Query item: Toy Story (1995)
# bpr:
# ['Toy Story 2 (1999)', 'Aladdin (1992)', 'Babe (1995)', "Bug's Life, A (1998)", 'Antz (1998)']
# warp:
# ['Toy Story 2 (1999)', 'Babe (1995)', "Bug's Life, A (1998)", 'Groundhog Day (1993)', 'Aladdin (1992)']
# svd:
# ['Toy Story 2 (1999)', 'Babe (1995)', 'Aladdin (1992)', "Bug's Life, A (1998)", 'Babe: Pig in the City (1998)']
対象の映画の続編や、90年代の子供向け映画が類似したベクトルを持っていると判断されており、妥当な結果と言えるでしょう。item_id=1270などでは、BPR,WARPとSVDで若干異なった結果が出てきます。
show_similar_items(item_id=1270)
# Query item: Back to the Future (1985)
# bpr:
# ['Back to the Future Part II (1989)', 'Big (1988)', "Ferris Bueller's Day Off (1986)", 'Cocoon (1985)', 'Spaceballs (1987)']
# warp:
# ['Back to the Future Part II (1989)', "Ferris Bueller's Day Off (1986)", 'Cocoon (1985)', 'Star Wars: Episode V - The Empire Strikes Back (1980)', 'Big (1988)']
# svd:
# ["Ferris Bueller's Day Off (1986)", 'Breakfast Club, The (1985)', 'E.T. the Extra-Terrestrial (1982)', 'Bull Durham (1988)', 'Star Wars: Episode VI - Return of the Jedi (1983)']
この場合は「バック・トゥー・ザ・フューチャーPart I」に対して「Part II」が近いと判断しているBPR,WARPの方がSVDより良い結果を出しているように思えます。
ただし、前回からの繰り返しになりますが、定性的にどのアルゴリズムが秀でているのかを人間が判断するのはドメイン知識と労力を要する作業であり、連載後半で述べるオフライン検証手続きに沿って定量的にパフォーマンスを評価する必要があるでしょう。
