Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NCE-PLRec #670

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions recbole/model/general_recommender/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from recbole.model.general_recommender.multidae import MultiDAE
from recbole.model.general_recommender.multivae import MultiVAE
from recbole.model.general_recommender.nais import NAIS
from recbole.model.general_recommender.nceplrec import NCEPLRec
from recbole.model.general_recommender.neumf import NeuMF
from recbole.model.general_recommender.ngcf import NGCF
from recbole.model.general_recommender.pop import Pop
Expand Down
96 changes: 96 additions & 0 deletions recbole/model/general_recommender/nceplrec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
r"""
NCE-PLRec
################################################
Reference:
Ga Wu, et al. "Noise Contrastive Estimation for One-Class Collaborative Filtering" in Sigir 2019.

Reference code:
https://github.com/wuga214/NCE_Projected_LRec
"""


from recbole.utils.enum_type import ModelType
import numpy as np
import scipy.sparse as sp
import torch
from sklearn.utils.extmath import randomized_svd

from recbole.utils import InputType
from recbole.model.abstract_recommender import GeneralRecommender


class NCEPLRec(GeneralRecommender):
input_type = InputType.POINTWISE
type = ModelType.TRADITIONAL

def __init__(self, config, dataset):
super().__init__(config, dataset)

# need at least one param
self.dummy_param = torch.nn.Parameter(torch.zeros(1))

R = dataset.inter_matrix(
form='csr').astype(np.float32)

beta = config['beta']
rank = int(config['rank'])
reg_weight = config['reg_weight']
seed = config['seed']

# just directly calculate the entire score matrix in init
# (can't be done incrementally)
num_users, num_items = R.shape

item_popularities = R.sum(axis=0)

D_rows = []
for i in range(num_users):
row_index, col_index = R[i].nonzero()
if len(row_index) > 0:
values = item_popularities[:, col_index].getA1()
# note this is a slight variation of what's in the paper, for convenience
# see https://github.com/wuga214/NCE_Projected_LRec/issues/38
values = np.maximum(
np.log(num_users/np.power(values, beta)), 0)
D_rows.append(sp.coo_matrix(
(values, (row_index, col_index)), shape=(1, num_items)))
else:
D_rows.append(sp.coo_matrix((1, num_items)))

D = sp.vstack(D_rows)

_, sigma, Vt = randomized_svd(D, n_components=rank,
n_iter='auto',
power_iteration_normalizer='QR',
random_state=seed)

sqrt_Sigma = np.diag(np.power(sigma, 1/2))

V_star = Vt.T @ sqrt_Sigma

Q = R @ V_star
# Vt.shape[0] instead of rank for cases when the interaction matrix is smaller than given rank
W = np.linalg.inv(Q.T @ Q + reg_weight * np.identity(Vt.shape[0])) @ Q.T @ R

# instead of computing and storing the entire score matrix, just store Q and W and compute the scores on demand

self.user_embeddings = torch.from_numpy(Q)
self.item_embeddings = torch.from_numpy(W)

def forward(self):
pass

def calculate_loss(self, interaction):
return torch.nn.Parameter(torch.zeros(1))

def predict(self, interaction):
user = interaction[self.USER_ID]
item = interaction[self.ITEM_ID]

return (self.user_embeddings[user, :] * self.item_embeddings[:, item].T).sum(axis=1)

def full_sort_predict(self, interaction):
user = interaction[self.USER_ID]

r = self.user_embeddings[user, :] @ self.item_embeddings
return r.flatten()
3 changes: 3 additions & 0 deletions recbole/properties/model/NCEPLRec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
beta: 1.0
rank: 450
reg_weight: 15000
4 changes: 4 additions & 0 deletions run_test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
'model': 'MacridVAE',
'dataset': 'ml-100k',
},
'Test NCEPLRec': {
'model': 'NCEPLRec',
'dataset': 'ml-100k',
},

# Context-aware Recommendation
'Test FM': {
Expand Down
6 changes: 6 additions & 0 deletions tests/model/test_model_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ def test_CDAE(self):
}
quick_test(config_dict)

def test_nceplrec(self):
config_dict = {
'model': 'NCEPLRec',
}
quick_test(config_dict)

class TestContextRecommender(unittest.TestCase):
# todo: more complex context information should be test, such as criteo dataset

Expand Down