推薦結果の作成
ユーザー/アイテムのベクトル,が求まりましたので、このベクトルを使ってユーザー\(u\)に対して推薦すべきアイテムを選抜します。この計算においては次の点を気を付ける必要があります。
- すでに接触済みのアイテム()を推薦しても意味がない
- まだ接触していないアイテムi()を、接触しそうな順に推薦する必要がある
そのため、計算されたベクトルを使って以下の手順でアイテムを順序付きで推薦します(これ以降で述べる全ての行列分解による方法に共通する手続きとなります)。
- 各アイテムに対してでスコアを計算する
- ユーザーとの接触履歴がまだ存在しないアイテムi()について、の大きい順にアイテムを推薦していく
ここでの「スコア」とは、値の高いアイテムほどユーザーが接触しそうな傾向があることを示す数値のことです。SVDの場合「Rui=0であるがスコア が大きいアイテムの組み合わせ」とは、「本来なら接触履歴があっても(でも)おかしくないが、何かしらの事情でとなっている」と考えられるため、このスコアが高いほど推薦順位を上位に表示したほうが良いことになります(この点は明示的フィードバックにおける行列分解と全く考え方が異なりますのでご注意ください)。
さて、良い推薦結果を作成するためには、は大きすぎても小さすぎてもいけません。これは次の理由によります:
-
が小さすぎる場合
- SVD の近似による誤差が大きくなります。そのため、ユーザー/アイテムが持つ性質をそれぞれのベクトルに十分に反映できない可能性があります。
- 例えば の時には、もも 1 次元なので、は単なる実数同士の掛け算であり、「が正のユーザーにはが大きいアイテムから順に推薦し、負のユーザーには が小さいアイテムから推薦する」という高々2通りの推薦パターンしか存在しなくなり、ユーザーの多様なニーズを反映することができなくなります。
-
が大きすぎる場合
- SVD の近似誤差は小さくなりますが、ユーザー/アイテムのベクトルが接触データに対して過剰に適合してしまうために、となるところでは常にになってしまい、優先順位がつけられず、適切な推薦結果を作ることができなくなります。
そのため、データの状況に合わせて、誤差が大きすぎず、またデータに過剰に適合しないような、バランスの取れたを決定して使う必要があります。
SVDのコード例
それではMovielens 1Mという中規模データセットについて、実際にSVDを用いた推薦結果を表示してみましょう。ここではデータセットの読み込み行列の作成部分について、のちにより詳しく紹介するirspackというライブラリを用います。このライブラリでは、今回説明するSVD/WMFを含む行列分解手法のほか、暗黙的フィードバックのデータを用いた推薦システムに用いる複数のアルゴリズムを実装しており、ベンチマークデータセットの準備や、アルゴリズムの評価のための機能を含んでいます。
コードを利用する前に、事前にirspackをインストールしておく必要があります。
pip install irspack==0.1.18
このirspackを用いて、まずは閲覧履歴を表すテーブル(rating_df)を疎行列に変換します。
import numpy as np from irspack.dataset import MovieLens1MDataManager from irspack.utils import df_to_sparse from sklearn.utils.extmath import randomized_svd # 初回は公式サイトからダウンロードの許可を求められます。 data_manager = MovieLens1MDataManager() rating_df = data_manager.read_interaction() # rating_df は以下のような userId, movieId, rating, 閲覧時刻を表す4つの列からなります。 print(rating_df.head(3)) # userId movieId rating timestamp # 0 1 1193 5 2000-12-31 22:12:40 # 1 1 661 3 2000-12-31 22:35:09 # 2 1 914 3 2000-12-31 22:32:48 # np.uniqueを用いた連載第一回の方法も参照のこと。 # R の 第 i 行は userIdが user_ids[i] のユーザーの行動履歴に相当し、 # 第 j 行は movieIdが movie_ids[i] のアイテムの視聴履歴に相当します。 R, user_ids, movie_ids = df_to_sparse(rating_df, "userId", "movieId") # R は ユーザー数 U=6040, アイテム数 I = 3706の疎行列になります。 print(repr(R)) # <6040x3706 sparse matrix of type '<class 'numpy.float64'>' # with 1000209 stored elements in Compressed Sparse Row format>
データの準備ができましたので、SVDを実行します。ここでは、特徴ベクトルの次元をとする分解を行います。この60は交差検証手続きによって最適な次元として求められたものです(交差検証や最適パラメータの求め方は本連載後半で触れる予定です)。
O, Sigma, QT = randomized_svd(R, n_components=60) P = O * Sigma Q = QT.T
試しに、の最初の行に相当するユーザーに対して推薦結果Top 10を取得してみましょう。
user_index = 0 user_id = user_ids[user_index] score = P[user_index].dot(QT) # ユーザーが既に閲覧済みのものは推薦されないように -np.inf (-∞)を詰める already_watched_indices = R[user_index].nonzero()[1] score[already_watched_indices] = -np.inf # argsortは「値が小さい順にそのインデックスを返す」関数なので、 # 一番後ろから10件をとってくる。 recommendation_indices = score.argsort()[::-1][:10] recommendation_ids = movie_ids[recommendation_indices] print(recommendation_ids)
recommendation_idがuser_index=0(userIdは1)に対する推薦結果に相当します。
このとき、我々は常にの行と実際のユーザーIDを正確に区別しながら物事を進める必要があります。
残念ながらこれでは推薦結果が妥当かは分からないのでmovieIdと映画のメタ情報の関係をみてみましょう。
まず映画のメタデータ情報を読み込みます。
# movie_df は movieId と実際の映画の対応関係を教えてくれます。 movie_df = data_manager.read_item_info() print(movie_df.head(3)) # title genres release_year # 1 Toy Story (1995) Animation|Children's|Comedy 1995 # 2 Jumanji (1995) Adventure|Children's|Fantasy 1995 # 3 Grumpier Old Men (1995) Comedy|Romance 1995
続いて、ランダムにユーザーを持ってきてその閲覧済み映画と推薦結果とを対比してみましょう。
user_index = np.random.randint(0, R.shape[0])
user_id = user_ids[user_index]
score = P[user_index].dot(QT)
already_watched_indices = R[user_index].nonzero()[1]
already_watched_ids = movie_ids[already_watched_indices]
score[already_watched_indices] = -np.inf
recommendation_indices = score.argsort()[::-1][:10]
recommendation_ids = movie_ids[recommendation_indices]
print("History:")
print(movie_df.reindex(already_watched_ids).sample(5))
print("\nRecommendation:")
print(movie_df.reindex(recommendation_ids))
"Drama"をよく見ている人には"Drama"を当てたり、あるいは制作年代が古い映画を好んで見ている人にはやはりそのような映画を推薦したり、という風にSVDが「好みの軸」を発見しているのが見て取れるかと思います。
コード例(WMF編)
前回の行列分解の手法の説明において、WMFは SVD を拡張したもので、しばしばSVDより精度が向上する、ということを述べました。
ここでは、WMFがSVDよりも大幅に優れた性能を出す例として、CiteULike-aというオープンなデータセットを用います。これはCiteULikeというサービス(現在はサービス終了)において、さまざまな分野の研究者たちが自分に関連がある論文をコレクションした結果です。このデータを使って「あなたにお薦めの論文を推薦する」という問題設定を考えましょう。この場合ユーザーは「研究者」であり、アイテムは「論文」に相当します。この例でも引き続きirspack 0.1.18を使用していきます。
import numpy as np from sklearn.utils.extmath import randomized_svd from irspack.dataset import CiteULikeADataManager from irspack import df_to_sparse, IALSRecommender # 初回はダウンロードの許可を求められます。 data_manager = CiteULikeADataManager() collection_df = data_manager.read_interaction() item_titles = data_manager.read_item_meta()["raw.title"] # collection_df は以下のような user_id (研究者), item_id (論文) の2列からなります。 print(collection_df.head(3)) # user_id item_id # 0 0 495 # 1 0 1631 # 2 0 2317 R, user_ids, item_ids = df_to_sparse(collection_df, "user_id", "item_id") # R は ユーザー数 U = 5551, アイテム数 I = 16980 の疎行列になります。 print(repr(R)) # <5551x16980 sparse matrix of type '<class 'numpy.float64'>' # with 204986 stored elements in Compressed Sparse Row format>
このデータに対して、同じ300次元への行列分解でSVDとWMFの推薦結果を比較しましょう。まず上と同様にSVDを実行します。
O, Sigma, QT = randomized_svd(R, n_components=300) P_svd = O * Sigma Q_svd = QT.T
続いて\(R\)からWMFによる分解を得ます。WMFはirspackにIALSRecommenderとして実装されています。
パラメータalpha,reg,max_epochの値も交差検証によって決定されています。
wmf_model = IALSRecommender( R, n_components=300, alpha=97, reg=1.7e-7, max_epoch=10 ).learn() P_wmf = wmf_model.get_user_embedding() Q_wmf = wmf_model.get_item_embedding()
SVDとWMFの結果を比較するため、\(R\)第\(u\)行目に相当するユーザーに対する推薦結果を、特徴ベクトルが格納された行列\(P,Q\)から作成する関数を定義します。これには上述の SVD の場合を踏襲します。
def show_recommendation(u, P, Q): score = P[u].dot(Q.T) already_watched_indices = R[u].nonzero()[1] score[already_watched_indices] = -np.inf recommendation_indices = score.argsort()[::-1][:5] recommendation_ids = item_ids[recommendation_indices] print(item_titles.reindex(recommendation_ids))
データセットがアカデミック向け、ということもあり定性的な比較は難しいのですが、比較的 SVDとWMFの違いがわかりやすい例として、\(u=4386\)が何をコレクションに加えていたのか、そしてモデルの推薦結果がどうなるのかをみてみましょう。
user_index = 4386
# 上記以外のランダムなユーザーに対する結果を見たい場合は下の行のコメントアウトを外してください。
# user_index = np.random.randint(0, R.shape[0])
print(f"user {user_index}'s collection:")
print(
item_titles.reindex(
collection_df[collection_df.user_id == user_ids[user_index]].item_id
)
)
# 1509 Numerical {L}inear {A}lgebra
# 1715 Ergodic theory of chaos and strange attractors
# 1950 The Fractal Geometry of Nature
# 2858 The Local Structure of Turbulence in Incompres...
# 2859 A First Course in Turbulence
# 3704 Pattern formation outside of equilibrium
# 6409 Ordinary Differential Equations
# 6712 Turbulence statistics in fully developed chann...
# 7947 Deterministic nonperiodic flow
# 8411 The Design and Implementation of {FFTW3}
print("\nResult using svd:\n")
show_recommendation(user_index, P_svd, Q_svd)
# 1700 Numerical Recipes in {C}: The Art of Scientifi...
# 28 The structure and function of complex networks
# 7147 A New Look at the Statistical Model Identifica...
# 1200 Statistical mechanics of complex networks
# 1194 The Nature of Statistical Learning Theory
print("\nResult using wmf:\n")
show_recommendation(user_index, P_wmf, Q_wmf)
# 6119 A Modern Course in Statistical Physics
# 4918 Computational Methods for Fluid Dynamics
# 5743 Computer simulation using particles.
# 1506 Advanced {E}ngineering {M}athematics
# 2158 Ten Lectures on Wavelets
このユーザーは "Turbulence" (乱気流)をタイトルに含む論文を多くコレクションに加えていることから、流体力学などに興味を持っていることが推察されます。また、"Numerical Linear Algebra" (数値的線形代数)や "FFTW3" (高速フーリエ変換の有名なパッケージ) などもリストにはいっているので、「流体の数値的シミュレーションなどに興味がある工学あるいは応用物理の研究者」というイメージが湧きます。
他方で推薦結果をみてみると、SVDでは数値計算的なトピックに引っ張られて流体力学関連の文献は推薦できていませんが、WMFでは統計力学・流体力学などの応用物理に関する論文が含まれています。同じ300次元への行列分解ですが、この場合はWMFのほうがより詳細にトピックを把握できている様子が見て取られます。
この例はデータセットの性質上、アカデミック寄りの結果となっていますが、大事なことは
- WMFはSVDと大きく異なる結果を与えることがあり、前者は後者で拾えないようなより詳細なトピックを拾える場合がある
- 本連載後半で紹介する分割検証の手法を用いると、定性的な判断をするための専門知識がなくとも、どの推薦システムがより高品質な結果を返すかを定量的な指標で測ることができる(場合がある)
ということです。
