- タイトル:Modeling User Exposure in Recommendation
- 著者: Dawen Liang, Laurent Charlin, James McInerney, David M. Blei
- 所属:Columbia University, McGill University
- 発表会議:WWW 2016
[link]
はじめに
こんにちは、さとうみなとです。2019年最後は前々から読もうと思っていたExpoMFで締めたいと思います。最近userの閲覧確率を考慮したMatrix Factorizationでも解説されたりしていましたが、自分なりに読んで実装まで落とし込みたいと思いました。
論文概要
Implicit-feedback(e.g. クリックした/していない)を用いた推薦システムについて通常ユーザーがconsumeしていないアイテムを含む全てのアイテムが考慮されています。
しかし、ユーザーはすべてのアイテムを知覚することができないので、直感的にはおかしな仮定に思えます。
本論文では、アイテムへのユーザーのexposureを組み込む新しい確率的アプローチを提案しました。
平たく言えば下記のようなモデルを仮定しています。[参考]
クリックされたかどうかyui=アイテムへユーザーがexposeしたかどうかaui×relevancerui
つまり、アイテムへユーザーがexposeする、且つrelevanceがあった場合にはじめて yui=1 となります。逆にrelevanceがあってもexposeされな(i.e. ユーザーがアイテムを認知しな)ければ yui=0 となります。
Exposure Matrix Factorization (提案手法)
Notation
- u:ユーザー。1,⋯,U
- i:アイテム。1,⋯,I
- A=aui:ユーザー u がアイテム i にexposeされたかどうかを表す行列。
- Y=yui:ユーザー u がアイテム i をクリックしたかどうかを表す行列。
Model Description
次のような確率モデルを仮定する。
θuβiyui∣aui=1yui∣aui=0∼N(0,λθ−1IK)∼N(0,λβ−1IK)∼N(θuTβi,λy−1)∼δ0
ここで、δ0 は p(yui=0∣aui=0)=1を表す。
つまり、そもそもユーザーにアイテムがexposeされなければ必ず yui=0 となる。
また、μui はexposureの事前確率。
ユーザー u がアイテム i にexposeされたかどうかについてベルヌーイ分布を仮定して
p(aui,yui∣μui,θu,βi,λy−1)=p(aui∣μui,θu,βi,λy−1)×p(yui∣aui,μui,θu,βi,λy−1)=Bernoulli(aui∣μui)×(N(yui∣θuTβi,λy−1)aui∗I[yui=0](1−aui))
以上から対数尤度は
logp(aui,yui∣μui,θu,βi,λy−1)=logBernoulli(aui∣μui)+auilogN(yui∣θuTβi,λy−1)+(1−aui)logI[yui=0]
となる。
また、今回はexposureを表す方法としてアイテムのpopularityを用いる方法を採用。共役事前分布として下記のベータ分布を仮定する。
μi∼Beta(α1,α2)
Inference
論文中ではEMアルゴリズムでの学習方法を紹介。
E-step
Eステップでは、positive feedbackの観測されなかった全てのユーザー、アイテムの組み合わせ に対するexposureの期待値を計算します。
(positive feedbackの観測された u,i のペアは必ず aui=1 なので。)
E[aui∣θu,βi,μui,yui=0]=aui∈0,1∑aui×p(aui∣yui=0)=p(aui=1∣yui=0)=p(yui=0)p(yui=0∣aui=1)×p(aui=1)=p(yui=0,aui=0)+p(yui=0,aui=1)p(yui=0∣aui=1)×p(aui=1)=p(aui=1)×p(yui=0∣aui=1)+=1−p(aui=1)=1−μuip(aui=0)×=1p(yui=0∣aui=0)p(aui=1)μui×p(yui=0∣aui=1)=μui⋅N(0∣θuTβi,λy−1)+(1−μui)μui⋅N(0∣θuTβi,λy−1)
M-step
対数尤度の aui についてEステップで計算した期待値
pui=E[aui∣θu,βi,μui,yui]
を用いて、各パラメータ θu、βi で偏微分し =0 とおいて求めた値で更新する。
[2020/01/05更新追記]
対数尤度は
u,i∑logp(aui,yui∣μui,θu,βi,λy−1)+u∑logp(θu)+i∑logp(βi)=u,i∑puilogN(yui∣θuTβi,λy−1)+u∑logp(θu)+i∑logp(βi)+Const.=u,i∑pui×log2πλy−11exp(−2(yui−θuTβi)2λy)+u∑logp(θu)+i∑logp(βi)+Const.=u,i∑pui(−2(yui−θuTβi)2λy)+u∑logp(θu)+i∑logp(βi)+Const.
これについて
∂θu∂u,i∑pui(−2(yui−θuTβi)2λy)+∂θu∂u∑logp(θu)=∂θu∂u,i∑pui(−2(yui2−2yuiθuTβi+(θuTβi)2)λy)−λθθu=i∑λypui(yuiβi−βiβiTθu)−λθθu=0
したがって
θu←(λyi∑puiβiβiT+λθIK)−1(i∑λypuiyuiβi)βi←(λyu∑puiθuθuT+λβIK)−1(u∑λypuiyuiθu)
を得る。
[2020/01/06更新追記]
また、各 μi ついては事後分布を計算すると、
p(μi∣a1:U,i)∝p(a1:U,i∣μi)×p(μi)∝μia1i(1−μi)1−a1i⋯μiaUi(1−μi)1−aUi×Γ(α1)+Γ(α2)Γ(α1+α2)μiα1−1(1−μi)α2−1∝μiα1+∑uaui−1(1−μi)α2+(U−∑uaui)−1
この式はベータ分布
Beta(α1+u∑aui,α2+U−u∑aui)
に従っている。aui は実際には期待値を用いるので pui で置き換えて
Beta(α1+u∑pui,α2+U−u∑pui)
の最頻値
μi←α1+α2+U−2α1+∑upui−1
を用いて更新する。
擬似コード
以上を踏まえた学習の擬似コードは下記の通り。
01: input: click matrix Y
02: randomly initialize user factors θ1:U
03: randomly initialize item factors β1:I, exposure priors μ1:I
04: loop for each iteration
05: Compute expected exposure A (e-step)
06: Update user factors θ1:U (m-step)
07: Update item factors β1:I (m-step)
08: Update priors μi (m-step)
Pythonコード
ExpoMFは著者実装があって幸いPython3系でも割と簡単に動かすことができるのですが、せっかくならなるべく数式を追って理解したいので自分でも実装してみたくなりました。
movielensのデータ取得の部分でlightfmの力を拝借しておりますので
としてインストールください。
コード中に注釈をつけておりますので擬似コードと照らし合わせてください。
import numpy as np
from lightfm.datasets import fetch_movielens
dataset = fetch_movielens(min_rating=4.0)
iterations = 3
K = 20
lambda_theta = 1e-2
lambda_beta = 1e-2
lambda_y = 1e-2
alpha_1 = 1.0
alpha_2 = 1.0
init_mu = 0.01
U, I = dataset["train"].shape
A = np.zeros(dataset["train"].shape)
Y = dataset["train"].toarray()
Y[Y > 0] = 1.0
theta = np.random.uniform(low=-0.1, high=0.1, size=(U, K)) / K
beta = np.random.uniform(low=-0.1, high=0.1, size=(I, K)) / K
mu = np.ones(I) * init_mu
for iteration in range(iterations):
print(f"CURRENT ITERATION = {iteration+1}")
for u in range(U):
for i in range(I):
if Y[u, i] == 1.0:
A[u, i] = 1.0
else:
n_ui = lambda_y / np.sqrt(2*np.pi) * np.exp(- np.dot(theta[u], beta[i]) ** 2 * lambda_y ** 2)
A[u, i] = n_ui / (n_ui + (1 - mu[i]) / mu[i])
for u in range(U):
B = np.zeros((K, K))
a = np.zeros(K)
for i in range(I):
B += A[u, i] * beta[i, None] * beta[i, None].T
a += lambda_y * A[u, i] * Y[u, i] * beta[i]
B *= lambda_y
B += lambda_theta * np.eye(K)
new_theta = np.linalg.solve(B, a)
theta[u] = new_theta
for i in range(I):
B = np.zeros((K, K))
a = np.zeros(K)
for u in range(U):
B += A[u, i] * theta[u, None] * theta[u, None].T
a += lambda_y * A[u, i] * Y[u, i] * theta[u]
B *= lambda_y
B += lambda_beta * np.eye(K)
new_beta = np.linalg.solve(B, a)
beta[i] = new_beta
mu[:] = (alpha_1 + A.sum(axis=0) - 1) / (alpha_1 + alpha_2 + U - 2)
Y_test = dataset["test"].toarray()
Y_test[Y_test > 0] = 1.0
from sklearn import metrics
predicted = theta.dot(beta.T)
scores = np.zeros(U)
for u in range(U):
fpr, tpr, thresholds = metrics.roc_curve(Y_test[u], predicted[u])
scores[u] = metrics.auc(fpr, tpr) if len(set(Y_test[u])) != 1 else 0.0
print(f"test mean auc: {scores.mean()}")
僕の環境での実行結果は
CURRENT ITERATION = 1
CURRENT ITERATION = 2
CURRENT ITERATION = 3
test mean auc: 0.8930167190209362
となりました。
さいごに
今回はExpoMFについて簡単に紹介と実装を行ってみました。数式やコード、説明に間違いございましたらTwitterかコメント欄までお願い致します。