From e28e150745319d0ac7abca8636358a871f8ff9dd Mon Sep 17 00:00:00 2001 From: Deklan Webster Date: Tue, 29 Dec 2020 11:52:14 -0500 Subject: [PATCH] Add RP3Beta --- recbole/model/general_recommender/__init__.py | 1 + recbole/model/general_recommender/rp3beta.py | 98 +++++++++++++++++++ recbole/properties/model/RP3Beta.yaml | 1 + run_test_example.py | 4 + tests/model/test_model_auto.py | 7 ++ 5 files changed, 111 insertions(+) create mode 100644 recbole/model/general_recommender/rp3beta.py create mode 100644 recbole/properties/model/RP3Beta.yaml diff --git a/recbole/model/general_recommender/__init__.py b/recbole/model/general_recommender/__init__.py index 373cc7c75..c47d6f2fc 100644 --- a/recbole/model/general_recommender/__init__.py +++ b/recbole/model/general_recommender/__init__.py @@ -11,4 +11,5 @@ from recbole.model.general_recommender.neumf import NeuMF from recbole.model.general_recommender.ngcf import NGCF from recbole.model.general_recommender.pop import Pop +from recbole.model.general_recommender.rp3beta import RP3Beta from recbole.model.general_recommender.spectralcf import SpectralCF diff --git a/recbole/model/general_recommender/rp3beta.py b/recbole/model/general_recommender/rp3beta.py new file mode 100644 index 000000000..270cbe65b --- /dev/null +++ b/recbole/model/general_recommender/rp3beta.py @@ -0,0 +1,98 @@ +r""" +RP3Beta +################################################ +Reference: + Paudel, Bibek et al. Updatable, Accurate, Diverse, and Scalable Recommendations for Interactive Applications. https://doi.org/10.1145/2955101 + +Reference code: + https://github.com/MaurizioFD/RecSys2019_DeepLearning_Evaluation/blob/master/GraphBased/RP3betaRecommender.py +""" + + +from recbole.utils.enum_type import ModelType +import numpy as np +import scipy.sparse as sp +import torch + +from recbole.utils import InputType +from recbole.model.abstract_recommender import GeneralRecommender + + +def get_inv_degree_matrix(A): + # add epsilon to degree sums to suppress warning about division by zero when we later divide + degree_sums = A.sum(axis=1).getA1() + 1e-7 + + return sp.diags(1/degree_sums) + + +# for reference, doing it in one computation +# since the resultant matrix is dense, I'll refrain from doing this +def calculate_rp3beta(B, beta): + user_degree_inv = get_inv_degree_matrix(B) + item_degree_inv = get_inv_degree_matrix(B.T) + + # multiplication on left for row-wise scaling + user_transition = user_degree_inv @ B + item_transition = item_degree_inv @ B.T + + P3 = user_transition @ item_transition @ user_transition + + # multiplication on right for column-wise scaling (i.e., we're reweighting by inverse item popularity to a power) + RP3Beta = P3 @ item_degree_inv.power(beta) + + return RP3Beta + + +class RP3Beta(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)) + + B = dataset.inter_matrix( + form='coo').astype(np.float32) + + self.beta = config['beta'] + + user_degree_inv = get_inv_degree_matrix(B) + item_degree_inv = get_inv_degree_matrix(B.T) + + self.user_transition = user_degree_inv @ B + self.item_transition = item_degree_inv @ B.T + self.item_degree_inv = item_degree_inv + + 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].cpu().numpy() + item = interaction[self.ITEM_ID].cpu().numpy() + + specific_user_transitions = self.user_transition[user] + + # make all item predictions for specified users + user_all_items = specific_user_transitions @ self.item_transition @ self.user_transition @ self.item_degree_inv.power( + self.beta) + + # then narrow down to specific items + # without this copy(): "cannot set WRITEABLE flag..." + item_predictions = user_all_items[range(len(user)), item.copy()] + + return torch.from_numpy(item_predictions.getA1()) + + def full_sort_predict(self, interaction): + user = interaction[self.USER_ID].cpu().numpy() + + specific_user_transitions = self.user_transition[user] + + item_predictions = specific_user_transitions @ self.item_transition @ self.user_transition @ self.item_degree_inv.power( + self.beta) + + return torch.from_numpy(item_predictions.todense().getA1()) diff --git a/recbole/properties/model/RP3Beta.yaml b/recbole/properties/model/RP3Beta.yaml new file mode 100644 index 000000000..068f3e6cd --- /dev/null +++ b/recbole/properties/model/RP3Beta.yaml @@ -0,0 +1 @@ +beta: 0.55 \ No newline at end of file diff --git a/run_test_example.py b/run_test_example.py index 58baee213..52da51b52 100644 --- a/run_test_example.py +++ b/run_test_example.py @@ -134,6 +134,10 @@ 'model': 'LINE', 'dataset': 'ml-100k', }, + 'Test RP3Beta': { + 'model': 'RP3Beta', + 'dataset': 'ml-100k', + }, # Context-aware Recommendation 'Test FM': { diff --git a/tests/model/test_model_auto.py b/tests/model/test_model_auto.py index 18ae0888c..147eb553d 100644 --- a/tests/model/test_model_auto.py +++ b/tests/model/test_model_auto.py @@ -116,6 +116,13 @@ def test_line(self): objective_function(config_dict=config_dict, config_file_list=config_file_list, saved=False) + def test_rp3beta(self): + config_dict = { + 'model': 'RP3Beta', + } + objective_function(config_dict=config_dict, + config_file_list=config_file_list, saved=False) + class TestContextRecommender(unittest.TestCase): # todo: more complex context information should be test, such as criteo dataset