From 8a3532f57dc9804c00ec73abe93d7a843de0a84a Mon Sep 17 00:00:00 2001 From: martincousi Date: Mon, 26 Mar 2018 16:45:55 -0400 Subject: [PATCH 01/60] added asym_rmse and asym_mae --- surprise/accuracy.py | 75 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 1e9e4855..04ffca0f 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -88,6 +88,81 @@ def mae(predictions, verbose=True): return mae_ +def asym_rmse(predictions, weight=0.5, verbose=True): + """Compute Asymmetric RMSE (Root Mean Squared Error). + + .. math:: + \\text{Asymmetric RMSE} = \\sqrt{\\frac{1}{|\\hat{R}|} + \\sum_{\\hat{r}_{ui} \in \\hat{R}}(r_{ui} - \\hat{r}_{ui})^2 |\\omega + - 1_{r_{ui} - \\hat{r}_{ui} < 0}|}. + + Args: + predictions (:obj:`list` of :obj:`Prediction\ + `): + A list of predictions, as returned by the :meth:`test() + ` method. + weight (int): Weight used to characterize asymmetry. + verbose: If True, will print computed value. Default is ``True``. + + + Returns: + The Asymmetric Root Mean Squared Error of predictions. + + Raises: + ValueError: When ``predictions`` is empty. + """ + + if not predictions: + raise ValueError('Prediction list is empty.') + + res = np.array([float(true_r - est) + for (_, _, true_r, est, _) in predictions]) + asym_rmse_ = np.sqrt(np.mean(res**2 * np.abs(weight - + (res<0).astype(int)))) + + if verbose: + print('Asymmetric RMSE: {0:1.4f}'.format(asym_rmse_)) + + return asym_rmse_ + + +def asym_mae(predictions, weight=0.5, verbose=True): + """Compute Asymmetric MAE (Mean Absolute Error). + + .. math:: + \\text{Asymmetric MAE} = \\frac{1}{|\\hat{R}|} \\sum_{\\hat{r}_{ui} \in + \\hat{R}}|r_{ui} - \\hat{r}_{ui}| |\\omega - 1_{r_{ui} - \\hat{r}_{ui} + < 0}|. + + Args: + predictions (:obj:`list` of :obj:`Prediction\ + `): + A list of predictions, as returned by the :meth:`test() + ` method. + weight (int): Weight used to characterize asymmetry. + verbose: If True, will print computed value. Default is ``True``. + + + Returns: + The Asymmetric Mean Absolute Error of predictions. + + Raises: + ValueError: When ``predictions`` is empty. + """ + + if not predictions: + raise ValueError('Prediction list is empty.') + + res = np.array([float(true_r - est) + for (_, _, true_r, est, _) in predictions]) + asym_mae_ = np.mean(np.abs(res) * np.abs(weight - (res<0).astype(int))) + + if verbose: + print('Asymmetric MAE: {0:1.4f}'.format(asym_mae_)) + + return asym_mae_ + + def fcp(predictions, verbose=True): """Compute FCP (Fraction of Concordant Pairs). From 3f6b1d029d6fbada68bed45268675661da62f4dd Mon Sep 17 00:00:00 2001 From: martincousi Date: Tue, 27 Mar 2018 13:53:19 -0400 Subject: [PATCH 02/60] disable print in AlgoBase.compute_baselines() --- surprise/prediction_algorithms/algo_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 844cb44e..ce4a3e50 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -240,7 +240,7 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - print('Estimating biases using', method_name + '...') + # print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: From daab1bac3b19a02b84085ad280c89a7de3016dcf Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 28 Mar 2018 14:34:35 -0400 Subject: [PATCH 03/60] Cancel printing of computation of similarities --- surprise/prediction_algorithms/algo_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index ce4a3e50..3a80c4d4 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -287,9 +287,9 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - print('Computing the {0} similarity matrix...'.format(name)) + # print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - print('Done computing similarity matrix.') + # print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + From 05ef072d85ba00dd821a57122c2c9a689b95f401 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 28 Mar 2018 16:30:31 -0400 Subject: [PATCH 04/60] Cancel printing of similiraty computation --- surprise/prediction_algorithms/algo_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index ce4a3e50..3a80c4d4 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -287,9 +287,9 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - print('Computing the {0} similarity matrix...'.format(name)) + # print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - print('Done computing similarity matrix.') + # print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + From 902246f32cccd077517f25a6c972f528dcfa023d Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 14:37:26 -0400 Subject: [PATCH 05/60] add load_features_df() method --- .gitignore | 3 ++- surprise/dataset.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bd32b905..24fd8009 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ _site .coverage tags -settings.json \ No newline at end of file +settings.json +surprise/.DS_Store diff --git a/surprise/dataset.py b/surprise/dataset.py index 17638b6c..f5df2f6a 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,6 +53,8 @@ class Dataset: def __init__(self, reader): self.reader = reader + self.raw_user_features = None + self.raw_item_features = None @classmethod def load_builtin(cls, name='ml-100k'): @@ -165,6 +167,28 @@ def load_from_df(cls, df, reader): return DatasetAutoFolds(reader=reader, df=df) + def load_features_df(self, features_df, user_features=True): + """Load features from a pandas dataframe into a dataset. + + Use this if you want to add user or item features to a dataset. Only + certain prediction algorithms in the :mod:`prediction_algorithms` + package support this additional data. + + Args: + features_df(`Dataframe`): The dataframe containing the features. It + must have two columns or more, corresponding to the user or + item (raw) ids, and the features, in this order. + user_features(:obj:`bool`): Whether the features are for the users + or the items. Default is ``True``. + """ + + if user_features: + self.user_features_df = features_df + self.raw_user_features = features_df.values.tolist() + else: + self.item_features_df = features_df + self.raw_item_features = features_df.values.tolist() + def read_ratings(self, file_name): """Return a list of ratings (user, item, rating, timestamp) read from file_name""" From fb64e9897ab40c1c072838305c4e11215cfd5aa8 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 16:07:15 -0400 Subject: [PATCH 06/60] modified construct_trainset() and load_features_df() --- surprise/dataset.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index f5df2f6a..8953a105 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,8 +53,8 @@ class Dataset: def __init__(self, reader): self.reader = reader - self.raw_user_features = None - self.raw_item_features = None + self.user_features = None + self.item_features = None @classmethod def load_builtin(cls, name='ml-100k'): @@ -184,10 +184,16 @@ def load_features_df(self, features_df, user_features=True): if user_features: self.user_features_df = features_df - self.raw_user_features = features_df.values.tolist() + self.user_features = {urid: features for (urid, *features) in + features_df.itertuples(index=False)} + self.user_features_labels = features_df.columns.values.tolist()[1:] + self.user_features_nb = len(self.user_features_labels) else: self.item_features_df = features_df - self.raw_item_features = features_df.values.tolist() + self.item_features = {irid: features for (irid, *features) in + features_df.itertuples(index=False)} + self.item_features_labels = features_df.columns.values.tolist()[1:] + self.item_features_nb = len(self.item_features_labels) def read_ratings(self, file_name): """Return a list of ratings (user, item, rating, timestamp) read from @@ -231,6 +237,8 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) + u_features = defaultdict(list) + i_features = defaultdict(list) # user raw id, item raw id, translated rating, time stamp for urid, irid, r, timestamp in raw_trainset: @@ -240,12 +248,26 @@ def construct_trainset(self, raw_trainset): uid = current_u_index raw2inner_id_users[urid] = current_u_index current_u_index += 1 + if self.user_features is not None: + try: + u_features[uid] = self.user_features[urid] + except KeyError: + print('user ' + urid + ' does not exist in ' + + 'self.user_features') + raise try: iid = raw2inner_id_items[irid] except KeyError: iid = current_i_index raw2inner_id_items[irid] = current_i_index current_i_index += 1 + if self.item_features is not None: + try: + i_features[iid] = self.item_features[irid] + except KeyError: + print('item ' + irid + ' does not exist in ' + + 'self.item_features') + raise ur[uid].append((iid, r)) ir[iid].append((uid, r)) From 13f3a287ca7fc2287c68d1919d3aea2721d05a6e Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 16:18:00 -0400 Subject: [PATCH 07/60] modified Trainset.__init__() --- surprise/dataset.py | 6 ++++++ surprise/trainset.py | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index 8953a105..0f386626 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -274,12 +274,18 @@ def construct_trainset(self, raw_trainset): n_users = len(ur) # number of users n_items = len(ir) # number of items + n_user_features = len(u_features) + n_item_features = len(i_features) n_ratings = len(raw_trainset) trainset = Trainset(ur, ir, + u_features, + i_features, n_users, n_items, + n_user_features, + n_item_features, n_ratings, self.reader.rating_scale, self.reader.offset, diff --git a/surprise/trainset.py b/surprise/trainset.py index ebb95204..885bc41c 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -33,21 +33,34 @@ class Trainset: ir(:obj:`defaultdict` of :obj:`list`): The items ratings. This is a dictionary containing lists of tuples of the form ``(user_inner_id, rating)``. The keys are item inner ids. + u_features(:obj:`defaultdict` of :obj:`list`): The user features. This + is a dictionary containing lists of features. The keys are user + inner ids. + i_features(:obj:`defaultdict` of :obj:`list`): The item features. This + is a dictionary containing lists of features. The keys are item + inner ids. n_users: Total number of users :math:`|U|`. n_items: Total number of items :math:`|I|`. + n_user_features: Total number of user features. + n_item_features: Total number of item features. n_ratings: Total number of ratings :math:`|R_{train}|`. rating_scale(tuple): The minimum and maximal rating of the rating scale. global_mean: The mean of all ratings :math:`\\mu`. """ - def __init__(self, ur, ir, n_users, n_items, n_ratings, rating_scale, + def __init__(self, ur, ir, u_features, i_features, n_users, n_items, + n_user_features, n_item_features, n_ratings, rating_scale, offset, raw2inner_id_users, raw2inner_id_items): self.ur = ur self.ir = ir + self.u_features = u_features + self.i_features = i_features self.n_users = n_users self.n_items = n_items + self.n_user_features = n_user_features + self.n_item_features = n_item_features self.n_ratings = n_ratings self.rating_scale = rating_scale self.offset = offset From 900c0c0cb02e15e29b107657f1b3b33b9f3416c9 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 17:23:02 -0400 Subject: [PATCH 08/60] corrected bugs in print statement --- surprise/dataset.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index 0f386626..e93b6d2b 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -195,6 +195,8 @@ def load_features_df(self, features_df, user_features=True): self.item_features_labels = features_df.columns.values.tolist()[1:] self.item_features_nb = len(self.item_features_labels) + return self + def read_ratings(self, file_name): """Return a list of ratings (user, item, rating, timestamp) read from file_name""" @@ -252,7 +254,7 @@ def construct_trainset(self, raw_trainset): try: u_features[uid] = self.user_features[urid] except KeyError: - print('user ' + urid + ' does not exist in ' + + print('user ' + str(urid) + ' does not exist in ' + 'self.user_features') raise try: @@ -265,7 +267,7 @@ def construct_trainset(self, raw_trainset): try: i_features[iid] = self.item_features[irid] except KeyError: - print('item ' + irid + ' does not exist in ' + + print('item ' + str(irid) + ' does not exist in ' + 'self.item_features') raise From 68ccfca4f3645c94bbc6e43fc7ea2b587cee2f24 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 21:01:45 -0400 Subject: [PATCH 09/60] use user_features_nb to test if initialized --- surprise/dataset.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index e93b6d2b..36b96e36 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,8 +53,8 @@ class Dataset: def __init__(self, reader): self.reader = reader - self.user_features = None - self.item_features = None + self.user_features_nb = 0 + self.item_features_nb = 0 @classmethod def load_builtin(cls, name='ml-100k'): @@ -250,7 +250,7 @@ def construct_trainset(self, raw_trainset): uid = current_u_index raw2inner_id_users[urid] = current_u_index current_u_index += 1 - if self.user_features is not None: + if self.user_features_nb > 0: try: u_features[uid] = self.user_features[urid] except KeyError: @@ -263,7 +263,7 @@ def construct_trainset(self, raw_trainset): iid = current_i_index raw2inner_id_items[irid] = current_i_index current_i_index += 1 - if self.item_features is not None: + if self.item_features_nb > 0: try: i_features[iid] = self.item_features[irid] except KeyError: @@ -276,8 +276,6 @@ def construct_trainset(self, raw_trainset): n_users = len(ur) # number of users n_items = len(ir) # number of items - n_user_features = len(u_features) - n_item_features = len(i_features) n_ratings = len(raw_trainset) trainset = Trainset(ur, @@ -286,8 +284,8 @@ def construct_trainset(self, raw_trainset): i_features, n_users, n_items, - n_user_features, - n_item_features, + self.user_features_nb, + self.item_features_nb, n_ratings, self.reader.rating_scale, self.reader.offset, From f7fa4d89eff119c05437089ab8102af41b63c06f Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 21:51:13 -0400 Subject: [PATCH 10/60] revert back changes to accuracy.py --- surprise/accuracy.py | 75 -------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 04ffca0f..1e9e4855 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -88,81 +88,6 @@ def mae(predictions, verbose=True): return mae_ -def asym_rmse(predictions, weight=0.5, verbose=True): - """Compute Asymmetric RMSE (Root Mean Squared Error). - - .. math:: - \\text{Asymmetric RMSE} = \\sqrt{\\frac{1}{|\\hat{R}|} - \\sum_{\\hat{r}_{ui} \in \\hat{R}}(r_{ui} - \\hat{r}_{ui})^2 |\\omega - - 1_{r_{ui} - \\hat{r}_{ui} < 0}|}. - - Args: - predictions (:obj:`list` of :obj:`Prediction\ - `): - A list of predictions, as returned by the :meth:`test() - ` method. - weight (int): Weight used to characterize asymmetry. - verbose: If True, will print computed value. Default is ``True``. - - - Returns: - The Asymmetric Root Mean Squared Error of predictions. - - Raises: - ValueError: When ``predictions`` is empty. - """ - - if not predictions: - raise ValueError('Prediction list is empty.') - - res = np.array([float(true_r - est) - for (_, _, true_r, est, _) in predictions]) - asym_rmse_ = np.sqrt(np.mean(res**2 * np.abs(weight - - (res<0).astype(int)))) - - if verbose: - print('Asymmetric RMSE: {0:1.4f}'.format(asym_rmse_)) - - return asym_rmse_ - - -def asym_mae(predictions, weight=0.5, verbose=True): - """Compute Asymmetric MAE (Mean Absolute Error). - - .. math:: - \\text{Asymmetric MAE} = \\frac{1}{|\\hat{R}|} \\sum_{\\hat{r}_{ui} \in - \\hat{R}}|r_{ui} - \\hat{r}_{ui}| |\\omega - 1_{r_{ui} - \\hat{r}_{ui} - < 0}|. - - Args: - predictions (:obj:`list` of :obj:`Prediction\ - `): - A list of predictions, as returned by the :meth:`test() - ` method. - weight (int): Weight used to characterize asymmetry. - verbose: If True, will print computed value. Default is ``True``. - - - Returns: - The Asymmetric Mean Absolute Error of predictions. - - Raises: - ValueError: When ``predictions`` is empty. - """ - - if not predictions: - raise ValueError('Prediction list is empty.') - - res = np.array([float(true_r - est) - for (_, _, true_r, est, _) in predictions]) - asym_mae_ = np.mean(np.abs(res) * np.abs(weight - (res<0).astype(int))) - - if verbose: - print('Asymmetric MAE: {0:1.4f}'.format(asym_mae_)) - - return asym_mae_ - - def fcp(predictions, verbose=True): """Compute FCP (Fraction of Concordant Pairs). From c6591ae125c7a9016c89099f36b36b619ebbefc5 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 21:52:26 -0400 Subject: [PATCH 11/60] revert back changes to AlgoBase --- surprise/prediction_algorithms/algo_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 3a80c4d4..844cb44e 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -240,7 +240,7 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - # print('Estimating biases using', method_name + '...') + print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: @@ -287,9 +287,9 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - # print('Computing the {0} similarity matrix...'.format(name)) + print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - # print('Done computing similarity matrix.') + print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + From e31e857a7aa094326d36e79e24729a881f7ba31e Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 21:55:52 -0400 Subject: [PATCH 12/60] Update .gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 24fd8009..bd9d038c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,3 @@ _site .coverage tags -settings.json -surprise/.DS_Store From 7d679630ba2f2329eb4ea4e9c7f1fba2a9720fbf Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 21:56:19 -0400 Subject: [PATCH 13/60] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bd9d038c..45019cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ _site .coverage tags +settings.json From 73bea5018ee1137bba1f0792428d133e969a4c04 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 29 Mar 2018 22:17:41 -0400 Subject: [PATCH 14/60] fixed python 2 compatibility --- surprise/dataset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index 36b96e36..61170bb6 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -184,13 +184,13 @@ def load_features_df(self, features_df, user_features=True): if user_features: self.user_features_df = features_df - self.user_features = {urid: features for (urid, *features) in + self.user_features = {tup[0]: tup[1:] for tup in features_df.itertuples(index=False)} self.user_features_labels = features_df.columns.values.tolist()[1:] self.user_features_nb = len(self.user_features_labels) else: self.item_features_df = features_df - self.item_features = {irid: features for (irid, *features) in + self.item_features = {tup[0]: tup[1:] for tup in features_df.itertuples(index=False)} self.item_features_labels = features_df.columns.values.tolist()[1:] self.item_features_nb = len(self.item_features_labels) From 4063da8286d165f7bd0b47b1bcc4e9006ecc4c31 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 4 Apr 2018 09:51:43 -0400 Subject: [PATCH 15/60] construction of Lasso.fit() --- surprise/__init__.py | 4 +- surprise/dataset.py | 3 +- surprise/prediction_algorithms/__init__.py | 3 +- surprise/prediction_algorithms/features.py | 97 +++++++++++++++++++ .../matrix_factorization.pyx | 2 +- surprise/trainset.py | 24 +++++ 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 surprise/prediction_algorithms/features.py diff --git a/surprise/__init__.py b/surprise/__init__.py index e87ca980..82de2460 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -12,6 +12,7 @@ from .prediction_algorithms import NMF from .prediction_algorithms import SlopeOne from .prediction_algorithms import CoClustering +from .prediction_algorithms import Lasso from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -30,6 +31,7 @@ 'KNNWithMeans', 'KNNBaseline', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', - 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection'] + 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', + 'Lasso'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/dataset.py b/surprise/dataset.py index 61170bb6..980f797d 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -239,6 +239,7 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) + u_features = defaultdict(list) i_features = defaultdict(list) @@ -297,7 +298,7 @@ def construct_trainset(self, raw_trainset): def construct_testset(self, raw_testset): return [(ruid, riid, r_ui_trans) - for (ruid, riid, r_ui_trans, _) in raw_testset] + for (ruid, riid, r_ui_trans, __) in raw_testset] class DatasetUserFolds(Dataset): diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index d5ce8288..61091131 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -32,6 +32,7 @@ from .matrix_factorization import NMF from .slope_one import SlopeOne from .co_clustering import CoClustering +from .features import Lasso from .predictions import PredictionImpossible from .predictions import Prediction @@ -39,4 +40,4 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore'] + 'KNNWithZScore', 'Lasso'] diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/features.py new file mode 100644 index 00000000..b7d435c7 --- /dev/null +++ b/surprise/prediction_algorithms/features.py @@ -0,0 +1,97 @@ +""" +the :mod:`features` module includes some features-based algorithms. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import numpy as np +from six import iteritems +import heapq +from sklearn import linear_model + +from .predictions import PredictionImpossible +from .algo_base import AlgoBase + + +class Lasso(AlgoBase): + """A basic linear regression algorithm. + + The prediction :math:`\\hat{r}_{ui}` is set as: + + .. math:: + \hat{r}_{ui} = \\frac{ + \\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v) \cdot r_{vi}} + {\\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v)} + + or + + .. math:: + \hat{r}_{ui} = \\frac{ + \\sum\\limits_{j \in N^k_u(i)} \\text{sim}(i, j) \cdot r_{uj}} + {\\sum\\limits_{j \in N^k_u(j)} \\text{sim}(i, j)} + + depending on the ``user_based`` field of the ``sim_options`` parameter. + + Args: + k(int): The (max) number of neighbors to take into account for + aggregation (see :ref:`this note `). Default is + ``40``. + min_k(int): The minimum number of neighbors to take into account for + aggregation. If there are not enough neighbors, the prediction is + set the the global mean of all ratings. Default is ``1``. + sim_options(dict): A dictionary of options for the similarity + measure. See :ref:`similarity_measures_configuration` for accepted + options. + """ + + def __init__(self, **kwargs): + + AlgoBase.__init__(self, **kwargs) + + def fit(self, trainset): + + AlgoBase.fit(self, trainset) + self.lasso(trainset) + + return self + + def lasso(self, trainset): + + if (self.trainset.n_user_features == 0 or + self.trainset.n_item_features == 0): + raise ValueError('trainset does not contain user and/or item features.') + + n_ratings = self.trainset.n_ratings + n_uf = self.trainset.n_user_features + n_if = self.trainset.n_item_features + u_features = self.trainset.u_features + i_features = self.trainset.i_features + + X = np.empty((n_ratings, n_uf + n_if)) + y = np.empty((n_ratings,)) + for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): + y[k] = rating + X[k, :n_uf] = u_features[uid] + X[k, n_uf:] = i_features[iid] + + reg = linear_model.Lasso(alpha=0.1) + reg.fit(X, y) + + # self.X = X + # self.y = y + self.coef = reg.coef_ + self.intercept = reg.intercept_ + + def estimate(self, u, i): + + if not (self.trainset.has_user_features(u) and + self.trainset.has_item_features(i)): + raise PredictionImpossible('User and/or item features ' + 'are unknown.') + + x = np.concatenate((self.trainset.u_features[u], + self.trainset.i_features[i])) + + est = self.intercept + np.dot(x, self.coef) + + return est diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index 0e898632..fe2f1e56 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -275,7 +275,7 @@ class SVD(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unkown.') + raise PredictionImpossible('User and item are unknown.') return est diff --git a/surprise/trainset.py b/surprise/trainset.py index 885bc41c..4da7ddf1 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -100,6 +100,30 @@ def knows_item(self, iid): return iid in self.ir + def has_user_features(self, uid): + """Indicate if the user features are part of the trainset. + + Args: + uid(int): The (inner) user id. See :ref:`this + note`. + Returns: + ``True`` if user features are part of the trainset, else ``False``. + """ + + return uid in self.u_features + + def has_item_features(self, iid): + """Indicate if the item features are part of the trainset. + + Args: + iid(int): The (inner) item id. See :ref:`this + note`. + Returns: + ``True`` if item features are part of the trainset, else ``False``. + """ + + return iid in self.i_features + def to_inner_uid(self, ruid): """Convert a **user** raw id to an inner id. From 34dd04bfbfafbb9578993625866f4fb9ae8e87ab Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 4 Apr 2018 10:26:44 -0400 Subject: [PATCH 16/60] modified predict and estimate methods --- surprise/prediction_algorithms/algo_base.py | 18 ++++++++++++------ surprise/prediction_algorithms/features.py | 17 ++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 844cb44e..2f8cf950 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -9,6 +9,7 @@ class :class:`AlgoBase` from which every single prediction algorithm has to import warnings from six import get_unbound_function as guf +import numpy as np from .. import similarities as sims from .predictions import PredictionImpossible @@ -37,7 +38,7 @@ def __init__(self, **kwargs): self.skip_train = False if (guf(self.__class__.fit) is guf(AlgoBase.fit) and - guf(self.__class__.train) is not guf(AlgoBase.train)): + guf(self.__class__.train) is not guf(AlgoBase.train)): warnings.warn('It looks like this algorithm (' + str(self.__class__) + ') implements train() ' @@ -96,7 +97,8 @@ def fit(self, trainset): return self - def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): + def predict(self, uid, iid, u_features=[], i_features=[], r_ui=None, + clip=True, verbose=False): """Compute the rating prediction for given user and item. The ``predict`` method converts raw ids to inner ids and then calls the @@ -108,6 +110,10 @@ def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): Args: uid: (Raw) id of the user. See :ref:`this note`. iid: (Raw) id of the item. See :ref:`this note`. + u_features: List of user features in the same order as used in + the ``fit`` method. Optional, default is empty list. + i_features: List of item features in the same order as used in + the ``fit`` method. Optional, default is empty list. r_ui(float): The true rating :math:`r_{ui}`. Optional, default is ``None``. clip(bool): Whether to clip the estimation into the rating scale. @@ -143,7 +149,7 @@ def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): details = {} try: - est = self.estimate(iuid, iiid) + est = self.estimate(iuid, iiid, u_features, i_features) # If the details dict was also returned if isinstance(est, tuple): @@ -240,7 +246,7 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - print('Estimating biases using', method_name + '...') + # print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: @@ -287,9 +293,9 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - print('Computing the {0} similarity matrix...'.format(name)) + # print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - print('Done computing similarity matrix.') + # print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/features.py index b7d435c7..774678af 100644 --- a/surprise/prediction_algorithms/features.py +++ b/surprise/prediction_algorithms/features.py @@ -59,7 +59,8 @@ def lasso(self, trainset): if (self.trainset.n_user_features == 0 or self.trainset.n_item_features == 0): - raise ValueError('trainset does not contain user and/or item features.') + raise ValueError('trainset does not contain user and/or item ' + 'features.') n_ratings = self.trainset.n_ratings n_uf = self.trainset.n_user_features @@ -82,16 +83,14 @@ def lasso(self, trainset): self.coef = reg.coef_ self.intercept = reg.intercept_ - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): - if not (self.trainset.has_user_features(u) and - self.trainset.has_item_features(i)): - raise PredictionImpossible('User and/or item features ' - 'are unknown.') + features = np.concatenate([u_features, i_features]) - x = np.concatenate((self.trainset.u_features[u], - self.trainset.i_features[i])) + if len(features) != len(self.coef): + raise PredictionImpossible('User and/or item features ' + 'are incomplete.') - est = self.intercept + np.dot(x, self.coef) + est = self.intercept + np.dot(features, self.coef) return est From d275f8463fdf686f2e33ef22c42a5b8980bdce88 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 4 Apr 2018 16:35:18 -0400 Subject: [PATCH 17/60] include features in testset and prediction objects --- surprise/accuracy.py | 6 ++-- surprise/dataset.py | 32 +++++++++---------- surprise/evaluate.py | 2 ++ surprise/model_selection/split.py | 4 +-- surprise/prediction_algorithms/algo_base.py | 14 ++++---- .../prediction_algorithms/baseline_only.py | 2 +- surprise/prediction_algorithms/features.py | 18 ++++++++--- surprise/prediction_algorithms/knns.py | 8 ++--- surprise/prediction_algorithms/predictions.py | 13 +++++++- surprise/trainset.py | 24 ++++++++++---- 10 files changed, 78 insertions(+), 45 deletions(-) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 1e9e4855..d6e8a88f 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (_, _, true_r, est, _) in predictions]) + for (__, __, __, __, true_r, est, __) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (_, _, true_r, est, _) in predictions]) + for (__, __, __, __, true_r, est, __) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -117,7 +117,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, _, r0, est, _ in predictions: + for u0, __, __, __, r0, est, __ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/dataset.py b/surprise/dataset.py index 980f797d..00dd623f 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -55,6 +55,8 @@ def __init__(self, reader): self.reader = reader self.user_features_nb = 0 self.item_features_nb = 0 + self.user_features = {} + self.item_features = {} @classmethod def load_builtin(cls, name='ml-100k'): @@ -240,11 +242,11 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) - u_features = defaultdict(list) - i_features = defaultdict(list) + u_features = {} + i_features = {} # user raw id, item raw id, translated rating, time stamp - for urid, irid, r, timestamp in raw_trainset: + for urid, irid, r, __ in raw_trainset: try: uid = raw2inner_id_users[urid] except KeyError: @@ -252,12 +254,8 @@ def construct_trainset(self, raw_trainset): raw2inner_id_users[urid] = current_u_index current_u_index += 1 if self.user_features_nb > 0: - try: - u_features[uid] = self.user_features[urid] - except KeyError: - print('user ' + str(urid) + ' does not exist in ' + - 'self.user_features') - raise + u_features[uid] = self.user_features.get(urid, None) + try: iid = raw2inner_id_items[irid] except KeyError: @@ -265,12 +263,7 @@ def construct_trainset(self, raw_trainset): raw2inner_id_items[irid] = current_i_index current_i_index += 1 if self.item_features_nb > 0: - try: - i_features[iid] = self.item_features[irid] - except KeyError: - print('item ' + str(irid) + ' does not exist in ' + - 'self.item_features') - raise + i_features[iid] = self.item_features.get(irid, None) ur[uid].append((iid, r)) ir[iid].append((uid, r)) @@ -297,8 +290,13 @@ def construct_trainset(self, raw_trainset): def construct_testset(self, raw_testset): - return [(ruid, riid, r_ui_trans) - for (ruid, riid, r_ui_trans, __) in raw_testset] + testset = [] + for (ruid, riid, r_ui_trans, __) in raw_testset: + u_features = self.user_features.get(ruid, None) + i_features = self.item_features.get(riid, None) + testset.append((ruid, riid, u_features, i_features, r_ui_trans)) + + return testset class DatasetUserFolds(Dataset): diff --git a/surprise/evaluate.py b/surprise/evaluate.py index 19e80fa5..bb283356 100644 --- a/surprise/evaluate.py +++ b/surprise/evaluate.py @@ -301,6 +301,7 @@ class CaseInsensitiveDefaultDict(defaultdict): Used for the returned dict, so that users can use perf['RMSE'] or perf['rmse'] indifferently. """ + def __setitem__(self, key, value): super(CaseInsensitiveDefaultDict, self).__setitem__(key.lower(), value) @@ -333,4 +334,5 @@ def seed_and_eval(seed, *args): different processes.""" random.seed(seed) + return evaluate(*args, verbose=0) diff --git a/surprise/model_selection/split.py b/surprise/model_selection/split.py index 14697911..ee9dfc64 100644 --- a/surprise/model_selection/split.py +++ b/surprise/model_selection/split.py @@ -276,7 +276,7 @@ def split(self, data): self.test_size, self.train_size, len(data.raw_ratings)) rng = get_rng(self.random_state) - for _ in range(self.n_splits): + for __ in range(self.n_splits): if self.shuffle: permutation = rng.permutation(len(data.raw_ratings)) @@ -372,7 +372,7 @@ def split(self, data): Args: data(:obj:`Dataset`): The data containing - ratings that will be devided into trainsets and testsets. + ratings that will be divided into trainsets and testsets. Yields: tuple of (trainset, testset) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 2f8cf950..41692cae 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -9,7 +9,6 @@ class :class:`AlgoBase` from which every single prediction algorithm has to import warnings from six import get_unbound_function as guf -import numpy as np from .. import similarities as sims from .predictions import PredictionImpossible @@ -97,7 +96,7 @@ def fit(self, trainset): return self - def predict(self, uid, iid, u_features=[], i_features=[], r_ui=None, + def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, clip=True, verbose=False): """Compute the rating prediction for given user and item. @@ -111,9 +110,9 @@ def predict(self, uid, iid, u_features=[], i_features=[], r_ui=None, uid: (Raw) id of the user. See :ref:`this note`. iid: (Raw) id of the item. See :ref:`this note`. u_features: List of user features in the same order as used in - the ``fit`` method. Optional, default is empty list. + the ``fit`` method. Optional, default is ``None``. i_features: List of item features in the same order as used in - the ``fit`` method. Optional, default is empty list. + the ``fit`` method. Optional, default is ``None``. r_ui(float): The true rating :math:`r_{ui}`. Optional, default is ``None``. clip(bool): Whether to clip the estimation into the rating scale. @@ -172,7 +171,7 @@ def predict(self, uid, iid, u_features=[], i_features=[], r_ui=None, est = min(higher_bound, est) est = max(lower_bound, est) - pred = Prediction(uid, iid, r_ui, est, details) + pred = Prediction(uid, iid, u_features, i_features, r_ui, est, details) if verbose: print(pred) @@ -213,9 +212,12 @@ def test(self, testset, verbose=False): # The ratings are translated back to their original scale. predictions = [self.predict(uid, iid, + u_features, + i_features, r_ui_trans - self.trainset.offset, verbose=verbose) - for (uid, iid, r_ui_trans) in testset] + for (uid, iid, u_features, i_features, r_ui_trans) + in testset] return predictions def compute_baselines(self): diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index 85221114..b3590d57 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -35,7 +35,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): est = self.trainset.global_mean if self.trainset.knows_user(u): diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/features.py index 774678af..6133816b 100644 --- a/surprise/prediction_algorithms/features.py +++ b/surprise/prediction_algorithms/features.py @@ -72,8 +72,16 @@ def lasso(self, trainset): y = np.empty((n_ratings,)) for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): y[k] = rating - X[k, :n_uf] = u_features[uid] - X[k, n_uf:] = i_features[iid] + try: + X[k, :n_uf] = u_features[uid] + except KeyError: + raise KeyError('No features for user ' + + str(self.trainset.to_raw_uid(uid))) + try: + X[k, n_uf:] = i_features[iid] + except KeyError: + raise KeyError('No features for item ' + + str(self.trainset.to_raw_iid(iid))) reg = linear_model.Lasso(alpha=0.1) reg.fit(X, y) @@ -87,9 +95,11 @@ def estimate(self, u, i, u_features, i_features): features = np.concatenate([u_features, i_features]) - if len(features) != len(self.coef): + if (u_features is None or + i_features is None or + len(features) != len(self.coef)): raise PredictionImpossible('User and/or item features ' - 'are incomplete.') + 'are missing.') est = self.intercept + np.dot(features, self.coef) diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 069da4d3..410d5d17 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -96,7 +96,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -176,7 +176,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -271,7 +271,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): est = self.trainset.global_mean if self.trainset.knows_user(u): @@ -370,7 +370,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 76bc8ddc..9e971ee9 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -21,7 +21,8 @@ class PredictionImpossible(Exception): class Prediction(namedtuple('Prediction', - ['uid', 'iid', 'r_ui', 'est', 'details'])): + ['uid', 'iid', 'u_features', 'i_features', 'r_ui', + 'est', 'details'])): """A named tuple for storing the results of a prediction. It's wrapped in a class, but only for documentation and printing purposes. @@ -29,6 +30,8 @@ class Prediction(namedtuple('Prediction', Args: uid: The (raw) user id. See :ref:`this note`. iid: The (raw) item id. See :ref:`this note`. + u_features: The user features. + i_features: The item features. r_ui(float): The true rating :math:`r_{ui}`. est(float): The estimated rating :math:`\\hat{r}_{ui}`. details (dict): Stores additional details about the prediction that @@ -40,6 +43,14 @@ class Prediction(namedtuple('Prediction', def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) + if self.u_features is not None: + pass + else: + s += 'u_features = None ' + if self.i_features is not None: + pass + else: + s += 'i_features = None ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: diff --git a/surprise/trainset.py b/surprise/trainset.py index 4da7ddf1..fc1cebe7 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -237,8 +237,14 @@ def build_testset(self): cases where you want to to test your algorithm on the trainset. """ - return [(self.to_raw_uid(u), self.to_raw_iid(i), r) - for (u, i, r) in self.all_ratings()] + testset = [] + for (u, i, r) in self.all_ratings(): + u_features = self.u_features.get(u, None) + i_features = self.i_features.get(i, None) + testset.append((self.to_raw_uid(u), self.to_raw_iid(i), u_features, + i_features, r)) + + return testset def build_anti_testset(self, fill=None): """Return a list of ratings that can be used as a testset in the @@ -264,10 +270,14 @@ def build_anti_testset(self, fill=None): anti_testset = [] for u in self.all_users(): - user_items = set([j for (j, _) in self.ur[u]]) - anti_testset += [(self.to_raw_uid(u), self.to_raw_iid(i), fill) for - i in self.all_items() if - i not in user_items] + user_items = set([j for (j, __) in self.ur[u]]) + anti_testset += [(self.to_raw_uid(u), + self.to_raw_iid(i), + self.u_features.get(u, None), + self.i_features.get(i, None), + fill) + for i in self.all_items() + if i not in user_items] return anti_testset def all_users(self): @@ -292,7 +302,7 @@ def global_mean(self): It's only computed once.""" if self._global_mean is None: - self._global_mean = np.mean([r for (_, _, r) in + self._global_mean = np.mean([r for (__, __, r) in self.all_ratings()]) return self._global_mean From 14d1248229b74d4ca2e3181c6189dadb97cd62ba Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 4 Apr 2018 17:53:21 -0400 Subject: [PATCH 18/60] update matrix factorization estimate method --- surprise/prediction_algorithms/matrix_factorization.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index fe2f1e56..1a790e5f 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -253,7 +253,7 @@ class SVD(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -484,7 +484,7 @@ class SVDpp(AlgoBase): self.qi = qi self.yj = yj - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): est = self.trainset.global_mean @@ -715,7 +715,7 @@ class NMF(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i): + def estimate(self, u, i, u_features, i_features): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -737,6 +737,6 @@ class NMF(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unkown.') + raise PredictionImpossible('User and item are unknown.') return est From a2b87c47b2613fde54532b8bc715df598e309820 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 4 Apr 2018 18:01:39 -0400 Subject: [PATCH 19/60] adapt estimate methods for all prediction algorithms --- surprise/prediction_algorithms/baseline_only.py | 2 +- surprise/prediction_algorithms/co_clustering.pyx | 2 +- surprise/prediction_algorithms/knns.py | 8 ++++---- surprise/prediction_algorithms/matrix_factorization.pyx | 6 +++--- surprise/prediction_algorithms/random_pred.py | 2 +- surprise/prediction_algorithms/slope_one.pyx | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index b3590d57..e4d9778c 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -35,7 +35,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): est = self.trainset.global_mean if self.trainset.knows_user(u): diff --git a/surprise/prediction_algorithms/co_clustering.pyx b/surprise/prediction_algorithms/co_clustering.pyx index 408780fc..56dea0d9 100644 --- a/surprise/prediction_algorithms/co_clustering.pyx +++ b/surprise/prediction_algorithms/co_clustering.pyx @@ -236,7 +236,7 @@ class CoClustering(AlgoBase): return avg_cltr_u, avg_cltr_i, avg_cocltr - def estimate(self, u, i): + def estimate(self, u, i, *__): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): return self.trainset.global_mean diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 410d5d17..4b71ccd5 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -96,7 +96,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -176,7 +176,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -271,7 +271,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): est = self.trainset.global_mean if self.trainset.knows_user(u): @@ -370,7 +370,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index 1a790e5f..98a87e5e 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -253,7 +253,7 @@ class SVD(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -484,7 +484,7 @@ class SVDpp(AlgoBase): self.qi = qi self.yj = yj - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): est = self.trainset.global_mean @@ -715,7 +715,7 @@ class NMF(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, u_features, i_features): + def estimate(self, u, i, *__): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) diff --git a/surprise/prediction_algorithms/random_pred.py b/surprise/prediction_algorithms/random_pred.py index 86c4dcf1..4196fd49 100644 --- a/surprise/prediction_algorithms/random_pred.py +++ b/surprise/prediction_algorithms/random_pred.py @@ -40,6 +40,6 @@ def fit(self, trainset): return self - def estimate(self, *_): + def estimate(self, *__): return np.random.normal(self.trainset.global_mean, self.sigma) diff --git a/surprise/prediction_algorithms/slope_one.pyx b/surprise/prediction_algorithms/slope_one.pyx index 8049a6cf..52320196 100644 --- a/surprise/prediction_algorithms/slope_one.pyx +++ b/surprise/prediction_algorithms/slope_one.pyx @@ -79,7 +79,7 @@ class SlopeOne(AlgoBase): return self - def estimate(self, u, i): + def estimate(self, u, i, *__): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') From 3c5f7e6beb23ea335e6517e7cb8d5834af3eb39e Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 09:41:19 -0400 Subject: [PATCH 20/60] add sklearn arguments to Lasso --- surprise/prediction_algorithms/features.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/features.py index 6133816b..5c107902 100644 --- a/surprise/prediction_algorithms/features.py +++ b/surprise/prediction_algorithms/features.py @@ -44,9 +44,20 @@ class Lasso(AlgoBase): options. """ - def __init__(self, **kwargs): + def __init__(self, alpha=1.0, fit_intercept=True, normalize=False, + precompute=False, max_iter=1000, tol=0.0001, positive=False, + random_state=None, selection='cyclic', **kwargs): AlgoBase.__init__(self, **kwargs) + self.alpha = alpha + self.fit_intercept = fit_intercept + self.normalize = normalize + self.precompute = precompute + self.max_iter = max_iter + self.tol = tol + self.positive = positive + self.random_state = random_state + self.selection = selection def fit(self, trainset): @@ -83,7 +94,11 @@ def lasso(self, trainset): raise KeyError('No features for item ' + str(self.trainset.to_raw_iid(iid))) - reg = linear_model.Lasso(alpha=0.1) + reg = linear_model.Lasso( + alpha=self.alpha, fit_intercept=self.fit_intercept, + normalize=self.normalize, precompute=self.precompute, + max_iter=self.max_iter, tol=self.tol, positive=self.positive, + random_state=self.random_state, selection=self.selection) reg.fit(X, y) # self.X = X From 7b82e78a3cf2dfd094cd93a2717161571f8afcea Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 10:08:12 -0400 Subject: [PATCH 21/60] single underscore for dummy variable --- surprise/accuracy.py | 6 +++--- surprise/dataset.py | 4 ++-- surprise/model_selection/search.py | 2 ++ surprise/model_selection/split.py | 2 +- surprise/prediction_algorithms/baseline_only.py | 2 +- surprise/prediction_algorithms/co_clustering.pyx | 2 +- surprise/prediction_algorithms/features.py | 2 -- surprise/prediction_algorithms/knns.py | 8 ++++---- surprise/prediction_algorithms/matrix_factorization.pyx | 6 +++--- surprise/prediction_algorithms/random_pred.py | 2 +- surprise/prediction_algorithms/slope_one.pyx | 2 +- surprise/trainset.py | 4 ++-- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index d6e8a88f..0bfcd1af 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (__, __, __, __, true_r, est, __) in predictions]) + for (_, _, _, _, true_r, est, _) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (__, __, __, __, true_r, est, __) in predictions]) + for (_, _, _, _, true_r, est, _) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -117,7 +117,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, __, __, __, r0, est, __ in predictions: + for u0, _, _, _, r0, est, _ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/dataset.py b/surprise/dataset.py index 00dd623f..349e9875 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -246,7 +246,7 @@ def construct_trainset(self, raw_trainset): i_features = {} # user raw id, item raw id, translated rating, time stamp - for urid, irid, r, __ in raw_trainset: + for urid, irid, r, _ in raw_trainset: try: uid = raw2inner_id_users[urid] except KeyError: @@ -291,7 +291,7 @@ def construct_trainset(self, raw_trainset): def construct_testset(self, raw_testset): testset = [] - for (ruid, riid, r_ui_trans, __) in raw_testset: + for (ruid, riid, r_ui_trans, _) in raw_testset: u_features = self.user_features.get(ruid, None) i_features = self.item_features.get(riid, None) testset.append((ruid, riid, u_features, i_features, r_ui_trans)) diff --git a/surprise/model_selection/search.py b/surprise/model_selection/search.py index 9489c88b..afa2e2a6 100644 --- a/surprise/model_selection/search.py +++ b/surprise/model_selection/search.py @@ -294,6 +294,7 @@ class GridSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ + def __init__(self, algo_class, param_grid, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, pre_dispatch='2*n_jobs', joblib_verbose=0): @@ -410,6 +411,7 @@ class RandomizedSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ + def __init__(self, algo_class, param_distributions, n_iter=10, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, diff --git a/surprise/model_selection/split.py b/surprise/model_selection/split.py index ee9dfc64..5c656565 100644 --- a/surprise/model_selection/split.py +++ b/surprise/model_selection/split.py @@ -276,7 +276,7 @@ def split(self, data): self.test_size, self.train_size, len(data.raw_ratings)) rng = get_rng(self.random_state) - for __ in range(self.n_splits): + for _ in range(self.n_splits): if self.shuffle: permutation = rng.permutation(len(data.raw_ratings)) diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index e4d9778c..7ae10a22 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -35,7 +35,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): est = self.trainset.global_mean if self.trainset.knows_user(u): diff --git a/surprise/prediction_algorithms/co_clustering.pyx b/surprise/prediction_algorithms/co_clustering.pyx index 56dea0d9..8fded133 100644 --- a/surprise/prediction_algorithms/co_clustering.pyx +++ b/surprise/prediction_algorithms/co_clustering.pyx @@ -236,7 +236,7 @@ class CoClustering(AlgoBase): return avg_cltr_u, avg_cltr_i, avg_cocltr - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): return self.trainset.global_mean diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/features.py index 5c107902..cc41e4df 100644 --- a/surprise/prediction_algorithms/features.py +++ b/surprise/prediction_algorithms/features.py @@ -5,8 +5,6 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import numpy as np -from six import iteritems -import heapq from sklearn import linear_model from .predictions import PredictionImpossible diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 4b71ccd5..e5c5d8ad 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -96,7 +96,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -176,7 +176,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -271,7 +271,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): est = self.trainset.global_mean if self.trainset.knows_user(u): @@ -370,7 +370,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index 98a87e5e..7a3cede5 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -253,7 +253,7 @@ class SVD(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -484,7 +484,7 @@ class SVDpp(AlgoBase): self.qi = qi self.yj = yj - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): est = self.trainset.global_mean @@ -715,7 +715,7 @@ class NMF(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) diff --git a/surprise/prediction_algorithms/random_pred.py b/surprise/prediction_algorithms/random_pred.py index 4196fd49..86c4dcf1 100644 --- a/surprise/prediction_algorithms/random_pred.py +++ b/surprise/prediction_algorithms/random_pred.py @@ -40,6 +40,6 @@ def fit(self, trainset): return self - def estimate(self, *__): + def estimate(self, *_): return np.random.normal(self.trainset.global_mean, self.sigma) diff --git a/surprise/prediction_algorithms/slope_one.pyx b/surprise/prediction_algorithms/slope_one.pyx index 52320196..f986e496 100644 --- a/surprise/prediction_algorithms/slope_one.pyx +++ b/surprise/prediction_algorithms/slope_one.pyx @@ -79,7 +79,7 @@ class SlopeOne(AlgoBase): return self - def estimate(self, u, i, *__): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/trainset.py b/surprise/trainset.py index fc1cebe7..6aec0744 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -270,7 +270,7 @@ def build_anti_testset(self, fill=None): anti_testset = [] for u in self.all_users(): - user_items = set([j for (j, __) in self.ur[u]]) + user_items = set([j for (j, _) in self.ur[u]]) anti_testset += [(self.to_raw_uid(u), self.to_raw_iid(i), self.u_features.get(u, None), @@ -302,7 +302,7 @@ def global_mean(self): It's only computed once.""" if self._global_mean is None: - self._global_mean = np.mean([r for (__, __, r) in + self._global_mean = np.mean([r for (_, _, r) in self.all_ratings()]) return self._global_mean From bf335c28e08c423380eb8e7ce8fca6fd36b1dcff Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 10:27:53 -0400 Subject: [PATCH 22/60] update documentation for Lasso and change filename --- surprise/prediction_algorithms/__init__.py | 2 +- .../{features.py => linear.py} | 40 +++++++------------ 2 files changed, 16 insertions(+), 26 deletions(-) rename surprise/prediction_algorithms/{features.py => linear.py} (68%) diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 61091131..1a719a97 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -32,7 +32,7 @@ from .matrix_factorization import NMF from .slope_one import SlopeOne from .co_clustering import CoClustering -from .features import Lasso +from .linear import Lasso from .predictions import PredictionImpossible from .predictions import Prediction diff --git a/surprise/prediction_algorithms/features.py b/surprise/prediction_algorithms/linear.py similarity index 68% rename from surprise/prediction_algorithms/features.py rename to surprise/prediction_algorithms/linear.py index cc41e4df..5a56f91d 100644 --- a/surprise/prediction_algorithms/features.py +++ b/surprise/prediction_algorithms/linear.py @@ -1,5 +1,5 @@ """ -the :mod:`features` module includes some features-based algorithms. +the :mod:`linear` module includes linear features-based algorithms. """ from __future__ import (absolute_import, division, print_function, @@ -12,39 +12,29 @@ class Lasso(AlgoBase): - """A basic linear regression algorithm. + """A basic lasso algorithm with user-item interaction terms. The prediction :math:`\\hat{r}_{ui}` is set as: .. math:: - \hat{r}_{ui} = \\frac{ - \\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v) \cdot r_{vi}} - {\\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v)} + \hat{r}_{ui} = \alpha_0 + \alpha_1^\top y_u + \alpha_2^\top z_i + + \alpha_3^\top \text{vec}(y_u \otimes z_i) - or - - .. math:: - \hat{r}_{ui} = \\frac{ - \\sum\\limits_{j \in N^k_u(i)} \\text{sim}(i, j) \cdot r_{uj}} - {\\sum\\limits_{j \in N^k_u(j)} \\text{sim}(i, j)} - - depending on the ``user_based`` field of the ``sim_options`` parameter. + where :math:`\alpha_0 \in \mathbb{R}, \alpha_1 \in \mathbb{R}^o, \alpha_2 + \in \mathbb{R}^p` and :math:`\alpha_3 \in \mathbb{R}^{op}` are coefficient + vectors, and :math:`\otimes` represent the Kronecker product of two vectors + (i.e., all possible cross-product combinations). Args: - k(int): The (max) number of neighbors to take into account for - aggregation (see :ref:`this note `). Default is - ``40``. - min_k(int): The minimum number of neighbors to take into account for - aggregation. If there are not enough neighbors, the prediction is - set the the global mean of all ratings. Default is ``1``. - sim_options(dict): A dictionary of options for the similarity - measure. See :ref:`similarity_measures_configuration` for accepted - options. + add_interactions(bool): Whether to add user-item interaction terms. + Optional, default is True. + other args: See ``sklearn`` documentation for ``linear_model.Lasso``. """ - def __init__(self, alpha=1.0, fit_intercept=True, normalize=False, - precompute=False, max_iter=1000, tol=0.0001, positive=False, - random_state=None, selection='cyclic', **kwargs): + def __init__(self, add_interactions=True, alpha=1.0, fit_intercept=True, + normalize=False, precompute=False, max_iter=1000, tol=0.0001, + positive=False, random_state=None, selection='cyclic', + **kwargs): AlgoBase.__init__(self, **kwargs) self.alpha = alpha From e34a5f9d3c30bafce876528bb1fce845e21a5507 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 10:41:56 -0400 Subject: [PATCH 23/60] correct conflict with master --- surprise/prediction_algorithms/algo_base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 41692cae..4fdcd8f0 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -248,7 +248,8 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - # print('Estimating biases using', method_name + '...') + if self.verbose: + print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: @@ -295,9 +296,11 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - # print('Computing the {0} similarity matrix...'.format(name)) + if self.verbose: + print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - # print('Done computing similarity matrix.') + if self.verbose: + print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + From 4081244a92648fbc51ff0e8549a4b6d34b778916 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 11:20:46 -0400 Subject: [PATCH 24/60] add interaction terms in Lasso --- surprise/prediction_algorithms/linear.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 5a56f91d..282d53c9 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -82,6 +82,11 @@ def lasso(self, trainset): raise KeyError('No features for item ' + str(self.trainset.to_raw_iid(iid))) + if self.add_interactions: + temp = np.array([X[:, u] * X[:, i] for u in range(n_uf) + for i in range(n_uf, n_uf + n_if)]).T + X = np.concatenate([X, temp], axis=1) + reg = linear_model.Lasso( alpha=self.alpha, fit_intercept=self.fit_intercept, normalize=self.normalize, precompute=self.precompute, @@ -89,8 +94,8 @@ def lasso(self, trainset): random_state=self.random_state, selection=self.selection) reg.fit(X, y) - # self.X = X - # self.y = y + self.X = X + self.y = y self.coef = reg.coef_ self.intercept = reg.intercept_ From d3dd0dd2bf55420009549041e8eb81144469fbbf Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 11:43:11 -0400 Subject: [PATCH 25/60] add interaction terms to Lasso.estimate --- surprise/prediction_algorithms/linear.py | 42 ++++++++++++++++-------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 282d53c9..5d29b96f 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -74,18 +74,16 @@ def lasso(self, trainset): try: X[k, :n_uf] = u_features[uid] except KeyError: - raise KeyError('No features for user ' + - str(self.trainset.to_raw_uid(uid))) + raise ValueError('No features for user ' + + str(self.trainset.to_raw_uid(uid))) try: X[k, n_uf:] = i_features[iid] except KeyError: - raise KeyError('No features for item ' + - str(self.trainset.to_raw_iid(iid))) + raise ValueError('No features for item ' + + str(self.trainset.to_raw_iid(iid))) if self.add_interactions: - temp = np.array([X[:, u] * X[:, i] for u in range(n_uf) - for i in range(n_uf, n_uf + n_if)]).T - X = np.concatenate([X, temp], axis=1) + X = self.add_interactions(X) reg = linear_model.Lasso( alpha=self.alpha, fit_intercept=self.fit_intercept, @@ -99,16 +97,32 @@ def lasso(self, trainset): self.coef = reg.coef_ self.intercept = reg.intercept_ - def estimate(self, u, i, u_features, i_features): + def add_interactions(self, X): + + n_uf = self.trainset.n_user_features + n_if = self.trainset.n_item_features + + temp = np.array([X[:, u] * X[:, i] for u in range(n_uf) + for i in range(n_uf, n_uf + n_if)]).T + X = np.concatenate([X, temp], axis=1) - features = np.concatenate([u_features, i_features]) + return X + + def estimate(self, u, i, u_features, i_features): if (u_features is None or - i_features is None or - len(features) != len(self.coef)): - raise PredictionImpossible('User and/or item features ' - 'are missing.') + len(u_features) != self.trainset.n_user_features): + raise PredictionImpossible('User features are missing.') + + if (i_features is None or + len(i_features) != self.trainset.n_item_features): + raise PredictionImpossible('Item features are missing.') + + X = np.concatenate([u_features, i_features]) + + if self.add_interactions: + X = self.add_interactions(X) - est = self.intercept + np.dot(features, self.coef) + est = self.intercept + np.dot(X, self.coef) return est From 47ff4771b5b544be8bff9e88ee4ed560298e6e04 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 11:52:07 -0400 Subject: [PATCH 26/60] correct conflicts with master --- surprise/prediction_algorithms/baseline_only.py | 3 ++- surprise/prediction_algorithms/co_clustering.pyx | 2 +- surprise/prediction_algorithms/knns.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index 7ae10a22..05886657 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -24,9 +24,10 @@ class BaselineOnly(AlgoBase): """ - def __init__(self, bsl_options={}): + def __init__(self, bsl_options={}, verbose=True): AlgoBase.__init__(self, bsl_options=bsl_options) + self.verbose = verbose def fit(self, trainset): diff --git a/surprise/prediction_algorithms/co_clustering.pyx b/surprise/prediction_algorithms/co_clustering.pyx index 8fded133..85837718 100644 --- a/surprise/prediction_algorithms/co_clustering.pyx +++ b/surprise/prediction_algorithms/co_clustering.pyx @@ -62,7 +62,7 @@ class CoClustering(AlgoBase): self.n_cltr_u = n_cltr_u self.n_cltr_i = n_cltr_i self.n_epochs = n_epochs - self.verbose=verbose + self.verbose = verbose self.random_state = random_state def fit(self, trainset): diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index e5c5d8ad..efd838c2 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -27,9 +27,10 @@ class SymmetricAlgo(AlgoBase): reversed. """ - def __init__(self, sim_options={}, **kwargs): + def __init__(self, sim_options={}, verbose=True, **kwargs): AlgoBase.__init__(self, sim_options=sim_options, **kwargs) + self.verbose = verbose def fit(self, trainset): From 127942414423c50ab5d18a5005b14952e731e6d2 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 12:04:46 -0400 Subject: [PATCH 27/60] correct verbose conflicts in knns --- surprise/prediction_algorithms/knns.py | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index efd838c2..aa3fb7d8 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -82,11 +82,14 @@ class KNNBasic(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k @@ -157,11 +160,14 @@ class KNNWithMeans(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, + verbose=verbose, **kwargs) self.k = k self.min_k = min_k @@ -248,17 +254,19 @@ class KNNBaseline(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. It is recommended to use the :func:`pearson_baseline ` similarity measure. - bsl_options(dict): A dictionary of options for the baseline estimates computation. See :ref:`baseline_estimates_configuration` for accepted options. - + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}): + def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, + verbose=True, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, - bsl_options=bsl_options) + bsl_options=bsl_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k @@ -343,11 +351,14 @@ class KNNWithZScore(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k From 62ccd84e75dbcbb8c3993692991134b2513f7a3f Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 12:15:45 -0400 Subject: [PATCH 28/60] add add_interactions to self in Lasso --- surprise/prediction_algorithms/linear.py | 1 + 1 file changed, 1 insertion(+) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 5d29b96f..562b9975 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -37,6 +37,7 @@ def __init__(self, add_interactions=True, alpha=1.0, fit_intercept=True, **kwargs): AlgoBase.__init__(self, **kwargs) + self.add_interactions = add_interactions self.alpha = alpha self.fit_intercept = fit_intercept self.normalize = normalize From 53b869711ce7208e5a79cfd15b7b826c536b7493 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 12:40:43 -0400 Subject: [PATCH 29/60] change add_interactions fn name --- surprise/prediction_algorithms/linear.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 562b9975..ce22d7eb 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -84,7 +84,7 @@ def lasso(self, trainset): str(self.trainset.to_raw_iid(iid))) if self.add_interactions: - X = self.add_interactions(X) + X = self.add_interactions_fn(X) reg = linear_model.Lasso( alpha=self.alpha, fit_intercept=self.fit_intercept, @@ -98,7 +98,7 @@ def lasso(self, trainset): self.coef = reg.coef_ self.intercept = reg.intercept_ - def add_interactions(self, X): + def add_interactions_fn(self, X): n_uf = self.trainset.n_user_features n_if = self.trainset.n_item_features @@ -122,7 +122,7 @@ def estimate(self, u, i, u_features, i_features): X = np.concatenate([u_features, i_features]) if self.add_interactions: - X = self.add_interactions(X) + X = self.add_interactions_fn(X) est = self.intercept + np.dot(X, self.coef) From 7e342989dd9999997d9c37bc3b6d1443889f1895 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 13:15:04 -0400 Subject: [PATCH 30/60] remove add_interactions_fn --- surprise/prediction_algorithms/linear.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index ce22d7eb..2a11edd8 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -84,7 +84,9 @@ def lasso(self, trainset): str(self.trainset.to_raw_iid(iid))) if self.add_interactions: - X = self.add_interactions_fn(X) + temp = np.array([X[:, v] * X[:, j] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)]).T + X = np.concatenate([X, temp], axis=1) reg = linear_model.Lasso( alpha=self.alpha, fit_intercept=self.fit_intercept, @@ -98,31 +100,23 @@ def lasso(self, trainset): self.coef = reg.coef_ self.intercept = reg.intercept_ - def add_interactions_fn(self, X): + def estimate(self, u, i, u_features, i_features): n_uf = self.trainset.n_user_features n_if = self.trainset.n_item_features - temp = np.array([X[:, u] * X[:, i] for u in range(n_uf) - for i in range(n_uf, n_uf + n_if)]).T - X = np.concatenate([X, temp], axis=1) - - return X - - def estimate(self, u, i, u_features, i_features): - - if (u_features is None or - len(u_features) != self.trainset.n_user_features): + if u_features is None or len(u_features) != n_uf: raise PredictionImpossible('User features are missing.') - if (i_features is None or - len(i_features) != self.trainset.n_item_features): + if i_features is None or len(i_features) != n_if: raise PredictionImpossible('Item features are missing.') X = np.concatenate([u_features, i_features]) if self.add_interactions: - X = self.add_interactions_fn(X) + temp = np.array([X[v] * X[i] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)]) + X = np.concatenate([X, temp]) est = self.intercept + np.dot(X, self.coef) From 39c2601ac71c42facfe4717de746922661174db8 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 13:40:12 -0400 Subject: [PATCH 31/60] correct bad index --- surprise/prediction_algorithms/linear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 2a11edd8..c3649802 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -114,7 +114,7 @@ def estimate(self, u, i, u_features, i_features): X = np.concatenate([u_features, i_features]) if self.add_interactions: - temp = np.array([X[v] * X[i] for v in range(n_uf) + temp = np.array([X[v] * X[j] for v in range(n_uf) for j in range(n_uf, n_uf + n_if)]) X = np.concatenate([X, temp]) From 4f3c3a86e462f30aede6ffd029a94b0025ad53cc Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 15:39:39 -0400 Subject: [PATCH 32/60] pep8 and description --- surprise/prediction_algorithms/linear.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index c3649802..d72c4679 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -17,11 +17,11 @@ class Lasso(AlgoBase): The prediction :math:`\\hat{r}_{ui}` is set as: .. math:: - \hat{r}_{ui} = \alpha_0 + \alpha_1^\top y_u + \alpha_2^\top z_i + - \alpha_3^\top \text{vec}(y_u \otimes z_i) + \hat{r}_{ui} = \alpha_1 + \alpha_2^\top y_u + \alpha_3^\top z_i + + \alpha_4^\top \text{vec}(y_u \otimes z_i) - where :math:`\alpha_0 \in \mathbb{R}, \alpha_1 \in \mathbb{R}^o, \alpha_2 - \in \mathbb{R}^p` and :math:`\alpha_3 \in \mathbb{R}^{op}` are coefficient + where :math:`\alpha_1 \in \mathbb{R}, \alpha_2 \in \mathbb{R}^o, \alpha_3 + \in \mathbb{R}^p` and :math:`\alpha_4 \in \mathbb{R}^{op}` are coefficient vectors, and :math:`\otimes` represent the Kronecker product of two vectors (i.e., all possible cross-product combinations). @@ -85,7 +85,7 @@ def lasso(self, trainset): if self.add_interactions: temp = np.array([X[:, v] * X[:, j] for v in range(n_uf) - for j in range(n_uf, n_uf + n_if)]).T + for j in range(n_uf, n_uf + n_if)]).T X = np.concatenate([X, temp], axis=1) reg = linear_model.Lasso( @@ -115,7 +115,7 @@ def estimate(self, u, i, u_features, i_features): if self.add_interactions: temp = np.array([X[v] * X[j] for v in range(n_uf) - for j in range(n_uf, n_uf + n_if)]) + for j in range(n_uf, n_uf + n_if)]) X = np.concatenate([X, temp]) est = self.intercept + np.dot(X, self.coef) From aab90a51304a5b2fdcb9a0802dcb442e91e99694 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 16:29:45 -0400 Subject: [PATCH 33/60] add feature labels --- surprise/dataset.py | 2 ++ surprise/prediction_algorithms/linear.py | 7 +++++++ surprise/trainset.py | 7 +++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index 349e9875..d5d74cb6 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -280,6 +280,8 @@ def construct_trainset(self, raw_trainset): n_items, self.user_features_nb, self.item_features_nb, + self.user_features_labels, + self.item_features_labels, n_ratings, self.reader.rating_scale, self.reader.offset, diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index d72c4679..81697f26 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -67,6 +67,8 @@ def lasso(self, trainset): n_if = self.trainset.n_item_features u_features = self.trainset.u_features i_features = self.trainset.i_features + uf_labels = self.trainset.user_features_labels + if_labels = self.trainset.item_features_labels X = np.empty((n_ratings, n_uf + n_if)) y = np.empty((n_ratings,)) @@ -83,10 +85,14 @@ def lasso(self, trainset): raise ValueError('No features for item ' + str(self.trainset.to_raw_iid(iid))) + coef_labels = uf_labels + if_labels if self.add_interactions: temp = np.array([X[:, v] * X[:, j] for v in range(n_uf) for j in range(n_uf, n_uf + n_if)]).T X = np.concatenate([X, temp], axis=1) + temp = [coef_labels[v] + '*' + coef_labels[j] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)] + coef_labels += temp reg = linear_model.Lasso( alpha=self.alpha, fit_intercept=self.fit_intercept, @@ -98,6 +104,7 @@ def lasso(self, trainset): self.X = X self.y = y self.coef = reg.coef_ + self.coef_labels = coef_labels self.intercept = reg.intercept_ def estimate(self, u, i, u_features, i_features): diff --git a/surprise/trainset.py b/surprise/trainset.py index 6aec0744..c7d091f6 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -50,8 +50,9 @@ class Trainset: """ def __init__(self, ur, ir, u_features, i_features, n_users, n_items, - n_user_features, n_item_features, n_ratings, rating_scale, - offset, raw2inner_id_users, raw2inner_id_items): + n_user_features, n_item_features, user_features_labels, + item_features_labels, n_ratings, rating_scale, offset, + raw2inner_id_users, raw2inner_id_items): self.ur = ur self.ir = ir @@ -61,6 +62,8 @@ def __init__(self, ur, ir, u_features, i_features, n_users, n_items, self.n_items = n_items self.n_user_features = n_user_features self.n_item_features = n_item_features + self.user_features_labels = user_features_labels + self.item_features_labels = item_features_labels self.n_ratings = n_ratings self.rating_scale = rating_scale self.offset = offset From bfb2b8dd41afcd1db3982e2e42bfccbb41250bcf Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 16:35:36 -0400 Subject: [PATCH 34/60] resolve conflicts --- surprise/prediction_algorithms/knns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index aa3fb7d8..301a8168 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -259,6 +259,7 @@ class KNNBaseline(SymmetricAlgo): accepted options. verbose(bool): Whether to print trace messages of bias estimation, similarity, etc. Default is True. + """ def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, From f9255e1cb1c350049aabb21fd095619768d23fb8 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 17:10:54 -0400 Subject: [PATCH 35/60] remove Lasso --- surprise/__init__.py | 4 +- surprise/prediction_algorithms/__init__.py | 3 +- surprise/prediction_algorithms/linear.py | 130 --------------------- 3 files changed, 2 insertions(+), 135 deletions(-) delete mode 100644 surprise/prediction_algorithms/linear.py diff --git a/surprise/__init__.py b/surprise/__init__.py index 82de2460..e87ca980 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -12,7 +12,6 @@ from .prediction_algorithms import NMF from .prediction_algorithms import SlopeOne from .prediction_algorithms import CoClustering -from .prediction_algorithms import Lasso from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -31,7 +30,6 @@ 'KNNWithMeans', 'KNNBaseline', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', - 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso'] + 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 1a719a97..d5ce8288 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -32,7 +32,6 @@ from .matrix_factorization import NMF from .slope_one import SlopeOne from .co_clustering import CoClustering -from .linear import Lasso from .predictions import PredictionImpossible from .predictions import Prediction @@ -40,4 +39,4 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore', 'Lasso'] + 'KNNWithZScore'] diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py deleted file mode 100644 index 81697f26..00000000 --- a/surprise/prediction_algorithms/linear.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -the :mod:`linear` module includes linear features-based algorithms. -""" - -from __future__ import (absolute_import, division, print_function, - unicode_literals) -import numpy as np -from sklearn import linear_model - -from .predictions import PredictionImpossible -from .algo_base import AlgoBase - - -class Lasso(AlgoBase): - """A basic lasso algorithm with user-item interaction terms. - - The prediction :math:`\\hat{r}_{ui}` is set as: - - .. math:: - \hat{r}_{ui} = \alpha_1 + \alpha_2^\top y_u + \alpha_3^\top z_i + - \alpha_4^\top \text{vec}(y_u \otimes z_i) - - where :math:`\alpha_1 \in \mathbb{R}, \alpha_2 \in \mathbb{R}^o, \alpha_3 - \in \mathbb{R}^p` and :math:`\alpha_4 \in \mathbb{R}^{op}` are coefficient - vectors, and :math:`\otimes` represent the Kronecker product of two vectors - (i.e., all possible cross-product combinations). - - Args: - add_interactions(bool): Whether to add user-item interaction terms. - Optional, default is True. - other args: See ``sklearn`` documentation for ``linear_model.Lasso``. - """ - - def __init__(self, add_interactions=True, alpha=1.0, fit_intercept=True, - normalize=False, precompute=False, max_iter=1000, tol=0.0001, - positive=False, random_state=None, selection='cyclic', - **kwargs): - - AlgoBase.__init__(self, **kwargs) - self.add_interactions = add_interactions - self.alpha = alpha - self.fit_intercept = fit_intercept - self.normalize = normalize - self.precompute = precompute - self.max_iter = max_iter - self.tol = tol - self.positive = positive - self.random_state = random_state - self.selection = selection - - def fit(self, trainset): - - AlgoBase.fit(self, trainset) - self.lasso(trainset) - - return self - - def lasso(self, trainset): - - if (self.trainset.n_user_features == 0 or - self.trainset.n_item_features == 0): - raise ValueError('trainset does not contain user and/or item ' - 'features.') - - n_ratings = self.trainset.n_ratings - n_uf = self.trainset.n_user_features - n_if = self.trainset.n_item_features - u_features = self.trainset.u_features - i_features = self.trainset.i_features - uf_labels = self.trainset.user_features_labels - if_labels = self.trainset.item_features_labels - - X = np.empty((n_ratings, n_uf + n_if)) - y = np.empty((n_ratings,)) - for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): - y[k] = rating - try: - X[k, :n_uf] = u_features[uid] - except KeyError: - raise ValueError('No features for user ' + - str(self.trainset.to_raw_uid(uid))) - try: - X[k, n_uf:] = i_features[iid] - except KeyError: - raise ValueError('No features for item ' + - str(self.trainset.to_raw_iid(iid))) - - coef_labels = uf_labels + if_labels - if self.add_interactions: - temp = np.array([X[:, v] * X[:, j] for v in range(n_uf) - for j in range(n_uf, n_uf + n_if)]).T - X = np.concatenate([X, temp], axis=1) - temp = [coef_labels[v] + '*' + coef_labels[j] for v in range(n_uf) - for j in range(n_uf, n_uf + n_if)] - coef_labels += temp - - reg = linear_model.Lasso( - alpha=self.alpha, fit_intercept=self.fit_intercept, - normalize=self.normalize, precompute=self.precompute, - max_iter=self.max_iter, tol=self.tol, positive=self.positive, - random_state=self.random_state, selection=self.selection) - reg.fit(X, y) - - self.X = X - self.y = y - self.coef = reg.coef_ - self.coef_labels = coef_labels - self.intercept = reg.intercept_ - - def estimate(self, u, i, u_features, i_features): - - n_uf = self.trainset.n_user_features - n_if = self.trainset.n_item_features - - if u_features is None or len(u_features) != n_uf: - raise PredictionImpossible('User features are missing.') - - if i_features is None or len(i_features) != n_if: - raise PredictionImpossible('Item features are missing.') - - X = np.concatenate([u_features, i_features]) - - if self.add_interactions: - temp = np.array([X[v] * X[j] for v in range(n_uf) - for j in range(n_uf, n_uf + n_if)]) - X = np.concatenate([X, temp]) - - est = self.intercept + np.dot(X, self.coef) - - return est From ed7180dde1f53b2d376105a65d55f9d8bc6415f4 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 17:24:23 -0400 Subject: [PATCH 36/60] Revert "Features dataset" --- .gitignore | 2 +- surprise/accuracy.py | 81 ++++++++++++++++++- surprise/dataset.py | 59 +------------- surprise/evaluate.py | 2 - surprise/model_selection/search.py | 2 - surprise/model_selection/split.py | 2 +- surprise/prediction_algorithms/algo_base.py | 27 ++----- .../prediction_algorithms/baseline_only.py | 5 +- .../prediction_algorithms/co_clustering.pyx | 4 +- surprise/prediction_algorithms/knns.py | 41 ++++------ .../matrix_factorization.pyx | 10 +-- surprise/prediction_algorithms/predictions.py | 13 +-- surprise/prediction_algorithms/slope_one.pyx | 2 +- surprise/trainset.py | 64 ++------------- 14 files changed, 123 insertions(+), 191 deletions(-) diff --git a/.gitignore b/.gitignore index 45019cb0..bd32b905 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ _site .coverage tags -settings.json +settings.json \ No newline at end of file diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 0bfcd1af..04ffca0f 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -88,6 +88,81 @@ def mae(predictions, verbose=True): return mae_ +def asym_rmse(predictions, weight=0.5, verbose=True): + """Compute Asymmetric RMSE (Root Mean Squared Error). + + .. math:: + \\text{Asymmetric RMSE} = \\sqrt{\\frac{1}{|\\hat{R}|} + \\sum_{\\hat{r}_{ui} \in \\hat{R}}(r_{ui} - \\hat{r}_{ui})^2 |\\omega + - 1_{r_{ui} - \\hat{r}_{ui} < 0}|}. + + Args: + predictions (:obj:`list` of :obj:`Prediction\ + `): + A list of predictions, as returned by the :meth:`test() + ` method. + weight (int): Weight used to characterize asymmetry. + verbose: If True, will print computed value. Default is ``True``. + + + Returns: + The Asymmetric Root Mean Squared Error of predictions. + + Raises: + ValueError: When ``predictions`` is empty. + """ + + if not predictions: + raise ValueError('Prediction list is empty.') + + res = np.array([float(true_r - est) + for (_, _, true_r, est, _) in predictions]) + asym_rmse_ = np.sqrt(np.mean(res**2 * np.abs(weight - + (res<0).astype(int)))) + + if verbose: + print('Asymmetric RMSE: {0:1.4f}'.format(asym_rmse_)) + + return asym_rmse_ + + +def asym_mae(predictions, weight=0.5, verbose=True): + """Compute Asymmetric MAE (Mean Absolute Error). + + .. math:: + \\text{Asymmetric MAE} = \\frac{1}{|\\hat{R}|} \\sum_{\\hat{r}_{ui} \in + \\hat{R}}|r_{ui} - \\hat{r}_{ui}| |\\omega - 1_{r_{ui} - \\hat{r}_{ui} + < 0}|. + + Args: + predictions (:obj:`list` of :obj:`Prediction\ + `): + A list of predictions, as returned by the :meth:`test() + ` method. + weight (int): Weight used to characterize asymmetry. + verbose: If True, will print computed value. Default is ``True``. + + + Returns: + The Asymmetric Mean Absolute Error of predictions. + + Raises: + ValueError: When ``predictions`` is empty. + """ + + if not predictions: + raise ValueError('Prediction list is empty.') + + res = np.array([float(true_r - est) + for (_, _, true_r, est, _) in predictions]) + asym_mae_ = np.mean(np.abs(res) * np.abs(weight - (res<0).astype(int))) + + if verbose: + print('Asymmetric MAE: {0:1.4f}'.format(asym_mae_)) + + return asym_mae_ + + def fcp(predictions, verbose=True): """Compute FCP (Fraction of Concordant Pairs). @@ -117,7 +192,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, _, _, _, r0, est, _ in predictions: + for u0, _, r0, est, _ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/dataset.py b/surprise/dataset.py index d5d74cb6..17638b6c 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,10 +53,6 @@ class Dataset: def __init__(self, reader): self.reader = reader - self.user_features_nb = 0 - self.item_features_nb = 0 - self.user_features = {} - self.item_features = {} @classmethod def load_builtin(cls, name='ml-100k'): @@ -169,36 +165,6 @@ def load_from_df(cls, df, reader): return DatasetAutoFolds(reader=reader, df=df) - def load_features_df(self, features_df, user_features=True): - """Load features from a pandas dataframe into a dataset. - - Use this if you want to add user or item features to a dataset. Only - certain prediction algorithms in the :mod:`prediction_algorithms` - package support this additional data. - - Args: - features_df(`Dataframe`): The dataframe containing the features. It - must have two columns or more, corresponding to the user or - item (raw) ids, and the features, in this order. - user_features(:obj:`bool`): Whether the features are for the users - or the items. Default is ``True``. - """ - - if user_features: - self.user_features_df = features_df - self.user_features = {tup[0]: tup[1:] for tup in - features_df.itertuples(index=False)} - self.user_features_labels = features_df.columns.values.tolist()[1:] - self.user_features_nb = len(self.user_features_labels) - else: - self.item_features_df = features_df - self.item_features = {tup[0]: tup[1:] for tup in - features_df.itertuples(index=False)} - self.item_features_labels = features_df.columns.values.tolist()[1:] - self.item_features_nb = len(self.item_features_labels) - - return self - def read_ratings(self, file_name): """Return a list of ratings (user, item, rating, timestamp) read from file_name""" @@ -242,28 +208,20 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) - u_features = {} - i_features = {} - # user raw id, item raw id, translated rating, time stamp - for urid, irid, r, _ in raw_trainset: + for urid, irid, r, timestamp in raw_trainset: try: uid = raw2inner_id_users[urid] except KeyError: uid = current_u_index raw2inner_id_users[urid] = current_u_index current_u_index += 1 - if self.user_features_nb > 0: - u_features[uid] = self.user_features.get(urid, None) - try: iid = raw2inner_id_items[irid] except KeyError: iid = current_i_index raw2inner_id_items[irid] = current_i_index current_i_index += 1 - if self.item_features_nb > 0: - i_features[iid] = self.item_features.get(irid, None) ur[uid].append((iid, r)) ir[iid].append((uid, r)) @@ -274,14 +232,8 @@ def construct_trainset(self, raw_trainset): trainset = Trainset(ur, ir, - u_features, - i_features, n_users, n_items, - self.user_features_nb, - self.item_features_nb, - self.user_features_labels, - self.item_features_labels, n_ratings, self.reader.rating_scale, self.reader.offset, @@ -292,13 +244,8 @@ def construct_trainset(self, raw_trainset): def construct_testset(self, raw_testset): - testset = [] - for (ruid, riid, r_ui_trans, _) in raw_testset: - u_features = self.user_features.get(ruid, None) - i_features = self.item_features.get(riid, None) - testset.append((ruid, riid, u_features, i_features, r_ui_trans)) - - return testset + return [(ruid, riid, r_ui_trans) + for (ruid, riid, r_ui_trans, _) in raw_testset] class DatasetUserFolds(Dataset): diff --git a/surprise/evaluate.py b/surprise/evaluate.py index bb283356..19e80fa5 100644 --- a/surprise/evaluate.py +++ b/surprise/evaluate.py @@ -301,7 +301,6 @@ class CaseInsensitiveDefaultDict(defaultdict): Used for the returned dict, so that users can use perf['RMSE'] or perf['rmse'] indifferently. """ - def __setitem__(self, key, value): super(CaseInsensitiveDefaultDict, self).__setitem__(key.lower(), value) @@ -334,5 +333,4 @@ def seed_and_eval(seed, *args): different processes.""" random.seed(seed) - return evaluate(*args, verbose=0) diff --git a/surprise/model_selection/search.py b/surprise/model_selection/search.py index afa2e2a6..9489c88b 100644 --- a/surprise/model_selection/search.py +++ b/surprise/model_selection/search.py @@ -294,7 +294,6 @@ class GridSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ - def __init__(self, algo_class, param_grid, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, pre_dispatch='2*n_jobs', joblib_verbose=0): @@ -411,7 +410,6 @@ class RandomizedSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ - def __init__(self, algo_class, param_distributions, n_iter=10, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, diff --git a/surprise/model_selection/split.py b/surprise/model_selection/split.py index 5c656565..14697911 100644 --- a/surprise/model_selection/split.py +++ b/surprise/model_selection/split.py @@ -372,7 +372,7 @@ def split(self, data): Args: data(:obj:`Dataset`): The data containing - ratings that will be divided into trainsets and testsets. + ratings that will be devided into trainsets and testsets. Yields: tuple of (trainset, testset) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 4fdcd8f0..3a80c4d4 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -37,7 +37,7 @@ def __init__(self, **kwargs): self.skip_train = False if (guf(self.__class__.fit) is guf(AlgoBase.fit) and - guf(self.__class__.train) is not guf(AlgoBase.train)): + guf(self.__class__.train) is not guf(AlgoBase.train)): warnings.warn('It looks like this algorithm (' + str(self.__class__) + ') implements train() ' @@ -96,8 +96,7 @@ def fit(self, trainset): return self - def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, - clip=True, verbose=False): + def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): """Compute the rating prediction for given user and item. The ``predict`` method converts raw ids to inner ids and then calls the @@ -109,10 +108,6 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, Args: uid: (Raw) id of the user. See :ref:`this note`. iid: (Raw) id of the item. See :ref:`this note`. - u_features: List of user features in the same order as used in - the ``fit`` method. Optional, default is ``None``. - i_features: List of item features in the same order as used in - the ``fit`` method. Optional, default is ``None``. r_ui(float): The true rating :math:`r_{ui}`. Optional, default is ``None``. clip(bool): Whether to clip the estimation into the rating scale. @@ -148,7 +143,7 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, details = {} try: - est = self.estimate(iuid, iiid, u_features, i_features) + est = self.estimate(iuid, iiid) # If the details dict was also returned if isinstance(est, tuple): @@ -171,7 +166,7 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, est = min(higher_bound, est) est = max(lower_bound, est) - pred = Prediction(uid, iid, u_features, i_features, r_ui, est, details) + pred = Prediction(uid, iid, r_ui, est, details) if verbose: print(pred) @@ -212,12 +207,9 @@ def test(self, testset, verbose=False): # The ratings are translated back to their original scale. predictions = [self.predict(uid, iid, - u_features, - i_features, r_ui_trans - self.trainset.offset, verbose=verbose) - for (uid, iid, u_features, i_features, r_ui_trans) - in testset] + for (uid, iid, r_ui_trans) in testset] return predictions def compute_baselines(self): @@ -248,8 +240,7 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - if self.verbose: - print('Estimating biases using', method_name + '...') + # print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: @@ -296,11 +287,9 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - if self.verbose: - print('Computing the {0} similarity matrix...'.format(name)) + # print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - if self.verbose: - print('Done computing similarity matrix.') + # print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index 05886657..85221114 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -24,10 +24,9 @@ class BaselineOnly(AlgoBase): """ - def __init__(self, bsl_options={}, verbose=True): + def __init__(self, bsl_options={}): AlgoBase.__init__(self, bsl_options=bsl_options) - self.verbose = verbose def fit(self, trainset): @@ -36,7 +35,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): est = self.trainset.global_mean if self.trainset.knows_user(u): diff --git a/surprise/prediction_algorithms/co_clustering.pyx b/surprise/prediction_algorithms/co_clustering.pyx index 85837718..408780fc 100644 --- a/surprise/prediction_algorithms/co_clustering.pyx +++ b/surprise/prediction_algorithms/co_clustering.pyx @@ -62,7 +62,7 @@ class CoClustering(AlgoBase): self.n_cltr_u = n_cltr_u self.n_cltr_i = n_cltr_i self.n_epochs = n_epochs - self.verbose = verbose + self.verbose=verbose self.random_state = random_state def fit(self, trainset): @@ -236,7 +236,7 @@ class CoClustering(AlgoBase): return avg_cltr_u, avg_cltr_i, avg_cocltr - def estimate(self, u, i, *_): + def estimate(self, u, i): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): return self.trainset.global_mean diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 301a8168..069da4d3 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -27,10 +27,9 @@ class SymmetricAlgo(AlgoBase): reversed. """ - def __init__(self, sim_options={}, verbose=True, **kwargs): + def __init__(self, sim_options={}, **kwargs): AlgoBase.__init__(self, sim_options=sim_options, **kwargs) - self.verbose = verbose def fit(self, trainset): @@ -82,14 +81,11 @@ class KNNBasic(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. - verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, - **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) self.k = k self.min_k = min_k @@ -100,7 +96,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -160,14 +156,11 @@ class KNNWithMeans(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. - verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, - verbose=verbose, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) self.k = k self.min_k = min_k @@ -183,7 +176,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -254,20 +247,17 @@ class KNNBaseline(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. It is recommended to use the :func:`pearson_baseline ` similarity measure. + bsl_options(dict): A dictionary of options for the baseline estimates computation. See :ref:`baseline_estimates_configuration` for accepted options. - verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, - verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}): SymmetricAlgo.__init__(self, sim_options=sim_options, - bsl_options=bsl_options, verbose=verbose, - **kwargs) + bsl_options=bsl_options) self.k = k self.min_k = min_k @@ -281,7 +271,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): est = self.trainset.global_mean if self.trainset.knows_user(u): @@ -352,14 +342,11 @@ class KNNWithZScore(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. - verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, - **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) self.k = k self.min_k = min_k @@ -383,7 +370,7 @@ def fit(self, trainset): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index 7a3cede5..0e898632 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -253,7 +253,7 @@ class SVD(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, *_): + def estimate(self, u, i): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -275,7 +275,7 @@ class SVD(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unknown.') + raise PredictionImpossible('User and item are unkown.') return est @@ -484,7 +484,7 @@ class SVDpp(AlgoBase): self.qi = qi self.yj = yj - def estimate(self, u, i, *_): + def estimate(self, u, i): est = self.trainset.global_mean @@ -715,7 +715,7 @@ class NMF(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i, *_): + def estimate(self, u, i): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -737,6 +737,6 @@ class NMF(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unknown.') + raise PredictionImpossible('User and item are unkown.') return est diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 9e971ee9..76bc8ddc 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -21,8 +21,7 @@ class PredictionImpossible(Exception): class Prediction(namedtuple('Prediction', - ['uid', 'iid', 'u_features', 'i_features', 'r_ui', - 'est', 'details'])): + ['uid', 'iid', 'r_ui', 'est', 'details'])): """A named tuple for storing the results of a prediction. It's wrapped in a class, but only for documentation and printing purposes. @@ -30,8 +29,6 @@ class Prediction(namedtuple('Prediction', Args: uid: The (raw) user id. See :ref:`this note`. iid: The (raw) item id. See :ref:`this note`. - u_features: The user features. - i_features: The item features. r_ui(float): The true rating :math:`r_{ui}`. est(float): The estimated rating :math:`\\hat{r}_{ui}`. details (dict): Stores additional details about the prediction that @@ -43,14 +40,6 @@ class Prediction(namedtuple('Prediction', def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) - if self.u_features is not None: - pass - else: - s += 'u_features = None ' - if self.i_features is not None: - pass - else: - s += 'i_features = None ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: diff --git a/surprise/prediction_algorithms/slope_one.pyx b/surprise/prediction_algorithms/slope_one.pyx index f986e496..8049a6cf 100644 --- a/surprise/prediction_algorithms/slope_one.pyx +++ b/surprise/prediction_algorithms/slope_one.pyx @@ -79,7 +79,7 @@ class SlopeOne(AlgoBase): return self - def estimate(self, u, i, *_): + def estimate(self, u, i): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/trainset.py b/surprise/trainset.py index c7d091f6..ebb95204 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -33,37 +33,21 @@ class Trainset: ir(:obj:`defaultdict` of :obj:`list`): The items ratings. This is a dictionary containing lists of tuples of the form ``(user_inner_id, rating)``. The keys are item inner ids. - u_features(:obj:`defaultdict` of :obj:`list`): The user features. This - is a dictionary containing lists of features. The keys are user - inner ids. - i_features(:obj:`defaultdict` of :obj:`list`): The item features. This - is a dictionary containing lists of features. The keys are item - inner ids. n_users: Total number of users :math:`|U|`. n_items: Total number of items :math:`|I|`. - n_user_features: Total number of user features. - n_item_features: Total number of item features. n_ratings: Total number of ratings :math:`|R_{train}|`. rating_scale(tuple): The minimum and maximal rating of the rating scale. global_mean: The mean of all ratings :math:`\\mu`. """ - def __init__(self, ur, ir, u_features, i_features, n_users, n_items, - n_user_features, n_item_features, user_features_labels, - item_features_labels, n_ratings, rating_scale, offset, - raw2inner_id_users, raw2inner_id_items): + def __init__(self, ur, ir, n_users, n_items, n_ratings, rating_scale, + offset, raw2inner_id_users, raw2inner_id_items): self.ur = ur self.ir = ir - self.u_features = u_features - self.i_features = i_features self.n_users = n_users self.n_items = n_items - self.n_user_features = n_user_features - self.n_item_features = n_item_features - self.user_features_labels = user_features_labels - self.item_features_labels = item_features_labels self.n_ratings = n_ratings self.rating_scale = rating_scale self.offset = offset @@ -103,30 +87,6 @@ def knows_item(self, iid): return iid in self.ir - def has_user_features(self, uid): - """Indicate if the user features are part of the trainset. - - Args: - uid(int): The (inner) user id. See :ref:`this - note`. - Returns: - ``True`` if user features are part of the trainset, else ``False``. - """ - - return uid in self.u_features - - def has_item_features(self, iid): - """Indicate if the item features are part of the trainset. - - Args: - iid(int): The (inner) item id. See :ref:`this - note`. - Returns: - ``True`` if item features are part of the trainset, else ``False``. - """ - - return iid in self.i_features - def to_inner_uid(self, ruid): """Convert a **user** raw id to an inner id. @@ -240,14 +200,8 @@ def build_testset(self): cases where you want to to test your algorithm on the trainset. """ - testset = [] - for (u, i, r) in self.all_ratings(): - u_features = self.u_features.get(u, None) - i_features = self.i_features.get(i, None) - testset.append((self.to_raw_uid(u), self.to_raw_iid(i), u_features, - i_features, r)) - - return testset + return [(self.to_raw_uid(u), self.to_raw_iid(i), r) + for (u, i, r) in self.all_ratings()] def build_anti_testset(self, fill=None): """Return a list of ratings that can be used as a testset in the @@ -274,13 +228,9 @@ def build_anti_testset(self, fill=None): anti_testset = [] for u in self.all_users(): user_items = set([j for (j, _) in self.ur[u]]) - anti_testset += [(self.to_raw_uid(u), - self.to_raw_iid(i), - self.u_features.get(u, None), - self.i_features.get(i, None), - fill) - for i in self.all_items() - if i not in user_items] + anti_testset += [(self.to_raw_uid(u), self.to_raw_iid(i), fill) for + i in self.all_items() if + i not in user_items] return anti_testset def all_users(self): From e3de2084b6598f0801222c581f0a5e70048ae3c4 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 17:37:27 -0400 Subject: [PATCH 37/60] Revert "Revert "Features dataset"" --- .gitignore | 2 +- surprise/accuracy.py | 81 +------------------ surprise/dataset.py | 59 +++++++++++++- surprise/evaluate.py | 2 + surprise/model_selection/search.py | 2 + surprise/model_selection/split.py | 2 +- surprise/prediction_algorithms/algo_base.py | 27 +++++-- .../prediction_algorithms/baseline_only.py | 5 +- .../prediction_algorithms/co_clustering.pyx | 4 +- surprise/prediction_algorithms/knns.py | 41 ++++++---- .../matrix_factorization.pyx | 10 +-- surprise/prediction_algorithms/predictions.py | 13 ++- surprise/prediction_algorithms/slope_one.pyx | 2 +- surprise/trainset.py | 64 +++++++++++++-- 14 files changed, 191 insertions(+), 123 deletions(-) diff --git a/.gitignore b/.gitignore index bd32b905..45019cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ _site .coverage tags -settings.json \ No newline at end of file +settings.json diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 04ffca0f..0bfcd1af 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (_, _, true_r, est, _) in predictions]) + for (_, _, _, _, true_r, est, _) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (_, _, true_r, est, _) in predictions]) + for (_, _, _, _, true_r, est, _) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -88,81 +88,6 @@ def mae(predictions, verbose=True): return mae_ -def asym_rmse(predictions, weight=0.5, verbose=True): - """Compute Asymmetric RMSE (Root Mean Squared Error). - - .. math:: - \\text{Asymmetric RMSE} = \\sqrt{\\frac{1}{|\\hat{R}|} - \\sum_{\\hat{r}_{ui} \in \\hat{R}}(r_{ui} - \\hat{r}_{ui})^2 |\\omega - - 1_{r_{ui} - \\hat{r}_{ui} < 0}|}. - - Args: - predictions (:obj:`list` of :obj:`Prediction\ - `): - A list of predictions, as returned by the :meth:`test() - ` method. - weight (int): Weight used to characterize asymmetry. - verbose: If True, will print computed value. Default is ``True``. - - - Returns: - The Asymmetric Root Mean Squared Error of predictions. - - Raises: - ValueError: When ``predictions`` is empty. - """ - - if not predictions: - raise ValueError('Prediction list is empty.') - - res = np.array([float(true_r - est) - for (_, _, true_r, est, _) in predictions]) - asym_rmse_ = np.sqrt(np.mean(res**2 * np.abs(weight - - (res<0).astype(int)))) - - if verbose: - print('Asymmetric RMSE: {0:1.4f}'.format(asym_rmse_)) - - return asym_rmse_ - - -def asym_mae(predictions, weight=0.5, verbose=True): - """Compute Asymmetric MAE (Mean Absolute Error). - - .. math:: - \\text{Asymmetric MAE} = \\frac{1}{|\\hat{R}|} \\sum_{\\hat{r}_{ui} \in - \\hat{R}}|r_{ui} - \\hat{r}_{ui}| |\\omega - 1_{r_{ui} - \\hat{r}_{ui} - < 0}|. - - Args: - predictions (:obj:`list` of :obj:`Prediction\ - `): - A list of predictions, as returned by the :meth:`test() - ` method. - weight (int): Weight used to characterize asymmetry. - verbose: If True, will print computed value. Default is ``True``. - - - Returns: - The Asymmetric Mean Absolute Error of predictions. - - Raises: - ValueError: When ``predictions`` is empty. - """ - - if not predictions: - raise ValueError('Prediction list is empty.') - - res = np.array([float(true_r - est) - for (_, _, true_r, est, _) in predictions]) - asym_mae_ = np.mean(np.abs(res) * np.abs(weight - (res<0).astype(int))) - - if verbose: - print('Asymmetric MAE: {0:1.4f}'.format(asym_mae_)) - - return asym_mae_ - - def fcp(predictions, verbose=True): """Compute FCP (Fraction of Concordant Pairs). @@ -192,7 +117,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, _, r0, est, _ in predictions: + for u0, _, _, _, r0, est, _ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/dataset.py b/surprise/dataset.py index 17638b6c..d5d74cb6 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,6 +53,10 @@ class Dataset: def __init__(self, reader): self.reader = reader + self.user_features_nb = 0 + self.item_features_nb = 0 + self.user_features = {} + self.item_features = {} @classmethod def load_builtin(cls, name='ml-100k'): @@ -165,6 +169,36 @@ def load_from_df(cls, df, reader): return DatasetAutoFolds(reader=reader, df=df) + def load_features_df(self, features_df, user_features=True): + """Load features from a pandas dataframe into a dataset. + + Use this if you want to add user or item features to a dataset. Only + certain prediction algorithms in the :mod:`prediction_algorithms` + package support this additional data. + + Args: + features_df(`Dataframe`): The dataframe containing the features. It + must have two columns or more, corresponding to the user or + item (raw) ids, and the features, in this order. + user_features(:obj:`bool`): Whether the features are for the users + or the items. Default is ``True``. + """ + + if user_features: + self.user_features_df = features_df + self.user_features = {tup[0]: tup[1:] for tup in + features_df.itertuples(index=False)} + self.user_features_labels = features_df.columns.values.tolist()[1:] + self.user_features_nb = len(self.user_features_labels) + else: + self.item_features_df = features_df + self.item_features = {tup[0]: tup[1:] for tup in + features_df.itertuples(index=False)} + self.item_features_labels = features_df.columns.values.tolist()[1:] + self.item_features_nb = len(self.item_features_labels) + + return self + def read_ratings(self, file_name): """Return a list of ratings (user, item, rating, timestamp) read from file_name""" @@ -208,20 +242,28 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) + u_features = {} + i_features = {} + # user raw id, item raw id, translated rating, time stamp - for urid, irid, r, timestamp in raw_trainset: + for urid, irid, r, _ in raw_trainset: try: uid = raw2inner_id_users[urid] except KeyError: uid = current_u_index raw2inner_id_users[urid] = current_u_index current_u_index += 1 + if self.user_features_nb > 0: + u_features[uid] = self.user_features.get(urid, None) + try: iid = raw2inner_id_items[irid] except KeyError: iid = current_i_index raw2inner_id_items[irid] = current_i_index current_i_index += 1 + if self.item_features_nb > 0: + i_features[iid] = self.item_features.get(irid, None) ur[uid].append((iid, r)) ir[iid].append((uid, r)) @@ -232,8 +274,14 @@ def construct_trainset(self, raw_trainset): trainset = Trainset(ur, ir, + u_features, + i_features, n_users, n_items, + self.user_features_nb, + self.item_features_nb, + self.user_features_labels, + self.item_features_labels, n_ratings, self.reader.rating_scale, self.reader.offset, @@ -244,8 +292,13 @@ def construct_trainset(self, raw_trainset): def construct_testset(self, raw_testset): - return [(ruid, riid, r_ui_trans) - for (ruid, riid, r_ui_trans, _) in raw_testset] + testset = [] + for (ruid, riid, r_ui_trans, _) in raw_testset: + u_features = self.user_features.get(ruid, None) + i_features = self.item_features.get(riid, None) + testset.append((ruid, riid, u_features, i_features, r_ui_trans)) + + return testset class DatasetUserFolds(Dataset): diff --git a/surprise/evaluate.py b/surprise/evaluate.py index 19e80fa5..bb283356 100644 --- a/surprise/evaluate.py +++ b/surprise/evaluate.py @@ -301,6 +301,7 @@ class CaseInsensitiveDefaultDict(defaultdict): Used for the returned dict, so that users can use perf['RMSE'] or perf['rmse'] indifferently. """ + def __setitem__(self, key, value): super(CaseInsensitiveDefaultDict, self).__setitem__(key.lower(), value) @@ -333,4 +334,5 @@ def seed_and_eval(seed, *args): different processes.""" random.seed(seed) + return evaluate(*args, verbose=0) diff --git a/surprise/model_selection/search.py b/surprise/model_selection/search.py index 9489c88b..afa2e2a6 100644 --- a/surprise/model_selection/search.py +++ b/surprise/model_selection/search.py @@ -294,6 +294,7 @@ class GridSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ + def __init__(self, algo_class, param_grid, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, pre_dispatch='2*n_jobs', joblib_verbose=0): @@ -410,6 +411,7 @@ class RandomizedSearchCV(BaseSearchCV): into a pandas `DataFrame` (see :ref:`example `). """ + def __init__(self, algo_class, param_distributions, n_iter=10, measures=['rmse', 'mae'], cv=None, refit=False, return_train_measures=False, n_jobs=-1, diff --git a/surprise/model_selection/split.py b/surprise/model_selection/split.py index 14697911..5c656565 100644 --- a/surprise/model_selection/split.py +++ b/surprise/model_selection/split.py @@ -372,7 +372,7 @@ def split(self, data): Args: data(:obj:`Dataset`): The data containing - ratings that will be devided into trainsets and testsets. + ratings that will be divided into trainsets and testsets. Yields: tuple of (trainset, testset) diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 3a80c4d4..4fdcd8f0 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -37,7 +37,7 @@ def __init__(self, **kwargs): self.skip_train = False if (guf(self.__class__.fit) is guf(AlgoBase.fit) and - guf(self.__class__.train) is not guf(AlgoBase.train)): + guf(self.__class__.train) is not guf(AlgoBase.train)): warnings.warn('It looks like this algorithm (' + str(self.__class__) + ') implements train() ' @@ -96,7 +96,8 @@ def fit(self, trainset): return self - def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): + def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, + clip=True, verbose=False): """Compute the rating prediction for given user and item. The ``predict`` method converts raw ids to inner ids and then calls the @@ -108,6 +109,10 @@ def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): Args: uid: (Raw) id of the user. See :ref:`this note`. iid: (Raw) id of the item. See :ref:`this note`. + u_features: List of user features in the same order as used in + the ``fit`` method. Optional, default is ``None``. + i_features: List of item features in the same order as used in + the ``fit`` method. Optional, default is ``None``. r_ui(float): The true rating :math:`r_{ui}`. Optional, default is ``None``. clip(bool): Whether to clip the estimation into the rating scale. @@ -143,7 +148,7 @@ def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): details = {} try: - est = self.estimate(iuid, iiid) + est = self.estimate(iuid, iiid, u_features, i_features) # If the details dict was also returned if isinstance(est, tuple): @@ -166,7 +171,7 @@ def predict(self, uid, iid, r_ui=None, clip=True, verbose=False): est = min(higher_bound, est) est = max(lower_bound, est) - pred = Prediction(uid, iid, r_ui, est, details) + pred = Prediction(uid, iid, u_features, i_features, r_ui, est, details) if verbose: print(pred) @@ -207,9 +212,12 @@ def test(self, testset, verbose=False): # The ratings are translated back to their original scale. predictions = [self.predict(uid, iid, + u_features, + i_features, r_ui_trans - self.trainset.offset, verbose=verbose) - for (uid, iid, r_ui_trans) in testset] + for (uid, iid, u_features, i_features, r_ui_trans) + in testset] return predictions def compute_baselines(self): @@ -240,7 +248,8 @@ def compute_baselines(self): method_name = self.bsl_options.get('method', 'als') try: - # print('Estimating biases using', method_name + '...') + if self.verbose: + print('Estimating biases using', method_name + '...') self.bu, self.bi = method[method_name](self) return self.bu, self.bi except KeyError: @@ -287,9 +296,11 @@ def compute_similarities(self): args += [self.trainset.global_mean, bx, by, shrinkage] try: - # print('Computing the {0} similarity matrix...'.format(name)) + if self.verbose: + print('Computing the {0} similarity matrix...'.format(name)) sim = construction_func[name](*args) - # print('Done computing similarity matrix.') + if self.verbose: + print('Done computing similarity matrix.') return sim except KeyError: raise NameError('Wrong sim name ' + name + '. Allowed values ' + diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index 85221114..05886657 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -24,9 +24,10 @@ class BaselineOnly(AlgoBase): """ - def __init__(self, bsl_options={}): + def __init__(self, bsl_options={}, verbose=True): AlgoBase.__init__(self, bsl_options=bsl_options) + self.verbose = verbose def fit(self, trainset): @@ -35,7 +36,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): est = self.trainset.global_mean if self.trainset.knows_user(u): diff --git a/surprise/prediction_algorithms/co_clustering.pyx b/surprise/prediction_algorithms/co_clustering.pyx index 408780fc..85837718 100644 --- a/surprise/prediction_algorithms/co_clustering.pyx +++ b/surprise/prediction_algorithms/co_clustering.pyx @@ -62,7 +62,7 @@ class CoClustering(AlgoBase): self.n_cltr_u = n_cltr_u self.n_cltr_i = n_cltr_i self.n_epochs = n_epochs - self.verbose=verbose + self.verbose = verbose self.random_state = random_state def fit(self, trainset): @@ -236,7 +236,7 @@ class CoClustering(AlgoBase): return avg_cltr_u, avg_cltr_i, avg_cocltr - def estimate(self, u, i): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): return self.trainset.global_mean diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 069da4d3..301a8168 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -27,9 +27,10 @@ class SymmetricAlgo(AlgoBase): reversed. """ - def __init__(self, sim_options={}, **kwargs): + def __init__(self, sim_options={}, verbose=True, **kwargs): AlgoBase.__init__(self, sim_options=sim_options, **kwargs) + self.verbose = verbose def fit(self, trainset): @@ -81,11 +82,14 @@ class KNNBasic(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k @@ -96,7 +100,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -156,11 +160,14 @@ class KNNWithMeans(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, + verbose=verbose, **kwargs) self.k = k self.min_k = min_k @@ -176,7 +183,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') @@ -247,17 +254,20 @@ class KNNBaseline(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. It is recommended to use the :func:`pearson_baseline ` similarity measure. - bsl_options(dict): A dictionary of options for the baseline estimates computation. See :ref:`baseline_estimates_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}): + def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, + verbose=True, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, - bsl_options=bsl_options) + bsl_options=bsl_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k @@ -271,7 +281,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): est = self.trainset.global_mean if self.trainset.knows_user(u): @@ -342,11 +352,14 @@ class KNNWithZScore(SymmetricAlgo): sim_options(dict): A dictionary of options for the similarity measure. See :ref:`similarity_measures_configuration` for accepted options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. """ - def __init__(self, k=40, min_k=1, sim_options={}, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): - SymmetricAlgo.__init__(self, sim_options=sim_options, **kwargs) + SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, + **kwargs) self.k = k self.min_k = min_k @@ -370,7 +383,7 @@ def fit(self, trainset): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/prediction_algorithms/matrix_factorization.pyx b/surprise/prediction_algorithms/matrix_factorization.pyx index 0e898632..7a3cede5 100644 --- a/surprise/prediction_algorithms/matrix_factorization.pyx +++ b/surprise/prediction_algorithms/matrix_factorization.pyx @@ -253,7 +253,7 @@ class SVD(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i): + def estimate(self, u, i, *_): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -275,7 +275,7 @@ class SVD(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unkown.') + raise PredictionImpossible('User and item are unknown.') return est @@ -484,7 +484,7 @@ class SVDpp(AlgoBase): self.qi = qi self.yj = yj - def estimate(self, u, i): + def estimate(self, u, i, *_): est = self.trainset.global_mean @@ -715,7 +715,7 @@ class NMF(AlgoBase): self.pu = pu self.qi = qi - def estimate(self, u, i): + def estimate(self, u, i, *_): # Should we cythonize this as well? known_user = self.trainset.knows_user(u) @@ -737,6 +737,6 @@ class NMF(AlgoBase): if known_user and known_item: est = np.dot(self.qi[i], self.pu[u]) else: - raise PredictionImpossible('User and item are unkown.') + raise PredictionImpossible('User and item are unknown.') return est diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 76bc8ddc..9e971ee9 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -21,7 +21,8 @@ class PredictionImpossible(Exception): class Prediction(namedtuple('Prediction', - ['uid', 'iid', 'r_ui', 'est', 'details'])): + ['uid', 'iid', 'u_features', 'i_features', 'r_ui', + 'est', 'details'])): """A named tuple for storing the results of a prediction. It's wrapped in a class, but only for documentation and printing purposes. @@ -29,6 +30,8 @@ class Prediction(namedtuple('Prediction', Args: uid: The (raw) user id. See :ref:`this note`. iid: The (raw) item id. See :ref:`this note`. + u_features: The user features. + i_features: The item features. r_ui(float): The true rating :math:`r_{ui}`. est(float): The estimated rating :math:`\\hat{r}_{ui}`. details (dict): Stores additional details about the prediction that @@ -40,6 +43,14 @@ class Prediction(namedtuple('Prediction', def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) + if self.u_features is not None: + pass + else: + s += 'u_features = None ' + if self.i_features is not None: + pass + else: + s += 'i_features = None ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: diff --git a/surprise/prediction_algorithms/slope_one.pyx b/surprise/prediction_algorithms/slope_one.pyx index 8049a6cf..f986e496 100644 --- a/surprise/prediction_algorithms/slope_one.pyx +++ b/surprise/prediction_algorithms/slope_one.pyx @@ -79,7 +79,7 @@ class SlopeOne(AlgoBase): return self - def estimate(self, u, i): + def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): raise PredictionImpossible('User and/or item is unkown.') diff --git a/surprise/trainset.py b/surprise/trainset.py index ebb95204..c7d091f6 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -33,21 +33,37 @@ class Trainset: ir(:obj:`defaultdict` of :obj:`list`): The items ratings. This is a dictionary containing lists of tuples of the form ``(user_inner_id, rating)``. The keys are item inner ids. + u_features(:obj:`defaultdict` of :obj:`list`): The user features. This + is a dictionary containing lists of features. The keys are user + inner ids. + i_features(:obj:`defaultdict` of :obj:`list`): The item features. This + is a dictionary containing lists of features. The keys are item + inner ids. n_users: Total number of users :math:`|U|`. n_items: Total number of items :math:`|I|`. + n_user_features: Total number of user features. + n_item_features: Total number of item features. n_ratings: Total number of ratings :math:`|R_{train}|`. rating_scale(tuple): The minimum and maximal rating of the rating scale. global_mean: The mean of all ratings :math:`\\mu`. """ - def __init__(self, ur, ir, n_users, n_items, n_ratings, rating_scale, - offset, raw2inner_id_users, raw2inner_id_items): + def __init__(self, ur, ir, u_features, i_features, n_users, n_items, + n_user_features, n_item_features, user_features_labels, + item_features_labels, n_ratings, rating_scale, offset, + raw2inner_id_users, raw2inner_id_items): self.ur = ur self.ir = ir + self.u_features = u_features + self.i_features = i_features self.n_users = n_users self.n_items = n_items + self.n_user_features = n_user_features + self.n_item_features = n_item_features + self.user_features_labels = user_features_labels + self.item_features_labels = item_features_labels self.n_ratings = n_ratings self.rating_scale = rating_scale self.offset = offset @@ -87,6 +103,30 @@ def knows_item(self, iid): return iid in self.ir + def has_user_features(self, uid): + """Indicate if the user features are part of the trainset. + + Args: + uid(int): The (inner) user id. See :ref:`this + note`. + Returns: + ``True`` if user features are part of the trainset, else ``False``. + """ + + return uid in self.u_features + + def has_item_features(self, iid): + """Indicate if the item features are part of the trainset. + + Args: + iid(int): The (inner) item id. See :ref:`this + note`. + Returns: + ``True`` if item features are part of the trainset, else ``False``. + """ + + return iid in self.i_features + def to_inner_uid(self, ruid): """Convert a **user** raw id to an inner id. @@ -200,8 +240,14 @@ def build_testset(self): cases where you want to to test your algorithm on the trainset. """ - return [(self.to_raw_uid(u), self.to_raw_iid(i), r) - for (u, i, r) in self.all_ratings()] + testset = [] + for (u, i, r) in self.all_ratings(): + u_features = self.u_features.get(u, None) + i_features = self.i_features.get(i, None) + testset.append((self.to_raw_uid(u), self.to_raw_iid(i), u_features, + i_features, r)) + + return testset def build_anti_testset(self, fill=None): """Return a list of ratings that can be used as a testset in the @@ -228,9 +274,13 @@ def build_anti_testset(self, fill=None): anti_testset = [] for u in self.all_users(): user_items = set([j for (j, _) in self.ur[u]]) - anti_testset += [(self.to_raw_uid(u), self.to_raw_iid(i), fill) for - i in self.all_items() if - i not in user_items] + anti_testset += [(self.to_raw_uid(u), + self.to_raw_iid(i), + self.u_features.get(u, None), + self.i_features.get(i, None), + fill) + for i in self.all_items() + if i not in user_items] return anti_testset def all_users(self): From 4fabe2916f37725aca044a4ce6d60e8909e79ee9 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 5 Apr 2018 17:45:29 -0400 Subject: [PATCH 38/60] add lasso --- surprise/__init__.py | 4 +- surprise/prediction_algorithms/__init__.py | 3 +- surprise/prediction_algorithms/linear.py | 130 +++++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 surprise/prediction_algorithms/linear.py diff --git a/surprise/__init__.py b/surprise/__init__.py index e87ca980..82de2460 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -12,6 +12,7 @@ from .prediction_algorithms import NMF from .prediction_algorithms import SlopeOne from .prediction_algorithms import CoClustering +from .prediction_algorithms import Lasso from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -30,6 +31,7 @@ 'KNNWithMeans', 'KNNBaseline', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', - 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection'] + 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', + 'Lasso'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index d5ce8288..1a719a97 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -32,6 +32,7 @@ from .matrix_factorization import NMF from .slope_one import SlopeOne from .co_clustering import CoClustering +from .linear import Lasso from .predictions import PredictionImpossible from .predictions import Prediction @@ -39,4 +40,4 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore'] + 'KNNWithZScore', 'Lasso'] diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py new file mode 100644 index 00000000..81697f26 --- /dev/null +++ b/surprise/prediction_algorithms/linear.py @@ -0,0 +1,130 @@ +""" +the :mod:`linear` module includes linear features-based algorithms. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import numpy as np +from sklearn import linear_model + +from .predictions import PredictionImpossible +from .algo_base import AlgoBase + + +class Lasso(AlgoBase): + """A basic lasso algorithm with user-item interaction terms. + + The prediction :math:`\\hat{r}_{ui}` is set as: + + .. math:: + \hat{r}_{ui} = \alpha_1 + \alpha_2^\top y_u + \alpha_3^\top z_i + + \alpha_4^\top \text{vec}(y_u \otimes z_i) + + where :math:`\alpha_1 \in \mathbb{R}, \alpha_2 \in \mathbb{R}^o, \alpha_3 + \in \mathbb{R}^p` and :math:`\alpha_4 \in \mathbb{R}^{op}` are coefficient + vectors, and :math:`\otimes` represent the Kronecker product of two vectors + (i.e., all possible cross-product combinations). + + Args: + add_interactions(bool): Whether to add user-item interaction terms. + Optional, default is True. + other args: See ``sklearn`` documentation for ``linear_model.Lasso``. + """ + + def __init__(self, add_interactions=True, alpha=1.0, fit_intercept=True, + normalize=False, precompute=False, max_iter=1000, tol=0.0001, + positive=False, random_state=None, selection='cyclic', + **kwargs): + + AlgoBase.__init__(self, **kwargs) + self.add_interactions = add_interactions + self.alpha = alpha + self.fit_intercept = fit_intercept + self.normalize = normalize + self.precompute = precompute + self.max_iter = max_iter + self.tol = tol + self.positive = positive + self.random_state = random_state + self.selection = selection + + def fit(self, trainset): + + AlgoBase.fit(self, trainset) + self.lasso(trainset) + + return self + + def lasso(self, trainset): + + if (self.trainset.n_user_features == 0 or + self.trainset.n_item_features == 0): + raise ValueError('trainset does not contain user and/or item ' + 'features.') + + n_ratings = self.trainset.n_ratings + n_uf = self.trainset.n_user_features + n_if = self.trainset.n_item_features + u_features = self.trainset.u_features + i_features = self.trainset.i_features + uf_labels = self.trainset.user_features_labels + if_labels = self.trainset.item_features_labels + + X = np.empty((n_ratings, n_uf + n_if)) + y = np.empty((n_ratings,)) + for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): + y[k] = rating + try: + X[k, :n_uf] = u_features[uid] + except KeyError: + raise ValueError('No features for user ' + + str(self.trainset.to_raw_uid(uid))) + try: + X[k, n_uf:] = i_features[iid] + except KeyError: + raise ValueError('No features for item ' + + str(self.trainset.to_raw_iid(iid))) + + coef_labels = uf_labels + if_labels + if self.add_interactions: + temp = np.array([X[:, v] * X[:, j] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)]).T + X = np.concatenate([X, temp], axis=1) + temp = [coef_labels[v] + '*' + coef_labels[j] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)] + coef_labels += temp + + reg = linear_model.Lasso( + alpha=self.alpha, fit_intercept=self.fit_intercept, + normalize=self.normalize, precompute=self.precompute, + max_iter=self.max_iter, tol=self.tol, positive=self.positive, + random_state=self.random_state, selection=self.selection) + reg.fit(X, y) + + self.X = X + self.y = y + self.coef = reg.coef_ + self.coef_labels = coef_labels + self.intercept = reg.intercept_ + + def estimate(self, u, i, u_features, i_features): + + n_uf = self.trainset.n_user_features + n_if = self.trainset.n_item_features + + if u_features is None or len(u_features) != n_uf: + raise PredictionImpossible('User features are missing.') + + if i_features is None or len(i_features) != n_if: + raise PredictionImpossible('Item features are missing.') + + X = np.concatenate([u_features, i_features]) + + if self.add_interactions: + temp = np.array([X[v] * X[j] for v in range(n_uf) + for j in range(n_uf, n_uf + n_if)]) + X = np.concatenate([X, temp]) + + est = self.intercept + np.dot(X, self.coef) + + return est From 885aab2a9c8000767af93f6be913476cbfd8c63f Mon Sep 17 00:00:00 2001 From: martincousi Date: Mon, 9 Apr 2018 10:56:57 -0400 Subject: [PATCH 39/60] add factorization_machines.py --- .../factorization_machines.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 surprise/prediction_algorithms/factorization_machines.py diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py new file mode 100644 index 00000000..fa10e341 --- /dev/null +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -0,0 +1,85 @@ +""" +the :mod:`knns` module includes some k-NN inspired algorithms. +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import numpy as np +from six import iteritems +import heapq + +from .predictions import PredictionImpossible +from .algo_base import AlgoBase + + +class KNNBasic(AlgoBase): + """A basic collaborative filtering algorithm. + + The prediction :math:`\\hat{r}_{ui}` is set as: + + .. math:: + \hat{r}_{ui} = \\frac{ + \\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v) \cdot r_{vi}} + {\\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v)} + + or + + .. math:: + \hat{r}_{ui} = \\frac{ + \\sum\\limits_{j \in N^k_u(i)} \\text{sim}(i, j) \cdot r_{uj}} + {\\sum\\limits_{j \in N^k_u(j)} \\text{sim}(i, j)} + + depending on the ``user_based`` field of the ``sim_options`` parameter. + + Args: + k(int): The (max) number of neighbors to take into account for + aggregation (see :ref:`this note `). Default is + ``40``. + min_k(int): The minimum number of neighbors to take into account for + aggregation. If there are not enough neighbors, the prediction is + set the the global mean of all ratings. Default is ``1``. + sim_options(dict): A dictionary of options for the similarity + measure. See :ref:`similarity_measures_configuration` for accepted + options. + verbose(bool): Whether to print trace messages of bias estimation, + similarity, etc. Default is True. + """ + + def __init__(self, **kwargs): + + AlgoBase.__init__(self, **kwargs) + + def fit(self, trainset): + + AlgoBase.fit(self, trainset) + + + + return self + + def estimate(self, u, i, *_): + + if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): + raise PredictionImpossible('User and/or item is unkown.') + + x, y = self.switch(u, i) + + neighbors = [(self.sim[x, x2], r) for (x2, r) in self.yr[y]] + k_neighbors = heapq.nlargest(self.k, neighbors, key=lambda t: t[0]) + + # compute weighted average + sum_sim = sum_ratings = actual_k = 0 + for (sim, r) in k_neighbors: + if sim > 0: + sum_sim += sim + sum_ratings += sim * r + actual_k += 1 + + if actual_k < self.min_k: + raise PredictionImpossible('Not enough neighbors.') + + est = sum_ratings / sum_sim + + details = {'actual_k': actual_k} + + return est, details From 69c281f421b96da8f41a5b9db328640230f3aa12 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 11 Apr 2018 14:41:38 -0400 Subject: [PATCH 40/60] add FMAlgo and FMBasic --- surprise/__init__.py | 3 +- surprise/prediction_algorithms/__init__.py | 3 +- .../factorization_machines.py | 188 +++++++++++++----- 3 files changed, 141 insertions(+), 53 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index 82de2460..ef4c82b1 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -13,6 +13,7 @@ from .prediction_algorithms import SlopeOne from .prediction_algorithms import CoClustering from .prediction_algorithms import Lasso +from .prediction_algorithms import FMBasic from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -32,6 +33,6 @@ 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso'] + 'Lasso', 'FMBasic'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 1a719a97..30843f97 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -33,6 +33,7 @@ from .slope_one import SlopeOne from .co_clustering import CoClustering from .linear import Lasso +from .factorization_machines import FMBasic from .predictions import PredictionImpossible from .predictions import Prediction @@ -40,4 +41,4 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore', 'Lasso'] + 'KNNWithZScore', 'Lasso', FMBasic] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index fa10e341..5069172d 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -5,81 +5,167 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import numpy as np +import scipy.sparse as sps from six import iteritems import heapq +from tffm import TFFMClassifier, TFFMRegressor from .predictions import PredictionImpossible from .algo_base import AlgoBase -class KNNBasic(AlgoBase): - """A basic collaborative filtering algorithm. - - The prediction :math:`\\hat{r}_{ui}` is set as: - - .. math:: - \hat{r}_{ui} = \\frac{ - \\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v) \cdot r_{vi}} - {\\sum\\limits_{v \in N^k_i(u)} \\text{sim}(u, v)} - - or - - .. math:: - \hat{r}_{ui} = \\frac{ - \\sum\\limits_{j \in N^k_u(i)} \\text{sim}(i, j) \cdot r_{uj}} - {\\sum\\limits_{j \in N^k_u(j)} \\text{sim}(i, j)} - - depending on the ``user_based`` field of the ``sim_options`` parameter. - - Args: - k(int): The (max) number of neighbors to take into account for - aggregation (see :ref:`this note `). Default is - ``40``. - min_k(int): The minimum number of neighbors to take into account for - aggregation. If there are not enough neighbors, the prediction is - set the the global mean of all ratings. Default is ``1``. - sim_options(dict): A dictionary of options for the similarity - measure. See :ref:`similarity_measures_configuration` for accepted - options. - verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. +class FMAlgo(AlgoBase): + """This is an abstract class aimed to reduce code redundancy for + factoration machines. """ - def __init__(self, **kwargs): + def __init__(self, order=2, n_factors=5, input_type='dense', + loss_function='mse', optimizer=None, reg_all=1., + use_diag=False, reweight_reg=False, init_std=0.01, + batch_size=-1, n_epochs=100, log_dir=None, + session_config=None, random_state=None, verbose=False, + **kwargs): AlgoBase.__init__(self, **kwargs) + self.order = order + self.n_factors = n_factors # rank in `tffm` + self.input_type = input_type + self.loss_function = loss_function # {'mse', 'loss_logistic'} + # https://www.tensorflow.org/api_guides/python/train#Optimizers + # tf.train.Optimizer, default: AdamOptimizer(learning_rate=0.01) + self.optimizer = optimizer + self.reg_all = reg_all # reg in `tffm` + self.use_diag = use_diag + self.reweight_reg = reweight_reg + self.init_std = init_std + self.batch_size = batch_size + self.n_epochs = n_epochs + self.log_dir = log_dir + self.session_config = session_config + self.random_state = random_state # seed in `tffm` + self.verbose = verbose + + if self.loss_function == 'mse': + self.tffm_model = TFFMRegressor( + order=self.order, rank=self.n_factors, + input_type=self.input_type, optimizer=self.optimizer, + reg=self.reg_all, use_diag=self.use_diag, + reweight_reg=self.reweight_reg, init_std=self.init_std, + batch_size=self.batch_size, n_epochs=self.n_epochs, + log_dir=self.log_dir, session_config=self.session_config, + seed=self.random_state, verbose=self.verbose) + elif self.loss_function == 'loss_logistic': + # See issue #157 + raise ValueError('loss_logistic is not supported at the moment') + self.tffm_model = TFFMClassifier( + order=self.order, rank=self.n_factors, + input_type=self.input_type, optimizer=self.optimizer, + reg=self.reg_all, use_diag=self.use_diag, + reweight_reg=self.reweight_reg, init_std=self.init_std, + batch_size=self.batch_size, n_epochs=self.n_epochs, + log_dir=self.log_dir, session_config=self.session_config, + seed=self.random_state, verbose=self.verbose) + else: + raise ValueError(('Unknown value {} for parameter' + 'loss_function').format(self.loss_function)) def fit(self, trainset): AlgoBase.fit(self, trainset) - + if self.verbose > 0: + self.show_progress = True return self - def estimate(self, u, i, *_): - if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - raise PredictionImpossible('User and/or item is unkown.') +class FMBasic(FMAlgo): + """A basic factorization machine algorithm. With order 2, this algorithm + is equivalent to the biased SVD algorithm. - x, y = self.switch(u, i) + This code is an interface to the `tffm` library. - neighbors = [(self.sim[x, x2], r) for (x2, r) in self.yr[y]] - k_neighbors = heapq.nlargest(self.k, neighbors, key=lambda t: t[0]) + Args: + """ + + def __init__(self, order=2, n_factors=5, loss_function='mse', + optimizer=None, reg_all=1., use_diag=False, + reweight_reg=False, init_std=0.01, batch_size=-1, + n_epochs=100, log_dir=None, session_config=None, + random_state=None, verbose=False, **kwargs): - # compute weighted average - sum_sim = sum_ratings = actual_k = 0 - for (sim, r) in k_neighbors: - if sim > 0: - sum_sim += sim - sum_ratings += sim * r - actual_k += 1 + input_type = 'sparse' - if actual_k < self.min_k: - raise PredictionImpossible('Not enough neighbors.') + FMAlgo.__init__(self, order=order, n_factors=n_factors, + input_type=input_type, loss_function=loss_function, + optimizer=optimizer, reg_all=reg_all, + use_diag=use_diag, reweight_reg=reweight_reg, + init_std=init_std, batch_size=batch_size, + n_epochs=n_epochs, log_dir=log_dir, + session_config=session_config, + random_state=random_state, verbose=verbose, **kwargs) + + def fit(self, trainset): - est = sum_ratings / sum_sim + FMAlgo.fit(self, trainset) + self.fm(trainset) - details = {'actual_k': actual_k} + return self + + def fm(self, trainset): + + n_ratings = self.trainset.n_ratings + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + # Construct sparse X and y + row_ind = np.empty(n_ratings) + col_ind = np.empty(n_ratings) + data = np.ones(n_ratings) + y_train = np.empty(n_ratings) + nonzero_counter = 0 + rating_counter = 0 + for uid, iid, rating in self.trainset.all_ratings(): + # Add user + row_ind[nonzero_counter] = rating_counter + col_ind[nonzero_counter] = uid + nonzero_counter += 1 + # Add item + row_ind[nonzero_counter] = rating_counter + col_ind[nonzero_counter] = n_users + iid + nonzero_counter += 1 + # Add rating + y_train[rating_counter] = rating + rating_counter += 1 + X_train = sps.csr_matrix(data, (row_ind, col_ind), + shape=(n_ratings, n_users + n_items)) + + # `Dataset` and `Trainset` do not support sample_weight at the moment. + self.tffm_model.fit(X_train, y_train, sample_weight=None, + show_progress=self.show_progress) + self.X_train = X_train + self.y_train = y_train + + def estimate(self, u, i, *_): - return est, details + # what happens for new user/item in predict? + # if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): + # raise PredictionImpossible('User and/or item is unknown.') + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + if self.trainset.knows_user(u) and self.trainset.knows_item(i): + X_test = sps.csr_matrix([1., 1.], ([0, 0], [u, n_users + i]), + shape=(1, n_users + n_items)) + elif self.trainset.knows_user(u): + X_test = sps.csr_matrix([1.], ([0], [u]), + shape=(1, n_users + n_items)) + elif self.trainset.knows_item(u): + X_test = sps.csr_matrix([1.], ([0], [n_users + i]), + shape=(1, n_users + n_items)) + else: + X_test = sps.csr_matrix((1, n_users + n_items)) + + est = self.tffm_model.predict(X_test)[0] + + return est From b8132e5889b829e8e607c46bb182209ef0415a1a Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 12 Apr 2018 16:09:19 -0400 Subject: [PATCH 41/60] solve bugs --- .../factorization_machines.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 5069172d..67816b69 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -5,6 +5,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import numpy as np +import tensorflow as tf import scipy.sparse as sps from six import iteritems import heapq @@ -33,7 +34,10 @@ def __init__(self, order=2, n_factors=5, input_type='dense', self.loss_function = loss_function # {'mse', 'loss_logistic'} # https://www.tensorflow.org/api_guides/python/train#Optimizers # tf.train.Optimizer, default: AdamOptimizer(learning_rate=0.01) - self.optimizer = optimizer + if optimizer is None: + self.optimizer = tf.train.AdamOptimizer(learning_rate=0.001) + else: + self.optimizer = optimizer self.reg_all = reg_all # reg in `tffm` self.use_diag = use_diag self.reweight_reg = reweight_reg @@ -75,6 +79,8 @@ def fit(self, trainset): if self.verbose > 0: self.show_progress = True + else: + self.show_progress = False return self @@ -119,10 +125,10 @@ def fm(self, trainset): n_items = self.trainset.n_items # Construct sparse X and y - row_ind = np.empty(n_ratings) - col_ind = np.empty(n_ratings) - data = np.ones(n_ratings) - y_train = np.empty(n_ratings) + row_ind = np.empty(2 * n_ratings, dtype=int) + col_ind = np.empty(2 * n_ratings, dtype=int) + data = np.ones(2 * n_ratings, dtype=bool) + y_train = np.empty(n_ratings, dtype=float) nonzero_counter = 0 rating_counter = 0 for uid, iid, rating in self.trainset.all_ratings(): @@ -137,8 +143,9 @@ def fm(self, trainset): # Add rating y_train[rating_counter] = rating rating_counter += 1 - X_train = sps.csr_matrix(data, (row_ind, col_ind), - shape=(n_ratings, n_users + n_items)) + X_train = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(n_ratings, n_users + n_items), + dtype=bool) # `Dataset` and `Trainset` do not support sample_weight at the moment. self.tffm_model.fit(X_train, y_train, sample_weight=None, @@ -154,18 +161,27 @@ def estimate(self, u, i, *_): n_users = self.trainset.n_users n_items = self.trainset.n_items + details = {} if self.trainset.knows_user(u) and self.trainset.knows_item(i): - X_test = sps.csr_matrix([1., 1.], ([0, 0], [u, n_users + i]), - shape=(1, n_users + n_items)) + X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = True + details['knows_item'] = True elif self.trainset.knows_user(u): - X_test = sps.csr_matrix([1.], ([0], [u]), - shape=(1, n_users + n_items)) - elif self.trainset.knows_item(u): - X_test = sps.csr_matrix([1.], ([0], [n_users + i]), - shape=(1, n_users + n_items)) + X_test = sps.csr_matrix(([1.], ([0], [u])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = True + details['knows_item'] = False + elif self.trainset.knows_item(i): + X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = False + details['knows_item'] = True else: - X_test = sps.csr_matrix((1, n_users + n_items)) + X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) + details['knows_user'] = False + details['knows_item'] = False est = self.tffm_model.predict(X_test)[0] - return est + return est, details From b06a05dbc633c717f9ba93d06459aed9a5279b7d Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 12 Apr 2018 16:09:43 -0400 Subject: [PATCH 42/60] add indication of features in Prediction --- surprise/prediction_algorithms/predictions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 9e971ee9..537d7ab7 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -44,13 +44,13 @@ def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) if self.u_features is not None: - pass + s += 'u_features = True ' else: - s += 'u_features = None ' + s += 'u_features = False ' if self.i_features is not None: - pass + s += 'i_features = True ' else: - s += 'i_features = None ' + s += 'i_features = False ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: From 458a4193ccc0a4568372af63599ca43ef1985fcc Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 09:05:17 -0400 Subject: [PATCH 43/60] add FMBasic based on polylearn --- surprise/__init__.py | 3 +- surprise/prediction_algorithms/__init__.py | 3 +- .../factorization_machines.py | 103 ++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index ef4c82b1..87d5b279 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -14,6 +14,7 @@ from .prediction_algorithms import CoClustering from .prediction_algorithms import Lasso from .prediction_algorithms import FMBasic +from .prediction_algorithms import FMBasicPL from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -33,6 +34,6 @@ 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso', 'FMBasic'] + 'Lasso', 'FMBasic', 'FMBasicPL'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 30843f97..193bbe52 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -34,6 +34,7 @@ from .co_clustering import CoClustering from .linear import Lasso from .factorization_machines import FMBasic +from .factorization_machines import FMBasicPL from .predictions import PredictionImpossible from .predictions import Prediction @@ -41,4 +42,4 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore', 'Lasso', FMBasic] + 'KNNWithZScore', 'Lasso', 'FMBasic', 'FMBasicPL'] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 67816b69..5c9f5132 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -10,6 +10,7 @@ from six import iteritems import heapq from tffm import TFFMClassifier, TFFMRegressor +from polylearn import FactorizationMachineRegressor from .predictions import PredictionImpossible from .algo_base import AlgoBase @@ -185,3 +186,105 @@ def estimate(self, u, i, *_): est = self.tffm_model.predict(X_test)[0] return est, details + + +class FMBasicPL(AlgoBase): + """A basic factorization machine algorithm. With order 2, this algorithm + is equivalent to the biased SVD algorithm. + + This code is an interface to the `polylearn` library. + + Args: + """ + + def __init__(self, degree=2, n_factors=5, reg_all=1., reg_alpha=1., + reg_beta=1., tol=1e-6, + random_state=None, verbose=False, **kwargs): + + self.degree = degree + self.n_factors = n_factors # n_components in `polylearn` + self.reg_all = reg_all # not in `polylearn` + self.reg_alpha = reg_alpha # alpha in `polylearn` + self.reg_beta = reg_beta # beta in `polylearn` + self.tol = tol + # ... + self.random_state = random_state + self.verbose = verbose + + self.fm_model = FactorizationMachineRegressor( + degree=self.degree, n_components=self.n_factors, + alpha=self.reg_alpha, beta=self.reg_beta, tol=self.tol, + random_state=self.random_state, verbose=self.verbose) + + def fit(self, trainset): + + FMAlgo.fit(self, trainset) + self.fm(trainset) + + return self + + def fm(self, trainset): + + n_ratings = self.trainset.n_ratings + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + # Construct sparse X and y + row_ind = np.empty(2 * n_ratings, dtype=int) + col_ind = np.empty(2 * n_ratings, dtype=int) + data = np.ones(2 * n_ratings, dtype=bool) + y_train = np.empty(n_ratings, dtype=float) + nonzero_counter = 0 + rating_counter = 0 + for uid, iid, rating in self.trainset.all_ratings(): + # Add user + row_ind[nonzero_counter] = rating_counter + col_ind[nonzero_counter] = uid + nonzero_counter += 1 + # Add item + row_ind[nonzero_counter] = rating_counter + col_ind[nonzero_counter] = n_users + iid + nonzero_counter += 1 + # Add rating + y_train[rating_counter] = rating + rating_counter += 1 + X_train = sps.csc_matrix((data, (row_ind, col_ind)), + shape=(n_ratings, n_users + n_items), + dtype=bool) + + self.tffm_model.fit(X_train, y_train) + self.X_train = X_train + self.y_train = y_train + + def estimate(self, u, i, *_): + + # what happens for new user/item in predict? + # if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): + # raise PredictionImpossible('User and/or item is unknown.') + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + details = {} + if self.trainset.knows_user(u) and self.trainset.knows_item(i): + X_test = sps.csc_matrix(([1., 1.], ([0, 0], [u, n_users + i])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = True + details['knows_item'] = True + elif self.trainset.knows_user(u): + X_test = sps.csc_matrix(([1.], ([0], [u])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = True + details['knows_item'] = False + elif self.trainset.knows_item(i): + X_test = sps.csc_matrix(([1.], ([0], [n_users + i])), + shape=(1, n_users + n_items), dtype=bool) + details['knows_user'] = False + details['knows_item'] = True + else: + X_test = sps.csc_matrix((1, n_users + n_items), dtype=bool) + details['knows_user'] = False + details['knows_item'] = False + + est = self.tffm_model.predict(X_test)[0] + + return est, details From d8cea2f6b85eeb3116cd4f317475b21d50497371 Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 09:59:37 -0400 Subject: [PATCH 44/60] Remove features from Prediction --- surprise/accuracy.py | 6 +++--- surprise/prediction_algorithms/algo_base.py | 2 +- surprise/prediction_algorithms/knns.py | 7 +++---- surprise/prediction_algorithms/predictions.py | 13 +------------ 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 0bfcd1af..1e9e4855 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -117,7 +117,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, _, _, _, r0, est, _ in predictions: + for u0, _, r0, est, _ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 4fdcd8f0..5d4c5e02 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -171,7 +171,7 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, est = min(higher_bound, est) est = max(lower_bound, est) - pred = Prediction(uid, iid, u_features, i_features, r_ui, est, details) + pred = Prediction(uid, iid, r_ui, est, details) if verbose: print(pred) diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 301a8168..245a83dc 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -103,7 +103,7 @@ def fit(self, trainset): def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - raise PredictionImpossible('User and/or item is unkown.') + raise PredictionImpossible('User and/or item is unknown.') x, y = self.switch(u, i) @@ -186,7 +186,7 @@ def fit(self, trainset): def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - raise PredictionImpossible('User and/or item is unkown.') + raise PredictionImpossible('User and/or item is unknown.') x, y = self.switch(u, i) @@ -259,7 +259,6 @@ class KNNBaseline(SymmetricAlgo): accepted options. verbose(bool): Whether to print trace messages of bias estimation, similarity, etc. Default is True. - """ def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, @@ -386,7 +385,7 @@ def fit(self, trainset): def estimate(self, u, i, *_): if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - raise PredictionImpossible('User and/or item is unkown.') + raise PredictionImpossible('User and/or item is unknown.') x, y = self.switch(u, i) diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 537d7ab7..76bc8ddc 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -21,8 +21,7 @@ class PredictionImpossible(Exception): class Prediction(namedtuple('Prediction', - ['uid', 'iid', 'u_features', 'i_features', 'r_ui', - 'est', 'details'])): + ['uid', 'iid', 'r_ui', 'est', 'details'])): """A named tuple for storing the results of a prediction. It's wrapped in a class, but only for documentation and printing purposes. @@ -30,8 +29,6 @@ class Prediction(namedtuple('Prediction', Args: uid: The (raw) user id. See :ref:`this note`. iid: The (raw) item id. See :ref:`this note`. - u_features: The user features. - i_features: The item features. r_ui(float): The true rating :math:`r_{ui}`. est(float): The estimated rating :math:`\\hat{r}_{ui}`. details (dict): Stores additional details about the prediction that @@ -43,14 +40,6 @@ class Prediction(namedtuple('Prediction', def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) - if self.u_features is not None: - s += 'u_features = True ' - else: - s += 'u_features = False ' - if self.i_features is not None: - s += 'i_features = True ' - else: - s += 'i_features = False ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: From 4337b2488192b35944f53c1a0d3b4898f2b108ab Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 10:41:25 -0400 Subject: [PATCH 45/60] changed tests --- surprise/dataset.py | 6 ++++-- tests/custom_test | 6 +++--- tests/custom_train | 12 ++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index d5d74cb6..8ecda06c 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,10 +53,12 @@ class Dataset: def __init__(self, reader): self.reader = reader - self.user_features_nb = 0 - self.item_features_nb = 0 self.user_features = {} self.item_features = {} + self.user_features_nb = 0 + self.item_features_nb = 0 + self.user_features_labels = [] + self.item_features_labels = [] @classmethod def load_builtin(cls, name='ml-100k'): diff --git a/tests/custom_test b/tests/custom_test index 8c72670d..fdc8eb89 100644 --- a/tests/custom_test +++ b/tests/custom_test @@ -1,6 +1,6 @@ There are three lines to be ignored Line format is user - item - rating -user3 item0 5 -user0 item1 1 -user_neverseen item_neverseen 5 +user3 item0 None None 5 +user0 item1 None None 1 +user_neverseen item_neverseen None None 5 diff --git a/tests/custom_train b/tests/custom_train index 76e9a4e2..6e7148a2 100644 --- a/tests/custom_train +++ b/tests/custom_train @@ -1,9 +1,9 @@ There are three lines to be ignored Line format is user - item - rating -user0 item0 4 -user1 item0 4 -user1 item1 2 -user2 item0 1 -user2 item1 1 -user3 item1 5 +user0 item0 None None 4 +user1 item0 None None 4 +user1 item1 None None 2 +user2 item0 None None 1 +user2 item1 None None 1 +user3 item1 None None 5 From a0765a2bf3adf83ac72e03ac26f1bea6ff93dbb4 Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 11:14:50 -0400 Subject: [PATCH 46/60] correct tests --- surprise/accuracy.py | 6 ++--- surprise/dataset.py | 2 ++ surprise/prediction_algorithms/algo_base.py | 2 +- surprise/prediction_algorithms/predictions.py | 13 +---------- tests/test_dataset.py | 22 +++++++++---------- tests/test_split.py | 2 +- tests/test_train2fit.py | 4 ++-- 7 files changed, 21 insertions(+), 30 deletions(-) diff --git a/surprise/accuracy.py b/surprise/accuracy.py index 0bfcd1af..1e9e4855 100644 --- a/surprise/accuracy.py +++ b/surprise/accuracy.py @@ -45,7 +45,7 @@ def rmse(predictions, verbose=True): raise ValueError('Prediction list is empty.') mse = np.mean([float((true_r - est)**2) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) rmse_ = np.sqrt(mse) if verbose: @@ -80,7 +80,7 @@ def mae(predictions, verbose=True): raise ValueError('Prediction list is empty.') mae_ = np.mean([float(abs(true_r - est)) - for (_, _, _, _, true_r, est, _) in predictions]) + for (_, _, true_r, est, _) in predictions]) if verbose: print('MAE: {0:1.4f}'.format(mae_)) @@ -117,7 +117,7 @@ def fcp(predictions, verbose=True): nc_u = defaultdict(int) nd_u = defaultdict(int) - for u0, _, _, _, r0, est, _ in predictions: + for u0, _, r0, est, _ in predictions: predictions_u[u0].append((r0, est)) for u0, preds in iteritems(predictions_u): diff --git a/surprise/dataset.py b/surprise/dataset.py index d5d74cb6..da8a5ba4 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -57,6 +57,8 @@ def __init__(self, reader): self.item_features_nb = 0 self.user_features = {} self.item_features = {} + self.user_features_labels = [] + self.item_features_labels = [] @classmethod def load_builtin(cls, name='ml-100k'): diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 4fdcd8f0..5d4c5e02 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -171,7 +171,7 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, est = min(higher_bound, est) est = max(lower_bound, est) - pred = Prediction(uid, iid, u_features, i_features, r_ui, est, details) + pred = Prediction(uid, iid, r_ui, est, details) if verbose: print(pred) diff --git a/surprise/prediction_algorithms/predictions.py b/surprise/prediction_algorithms/predictions.py index 9e971ee9..76bc8ddc 100644 --- a/surprise/prediction_algorithms/predictions.py +++ b/surprise/prediction_algorithms/predictions.py @@ -21,8 +21,7 @@ class PredictionImpossible(Exception): class Prediction(namedtuple('Prediction', - ['uid', 'iid', 'u_features', 'i_features', 'r_ui', - 'est', 'details'])): + ['uid', 'iid', 'r_ui', 'est', 'details'])): """A named tuple for storing the results of a prediction. It's wrapped in a class, but only for documentation and printing purposes. @@ -30,8 +29,6 @@ class Prediction(namedtuple('Prediction', Args: uid: The (raw) user id. See :ref:`this note`. iid: The (raw) item id. See :ref:`this note`. - u_features: The user features. - i_features: The item features. r_ui(float): The true rating :math:`r_{ui}`. est(float): The estimated rating :math:`\\hat{r}_{ui}`. details (dict): Stores additional details about the prediction that @@ -43,14 +40,6 @@ class Prediction(namedtuple('Prediction', def __str__(self): s = 'user: {uid:<10} '.format(uid=self.uid) s += 'item: {iid:<10} '.format(iid=self.iid) - if self.u_features is not None: - pass - else: - s += 'u_features = None ' - if self.i_features is not None: - pass - else: - s += 'i_features = None ' if self.r_ui is not None: s += 'r_ui = {r_ui:1.2f} '.format(r_ui=self.r_ui) else: diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 69311404..2526d20c 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -145,12 +145,12 @@ def test_trainset_testset(): for i in range(4): assert trainset.to_inner_uid('user' + str(i)) == i with pytest.raises(ValueError): - trainset.to_inner_uid('unkown_user') + trainset.to_inner_uid('unknown_user') for i in range(2): assert trainset.to_inner_iid('item' + str(i)) == i with pytest.raises(ValueError): - trainset.to_inner_iid('unkown_item') + trainset.to_inner_iid('unknown_item') # test inner2raw assert trainset._inner2raw_id_users is None @@ -167,19 +167,19 @@ def test_trainset_testset(): algo.fit(trainset) testset = trainset.build_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', 4) in testset - assert ('user3', 'item1', 5) in testset - assert ('user3', 'item1', 0) not in testset + assert ('user0', 'item0', None, None, 4) in testset + assert ('user3', 'item1', None, None, 5) in testset + assert ('user3', 'item1', None, None, 0) not in testset # Test the build_anti_testset() method algo = BaselineOnly() algo.fit(trainset) testset = trainset.build_anti_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', trainset.global_mean) not in testset - assert ('user3', 'item1', trainset.global_mean) not in testset - assert ('user0', 'item1', trainset.global_mean) in testset - assert ('user3', 'item0', trainset.global_mean) in testset + assert ('user0', 'item0', None, None, trainset.global_mean) not in testset + assert ('user3', 'item1', None, None, trainset.global_mean) not in testset + assert ('user0', 'item1', None, None, trainset.global_mean) in testset + assert ('user3', 'item0', None, None, trainset.global_mean) in testset def test_load_form_df(): @@ -238,11 +238,11 @@ def test_build_anti_testset(): # fill with some specific value for fillvalue in (0, 42., -1): anti = trainset.build_anti_testset(fill=fillvalue) - for (u, i, r) in anti: + for (u, i, u_f, i_f, r) in anti: assert r == fillvalue # fill with global_mean anti = trainset.build_anti_testset(fill=None) - for (u, i, r) in anti: + for (u, i, u_f, i_f, r) in anti: assert r == trainset.global_mean expect = trainset.n_users * trainset.n_items assert trainset.n_ratings + len(anti) == expect diff --git a/tests/test_split.py b/tests/test_split.py index 0c12cb53..d55eb5ad 100644 --- a/tests/test_split.py +++ b/tests/test_split.py @@ -299,7 +299,7 @@ def test_LeaveOneOut(): # Make sure only one rating per user is present in the testset loo = LeaveOneOut() for _, testset in loo.split(data): - cnt = Counter([uid for (uid, _, _) in testset]) + cnt = Counter([uid for (uid, _, _, _, _) in testset]) assert all(val == 1 for val in itervalues(cnt)) # test the min_n_ratings parameter diff --git a/tests/test_train2fit.py b/tests/test_train2fit.py index ab0634e4..b2993031 100644 --- a/tests/test_train2fit.py +++ b/tests/test_train2fit.py @@ -35,7 +35,7 @@ def fit(self, trainset): self.bu, self.bi = 1, 1 self.cnt += 1 - def estimate(self, u, i): + def estimate(self, u, i, *_): return self.est algo = CustomAlgoFit() @@ -91,7 +91,7 @@ def train(self, trainset): self.bu, self.bi = 1, 1 self.cnt += 1 - def estimate(self, u, i): + def estimate(self, u, i, *_): return self.est with pytest.warns(UserWarning): From 1b6ee312dd0f59ac6c139a59501d2e9f6bb87e65 Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 11:19:36 -0400 Subject: [PATCH 47/60] correct tests --- .../factorization_machines.py | 3 --- tests/custom_test | 6 ++--- tests/custom_train | 12 +++++----- tests/test_dataset.py | 22 +++++++++---------- tests/test_split.py | 2 +- tests/test_train2fit.py | 4 ++-- 6 files changed, 23 insertions(+), 26 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 5c9f5132..792bb0e0 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -7,12 +7,9 @@ import numpy as np import tensorflow as tf import scipy.sparse as sps -from six import iteritems -import heapq from tffm import TFFMClassifier, TFFMRegressor from polylearn import FactorizationMachineRegressor -from .predictions import PredictionImpossible from .algo_base import AlgoBase diff --git a/tests/custom_test b/tests/custom_test index fdc8eb89..8c72670d 100644 --- a/tests/custom_test +++ b/tests/custom_test @@ -1,6 +1,6 @@ There are three lines to be ignored Line format is user - item - rating -user3 item0 None None 5 -user0 item1 None None 1 -user_neverseen item_neverseen None None 5 +user3 item0 5 +user0 item1 1 +user_neverseen item_neverseen 5 diff --git a/tests/custom_train b/tests/custom_train index 6e7148a2..76e9a4e2 100644 --- a/tests/custom_train +++ b/tests/custom_train @@ -1,9 +1,9 @@ There are three lines to be ignored Line format is user - item - rating -user0 item0 None None 4 -user1 item0 None None 4 -user1 item1 None None 2 -user2 item0 None None 1 -user2 item1 None None 1 -user3 item1 None None 5 +user0 item0 4 +user1 item0 4 +user1 item1 2 +user2 item0 1 +user2 item1 1 +user3 item1 5 diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 69311404..2526d20c 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -145,12 +145,12 @@ def test_trainset_testset(): for i in range(4): assert trainset.to_inner_uid('user' + str(i)) == i with pytest.raises(ValueError): - trainset.to_inner_uid('unkown_user') + trainset.to_inner_uid('unknown_user') for i in range(2): assert trainset.to_inner_iid('item' + str(i)) == i with pytest.raises(ValueError): - trainset.to_inner_iid('unkown_item') + trainset.to_inner_iid('unknown_item') # test inner2raw assert trainset._inner2raw_id_users is None @@ -167,19 +167,19 @@ def test_trainset_testset(): algo.fit(trainset) testset = trainset.build_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', 4) in testset - assert ('user3', 'item1', 5) in testset - assert ('user3', 'item1', 0) not in testset + assert ('user0', 'item0', None, None, 4) in testset + assert ('user3', 'item1', None, None, 5) in testset + assert ('user3', 'item1', None, None, 0) not in testset # Test the build_anti_testset() method algo = BaselineOnly() algo.fit(trainset) testset = trainset.build_anti_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', trainset.global_mean) not in testset - assert ('user3', 'item1', trainset.global_mean) not in testset - assert ('user0', 'item1', trainset.global_mean) in testset - assert ('user3', 'item0', trainset.global_mean) in testset + assert ('user0', 'item0', None, None, trainset.global_mean) not in testset + assert ('user3', 'item1', None, None, trainset.global_mean) not in testset + assert ('user0', 'item1', None, None, trainset.global_mean) in testset + assert ('user3', 'item0', None, None, trainset.global_mean) in testset def test_load_form_df(): @@ -238,11 +238,11 @@ def test_build_anti_testset(): # fill with some specific value for fillvalue in (0, 42., -1): anti = trainset.build_anti_testset(fill=fillvalue) - for (u, i, r) in anti: + for (u, i, u_f, i_f, r) in anti: assert r == fillvalue # fill with global_mean anti = trainset.build_anti_testset(fill=None) - for (u, i, r) in anti: + for (u, i, u_f, i_f, r) in anti: assert r == trainset.global_mean expect = trainset.n_users * trainset.n_items assert trainset.n_ratings + len(anti) == expect diff --git a/tests/test_split.py b/tests/test_split.py index 0c12cb53..d55eb5ad 100644 --- a/tests/test_split.py +++ b/tests/test_split.py @@ -299,7 +299,7 @@ def test_LeaveOneOut(): # Make sure only one rating per user is present in the testset loo = LeaveOneOut() for _, testset in loo.split(data): - cnt = Counter([uid for (uid, _, _) in testset]) + cnt = Counter([uid for (uid, _, _, _, _) in testset]) assert all(val == 1 for val in itervalues(cnt)) # test the min_n_ratings parameter diff --git a/tests/test_train2fit.py b/tests/test_train2fit.py index ab0634e4..b2993031 100644 --- a/tests/test_train2fit.py +++ b/tests/test_train2fit.py @@ -35,7 +35,7 @@ def fit(self, trainset): self.bu, self.bi = 1, 1 self.cnt += 1 - def estimate(self, u, i): + def estimate(self, u, i, *_): return self.est algo = CustomAlgoFit() @@ -91,7 +91,7 @@ def train(self, trainset): self.bu, self.bi = 1, 1 self.cnt += 1 - def estimate(self, u, i): + def estimate(self, u, i, *_): return self.est with pytest.warns(UserWarning): From bef04b9700356b95375c892409fe46bded8246bc Mon Sep 17 00:00:00 2001 From: martincousi Date: Fri, 13 Apr 2018 15:26:49 -0400 Subject: [PATCH 48/60] correct FMBasicPL --- .../factorization_machines.py | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 792bb0e0..0bcc293a 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -48,7 +48,7 @@ def __init__(self, order=2, n_factors=5, input_type='dense', self.verbose = verbose if self.loss_function == 'mse': - self.tffm_model = TFFMRegressor( + self.model = TFFMRegressor( order=self.order, rank=self.n_factors, input_type=self.input_type, optimizer=self.optimizer, reg=self.reg_all, use_diag=self.use_diag, @@ -59,7 +59,7 @@ def __init__(self, order=2, n_factors=5, input_type='dense', elif self.loss_function == 'loss_logistic': # See issue #157 raise ValueError('loss_logistic is not supported at the moment') - self.tffm_model = TFFMClassifier( + self.model = TFFMClassifier( order=self.order, rank=self.n_factors, input_type=self.input_type, optimizer=self.optimizer, reg=self.reg_all, use_diag=self.use_diag, @@ -146,8 +146,8 @@ def fm(self, trainset): dtype=bool) # `Dataset` and `Trainset` do not support sample_weight at the moment. - self.tffm_model.fit(X_train, y_train, sample_weight=None, - show_progress=self.show_progress) + self.model.fit(X_train, y_train, sample_weight=None, + show_progress=self.show_progress) self.X_train = X_train self.y_train = y_train @@ -180,7 +180,7 @@ def estimate(self, u, i, *_): details['knows_user'] = False details['knows_item'] = False - est = self.tffm_model.predict(X_test)[0] + est = self.model.predict(X_test)[0] return est, details @@ -195,7 +195,8 @@ class FMBasicPL(AlgoBase): """ def __init__(self, degree=2, n_factors=5, reg_all=1., reg_alpha=1., - reg_beta=1., tol=1e-6, + reg_beta=1., tol=1e-6, fit_lower='explicit', fit_linear=True, + warm_start=False, init_lambdas='ones', max_iter=10000, random_state=None, verbose=False, **kwargs): self.degree = degree @@ -204,23 +205,25 @@ def __init__(self, degree=2, n_factors=5, reg_all=1., reg_alpha=1., self.reg_alpha = reg_alpha # alpha in `polylearn` self.reg_beta = reg_beta # beta in `polylearn` self.tol = tol - # ... + self.fit_lower = fit_lower + self.fit_linear = fit_linear + self.warm_start = warm_start + self.init_lambdas = init_lambdas # what is this? + self.max_iter = max_iter self.random_state = random_state self.verbose = verbose - self.fm_model = FactorizationMachineRegressor( + self.model = FactorizationMachineRegressor( degree=self.degree, n_components=self.n_factors, alpha=self.reg_alpha, beta=self.reg_beta, tol=self.tol, - random_state=self.random_state, verbose=self.verbose) + fit_lower=self.fit_lower, fit_linear=self.fit_linear, + warm_start=self.warm_start, init_lambdas=self.init_lambdas, + max_iter=self.max_iter, random_state=self.random_state, + verbose=self.verbose) def fit(self, trainset): - FMAlgo.fit(self, trainset) - self.fm(trainset) - - return self - - def fm(self, trainset): + AlgoBase.fit(self, trainset) n_ratings = self.trainset.n_ratings n_users = self.trainset.n_users @@ -249,10 +252,12 @@ def fm(self, trainset): shape=(n_ratings, n_users + n_items), dtype=bool) - self.tffm_model.fit(X_train, y_train) + self.model.fit(X_train, y_train) self.X_train = X_train self.y_train = y_train + return self + def estimate(self, u, i, *_): # what happens for new user/item in predict? @@ -282,6 +287,6 @@ def estimate(self, u, i, *_): details['knows_user'] = False details['knows_item'] = False - est = self.tffm_model.predict(X_test)[0] + est = self.model.predict(X_test)[0] return est, details From c998024ac15338cadda54b37c34a2f70e9bc43bd Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 19 Apr 2018 21:58:30 -0400 Subject: [PATCH 49/60] add tests for datasets with features --- surprise/dataset.py | 30 +-- surprise/prediction_algorithms/algo_base.py | 6 +- tests/test_accuracy.py | 8 +- tests/test_algorithms.py | 80 +++++++- tests/test_dataset.py | 207 +++++++++++++++++++- 5 files changed, 300 insertions(+), 31 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index 8ecda06c..d65b9fb9 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -53,8 +53,8 @@ class Dataset: def __init__(self, reader): self.reader = reader - self.user_features = {} - self.item_features = {} + self.user_features = defaultdict(list) + self.item_features = defaultdict(list) self.user_features_nb = 0 self.item_features_nb = 0 self.user_features_labels = [] @@ -186,16 +186,22 @@ def load_features_df(self, features_df, user_features=True): or the items. Default is ``True``. """ + if len(features_df.columns) < 2: + raise ValueError('features_df requires at least 2 columns.') + + if not features_df.iloc[:, 0].is_unique: + raise ValueError('first column of features_df must be unique ids.') + if user_features: self.user_features_df = features_df - self.user_features = {tup[0]: tup[1:] for tup in - features_df.itertuples(index=False)} + for tup in features_df.itertuples(index=False): + self.user_features[tup[0]] = list(tup[1:]) self.user_features_labels = features_df.columns.values.tolist()[1:] self.user_features_nb = len(self.user_features_labels) else: self.item_features_df = features_df - self.item_features = {tup[0]: tup[1:] for tup in - features_df.itertuples(index=False)} + for tup in features_df.itertuples(index=False): + self.item_features[tup[0]] = list(tup[1:]) self.item_features_labels = features_df.columns.values.tolist()[1:] self.item_features_nb = len(self.item_features_labels) @@ -244,8 +250,8 @@ def construct_trainset(self, raw_trainset): ur = defaultdict(list) ir = defaultdict(list) - u_features = {} - i_features = {} + u_features = defaultdict(list) + i_features = defaultdict(list) # user raw id, item raw id, translated rating, time stamp for urid, irid, r, _ in raw_trainset: @@ -256,7 +262,7 @@ def construct_trainset(self, raw_trainset): raw2inner_id_users[urid] = current_u_index current_u_index += 1 if self.user_features_nb > 0: - u_features[uid] = self.user_features.get(urid, None) + u_features[uid] = self.user_features[urid] try: iid = raw2inner_id_items[irid] @@ -265,7 +271,7 @@ def construct_trainset(self, raw_trainset): raw2inner_id_items[irid] = current_i_index current_i_index += 1 if self.item_features_nb > 0: - i_features[iid] = self.item_features.get(irid, None) + i_features[iid] = self.item_features[irid] ur[uid].append((iid, r)) ir[iid].append((uid, r)) @@ -296,8 +302,8 @@ def construct_testset(self, raw_testset): testset = [] for (ruid, riid, r_ui_trans, _) in raw_testset: - u_features = self.user_features.get(ruid, None) - i_features = self.item_features.get(riid, None) + u_features = self.user_features[ruid] + i_features = self.item_features[riid] testset.append((ruid, riid, u_features, i_features, r_ui_trans)) return testset diff --git a/surprise/prediction_algorithms/algo_base.py b/surprise/prediction_algorithms/algo_base.py index 5d4c5e02..e482b374 100644 --- a/surprise/prediction_algorithms/algo_base.py +++ b/surprise/prediction_algorithms/algo_base.py @@ -96,7 +96,7 @@ def fit(self, trainset): return self - def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, + def predict(self, uid, iid, u_features=[], i_features=[], r_ui=None, clip=True, verbose=False): """Compute the rating prediction for given user and item. @@ -110,9 +110,9 @@ def predict(self, uid, iid, u_features=None, i_features=None, r_ui=None, uid: (Raw) id of the user. See :ref:`this note`. iid: (Raw) id of the item. See :ref:`this note`. u_features: List of user features in the same order as used in - the ``fit`` method. Optional, default is ``None``. + the ``fit`` method. Optional, default is ``[]``. i_features: List of item features in the same order as used in - the ``fit`` method. Optional, default is ``None``. + the ``fit`` method. Optional, default is ``[]``. r_ui(float): The true rating :math:`r_{ui}`. Optional, default is ``None``. clip(bool): Whether to clip the estimation into the rating scale. diff --git a/tests/test_accuracy.py b/tests/test_accuracy.py index e56821de..1d264f40 100644 --- a/tests/test_accuracy.py +++ b/tests/test_accuracy.py @@ -49,8 +49,8 @@ def test_rmse(): def test_fcp(): """Tests for the FCP function.""" - predictions = [pred(0, 0, u0='u1'), pred(1, 1, u0='u1'), pred(2, 2, - u0='u2'), pred(100, 100, u0='u2')] + predictions = [pred(0, 0, u0='u1'), pred(1, 1, u0='u1'), + pred(2, 2, u0='u2'), pred(100, 100, u0='u2')] assert fcp(predictions) == 1 predictions = [pred(0, 0, u0='u1'), pred(0, 0, u0='u1')] @@ -61,8 +61,8 @@ def test_fcp(): with pytest.raises(ValueError): fcp(predictions) - predictions = [pred(0, 1, u0='u1'), pred(1, 0, u0='u1'), pred(2, 0.5, - u0='u2'), pred(0, 0.6, u0='u2')] + predictions = [pred(0, 1, u0='u1'), pred(1, 0, u0='u1'), + pred(2, 0.5, u0='u2'), pred(0, 0.6, u0='u2')] assert fcp(predictions) == 0 with pytest.raises(ValueError): diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 15cfd5f1..25b9ea52 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -7,6 +7,7 @@ import os import pytest +import pandas as pd from surprise import NormalPredictor from surprise import BaselineOnly @@ -18,16 +19,19 @@ from surprise import NMF from surprise import SlopeOne from surprise import CoClustering +from surprise import KNNWithZScore +from surprise import Lasso from surprise import Dataset from surprise import Reader -from surprise import KNNWithZScore from surprise.model_selection import PredefinedKFold from surprise.model_selection import train_test_split def test_unknown_user_or_item(): """Ensure that all algorithms act gracefully when asked to predict a rating - of an unknown user, an unknown item, and when both are unknown. + of an unknown user and/or an unknown item with unknown or known user + features and/or unknown or known item features. Also, test how they react + when features are missing/added for fit. """ reader = Reader(line_format='user item rating', sep=' ', skip_lines=3, @@ -36,17 +40,83 @@ def test_unknown_user_or_item(): file_path = os.path.dirname(os.path.realpath(__file__)) + '/custom_dataset' data = Dataset.load_from_file(file_path=file_path, reader=reader) + data_u = Dataset.load_from_file(file_path=file_path, reader=reader) + data_i = Dataset.load_from_file(file_path=file_path, reader=reader) + data_ui = Dataset.load_from_file(file_path=file_path, reader=reader) + + u_features_df = pd.DataFrame( + {'urid': ['user0', 'user2', 'user3', 'user1', 'user4'], + 'isMale': [False, True, False, True, False]}, + columns=['urid', 'isMale']) + data_u = data_u.load_features_df(u_features_df, user_features=True) + data_ui = data_ui.load_features_df(u_features_df, user_features=True) + + i_features_df = pd.DataFrame( + {'irid': ['item0', 'item1'], + 'isNew': [False, True], + 'webRating': [4, 3], + 'isComedy': [True, False]}, + columns=['irid', 'isNew', 'webRating', 'isComedy']) + data_i = data_i.load_features_df(i_features_df, user_features=False) + data_ui = data_ui.load_features_df(i_features_df, user_features=False) + trainset = data.build_full_trainset() + trainset_u = data_u.build_full_trainset() + trainset_i = data_i.build_full_trainset() + trainset_ui = data_ui.build_full_trainset() + # algos not using features klasses = (NormalPredictor, BaselineOnly, KNNBasic, KNNWithMeans, KNNBaseline, SVD, SVDpp, NMF, SlopeOne, CoClustering, KNNWithZScore) for klass in klasses: algo = klass() algo.fit(trainset) - algo.predict('user0', 'unknown_item', None) - algo.predict('unkown_user', 'item0', None) - algo.predict('unkown_user', 'unknown_item', None) + algo.fit(trainset_u) + algo.fit(trainset_i) + algo.fit(trainset_ui) + algo.predict('user0', 'unknown_item') + algo.predict('unkown_user', 'item0') + algo.predict('unkown_user', 'unknown_item') + algo.predict('user0', 'unknown_item', [], []) + algo.predict('unkown_user', 'item0', [], []) + algo.predict('unkown_user', 'unknown_item', [], []) + algo.predict('user0', 'unknown_item', [False], []) + algo.predict('unkown_user', 'item0', [False], []) + algo.predict('unkown_user', 'unknown_item', [False], []) + algo.predict('user0', 'unknown_item', [], [False, 4, True]) + algo.predict('unkown_user', 'item0', [], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [], [False, 4, True]) + algo.predict('user0', 'unknown_item', [False], [False, 4, True]) + algo.predict('unkown_user', 'item0', [False], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [False], [False, 4, True]) + + # algos using user and item features + klasses_ui = (Lasso,) + for klass in klasses_ui: + algo = klass() + with pytest.raises(ValueError): + algo.fit(trainset) + with pytest.raises(ValueError): + algo.fit(trainset_u) + with pytest.raises(ValueError): + algo.fit(trainset_i) + algo.fit(trainset_ui) + algo.predict('user0', 'unknown_item') + algo.predict('unkown_user', 'item0') + algo.predict('unkown_user', 'unknown_item') + algo.predict('user0', 'unknown_item', [], []) + algo.predict('unkown_user', 'item0', [], []) + algo.predict('unkown_user', 'unknown_item', [], []) + algo.predict('user0', 'unknown_item', [False], []) + algo.predict('unkown_user', 'item0', [False], []) + algo.predict('unkown_user', 'unknown_item', [False], []) + algo.predict('user0', 'unknown_item', [], [False, 4, True]) + algo.predict('unkown_user', 'item0', [], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [], [False, 4, True]) + algo.predict('user0', 'unknown_item', [False], [False, 4, True]) + algo.predict('unkown_user', 'item0', [False], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [False], [False, 4, True]) # unrelated, but test the fit().test() one-liner: trainset, testset = train_test_split(data, test_size=2) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 2526d20c..e98e5640 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -23,6 +23,7 @@ def test_wrong_file_name(): """Ensure file names are checked when creating a (custom) Dataset.""" + wrong_files = [('does_not_exist', 'does_not_either')] with pytest.raises(ValueError): @@ -40,8 +41,77 @@ def test_build_full_trainset(): assert len(trainset.ur) == 5 assert len(trainset.ir) == 2 + assert len(trainset.u_features) == 0 + assert len(trainset.i_features) == 0 assert trainset.n_users == 5 assert trainset.n_items == 2 + assert trainset.n_user_features == 0 + assert trainset.n_item_features == 0 + assert len(trainset.user_features_labels) == 0 + assert len(trainset.item_features_labels) == 0 + + +def test_load_features_df_columns_number(): + """Ensure number of columns in features DataFrame is checked.""" + + custom_dataset_path = (os.path.dirname(os.path.realpath(__file__)) + + '/custom_dataset') + data = Dataset.load_from_file(file_path=custom_dataset_path, reader=reader) + onecol_df = pd.DataFrame({'test': [False, True]}, columns=['test']) + + with pytest.raises(ValueError): + data.load_features_df(onecol_df) + + +def test_load_features_df_unique_ids(): + """Ensure that there is a check for unique values in the first column of + the features DataFrame.""" + + custom_dataset_path = (os.path.dirname(os.path.realpath(__file__)) + + '/custom_dataset') + data = Dataset.load_from_file(file_path=custom_dataset_path, reader=reader) + nonunique_df = pd.DataFrame( + {'ids': ['user0', 'user1', 'user0'], + 'feature': [True, False, True]}, + columns=['ids', 'feature']) + + with pytest.raises(ValueError): + data.load_features_df(nonunique_df) + + +def test_build_full_trainset_ui_features(): + """Test the build_full_trainset method with user and item features.""" + + custom_dataset_path = (os.path.dirname(os.path.realpath(__file__)) + + '/custom_dataset') + data = Dataset.load_from_file(file_path=custom_dataset_path, reader=reader) + + u_features_df = pd.DataFrame( + {'urid': ['user0', 'user2', 'user3', 'user1', 'user4'], + 'isMale': [False, True, False, True, False]}, + columns=['urid', 'isMale']) + data = data.load_features_df(u_features_df, user_features=True) + + i_features_df = pd.DataFrame( + {'irid': ['item0', 'item1'], + 'isNew': [False, True], + 'webRating': [4, 3], + 'isComedy': [True, False]}, + columns=['irid', 'isNew', 'webRating', 'isComedy']) + data = data.load_features_df(i_features_df, user_features=False) + + trainset = data.build_full_trainset() + + assert len(trainset.ur) == 5 + assert len(trainset.ir) == 2 + assert len(trainset.u_features) == 5 + assert len(trainset.i_features) == 2 + assert trainset.n_users == 5 + assert trainset.n_items == 2 + assert trainset.n_user_features == 1 + assert trainset.n_item_features == 3 + assert len(trainset.user_features_labels) == 1 + assert len(trainset.item_features_labels) == 3 def test_no_call_to_split(): @@ -141,6 +211,126 @@ def test_trainset_testset(): assert trainset.n_ratings == 6 assert trainset.rating_scale == (1, 5) + # test user features + u_features = trainset.u_features + assert u_features[0] == [] # no u_features_df added + assert u_features[1] == [] # no u_features_df added + assert u_features[3] == [] # no u_features_df added + assert u_features[40] == [] # not in trainset and no u_features_df + assert trainset.user_features_labels == [] + assert trainset.n_user_features == 0 + + # test item features + i_features = trainset.i_features + assert i_features[0] == [] # no i_features_df added + assert i_features[1] == [] # no i_features_df added + assert i_features[20000] == [] # not in trainset and no i_features_df + assert trainset.item_features_labels == [] + assert trainset.n_item_features == 0 + + # test raw2inner + for i in range(4): + assert trainset.to_inner_uid('user' + str(i)) == i + with pytest.raises(ValueError): + trainset.to_inner_uid('unknown_user') + + for i in range(2): + assert trainset.to_inner_iid('item' + str(i)) == i + with pytest.raises(ValueError): + trainset.to_inner_iid('unknown_item') + + # test inner2raw + assert trainset._inner2raw_id_users is None + assert trainset._inner2raw_id_items is None + for i in range(4): + assert trainset.to_raw_uid(i) == 'user' + str(i) + for i in range(2): + assert trainset.to_raw_iid(i) == 'item' + str(i) + assert trainset._inner2raw_id_users is not None + assert trainset._inner2raw_id_items is not None + + # Test the build_testset() method + algo = BaselineOnly() + algo.fit(trainset) + testset = trainset.build_testset() + algo.test(testset) # ensure an algorithm can manage the data + assert ('user0', 'item0', [], [], 4) in testset + assert ('user3', 'item1', [], [], 5) in testset + assert ('user3', 'item1', [], [], 0) not in testset + + # Test the build_anti_testset() method + algo = BaselineOnly() + algo.fit(trainset) + testset = trainset.build_anti_testset() + algo.test(testset) # ensure an algorithm can manage the data + assert ('user0', 'item0', [], [], trainset.global_mean) not in testset + assert ('user3', 'item1', [], [], trainset.global_mean) not in testset + assert ('user0', 'item1', [], [], trainset.global_mean) in testset + assert ('user3', 'item0', [], [], trainset.global_mean) in testset + + +def test_trainset_testset_ui_features(): + """Test the construct_trainset and construct_testset methods with user and + item features.""" + + current_dir = os.path.dirname(os.path.realpath(__file__)) + folds_files = [(current_dir + '/custom_train', + current_dir + '/custom_test')] + + data = Dataset.load_from_folds(folds_files=folds_files, reader=reader) + + u_features_df = pd.DataFrame( + {'urid': ['user0', 'user2', 'user1'], # 'user3' is missing + 'isMale': [False, True, True]}, + columns=['urid', 'isMale']) + data = data.load_features_df(u_features_df, user_features=True) + + i_features_df = pd.DataFrame( + {'irid': ['item0'], + 'isNew': [False], + 'webRating': [4], + 'isComedy': [True]}, + columns=['irid', 'isNew', 'webRating', 'isComedy']) + data = data.load_features_df(i_features_df, user_features=False) + + with pytest.warns(UserWarning): + trainset, testset = next(data.folds()) + + # test ur + ur = trainset.ur + assert ur[0] == [(0, 4)] + assert ur[1] == [(0, 4), (1, 2)] + assert ur[40] == [] # not in the trainset + + # test ir + ir = trainset.ir + assert ir[0] == [(0, 4), (1, 4), (2, 1)] + assert ir[1] == [(1, 2), (2, 1), (3, 5)] + assert ir[20000] == [] # not in the trainset + + # test n_users, n_items, n_ratings, rating_scale + assert trainset.n_users == 4 + assert trainset.n_items == 2 + assert trainset.n_ratings == 6 + assert trainset.rating_scale == (1, 5) + + # test user features + u_features = trainset.u_features + assert u_features[0] == [False] + assert u_features[1] == [True] + assert u_features[3] == [] # not in u_features_df + assert u_features[40] == [] # not in trainset and u_features_df + assert trainset.user_features_labels == ['isMale'] + assert trainset.n_user_features == 1 + + # test item features + i_features = trainset.i_features + assert i_features[0] == [False, 4, True] + assert i_features[1] == [] # not in i_features_df + assert i_features[20000] == [] # not in trainset and i_features_df + assert trainset.item_features_labels == ['isNew', 'webRating', 'isComedy'] + assert trainset.n_item_features == 3 + # test raw2inner for i in range(4): assert trainset.to_inner_uid('user' + str(i)) == i @@ -167,19 +357,22 @@ def test_trainset_testset(): algo.fit(trainset) testset = trainset.build_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', None, None, 4) in testset - assert ('user3', 'item1', None, None, 5) in testset - assert ('user3', 'item1', None, None, 0) not in testset + assert ('user0', 'item0', [False], [False, 4, True], 4) in testset + assert ('user2', 'item1', [True], [], 1) in testset + assert ('user3', 'item1', [], [], 5) in testset + assert ('user3', 'item1', [], [], 0) not in testset # Test the build_anti_testset() method algo = BaselineOnly() algo.fit(trainset) testset = trainset.build_anti_testset() algo.test(testset) # ensure an algorithm can manage the data - assert ('user0', 'item0', None, None, trainset.global_mean) not in testset - assert ('user3', 'item1', None, None, trainset.global_mean) not in testset - assert ('user0', 'item1', None, None, trainset.global_mean) in testset - assert ('user3', 'item0', None, None, trainset.global_mean) in testset + assert (('user0', 'item0', [False], [False, 4, True], trainset.global_mean) + not in testset) + assert ('user3', 'item1', [], [], trainset.global_mean) not in testset + assert ('user0', 'item1', [False], [], trainset.global_mean) in testset + assert (('user3', 'item0', [], [False, 4, True], trainset.global_mean) + in testset) def test_load_form_df(): From 531b5362f39601113f20f483c537b4ea6c353cb3 Mon Sep 17 00:00:00 2001 From: martincousi Date: Thu, 19 Apr 2018 22:52:28 -0400 Subject: [PATCH 50/60] add test for missing user or item features --- surprise/prediction_algorithms/linear.py | 25 +++++++----- tests/test_algorithms.py | 52 ++++++++++++++++++++---- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 81697f26..737324e9 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -74,14 +74,18 @@ def lasso(self, trainset): y = np.empty((n_ratings,)) for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): y[k] = rating - try: - X[k, :n_uf] = u_features[uid] - except KeyError: + + temp = u_features[uid] + if temp: + X[k, :n_uf] = temp + else: raise ValueError('No features for user ' + str(self.trainset.to_raw_uid(uid))) - try: - X[k, n_uf:] = i_features[iid] - except KeyError: + + temp = i_features[iid] + if temp: + X[k, n_uf:] = temp + else: raise ValueError('No features for item ' + str(self.trainset.to_raw_iid(iid))) @@ -112,11 +116,10 @@ def estimate(self, u, i, u_features, i_features): n_uf = self.trainset.n_user_features n_if = self.trainset.n_item_features - if u_features is None or len(u_features) != n_uf: - raise PredictionImpossible('User features are missing.') - - if i_features is None or len(i_features) != n_if: - raise PredictionImpossible('Item features are missing.') + if (len(u_features) != n_uf or + len(i_features) != n_if): + raise PredictionImpossible( + 'User and/or item features are missing.') X = np.concatenate([u_features, i_features]) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 25b9ea52..c1504173 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -39,32 +39,60 @@ def test_unknown_user_or_item(): file_path = os.path.dirname(os.path.realpath(__file__)) + '/custom_dataset' - data = Dataset.load_from_file(file_path=file_path, reader=reader) - data_u = Dataset.load_from_file(file_path=file_path, reader=reader) - data_i = Dataset.load_from_file(file_path=file_path, reader=reader) - data_ui = Dataset.load_from_file(file_path=file_path, reader=reader) - + # df with all users u_features_df = pd.DataFrame( {'urid': ['user0', 'user2', 'user3', 'user1', 'user4'], 'isMale': [False, True, False, True, False]}, columns=['urid', 'isMale']) - data_u = data_u.load_features_df(u_features_df, user_features=True) - data_ui = data_ui.load_features_df(u_features_df, user_features=True) + # df with all items i_features_df = pd.DataFrame( {'irid': ['item0', 'item1'], 'isNew': [False, True], 'webRating': [4, 3], 'isComedy': [True, False]}, columns=['irid', 'isNew', 'webRating', 'isComedy']) - data_i = data_i.load_features_df(i_features_df, user_features=False) - data_ui = data_ui.load_features_df(i_features_df, user_features=False) + # df with missing user + u_features_m_df = pd.DataFrame( + {'urid': ['user0', 'user2', 'user3', 'user1'], + 'isMale': [False, True, False, True]}, + columns=['urid', 'isMale']) + + # df with missing item + i_features_m_df = pd.DataFrame( + {'irid': ['item0'], + 'isNew': [False], + 'webRating': [4], + 'isComedy': [True]}, + columns=['irid', 'isNew', 'webRating', 'isComedy']) + + data = Dataset.load_from_file(file_path=file_path, reader=reader) trainset = data.build_full_trainset() + + data_u = Dataset.load_from_file(file_path=file_path, reader=reader) + data_u.load_features_df(u_features_df, user_features=True) trainset_u = data_u.build_full_trainset() + + data_i = Dataset.load_from_file(file_path=file_path, reader=reader) + data_i.load_features_df(i_features_df, user_features=False) trainset_i = data_i.build_full_trainset() + + data_ui = Dataset.load_from_file(file_path=file_path, reader=reader) + data_ui.load_features_df(u_features_df, user_features=True) + data_ui.load_features_df(i_features_df, user_features=False) trainset_ui = data_ui.build_full_trainset() + data_ui_mu = Dataset.load_from_file(file_path=file_path, reader=reader) + data_ui_mu.load_features_df(u_features_m_df, user_features=True) + data_ui_mu.load_features_df(i_features_df, user_features=False) + trainset_ui_mu = data_ui_mu.build_full_trainset() + + data_ui_mi = Dataset.load_from_file(file_path=file_path, reader=reader) + data_ui_mi.load_features_df(u_features_df, user_features=True) + data_ui_mi.load_features_df(i_features_m_df, user_features=False) + trainset_ui_mi = data_ui_mi.build_full_trainset() + # algos not using features klasses = (NormalPredictor, BaselineOnly, KNNBasic, KNNWithMeans, KNNBaseline, SVD, SVDpp, NMF, SlopeOne, CoClustering, @@ -74,6 +102,8 @@ def test_unknown_user_or_item(): algo.fit(trainset) algo.fit(trainset_u) algo.fit(trainset_i) + algo.fit(trainset_ui_mu) + algo.fit(trainset_ui_mi) algo.fit(trainset_ui) algo.predict('user0', 'unknown_item') algo.predict('unkown_user', 'item0') @@ -101,6 +131,10 @@ def test_unknown_user_or_item(): algo.fit(trainset_u) with pytest.raises(ValueError): algo.fit(trainset_i) + with pytest.raises(ValueError): + algo.fit(trainset_ui_mu) + with pytest.raises(ValueError): + algo.fit(trainset_ui_mi) algo.fit(trainset_ui) algo.predict('user0', 'unknown_item') algo.predict('unkown_user', 'item0') From 991f30fa7339be888cc8ebc666e296c27ec3e822 Mon Sep 17 00:00:00 2001 From: martincousi Date: Tue, 24 Apr 2018 13:59:16 -0400 Subject: [PATCH 51/60] Add doc to FMBasic and FMBasicPL --- .../factorization_machines.py | 145 ++++++++++++++---- 1 file changed, 112 insertions(+), 33 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 0bcc293a..532cfa3e 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -7,7 +7,7 @@ import numpy as np import tensorflow as tf import scipy.sparse as sps -from tffm import TFFMClassifier, TFFMRegressor +from tffm import TFFMRegressor from polylearn import FactorizationMachineRegressor from .algo_base import AlgoBase @@ -18,9 +18,10 @@ class FMAlgo(AlgoBase): factoration machines. """ - def __init__(self, order=2, n_factors=5, input_type='dense', - loss_function='mse', optimizer=None, reg_all=1., - use_diag=False, reweight_reg=False, init_std=0.01, + def __init__(self, order=2, n_factors=2, input_type='dense', + loss_function='mse', + optimizer=tf.train.AdamOptimizer(learning_rate=0.01), + reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, batch_size=-1, n_epochs=100, log_dir=None, session_config=None, random_state=None, verbose=False, **kwargs): @@ -31,11 +32,7 @@ def __init__(self, order=2, n_factors=5, input_type='dense', self.input_type = input_type self.loss_function = loss_function # {'mse', 'loss_logistic'} # https://www.tensorflow.org/api_guides/python/train#Optimizers - # tf.train.Optimizer, default: AdamOptimizer(learning_rate=0.01) - if optimizer is None: - self.optimizer = tf.train.AdamOptimizer(learning_rate=0.001) - else: - self.optimizer = optimizer + self.optimizer = optimizer self.reg_all = reg_all # reg in `tffm` self.use_diag = use_diag self.reweight_reg = reweight_reg @@ -57,16 +54,8 @@ def __init__(self, order=2, n_factors=5, input_type='dense', log_dir=self.log_dir, session_config=self.session_config, seed=self.random_state, verbose=self.verbose) elif self.loss_function == 'loss_logistic': - # See issue #157 + # See issue #157 of Surprise raise ValueError('loss_logistic is not supported at the moment') - self.model = TFFMClassifier( - order=self.order, rank=self.n_factors, - input_type=self.input_type, optimizer=self.optimizer, - reg=self.reg_all, use_diag=self.use_diag, - reweight_reg=self.reweight_reg, init_std=self.init_std, - batch_size=self.batch_size, n_epochs=self.n_epochs, - log_dir=self.log_dir, session_config=self.session_config, - seed=self.random_state, verbose=self.verbose) else: raise ValueError(('Unknown value {} for parameter' 'loss_function').format(self.loss_function)) @@ -90,13 +79,65 @@ class FMBasic(FMAlgo): This code is an interface to the `tffm` library. Args: + order : int, default: 2 + Order of corresponding polynomial model. + All interaction from bias and linear to order will be included. + n_factors : int, default: 2 + Number of factors in low-rank appoximation. + This value is shared across different orders of interaction. + loss_function : str, default: 'mse' + 'mse' is the only supported loss_function at the moment. + optimizer : tf.train.Optimizer, + default: AdamOptimizer(learning_rate=0.01) + Optimization method used for training + reg_all : float, default: 0 + Strength of L2 regularization + use_diag : bool, default: False + Use diagonal elements of weights matrix or not. + In the other words, should terms like x^2 be included. + Often reffered as a "Polynomial Network". + Default value (False) corresponds to FM. + reweight_reg : bool, default: False + Use frequency of features as weights for regularization or not. + Should be useful for very sparse data and/or small batches + init_std : float, default: 0.01 + Amplitude of random initialization + batch_size : int, default: -1 + Number of samples in mini-batches. Shuffled every epoch. + Use -1 for full gradient (whole training set in each batch). + n_epoch : int, default: 100 + Default number of epoches. + It can be overrived by explicitly provided value in fit() method. + log_dir : str or None, default: None + Path for storing model stats during training. Used only if is not + None. WARNING: If such directory already exists, it will be + removed! You can use TensorBoard to visualize the stats: + `tensorboard --logdir={log_dir}` + session_config : tf.ConfigProto or None, default: None + Additional setting passed to tf.Session object. + Useful for CPU/GPU switching, setting number of threads and so on, + `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if + enabled). + WARNING: Use the value + `tf.ConfigProto(intra_op_parallelism_threads=1, + inter_op_parallelism_threads=1, + allow_soft_placement=True, + device_count={'CPU': 1, 'GPU': 0})` + within `GridSearchCV` and `RandomizedSearchCV`. + random_state : int or None, default: None + Random seed used at graph creating time + verbose : int, default: False + Level of verbosity. + Set 1 for tensorboard info only and 2 for additional stats every + epoch. """ - def __init__(self, order=2, n_factors=5, loss_function='mse', - optimizer=None, reg_all=1., use_diag=False, - reweight_reg=False, init_std=0.01, batch_size=-1, - n_epochs=100, log_dir=None, session_config=None, - random_state=None, verbose=False, **kwargs): + def __init__(self, order=2, n_factors=2, loss_function='mse', + optimizer=tf.train.AdamOptimizer(learning_rate=0.01), + reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, + batch_size=-1, n_epochs=100, log_dir=None, + session_config=None, random_state=None, verbose=False, + **kwargs): input_type = 'sparse' @@ -153,9 +194,6 @@ def fm(self, trainset): def estimate(self, u, i, *_): - # what happens for new user/item in predict? - # if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - # raise PredictionImpossible('User and/or item is unknown.') n_users = self.trainset.n_users n_items = self.trainset.n_items @@ -192,23 +230,67 @@ class FMBasicPL(AlgoBase): This code is an interface to the `polylearn` library. Args: + degree : int, default: 2 + Degree of the polynomial. Corresponds to the order of feature + interactions captured by the model. Currently only supports + degrees up to 3. + n_factors : int, default: 2 + Number of basis vectors to learn, a.k.a. the dimension of the + low-rank parametrization. + reg_alpha : float, default: 1 + Regularization amount for linear term (if ``fit_linear=True``). + reg_beta : float, default: 1 + Regularization amount for higher-order weights. + tol : float, default: 1e-6 + Tolerance for the stopping condition. + fit_lower : {'explicit'|'augment'|None}, default: 'explicit' + Whether and how to fit lower-order, non-homogeneous terms. + - 'explicit': fits a separate P directly for each lower order. + - 'augment': adds the required number of dummy columns (columns + that are 1 everywhere) in order to capture lower-order terms. + Adds ``degree - 2`` columns if ``fit_linear`` is true, or + ``degree - 1`` columns otherwise, to account for the linear + term. + - None: only learns weights for the degree given. If + ``degree == 3``, for example, the model will only have weights + for third-order feature interactions. + fit_linear : {True|False}, default: True + Whether to fit an explicit linear term to the model, using + coordinate descent. If False, the model can still capture linear + effects if ``fit_lower == 'augment'``. + warm_start : boolean, optional, default: False + Whether to use the existing solution, if available. Useful for + computing regularization paths or pre-initializing the model. + init_lambdas : {'ones'|'random_signs'}, default: 'ones' + How to initialize the predictive weights of each learned basis. The + lambdas are not trained; using alternate signs can theoretically + improve performance if the kernel degree is even. The default + value of 'ones' matches the original formulation of factorization + machines (Rendle, 2010). + To use custom values for the lambdas, ``warm_start`` may be used. + max_iter : int, optional, default: 10000 + Maximum number of passes over the dataset to perform. + random_state : int seed, RandomState instance, or None (default) + The seed of the pseudo random number generator to use for + initializing the parameters. + verbose : boolean, optional, default: False + Whether to print debugging information. """ - def __init__(self, degree=2, n_factors=5, reg_all=1., reg_alpha=1., - reg_beta=1., tol=1e-6, fit_lower='explicit', fit_linear=True, + def __init__(self, degree=2, n_factors=2, reg_alpha=1., reg_beta=1., + tol=1e-6, fit_lower='explicit', fit_linear=True, warm_start=False, init_lambdas='ones', max_iter=10000, random_state=None, verbose=False, **kwargs): self.degree = degree self.n_factors = n_factors # n_components in `polylearn` - self.reg_all = reg_all # not in `polylearn` self.reg_alpha = reg_alpha # alpha in `polylearn` self.reg_beta = reg_beta # beta in `polylearn` self.tol = tol self.fit_lower = fit_lower self.fit_linear = fit_linear self.warm_start = warm_start - self.init_lambdas = init_lambdas # what is this? + self.init_lambdas = init_lambdas self.max_iter = max_iter self.random_state = random_state self.verbose = verbose @@ -260,9 +342,6 @@ def fit(self, trainset): def estimate(self, u, i, *_): - # what happens for new user/item in predict? - # if not (self.trainset.knows_user(u) and self.trainset.knows_item(i)): - # raise PredictionImpossible('User and/or item is unknown.') n_users = self.trainset.n_users n_items = self.trainset.n_items From 95509336da77d15f446f8929b8fb8ae921970450 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 25 Apr 2018 14:17:54 -0400 Subject: [PATCH 52/60] Add FMImplicit and FMExplicit --- surprise/__init__.py | 4 +- surprise/prediction_algorithms/__init__.py | 5 +- .../factorization_machines.py | 350 +++++++++++++++++- 3 files changed, 350 insertions(+), 9 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index 87d5b279..7b59306c 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -14,6 +14,8 @@ from .prediction_algorithms import CoClustering from .prediction_algorithms import Lasso from .prediction_algorithms import FMBasic +from .prediction_algorithms import FMImplicit +from .prediction_algorithms import FMExplicit from .prediction_algorithms import FMBasicPL from .prediction_algorithms import PredictionImpossible @@ -34,6 +36,6 @@ 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso', 'FMBasic', 'FMBasicPL'] + 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMBasicPL'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 193bbe52..525a3e68 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -34,6 +34,8 @@ from .co_clustering import CoClustering from .linear import Lasso from .factorization_machines import FMBasic +from .factorization_machines import FMImplicit +from .factorization_machines import FMExplicit from .factorization_machines import FMBasicPL from .predictions import PredictionImpossible @@ -42,4 +44,5 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore', 'Lasso', 'FMBasic', 'FMBasicPL'] + 'KNNWithZScore', 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', + 'FMBasicPL'] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 532cfa3e..4e54d9b2 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -4,6 +4,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) + import numpy as np import tensorflow as tf import scipy.sparse as sps @@ -36,7 +37,7 @@ def __init__(self, order=2, n_factors=2, input_type='dense', self.reg_all = reg_all # reg in `tffm` self.use_diag = use_diag self.reweight_reg = reweight_reg - self.init_std = init_std + self.init_std = np.float32(init_std) self.batch_size = batch_size self.n_epochs = n_epochs self.log_dir = log_dir @@ -118,12 +119,6 @@ class FMBasic(FMAlgo): Useful for CPU/GPU switching, setting number of threads and so on, `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if enabled). - WARNING: Use the value - `tf.ConfigProto(intra_op_parallelism_threads=1, - inter_op_parallelism_threads=1, - allow_soft_placement=True, - device_count={'CPU': 1, 'GPU': 0})` - within `GridSearchCV` and `RandomizedSearchCV`. random_state : int or None, default: None Random seed used at graph creating time verbose : int, default: False @@ -223,6 +218,347 @@ def estimate(self, u, i, *_): return est, details +class FMImplicit(FMAlgo): + """A factorization machine algorithm that uses implicit ratings. With order + 2, this algorithm correspond to an extension of the biased SVD++ algorithm. + + This code is an interface to the `tffm` library. + + Args: + order : int, default: 2 + Order of corresponding polynomial model. + All interaction from bias and linear to order will be included. + n_factors : int, default: 2 + Number of factors in low-rank appoximation. + This value is shared across different orders of interaction. + loss_function : str, default: 'mse' + 'mse' is the only supported loss_function at the moment. + optimizer : tf.train.Optimizer, + default: AdamOptimizer(learning_rate=0.01) + Optimization method used for training + reg_all : float, default: 0 + Strength of L2 regularization + use_diag : bool, default: False + Use diagonal elements of weights matrix or not. + In the other words, should terms like x^2 be included. + Often reffered as a "Polynomial Network". + Default value (False) corresponds to FM. + reweight_reg : bool, default: False + Use frequency of features as weights for regularization or not. + Should be useful for very sparse data and/or small batches + init_std : float, default: 0.01 + Amplitude of random initialization + batch_size : int, default: -1 + Number of samples in mini-batches. Shuffled every epoch. + Use -1 for full gradient (whole training set in each batch). + n_epoch : int, default: 100 + Default number of epoches. + It can be overrived by explicitly provided value in fit() method. + log_dir : str or None, default: None + Path for storing model stats during training. Used only if is not + None. WARNING: If such directory already exists, it will be + removed! You can use TensorBoard to visualize the stats: + `tensorboard --logdir={log_dir}` + session_config : tf.ConfigProto or None, default: None + Additional setting passed to tf.Session object. + Useful for CPU/GPU switching, setting number of threads and so on, + `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if + enabled). + random_state : int or None, default: None + Random seed used at graph creating time + verbose : int, default: False + Level of verbosity. + Set 1 for tensorboard info only and 2 for additional stats every + epoch. + """ + + def __init__(self, order=2, n_factors=2, loss_function='mse', + optimizer=tf.train.AdamOptimizer(learning_rate=0.01), + reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, + batch_size=-1, n_epochs=100, log_dir=None, + session_config=None, random_state=None, verbose=False, + **kwargs): + + input_type = 'sparse' + + FMAlgo.__init__(self, order=order, n_factors=n_factors, + input_type=input_type, loss_function=loss_function, + optimizer=optimizer, reg_all=reg_all, + use_diag=use_diag, reweight_reg=reweight_reg, + init_std=init_std, batch_size=batch_size, + n_epochs=n_epochs, log_dir=log_dir, + session_config=session_config, + random_state=random_state, verbose=verbose, **kwargs) + + def fit(self, trainset): + + FMAlgo.fit(self, trainset) + self.fm(trainset) + + return self + + def fm(self, trainset): + + n_ratings = self.trainset.n_ratings + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + # Construct sparse X and y + row_ind = [] + col_ind = [] + data = [] + y_train = np.empty(n_ratings, dtype=np.float32) + rating_counter = 0 + for uid, iid, rating in self.trainset.all_ratings(): + # Add user + row_ind.append(rating_counter) + col_ind.append(uid) + data.append(1.) + # Add item + row_ind.append(rating_counter) + col_ind.append(n_users + iid) + data.append(1.) + # Add implicit ratings + sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) + for iid_imp, _ in self.trainset.ur[uid]: + row_ind.append(rating_counter) + col_ind.append(n_users + n_items + iid_imp) + data.append(1 / sqrt_Iu) + # Add rating + y_train[rating_counter] = rating + rating_counter += 1 + X_train = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(n_ratings, n_users + 2 * n_items), + dtype=np.float32) + + # `Dataset` and `Trainset` do not support sample_weight at the moment. + self.model.fit(X_train, y_train, sample_weight=None, + show_progress=self.show_progress) + self.X_train = X_train + self.y_train = y_train + + def estimate(self, u, i, *_): + + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + details = {} + if self.trainset.knows_user(u) and self.trainset.knows_item(i): + data = [1., 1.] + row_ind = [0, 0] + col_ind = [u, n_users + i] + details['knows_user'] = True + details['knows_item'] = True + elif self.trainset.knows_user(u): + data = [1.] + row_ind = [0] + col_ind = [u] + details['knows_user'] = True + details['knows_item'] = False + elif self.trainset.knows_item(i): + data = [1.] + row_ind = [0] + col_ind = [n_users + i] + details['knows_user'] = False + details['knows_item'] = True + else: + data = [] + row_ind = [] + col_ind = [] + details['knows_user'] = False + details['knows_item'] = False + + if self.trainset.knows_user(u): + sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) + for iid_imp, _ in self.trainset.ur[u]: + row_ind.append(0) + col_ind.append(n_users + n_items + iid_imp) + data.append(1 / sqrt_Iu) + + if data: + X_test = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(1, n_users + 2 * n_items), + dtype=np.float32) + else: + X_test = sps.csr_matrix((1, n_users + 2 * n_items), + dtype=np.float32) + + est = self.model.predict(X_test)[0] + + return est, details + + +class FMExplicit(FMAlgo): + """A factorization machine algorithm that uses explicit ratings. + + This code is an interface to the `tffm` library. + + Args: + order : int, default: 2 + Order of corresponding polynomial model. + All interaction from bias and linear to order will be included. + n_factors : int, default: 2 + Number of factors in low-rank appoximation. + This value is shared across different orders of interaction. + loss_function : str, default: 'mse' + 'mse' is the only supported loss_function at the moment. + optimizer : tf.train.Optimizer, + default: AdamOptimizer(learning_rate=0.01) + Optimization method used for training + reg_all : float, default: 0 + Strength of L2 regularization + use_diag : bool, default: False + Use diagonal elements of weights matrix or not. + In the other words, should terms like x^2 be included. + Often reffered as a "Polynomial Network". + Default value (False) corresponds to FM. + reweight_reg : bool, default: False + Use frequency of features as weights for regularization or not. + Should be useful for very sparse data and/or small batches + init_std : float, default: 0.01 + Amplitude of random initialization + batch_size : int, default: -1 + Number of samples in mini-batches. Shuffled every epoch. + Use -1 for full gradient (whole training set in each batch). + n_epoch : int, default: 100 + Default number of epoches. + It can be overrived by explicitly provided value in fit() method. + log_dir : str or None, default: None + Path for storing model stats during training. Used only if is not + None. WARNING: If such directory already exists, it will be + removed! You can use TensorBoard to visualize the stats: + `tensorboard --logdir={log_dir}` + session_config : tf.ConfigProto or None, default: None + Additional setting passed to tf.Session object. + Useful for CPU/GPU switching, setting number of threads and so on, + `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if + enabled). + random_state : int or None, default: None + Random seed used at graph creating time + verbose : int, default: False + Level of verbosity. + Set 1 for tensorboard info only and 2 for additional stats every + epoch. + """ + + def __init__(self, order=2, n_factors=2, loss_function='mse', + optimizer=tf.train.AdamOptimizer(learning_rate=0.01), + reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, + batch_size=-1, n_epochs=100, log_dir=None, + session_config=None, random_state=None, verbose=False, + **kwargs): + + input_type = 'sparse' + + FMAlgo.__init__(self, order=order, n_factors=n_factors, + input_type=input_type, loss_function=loss_function, + optimizer=optimizer, reg_all=reg_all, + use_diag=use_diag, reweight_reg=reweight_reg, + init_std=init_std, batch_size=batch_size, + n_epochs=n_epochs, log_dir=log_dir, + session_config=session_config, + random_state=random_state, verbose=verbose, **kwargs) + + def fit(self, trainset): + + FMAlgo.fit(self, trainset) + self.fm(trainset) + + return self + + def fm(self, trainset): + + n_ratings = self.trainset.n_ratings + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + # Construct sparse X and y + row_ind = [] + col_ind = [] + data = [] + y_train = np.empty(n_ratings, dtype=np.float32) + max_value = self.trainset.rating_scale[1] + self.trainset.offset + rating_counter = 0 + for uid, iid, rating in self.trainset.all_ratings(): + # Add user + row_ind.append(rating_counter) + col_ind.append(uid) + data.append(1.) + # Add item + row_ind.append(rating_counter) + col_ind.append(n_users + iid) + data.append(1.) + # Add explicit ratings + sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) + for iid_exp, rating_exp in self.trainset.ur[uid]: + row_ind.append(rating_counter) + col_ind.append(n_users + n_items + iid_exp) + data.append(rating_exp / (max_value * sqrt_Iu)) + # Add rating + y_train[rating_counter] = rating + rating_counter += 1 + X_train = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(n_ratings, n_users + 2 * n_items), + dtype=np.float32) + + # `Dataset` and `Trainset` do not support sample_weight at the moment. + self.model.fit(X_train, y_train, sample_weight=None, + show_progress=self.show_progress) + self.X_train = X_train + self.y_train = y_train + + def estimate(self, u, i, *_): + + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + details = {} + if self.trainset.knows_user(u) and self.trainset.knows_item(i): + data = [1., 1.] + row_ind = [0, 0] + col_ind = [u, n_users + i] + details['knows_user'] = True + details['knows_item'] = True + elif self.trainset.knows_user(u): + data = [1.] + row_ind = [0] + col_ind = [u] + details['knows_user'] = True + details['knows_item'] = False + elif self.trainset.knows_item(i): + data = [1.] + row_ind = [0] + col_ind = [n_users + i] + details['knows_user'] = False + details['knows_item'] = True + else: + data = [] + row_ind = [] + col_ind = [] + details['knows_user'] = False + details['knows_item'] = False + + if self.trainset.knows_user(u): + sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) + max_value = self.trainset.rating_scale[1] + self.trainset.offset + for iid_exp, rating_exp in self.trainset.ur[u]: + row_ind.append(0) + col_ind.append(n_users + n_items + iid_exp) + data.append(rating_exp / (max_value * sqrt_Iu)) + + if data: + X_test = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(1, n_users + 2 * n_items), + dtype=np.float32) + else: + X_test = sps.csr_matrix((1, n_users + 2 * n_items), + dtype=np.float32) + + est = self.model.predict(X_test)[0] + + return est, details + + class FMBasicPL(AlgoBase): """A basic factorization machine algorithm. With order 2, this algorithm is equivalent to the biased SVD algorithm. From e1cea2cfb57a52e49e633408d435c8fbcf7744e3 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 25 Apr 2018 16:13:52 -0400 Subject: [PATCH 53/60] Add FMFeatures --- surprise/__init__.py | 4 +- surprise/prediction_algorithms/__init__.py | 3 +- .../factorization_machines.py | 212 ++++++++++++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index 7b59306c..807213a4 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -16,6 +16,7 @@ from .prediction_algorithms import FMBasic from .prediction_algorithms import FMImplicit from .prediction_algorithms import FMExplicit +from .prediction_algorithms import FMFeatures from .prediction_algorithms import FMBasicPL from .prediction_algorithms import PredictionImpossible @@ -36,6 +37,7 @@ 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMBasicPL'] + 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMFeatures', + 'FMBasicPL'] __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 525a3e68..46a65ca9 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -36,6 +36,7 @@ from .factorization_machines import FMBasic from .factorization_machines import FMImplicit from .factorization_machines import FMExplicit +from .factorization_machines import FMFeatures from .factorization_machines import FMBasicPL from .predictions import PredictionImpossible @@ -45,4 +46,4 @@ 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', 'KNNWithZScore', 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', - 'FMBasicPL'] + 'FMFeatures', 'FMBasicPL'] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 4e54d9b2..2d548236 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -11,6 +11,7 @@ from tffm import TFFMRegressor from polylearn import FactorizationMachineRegressor +from .predictions import PredictionImpossible from .algo_base import AlgoBase @@ -559,6 +560,217 @@ def estimate(self, u, i, *_): return est, details +class FMFeatures(FMAlgo): + """A factorization machine algorithm that uses available features. + + WARNING: Features should be pre-scaled to an absolute value less or equal + to 1 if using a high value for `order`. + + This code is an interface to the `tffm` library. + + Args: + order : int, default: 2 + Order of corresponding polynomial model. + All interaction from bias and linear to order will be included. + n_factors : int, default: 2 + Number of factors in low-rank appoximation. + This value is shared across different orders of interaction. + loss_function : str, default: 'mse' + 'mse' is the only supported loss_function at the moment. + optimizer : tf.train.Optimizer, + default: AdamOptimizer(learning_rate=0.01) + Optimization method used for training + reg_all : float, default: 0 + Strength of L2 regularization + use_diag : bool, default: False + Use diagonal elements of weights matrix or not. + In the other words, should terms like x^2 be included. + Often reffered as a "Polynomial Network". + Default value (False) corresponds to FM. + reweight_reg : bool, default: False + Use frequency of features as weights for regularization or not. + Should be useful for very sparse data and/or small batches + init_std : float, default: 0.01 + Amplitude of random initialization + batch_size : int, default: -1 + Number of samples in mini-batches. Shuffled every epoch. + Use -1 for full gradient (whole training set in each batch). + n_epoch : int, default: 100 + Default number of epoches. + It can be overrived by explicitly provided value in fit() method. + log_dir : str or None, default: None + Path for storing model stats during training. Used only if is not + None. WARNING: If such directory already exists, it will be + removed! You can use TensorBoard to visualize the stats: + `tensorboard --logdir={log_dir}` + session_config : tf.ConfigProto or None, default: None + Additional setting passed to tf.Session object. + Useful for CPU/GPU switching, setting number of threads and so on, + `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if + enabled). + random_state : int or None, default: None + Random seed used at graph creating time + verbose : int, default: False + Level of verbosity. + Set 1 for tensorboard info only and 2 for additional stats every + epoch. + """ + + def __init__(self, order=2, n_factors=2, loss_function='mse', + optimizer=tf.train.AdamOptimizer(learning_rate=0.01), + reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, + batch_size=-1, n_epochs=100, log_dir=None, + session_config=None, random_state=None, verbose=False, + **kwargs): + + input_type = 'sparse' + + FMAlgo.__init__(self, order=order, n_factors=n_factors, + input_type=input_type, loss_function=loss_function, + optimizer=optimizer, reg_all=reg_all, + use_diag=use_diag, reweight_reg=reweight_reg, + init_std=init_std, batch_size=batch_size, + n_epochs=n_epochs, log_dir=log_dir, + session_config=session_config, + random_state=random_state, verbose=verbose, **kwargs) + + def fit(self, trainset): + + FMAlgo.fit(self, trainset) + self.fm(trainset) + + return self + + def fm(self, trainset): + + n_ratings = self.trainset.n_ratings + n_users = self.trainset.n_users + n_items = self.trainset.n_items + n_user_features = self.trainset.n_user_features + n_item_features = self.trainset.n_item_features + + # Construct sparse X and y + row_ind = [] + col_ind = [] + data = [] + y_train = np.empty(n_ratings, dtype=np.float32) + rating_counter = 0 + for uid, iid, rating in self.trainset.all_ratings(): + # Add user + row_ind.append(rating_counter) + col_ind.append(uid) + data.append(1.) + # Add item + row_ind.append(rating_counter) + col_ind.append(n_users + iid) + data.append(1.) + # Add user features (if any) + if n_user_features > 0: + if self.trainset.has_user_features(uid): + for n, value in enumerate(self.trainset.u_features[uid]): + if value != 0: + row_ind.append(rating_counter) + col_ind.append(n_users + n_items + n) + data.append(value) + else: + raise ValueError('No features for user ' + + str(self.trainset.to_raw_uid(uid))) + # Add item features (if any) + if n_item_features > 0: + if self.trainset.has_item_features(iid): + for n, value in enumerate(self.trainset.i_features[iid]): + if value != 0: + row_ind.append(rating_counter) + col_ind.append(n_users + n_items + + n_user_features + n) + data.append(value) + else: + raise ValueError('No features for item ' + + str(self.trainset.to_raw_iid(iid))) + # Add rating + y_train[rating_counter] = rating + rating_counter += 1 + X_train = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(n_ratings, n_users + n_items + + n_user_features + n_item_features), + dtype=np.float32) + + # `Dataset` and `Trainset` do not support sample_weight at the moment. + self.model.fit(X_train, y_train, sample_weight=None, + show_progress=self.show_progress) + self.X_train = X_train + self.y_train = y_train + + def estimate(self, u, i, u_features, i_features): + + n_users = self.trainset.n_users + n_items = self.trainset.n_items + n_user_features = self.trainset.n_user_features + n_item_features = self.trainset.n_item_features + + if (len(u_features) != n_user_features or + len(i_features) != n_item_features): + raise PredictionImpossible( + 'User and/or item features are missing.') + + details = {} + if self.trainset.knows_user(u) and self.trainset.knows_item(i): + data = [1., 1.] + row_ind = [0, 0] + col_ind = [u, n_users + i] + details['knows_user'] = True + details['knows_item'] = True + elif self.trainset.knows_user(u): + data = [1.] + row_ind = [0] + col_ind = [u] + details['knows_user'] = True + details['knows_item'] = False + elif self.trainset.knows_item(i): + data = [1.] + row_ind = [0] + col_ind = [n_users + i] + details['knows_user'] = False + details['knows_item'] = True + else: + data = [] + row_ind = [] + col_ind = [] + details['knows_user'] = False + details['knows_item'] = False + + # Add user features (if any) + if n_user_features > 0: + for n, value in enumerate(u_features): + if value != 0: + row_ind.append(0) + col_ind.append(n_users + n_items + n) + data.append(value) + + # Add item features (if any) + if n_item_features > 0: + for n, value in enumerate(i_features): + if value != 0: + row_ind.append(0) + col_ind.append(n_users + n_items + + n_user_features + n) + data.append(value) + + if data: + X_test = sps.csr_matrix((data, (row_ind, col_ind)), + shape=(1, n_users + n_items + + n_user_features + n_item_features), + dtype=np.float32) + else: + X_test = sps.csr_matrix((1, n_users + n_items + n_user_features + + n_item_features), + dtype=np.float32) + + est = self.model.predict(X_test)[0] + + return est, details + + class FMBasicPL(AlgoBase): """A basic factorization machine algorithm. With order 2, this algorithm is equivalent to the biased SVD algorithm. From 037bf10046a8c7d7ede23e868d1981ee79564e23 Mon Sep 17 00:00:00 2001 From: martincousi Date: Tue, 28 May 2019 10:19:15 -0400 Subject: [PATCH 54/60] Added incomplete implementation of FM (without sample_weights) --- surprise/__init__.py | 16 +- surprise/prediction_algorithms/__init__.py | 16 +- .../factorization_machines.py | 1997 ++++++++++------- 3 files changed, 1166 insertions(+), 863 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index 807213a4..5c8326c8 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -13,11 +13,12 @@ from .prediction_algorithms import SlopeOne from .prediction_algorithms import CoClustering from .prediction_algorithms import Lasso -from .prediction_algorithms import FMBasic -from .prediction_algorithms import FMImplicit -from .prediction_algorithms import FMExplicit -from .prediction_algorithms import FMFeatures -from .prediction_algorithms import FMBasicPL +from .prediction_algorithms import FM +# from .prediction_algorithms import FMBasic +# from .prediction_algorithms import FMImplicit +# from .prediction_algorithms import FMExplicit +# from .prediction_algorithms import FMFeatures +# from .prediction_algorithms import FMBasicPL from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -37,7 +38,8 @@ 'CoClustering', 'PredictionImpossible', 'Prediction', 'Dataset', 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', - 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMFeatures', - 'FMBasicPL'] + 'Lasso', 'FM'] + # 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMFeatures'] + # 'FMBasicPL' __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 46a65ca9..5c8d4bdd 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -33,11 +33,12 @@ from .slope_one import SlopeOne from .co_clustering import CoClustering from .linear import Lasso -from .factorization_machines import FMBasic -from .factorization_machines import FMImplicit -from .factorization_machines import FMExplicit -from .factorization_machines import FMFeatures -from .factorization_machines import FMBasicPL +from .factorization_machines import FM +# from .factorization_machines import FMBasic +# from .factorization_machines import FMImplicit +# from .factorization_machines import FMExplicit +# from .factorization_machines import FMFeatures +# from .factorization_machines import FMBasicPL from .predictions import PredictionImpossible from .predictions import Prediction @@ -45,5 +46,6 @@ __all__ = ['AlgoBase', 'NormalPredictor', 'BaselineOnly', 'KNNBasic', 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', - 'KNNWithZScore', 'Lasso', 'FMBasic', 'FMImplicit', 'FMExplicit', - 'FMFeatures', 'FMBasicPL'] + 'KNNWithZScore', 'Lasso', 'FM'] + # 'FMBasic', 'FMImplicit', 'FMExplicit', + # 'FMFeatures'] #, 'FMBasicPL'] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 2d548236..04d84d8b 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -6,887 +6,282 @@ unicode_literals) import numpy as np -import tensorflow as tf +import pandas as pd +# import tensorflow as tf import scipy.sparse as sps -from tffm import TFFMRegressor -from polylearn import FactorizationMachineRegressor +import xlearn as xl +# from tffm import TFFMRegressor +# from polylearn import FactorizationMachineRegressor from .predictions import PredictionImpossible from .algo_base import AlgoBase -class FMAlgo(AlgoBase): - """This is an abstract class aimed to reduce code redundancy for - factoration machines. - """ - - def __init__(self, order=2, n_factors=2, input_type='dense', - loss_function='mse', - optimizer=tf.train.AdamOptimizer(learning_rate=0.01), - reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, - batch_size=-1, n_epochs=100, log_dir=None, - session_config=None, random_state=None, verbose=False, - **kwargs): - - AlgoBase.__init__(self, **kwargs) - self.order = order - self.n_factors = n_factors # rank in `tffm` - self.input_type = input_type - self.loss_function = loss_function # {'mse', 'loss_logistic'} - # https://www.tensorflow.org/api_guides/python/train#Optimizers - self.optimizer = optimizer - self.reg_all = reg_all # reg in `tffm` - self.use_diag = use_diag - self.reweight_reg = reweight_reg - self.init_std = np.float32(init_std) - self.batch_size = batch_size - self.n_epochs = n_epochs - self.log_dir = log_dir - self.session_config = session_config - self.random_state = random_state # seed in `tffm` - self.verbose = verbose - - if self.loss_function == 'mse': - self.model = TFFMRegressor( - order=self.order, rank=self.n_factors, - input_type=self.input_type, optimizer=self.optimizer, - reg=self.reg_all, use_diag=self.use_diag, - reweight_reg=self.reweight_reg, init_std=self.init_std, - batch_size=self.batch_size, n_epochs=self.n_epochs, - log_dir=self.log_dir, session_config=self.session_config, - seed=self.random_state, verbose=self.verbose) - elif self.loss_function == 'loss_logistic': - # See issue #157 of Surprise - raise ValueError('loss_logistic is not supported at the moment') - else: - raise ValueError(('Unknown value {} for parameter' - 'loss_function').format(self.loss_function)) +class FM(AlgoBase): + """A factorization machine algorithm. - def fit(self, trainset): - - AlgoBase.fit(self, trainset) - - if self.verbose > 0: - self.show_progress = True - else: - self.show_progress = False - - return self - - -class FMBasic(FMAlgo): - """A basic factorization machine algorithm. With order 2, this algorithm - is equivalent to the biased SVD algorithm. - - This code is an interface to the `tffm` library. + This code is an interface to the `xlearn` library. Args: - order : int, default: 2 - Order of corresponding polynomial model. - All interaction from bias and linear to order will be included. - n_factors : int, default: 2 - Number of factors in low-rank appoximation. - This value is shared across different orders of interaction. - loss_function : str, default: 'mse' - 'mse' is the only supported loss_function at the moment. - optimizer : tf.train.Optimizer, - default: AdamOptimizer(learning_rate=0.01) - Optimization method used for training - reg_all : float, default: 0 - Strength of L2 regularization - use_diag : bool, default: False - Use diagonal elements of weights matrix or not. - In the other words, should terms like x^2 be included. - Often reffered as a "Polynomial Network". - Default value (False) corresponds to FM. - reweight_reg : bool, default: False - Use frequency of features as weights for regularization or not. - Should be useful for very sparse data and/or small batches - init_std : float, default: 0.01 - Amplitude of random initialization - batch_size : int, default: -1 - Number of samples in mini-batches. Shuffled every epoch. - Use -1 for full gradient (whole training set in each batch). - n_epoch : int, default: 100 - Default number of epoches. - It can be overrived by explicitly provided value in fit() method. - log_dir : str or None, default: None - Path for storing model stats during training. Used only if is not - None. WARNING: If such directory already exists, it will be - removed! You can use TensorBoard to visualize the stats: - `tensorboard --logdir={log_dir}` - session_config : tf.ConfigProto or None, default: None - Additional setting passed to tf.Session object. - Useful for CPU/GPU switching, setting number of threads and so on, - `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if - enabled). - random_state : int or None, default: None - Random seed used at graph creating time + rating_lst (list of str or `None`): This list specifies what + information from the `raw_ratings` to put in the `x` vector. + Accepted list values are 'userID', 'itemID', 'imp_u_rating' and + 'exp_u_rating'. Implicit and explicit user rating values are scaled + by the number of values. If `None`, no info is added. + user_lst (list of str or `None`): This list specifies what + information from the `user_features` to put in the `x` vector. + Accepted list values consist of the names of features. If `None`, + no info is added. + item_lst (list of str or `None`): This list specifies what + information from the `item_features` to put in the `x` vector. + Accepted list values consist of the names of features. If `None`, + no info is added. + task : str, default: 'reg' + 'binary' or 'reg'. + metric : str, default: 'rmse' + 'acc', 'prec', 'recall', 'f1', 'auc' for classification. + 'mae', 'mape', 'rmse', 'rmsd' for regression. + lr : float, default: 0.2 + Learning rate for optimization method. If you choose 'adagrad' + method, the learning rate will be changed adaptively. + reg : float, default: 0.00002 + Strength of L2 regularization. It can be disabled by setting it to + zero. + k : int, default: 4 + Number of latent factors in low-rank appoximation. + init : float, default: 0.66 + Used to initialize model. + alpha : float, default: 0.3 + Hyper parameter for 'ftrl'. + beta : float, default: 1.0 + Hyper parameter for 'ftrl'. + lambda_1 : float, default : 0.00001 + Hyper parameter for 'ftrl'. + lambda_2 : float, default : 0.00002 + Hyper parameter for 'ftrl'. + nthread : int, default : 1 + Number of CPU cores. + epoch : int, default : 10 + Number of epochs. This value could be changed in early-stop. + opt : str, default : 'adagrad' + 'sgd', 'adagrad' and 'ftrl' are accepted values for the + optimization method. + stop_window : int, default : 2 + Size of the stop window for early-stopping. + use_bin : bool, default : True + Generate bin file for training and testing. + use_norm : bool, default : True + Use instance-wise normalization. + use_lock_free : bool, default : True + Use lock-free training. This does not allow reproducible results. + use_early_stop : bool, default : True + Use early stopping. + random_state : int, default: 1 + Random seed used to shuffle data set. verbose : int, default: False Level of verbosity. - Set 1 for tensorboard info only and 2 for additional stats every - epoch. + modeltxtpath : 'str', default: 'model.txt' + Path and filename of model in text format. + modelpath : 'str', default: 'model.out' + Path and filename of model in binary format. """ - def __init__(self, order=2, n_factors=2, loss_function='mse', - optimizer=tf.train.AdamOptimizer(learning_rate=0.01), - reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, - batch_size=-1, n_epochs=100, log_dir=None, - session_config=None, random_state=None, verbose=False, - **kwargs): - - input_type = 'sparse' + def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, + item_lst=None, task='reg', metric='rmse', lr=0.2, reg=0.00002, + k=4, init=0.66, alpha=0.3, beta=1.0, lambda_1=0.00001, + lambda_2=0.00002, nthread=1, epoch=10, opt='adagrad', + stop_window=2, use_bin=True, use_norm=True, + use_lock_free=True, use_early_stop=True, + random_state=1, verbose=False, modeltxtpath='model.txt', + modelpath='model.out', **kwargs): - FMAlgo.__init__(self, order=order, n_factors=n_factors, - input_type=input_type, loss_function=loss_function, - optimizer=optimizer, reg_all=reg_all, - use_diag=use_diag, reweight_reg=reweight_reg, - init_std=init_std, batch_size=batch_size, - n_epochs=n_epochs, log_dir=log_dir, - session_config=session_config, - random_state=random_state, verbose=verbose, **kwargs) + AlgoBase.__init__(self, **kwargs) + self.rating_lst = rating_lst + self.user_lst = user_lst + self.item_lst = item_lst + self.task = task + self.metric = metric + self.lr = lr + self.reg = reg + self.k = k + self.init = init + self.alpha = alpha + self.beta = beta + self.lambda_1 = lambda_1 + self.lambda_2 = lambda_2 + self.nthread = nthread + self.epoch = epoch + self.opt = opt + self.stop_window = stop_window + self.use_bin = use_bin + self.use_norm = use_norm + self.use_lock_free = use_lock_free + self.use_early_stop = use_early_stop + self.random_state = random_state + self.verbose = verbose + self.modeltxtpath = modeltxtpath + self.modelpath = modelpath + + self.model = xl.create_fm() + self.param = {'task': self.task, + 'metric': self.metric, + 'lr': self.lr, + 'lambda': self.reg, + 'k': self.k, + 'init': self.init, + 'alpha': self.alpha, + 'beta': self.beta, + 'lambda_1': self.lambda_1, + 'lambda_2': self.lambda_2, + 'nthread': self.nthread, + 'epoch': self.epoch, + 'opt': self.opt, + 'stop_window': self.stop_window} + if not self.use_bin: + self.model.setNoBin() + if not self.use_norm: + self.model.disableNorm() + if not self.use_lock_free: + self.model.disableLockFree() + if not self.use_early_stop: + self.model.disableEarlyStop() + if not verbose: + self.model.setQuiet() def fit(self, trainset): - FMAlgo.fit(self, trainset) - self.fm(trainset) - - return self - - def fm(self, trainset): - - n_ratings = self.trainset.n_ratings - n_users = self.trainset.n_users - n_items = self.trainset.n_items - - # Construct sparse X and y - row_ind = np.empty(2 * n_ratings, dtype=int) - col_ind = np.empty(2 * n_ratings, dtype=int) - data = np.ones(2 * n_ratings, dtype=bool) - y_train = np.empty(n_ratings, dtype=float) - nonzero_counter = 0 - rating_counter = 0 - for uid, iid, rating in self.trainset.all_ratings(): - # Add user - row_ind[nonzero_counter] = rating_counter - col_ind[nonzero_counter] = uid - nonzero_counter += 1 - # Add item - row_ind[nonzero_counter] = rating_counter - col_ind[nonzero_counter] = n_users + iid - nonzero_counter += 1 - # Add rating - y_train[rating_counter] = rating - rating_counter += 1 - X_train = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(n_ratings, n_users + n_items), - dtype=bool) - - # `Dataset` and `Trainset` do not support sample_weight at the moment. - self.model.fit(X_train, y_train, sample_weight=None, - show_progress=self.show_progress) - self.X_train = X_train - self.y_train = y_train - - def estimate(self, u, i, *_): - - n_users = self.trainset.n_users - n_items = self.trainset.n_items - - details = {} - if self.trainset.knows_user(u) and self.trainset.knows_item(i): - X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), - shape=(1, n_users + n_items), dtype=bool) - details['knows_user'] = True - details['knows_item'] = True - elif self.trainset.knows_user(u): - X_test = sps.csr_matrix(([1.], ([0], [u])), - shape=(1, n_users + n_items), dtype=bool) - details['knows_user'] = True - details['knows_item'] = False - elif self.trainset.knows_item(i): - X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), - shape=(1, n_users + n_items), dtype=bool) - details['knows_user'] = False - details['knows_item'] = True - else: - X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) - details['knows_user'] = False - details['knows_item'] = False - - est = self.model.predict(X_test)[0] - - return est, details - - -class FMImplicit(FMAlgo): - """A factorization machine algorithm that uses implicit ratings. With order - 2, this algorithm correspond to an extension of the biased SVD++ algorithm. - - This code is an interface to the `tffm` library. - - Args: - order : int, default: 2 - Order of corresponding polynomial model. - All interaction from bias and linear to order will be included. - n_factors : int, default: 2 - Number of factors in low-rank appoximation. - This value is shared across different orders of interaction. - loss_function : str, default: 'mse' - 'mse' is the only supported loss_function at the moment. - optimizer : tf.train.Optimizer, - default: AdamOptimizer(learning_rate=0.01) - Optimization method used for training - reg_all : float, default: 0 - Strength of L2 regularization - use_diag : bool, default: False - Use diagonal elements of weights matrix or not. - In the other words, should terms like x^2 be included. - Often reffered as a "Polynomial Network". - Default value (False) corresponds to FM. - reweight_reg : bool, default: False - Use frequency of features as weights for regularization or not. - Should be useful for very sparse data and/or small batches - init_std : float, default: 0.01 - Amplitude of random initialization - batch_size : int, default: -1 - Number of samples in mini-batches. Shuffled every epoch. - Use -1 for full gradient (whole training set in each batch). - n_epoch : int, default: 100 - Default number of epoches. - It can be overrived by explicitly provided value in fit() method. - log_dir : str or None, default: None - Path for storing model stats during training. Used only if is not - None. WARNING: If such directory already exists, it will be - removed! You can use TensorBoard to visualize the stats: - `tensorboard --logdir={log_dir}` - session_config : tf.ConfigProto or None, default: None - Additional setting passed to tf.Session object. - Useful for CPU/GPU switching, setting number of threads and so on, - `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if - enabled). - random_state : int or None, default: None - Random seed used at graph creating time - verbose : int, default: False - Level of verbosity. - Set 1 for tensorboard info only and 2 for additional stats every - epoch. - """ - - def __init__(self, order=2, n_factors=2, loss_function='mse', - optimizer=tf.train.AdamOptimizer(learning_rate=0.01), - reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, - batch_size=-1, n_epochs=100, log_dir=None, - session_config=None, random_state=None, verbose=False, - **kwargs): + AlgoBase.fit(self, trainset) + self._construct_libsvm() - input_type = 'sparse' + # Fit without validation data set + xdm_train = xl.DMatrix( + self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'], + self.libsvm_df.loc[:, 'rating']) + self.model.setTrain(xdm_train) + self.model.setTXTModel(self.modeltxtpath) + self.model.fit(self.param, self.modelpath) - FMAlgo.__init__(self, order=order, n_factors=n_factors, - input_type=input_type, loss_function=loss_function, - optimizer=optimizer, reg_all=reg_all, - use_diag=use_diag, reweight_reg=reweight_reg, - init_std=init_std, batch_size=batch_size, - n_epochs=n_epochs, log_dir=log_dir, - session_config=session_config, - random_state=random_state, verbose=verbose, **kwargs) + # Fit with validation data set (TODO) - def fit(self, trainset): + # Load text model + # This is used to define `self.bias`, `self.linear_coefs` and + # `self.inter_coefs` + self._load_txt_model() - FMAlgo.fit(self, trainset) - self.fm(trainset) + # Delete model files (TODO) return self - def fm(self, trainset): - - n_ratings = self.trainset.n_ratings - n_users = self.trainset.n_users - n_items = self.trainset.n_items + def _construct_libsvm(self): + """ Outputs the data in a libsvm format. This format is used by FM + algorithms. It is assumed that these features are correctly encoded. + These dummies are created (if needed) using only the info in the + trainset. + """ - # Construct sparse X and y - row_ind = [] - col_ind = [] - data = [] - y_train = np.empty(n_ratings, dtype=np.float32) - rating_counter = 0 - for uid, iid, rating in self.trainset.all_ratings(): - # Add user - row_ind.append(rating_counter) - col_ind.append(uid) - data.append(1.) - # Add item - row_ind.append(rating_counter) - col_ind.append(n_users + iid) - data.append(1.) - # Add implicit ratings - sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) - for iid_imp, _ in self.trainset.ur[uid]: - row_ind.append(rating_counter) - col_ind.append(n_users + n_items + iid_imp) - data.append(1 / sqrt_Iu) - # Add rating - y_train[rating_counter] = rating - rating_counter += 1 - X_train = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(n_ratings, n_users + 2 * n_items), - dtype=np.float32) - - # `Dataset` and `Trainset` do not support sample_weight at the moment. - self.model.fit(X_train, y_train, sample_weight=None, - show_progress=self.show_progress) - self.X_train = X_train - self.y_train = y_train - - def estimate(self, u, i, *_): - - n_users = self.trainset.n_users - n_items = self.trainset.n_items - - details = {} - if self.trainset.knows_user(u) and self.trainset.knows_item(i): - data = [1., 1.] - row_ind = [0, 0] - col_ind = [u, n_users + i] - details['knows_user'] = True - details['knows_item'] = True - elif self.trainset.knows_user(u): - data = [1.] - row_ind = [0] - col_ind = [u] - details['knows_user'] = True - details['knows_item'] = False - elif self.trainset.knows_item(i): - data = [1.] - row_ind = [0] - col_ind = [n_users + i] - details['knows_user'] = False - details['knows_item'] = True - else: - data = [] - row_ind = [] - col_ind = [] - details['knows_user'] = False - details['knows_item'] = False - - if self.trainset.knows_user(u): - sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) - for iid_imp, _ in self.trainset.ur[u]: - row_ind.append(0) - col_ind.append(n_users + n_items + iid_imp) - data.append(1 / sqrt_Iu) - - if data: - X_test = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(1, n_users + 2 * n_items), - dtype=np.float32) - else: - X_test = sps.csr_matrix((1, n_users + 2 * n_items), - dtype=np.float32) - - est = self.model.predict(X_test)[0] - - return est, details - - -class FMExplicit(FMAlgo): - """A factorization machine algorithm that uses explicit ratings. - - This code is an interface to the `tffm` library. - - Args: - order : int, default: 2 - Order of corresponding polynomial model. - All interaction from bias and linear to order will be included. - n_factors : int, default: 2 - Number of factors in low-rank appoximation. - This value is shared across different orders of interaction. - loss_function : str, default: 'mse' - 'mse' is the only supported loss_function at the moment. - optimizer : tf.train.Optimizer, - default: AdamOptimizer(learning_rate=0.01) - Optimization method used for training - reg_all : float, default: 0 - Strength of L2 regularization - use_diag : bool, default: False - Use diagonal elements of weights matrix or not. - In the other words, should terms like x^2 be included. - Often reffered as a "Polynomial Network". - Default value (False) corresponds to FM. - reweight_reg : bool, default: False - Use frequency of features as weights for regularization or not. - Should be useful for very sparse data and/or small batches - init_std : float, default: 0.01 - Amplitude of random initialization - batch_size : int, default: -1 - Number of samples in mini-batches. Shuffled every epoch. - Use -1 for full gradient (whole training set in each batch). - n_epoch : int, default: 100 - Default number of epoches. - It can be overrived by explicitly provided value in fit() method. - log_dir : str or None, default: None - Path for storing model stats during training. Used only if is not - None. WARNING: If such directory already exists, it will be - removed! You can use TensorBoard to visualize the stats: - `tensorboard --logdir={log_dir}` - session_config : tf.ConfigProto or None, default: None - Additional setting passed to tf.Session object. - Useful for CPU/GPU switching, setting number of threads and so on, - `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if - enabled). - random_state : int or None, default: None - Random seed used at graph creating time - verbose : int, default: False - Level of verbosity. - Set 1 for tensorboard info only and 2 for additional stats every - epoch. - """ - - def __init__(self, order=2, n_factors=2, loss_function='mse', - optimizer=tf.train.AdamOptimizer(learning_rate=0.01), - reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, - batch_size=-1, n_epochs=100, log_dir=None, - session_config=None, random_state=None, verbose=False, - **kwargs): - - input_type = 'sparse' - - FMAlgo.__init__(self, order=order, n_factors=n_factors, - input_type=input_type, loss_function=loss_function, - optimizer=optimizer, reg_all=reg_all, - use_diag=use_diag, reweight_reg=reweight_reg, - init_std=init_std, batch_size=batch_size, - n_epochs=n_epochs, log_dir=log_dir, - session_config=session_config, - random_state=random_state, verbose=verbose, **kwargs) - - def fit(self, trainset): - - FMAlgo.fit(self, trainset) - self.fm(trainset) - - return self - - def fm(self, trainset): + if self.user_lst and (self.trainset.n_user_features == 0): + raise ValueError('user_lst cannot be used since ' + 'there are no user_features') + if self.item_lst and (self.trainset.n_item_features == 0): + raise ValueError('item_lst cannot be used since ' + 'there are no item_features') n_ratings = self.trainset.n_ratings n_users = self.trainset.n_users n_items = self.trainset.n_items - # Construct sparse X and y - row_ind = [] - col_ind = [] - data = [] - y_train = np.empty(n_ratings, dtype=np.float32) - max_value = self.trainset.rating_scale[1] + self.trainset.offset - rating_counter = 0 - for uid, iid, rating in self.trainset.all_ratings(): - # Add user - row_ind.append(rating_counter) - col_ind.append(uid) - data.append(1.) - # Add item - row_ind.append(rating_counter) - col_ind.append(n_users + iid) - data.append(1.) - # Add explicit ratings - sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) - for iid_exp, rating_exp in self.trainset.ur[uid]: - row_ind.append(rating_counter) - col_ind.append(n_users + n_items + iid_exp) - data.append(rating_exp / (max_value * sqrt_Iu)) - # Add rating - y_train[rating_counter] = rating - rating_counter += 1 - X_train = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(n_ratings, n_users + 2 * n_items), - dtype=np.float32) - - # `Dataset` and `Trainset` do not support sample_weight at the moment. - self.model.fit(X_train, y_train, sample_weight=None, - show_progress=self.show_progress) - self.X_train = X_train - self.y_train = y_train - - def estimate(self, u, i, *_): - - n_users = self.trainset.n_users - n_items = self.trainset.n_items - - details = {} - if self.trainset.knows_user(u) and self.trainset.knows_item(i): - data = [1., 1.] - row_ind = [0, 0] - col_ind = [u, n_users + i] - details['knows_user'] = True - details['knows_item'] = True - elif self.trainset.knows_user(u): - data = [1.] - row_ind = [0] - col_ind = [u] - details['knows_user'] = True - details['knows_item'] = False - elif self.trainset.knows_item(i): - data = [1.] - row_ind = [0] - col_ind = [n_users + i] - details['knows_user'] = False - details['knows_item'] = True - else: - data = [] - row_ind = [] - col_ind = [] - details['knows_user'] = False - details['knows_item'] = False - - if self.trainset.knows_user(u): - sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) - max_value = self.trainset.rating_scale[1] + self.trainset.offset - for iid_exp, rating_exp in self.trainset.ur[u]: - row_ind.append(0) - col_ind.append(n_users + n_items + iid_exp) - data.append(rating_exp / (max_value * sqrt_Iu)) - - if data: - X_test = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(1, n_users + 2 * n_items), - dtype=np.float32) - else: - X_test = sps.csr_matrix((1, n_users + 2 * n_items), - dtype=np.float32) - - est = self.model.predict(X_test)[0] - - return est, details - - -class FMFeatures(FMAlgo): - """A factorization machine algorithm that uses available features. - - WARNING: Features should be pre-scaled to an absolute value less or equal - to 1 if using a high value for `order`. - - This code is an interface to the `tffm` library. - - Args: - order : int, default: 2 - Order of corresponding polynomial model. - All interaction from bias and linear to order will be included. - n_factors : int, default: 2 - Number of factors in low-rank appoximation. - This value is shared across different orders of interaction. - loss_function : str, default: 'mse' - 'mse' is the only supported loss_function at the moment. - optimizer : tf.train.Optimizer, - default: AdamOptimizer(learning_rate=0.01) - Optimization method used for training - reg_all : float, default: 0 - Strength of L2 regularization - use_diag : bool, default: False - Use diagonal elements of weights matrix or not. - In the other words, should terms like x^2 be included. - Often reffered as a "Polynomial Network". - Default value (False) corresponds to FM. - reweight_reg : bool, default: False - Use frequency of features as weights for regularization or not. - Should be useful for very sparse data and/or small batches - init_std : float, default: 0.01 - Amplitude of random initialization - batch_size : int, default: -1 - Number of samples in mini-batches. Shuffled every epoch. - Use -1 for full gradient (whole training set in each batch). - n_epoch : int, default: 100 - Default number of epoches. - It can be overrived by explicitly provided value in fit() method. - log_dir : str or None, default: None - Path for storing model stats during training. Used only if is not - None. WARNING: If such directory already exists, it will be - removed! You can use TensorBoard to visualize the stats: - `tensorboard --logdir={log_dir}` - session_config : tf.ConfigProto or None, default: None - Additional setting passed to tf.Session object. - Useful for CPU/GPU switching, setting number of threads and so on, - `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if - enabled). - random_state : int or None, default: None - Random seed used at graph creating time - verbose : int, default: False - Level of verbosity. - Set 1 for tensorboard info only and 2 for additional stats every - epoch. - """ - - def __init__(self, order=2, n_factors=2, loss_function='mse', - optimizer=tf.train.AdamOptimizer(learning_rate=0.01), - reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, - batch_size=-1, n_epochs=100, log_dir=None, - session_config=None, random_state=None, verbose=False, - **kwargs): - - input_type = 'sparse' - - FMAlgo.__init__(self, order=order, n_factors=n_factors, - input_type=input_type, loss_function=loss_function, - optimizer=optimizer, reg_all=reg_all, - use_diag=use_diag, reweight_reg=reweight_reg, - init_std=init_std, batch_size=batch_size, - n_epochs=n_epochs, log_dir=log_dir, - session_config=session_config, - random_state=random_state, verbose=verbose, **kwargs) - - def fit(self, trainset): - - FMAlgo.fit(self, trainset) - self.fm(trainset) - - return self - - def fm(self, trainset): - - n_ratings = self.trainset.n_ratings - n_users = self.trainset.n_users - n_items = self.trainset.n_items - n_user_features = self.trainset.n_user_features - n_item_features = self.trainset.n_item_features - - # Construct sparse X and y - row_ind = [] - col_ind = [] - data = [] - y_train = np.empty(n_ratings, dtype=np.float32) - rating_counter = 0 - for uid, iid, rating in self.trainset.all_ratings(): - # Add user - row_ind.append(rating_counter) - col_ind.append(uid) - data.append(1.) - # Add item - row_ind.append(rating_counter) - col_ind.append(n_users + iid) - data.append(1.) - # Add user features (if any) - if n_user_features > 0: - if self.trainset.has_user_features(uid): - for n, value in enumerate(self.trainset.u_features[uid]): - if value != 0: - row_ind.append(rating_counter) - col_ind.append(n_users + n_items + n) - data.append(value) + # Construct ratings_df from trainset + # The IDs are unique and start at 0 + ratings_df = pd.DataFrame([tup for tup in self.trainset.all_ratings()], + columns=['userID', 'itemID', 'rating']) + + # Initialize df with rating values + libsvm_df = pd.DataFrame(ratings_df['rating']) + + # Add rating features + if self.rating_lst: + for feature in self.rating_lst: + if feature == 'userID': + libsvm_df = pd.concat([libsvm_df, pd.get_dummies( + ratings_df['userID'], prefix='userID')], axis=1) + elif feature == 'itemID': + libsvm_df = pd.concat([libsvm_df, pd.get_dummies( + ratings_df['itemID'], prefix='itemID')], axis=1) + elif feature == 'imp_u_rating': + temp = np.zeros((n_ratings, n_items)) + for row in ratings_df.itertuples(): + iid = row.itemID + all_u_ratings = self.trainset.ur[row.userID] + for other_iid, rating in all_u_ratings: + if other_iid != iid: # only the other ratings + temp[row.Index, other_iid] = 1 + count = np.count_nonzero(temp, axis=1)[:, None] + count[count == 0] = 1 # remove zeros for division + temp = temp / count + cols = ['imp_u_rating_{}'.format(i) + for i in range(n_items)] + libsvm_df = pd.concat([libsvm_df, pd.DataFrame( + temp, columns=cols)], axis=1) + elif feature == 'exp_u_rating': + # a rating is at least 1 with the offset + temp = np.zeros((n_ratings, n_items)) + for row in ratings_df.itertuples(): + iid = row.itemID + all_u_ratings = self.trainset.ur[row.userID] + for other_iid, rating in all_u_ratings: + if other_iid != iid: # only the other ratings + temp[row.Index, other_iid] = rating + count = np.count_nonzero(temp, axis=1)[:, None] + count[count == 0] = 1 # remove zeros for division + temp = temp / count + cols = ['exp_u_rating_{}'.format(i) + for i in range(n_items)] + libsvm_df = pd.concat([libsvm_df, pd.DataFrame( + temp, columns=cols)], axis=1) else: - raise ValueError('No features for user ' + - str(self.trainset.to_raw_uid(uid))) - # Add item features (if any) - if n_item_features > 0: - if self.trainset.has_item_features(iid): - for n, value in enumerate(self.trainset.i_features[iid]): - if value != 0: - row_ind.append(rating_counter) - col_ind.append(n_users + n_items + - n_user_features + n) - data.append(value) + raise ValueError('{} is not an accepted value ' + 'for rating_lst'.format(feature)) + + # Add user features + if self.user_lst: + temp = pd.DataFrame( + [self.trainset.u_features[uid] + for uid in ratings_df['userID']], + columns=self.trainset.user_features_labels) + for feature in self.user_lst: + if feature in self.trainset.user_features_labels: + libsvm_df[feature] = temp[feature] else: - raise ValueError('No features for item ' + - str(self.trainset.to_raw_iid(iid))) - # Add rating - y_train[rating_counter] = rating - rating_counter += 1 - X_train = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(n_ratings, n_users + n_items + - n_user_features + n_item_features), - dtype=np.float32) - - # `Dataset` and `Trainset` do not support sample_weight at the moment. - self.model.fit(X_train, y_train, sample_weight=None, - show_progress=self.show_progress) - self.X_train = X_train - self.y_train = y_train - - def estimate(self, u, i, u_features, i_features): - - n_users = self.trainset.n_users - n_items = self.trainset.n_items - n_user_features = self.trainset.n_user_features - n_item_features = self.trainset.n_item_features - - if (len(u_features) != n_user_features or - len(i_features) != n_item_features): - raise PredictionImpossible( - 'User and/or item features are missing.') - - details = {} - if self.trainset.knows_user(u) and self.trainset.knows_item(i): - data = [1., 1.] - row_ind = [0, 0] - col_ind = [u, n_users + i] - details['knows_user'] = True - details['knows_item'] = True - elif self.trainset.knows_user(u): - data = [1.] - row_ind = [0] - col_ind = [u] - details['knows_user'] = True - details['knows_item'] = False - elif self.trainset.knows_item(i): - data = [1.] - row_ind = [0] - col_ind = [n_users + i] - details['knows_user'] = False - details['knows_item'] = True - else: - data = [] - row_ind = [] - col_ind = [] - details['knows_user'] = False - details['knows_item'] = False - - # Add user features (if any) - if n_user_features > 0: - for n, value in enumerate(u_features): - if value != 0: - row_ind.append(0) - col_ind.append(n_users + n_items + n) - data.append(value) - - # Add item features (if any) - if n_item_features > 0: - for n, value in enumerate(i_features): - if value != 0: - row_ind.append(0) - col_ind.append(n_users + n_items + - n_user_features + n) - data.append(value) - - if data: - X_test = sps.csr_matrix((data, (row_ind, col_ind)), - shape=(1, n_users + n_items + - n_user_features + n_item_features), - dtype=np.float32) - else: - X_test = sps.csr_matrix((1, n_users + n_items + n_user_features + - n_item_features), - dtype=np.float32) - - est = self.model.predict(X_test)[0] - - return est, details - - -class FMBasicPL(AlgoBase): - """A basic factorization machine algorithm. With order 2, this algorithm - is equivalent to the biased SVD algorithm. - - This code is an interface to the `polylearn` library. - - Args: - degree : int, default: 2 - Degree of the polynomial. Corresponds to the order of feature - interactions captured by the model. Currently only supports - degrees up to 3. - n_factors : int, default: 2 - Number of basis vectors to learn, a.k.a. the dimension of the - low-rank parametrization. - reg_alpha : float, default: 1 - Regularization amount for linear term (if ``fit_linear=True``). - reg_beta : float, default: 1 - Regularization amount for higher-order weights. - tol : float, default: 1e-6 - Tolerance for the stopping condition. - fit_lower : {'explicit'|'augment'|None}, default: 'explicit' - Whether and how to fit lower-order, non-homogeneous terms. - - 'explicit': fits a separate P directly for each lower order. - - 'augment': adds the required number of dummy columns (columns - that are 1 everywhere) in order to capture lower-order terms. - Adds ``degree - 2`` columns if ``fit_linear`` is true, or - ``degree - 1`` columns otherwise, to account for the linear - term. - - None: only learns weights for the degree given. If - ``degree == 3``, for example, the model will only have weights - for third-order feature interactions. - fit_linear : {True|False}, default: True - Whether to fit an explicit linear term to the model, using - coordinate descent. If False, the model can still capture linear - effects if ``fit_lower == 'augment'``. - warm_start : boolean, optional, default: False - Whether to use the existing solution, if available. Useful for - computing regularization paths or pre-initializing the model. - init_lambdas : {'ones'|'random_signs'}, default: 'ones' - How to initialize the predictive weights of each learned basis. The - lambdas are not trained; using alternate signs can theoretically - improve performance if the kernel degree is even. The default - value of 'ones' matches the original formulation of factorization - machines (Rendle, 2010). - To use custom values for the lambdas, ``warm_start`` may be used. - max_iter : int, optional, default: 10000 - Maximum number of passes over the dataset to perform. - random_state : int seed, RandomState instance, or None (default) - The seed of the pseudo random number generator to use for - initializing the parameters. - verbose : boolean, optional, default: False - Whether to print debugging information. - """ - - def __init__(self, degree=2, n_factors=2, reg_alpha=1., reg_beta=1., - tol=1e-6, fit_lower='explicit', fit_linear=True, - warm_start=False, init_lambdas='ones', max_iter=10000, - random_state=None, verbose=False, **kwargs): - - self.degree = degree - self.n_factors = n_factors # n_components in `polylearn` - self.reg_alpha = reg_alpha # alpha in `polylearn` - self.reg_beta = reg_beta # beta in `polylearn` - self.tol = tol - self.fit_lower = fit_lower - self.fit_linear = fit_linear - self.warm_start = warm_start - self.init_lambdas = init_lambdas - self.max_iter = max_iter - self.random_state = random_state - self.verbose = verbose - - self.model = FactorizationMachineRegressor( - degree=self.degree, n_components=self.n_factors, - alpha=self.reg_alpha, beta=self.reg_beta, tol=self.tol, - fit_lower=self.fit_lower, fit_linear=self.fit_linear, - warm_start=self.warm_start, init_lambdas=self.init_lambdas, - max_iter=self.max_iter, random_state=self.random_state, - verbose=self.verbose) - - def fit(self, trainset): + raise ValueError( + '{} is not part of user_features'.format(feature)) + + # Add item features + if self.item_lst: + temp = pd.DataFrame( + [self.trainset.i_features[iid] + for iid in ratings_df['itemID']], + columns=self.trainset.item_features_labels) + for feature in self.item_lst: + if feature in self.trainset.item_features_labels: + libsvm_df[feature] = temp[feature] + else: + raise ValueError( + '{} is not part of item_features'.format(feature)) - AlgoBase.fit(self, trainset) + self.libsvm_df = libsvm_df + self.libsvm_feature_nb = self.libsvm_df.shape[1] - 1 - n_ratings = self.trainset.n_ratings - n_users = self.trainset.n_users - n_items = self.trainset.n_items + def _load_txt_model(self): - # Construct sparse X and y - row_ind = np.empty(2 * n_ratings, dtype=int) - col_ind = np.empty(2 * n_ratings, dtype=int) - data = np.ones(2 * n_ratings, dtype=bool) - y_train = np.empty(n_ratings, dtype=float) - nonzero_counter = 0 - rating_counter = 0 - for uid, iid, rating in self.trainset.all_ratings(): - # Add user - row_ind[nonzero_counter] = rating_counter - col_ind[nonzero_counter] = uid - nonzero_counter += 1 - # Add item - row_ind[nonzero_counter] = rating_counter - col_ind[nonzero_counter] = n_users + iid - nonzero_counter += 1 - # Add rating - y_train[rating_counter] = rating - rating_counter += 1 - X_train = sps.csc_matrix((data, (row_ind, col_ind)), - shape=(n_ratings, n_users + n_items), - dtype=bool) - - self.model.fit(X_train, y_train) - self.X_train = X_train - self.y_train = y_train + temp = pd.read_csv(self.modeltxtpath, sep=':', header=None) + coefs = temp.loc[:, 1].tolist() + coefs = " ".join(coefs) + coefs = np.array(list(map(float, coefs.split()))) - return self + self.bias = coefs[0] + self.linear_coefs = coefs[1:self.libsvm_feature_nb + 1] + self.inter_coefs = coefs[self.libsvm_feature_nb + 1:].reshape( + self.libsvm_feature_nb, self.k) def estimate(self, u, i, *_): @@ -895,25 +290,929 @@ def estimate(self, u, i, *_): details = {} if self.trainset.knows_user(u) and self.trainset.knows_item(i): - X_test = sps.csc_matrix(([1., 1.], ([0, 0], [u, n_users + i])), + X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = True details['knows_item'] = True elif self.trainset.knows_user(u): - X_test = sps.csc_matrix(([1.], ([0], [u])), + X_test = sps.csr_matrix(([1.], ([0], [u])), shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = True details['knows_item'] = False elif self.trainset.knows_item(i): - X_test = sps.csc_matrix(([1.], ([0], [n_users + i])), + X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = False details['knows_item'] = True else: - X_test = sps.csc_matrix((1, n_users + n_items), dtype=bool) + X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) details['knows_user'] = False details['knows_item'] = False est = self.model.predict(X_test)[0] return est, details + + +# class FMAlgo(AlgoBase): +# """This is an abstract class aimed to reduce code redundancy for +# factoration machines. +# """ + +# def __init__(self, order=2, n_factors=2, input_type='dense', +# loss_function='mse', +# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), +# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, +# batch_size=-1, n_epochs=100, log_dir=None, +# session_config=None, random_state=None, verbose=False, +# **kwargs): + +# AlgoBase.__init__(self, **kwargs) +# self.order = order +# self.n_factors = n_factors # rank in `tffm` +# self.input_type = input_type +# self.loss_function = loss_function # {'mse', 'loss_logistic'} +# # https://www.tensorflow.org/api_guides/python/train#Optimizers +# self.optimizer = optimizer +# self.reg_all = reg_all # reg in `tffm` +# self.use_diag = use_diag +# self.reweight_reg = reweight_reg +# self.init_std = np.float32(init_std) +# self.batch_size = batch_size +# self.n_epochs = n_epochs +# self.log_dir = log_dir +# self.session_config = session_config +# self.random_state = random_state # seed in `tffm` +# self.verbose = verbose + +# if self.loss_function == 'mse': +# self.model = TFFMRegressor( +# order=self.order, rank=self.n_factors, +# input_type=self.input_type, optimizer=self.optimizer, +# reg=self.reg_all, use_diag=self.use_diag, +# reweight_reg=self.reweight_reg, init_std=self.init_std, +# batch_size=self.batch_size, n_epochs=self.n_epochs, +# log_dir=self.log_dir, session_config=self.session_config, +# seed=self.random_state, verbose=self.verbose) +# elif self.loss_function == 'loss_logistic': +# # See issue #157 of Surprise +# raise ValueError('loss_logistic is not supported at the moment') +# else: +# raise ValueError(('Unknown value {} for parameter' +# 'loss_function').format(self.loss_function)) + +# def fit(self, trainset): + +# AlgoBase.fit(self, trainset) + +# if self.verbose > 0: +# self.show_progress = True +# else: +# self.show_progress = False + +# return self + + +# class FMBasic(FMAlgo): +# """A basic factorization machine algorithm. With order 2, this algorithm +# is equivalent to the biased SVD algorithm. + +# This code is an interface to the `tffm` library. + +# Args: +# order : int, default: 2 +# Order of corresponding polynomial model. +# All interaction from bias and linear to order will be included. +# n_factors : int, default: 2 +# Number of factors in low-rank appoximation. +# This value is shared across different orders of interaction. +# loss_function : str, default: 'mse' +# 'mse' is the only supported loss_function at the moment. +# optimizer : tf.train.Optimizer, +# default: AdamOptimizer(learning_rate=0.01) +# Optimization method used for training +# reg_all : float, default: 0 +# Strength of L2 regularization +# use_diag : bool, default: False +# Use diagonal elements of weights matrix or not. +# In the other words, should terms like x^2 be included. +# Often reffered as a "Polynomial Network". +# Default value (False) corresponds to FM. +# reweight_reg : bool, default: False +# Use frequency of features as weights for regularization or not. +# Should be useful for very sparse data and/or small batches +# init_std : float, default: 0.01 +# Amplitude of random initialization +# batch_size : int, default: -1 +# Number of samples in mini-batches. Shuffled every epoch. +# Use -1 for full gradient (whole training set in each batch). +# n_epoch : int, default: 100 +# Default number of epoches. +# It can be overrived by explicitly provided value in fit() method. +# log_dir : str or None, default: None +# Path for storing model stats during training. Used only if is not +# None. WARNING: If such directory already exists, it will be +# removed! You can use TensorBoard to visualize the stats: +# `tensorboard --logdir={log_dir}` +# session_config : tf.ConfigProto or None, default: None +# Additional setting passed to tf.Session object. +# Useful for CPU/GPU switching, setting number of threads and so on, +# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if +# enabled). +# random_state : int or None, default: None +# Random seed used at graph creating time +# verbose : int, default: False +# Level of verbosity. +# Set 1 for tensorboard info only and 2 for additional stats every +# epoch. +# """ + +# def __init__(self, order=2, n_factors=2, loss_function='mse', +# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), +# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, +# batch_size=-1, n_epochs=100, log_dir=None, +# session_config=None, random_state=None, verbose=False, +# **kwargs): + +# input_type = 'sparse' + +# FMAlgo.__init__(self, order=order, n_factors=n_factors, +# input_type=input_type, loss_function=loss_function, +# optimizer=optimizer, reg_all=reg_all, +# use_diag=use_diag, reweight_reg=reweight_reg, +# init_std=init_std, batch_size=batch_size, +# n_epochs=n_epochs, log_dir=log_dir, +# session_config=session_config, +# random_state=random_state, verbose=verbose, **kwargs) + +# def fit(self, trainset): + +# FMAlgo.fit(self, trainset) +# self.fm(trainset) + +# return self + +# def fm(self, trainset): + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# # Construct sparse X and y +# row_ind = np.empty(2 * n_ratings, dtype=int) +# col_ind = np.empty(2 * n_ratings, dtype=int) +# data = np.ones(2 * n_ratings, dtype=bool) +# y_train = np.empty(n_ratings, dtype=float) +# nonzero_counter = 0 +# rating_counter = 0 +# for uid, iid, rating in self.trainset.all_ratings(): +# # Add user +# row_ind[nonzero_counter] = rating_counter +# col_ind[nonzero_counter] = uid +# nonzero_counter += 1 +# # Add item +# row_ind[nonzero_counter] = rating_counter +# col_ind[nonzero_counter] = n_users + iid +# nonzero_counter += 1 +# # Add rating +# y_train[rating_counter] = rating +# rating_counter += 1 +# X_train = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(n_ratings, n_users + n_items), +# dtype=bool) + +# # `Dataset` and `Trainset` do not support sample_weight at the moment. +# self.model.fit(X_train, y_train, sample_weight=None, +# show_progress=self.show_progress) +# self.X_train = X_train +# self.y_train = y_train + +# def estimate(self, u, i, *_): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# X_test = sps.csr_matrix(([1.], ([0], [u])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = False + +# est = self.model.predict(X_test)[0] + +# return est, details + + +# class FMImplicit(FMAlgo): +# """A factorization machine algorithm that uses implicit ratings. With order +# 2, this algorithm correspond to an extension of the biased SVD++ algorithm. + +# This code is an interface to the `tffm` library. + +# Args: +# order : int, default: 2 +# Order of corresponding polynomial model. +# All interaction from bias and linear to order will be included. +# n_factors : int, default: 2 +# Number of factors in low-rank appoximation. +# This value is shared across different orders of interaction. +# loss_function : str, default: 'mse' +# 'mse' is the only supported loss_function at the moment. +# optimizer : tf.train.Optimizer, +# default: AdamOptimizer(learning_rate=0.01) +# Optimization method used for training +# reg_all : float, default: 0 +# Strength of L2 regularization +# use_diag : bool, default: False +# Use diagonal elements of weights matrix or not. +# In the other words, should terms like x^2 be included. +# Often reffered as a "Polynomial Network". +# Default value (False) corresponds to FM. +# reweight_reg : bool, default: False +# Use frequency of features as weights for regularization or not. +# Should be useful for very sparse data and/or small batches +# init_std : float, default: 0.01 +# Amplitude of random initialization +# batch_size : int, default: -1 +# Number of samples in mini-batches. Shuffled every epoch. +# Use -1 for full gradient (whole training set in each batch). +# n_epoch : int, default: 100 +# Default number of epoches. +# It can be overrived by explicitly provided value in fit() method. +# log_dir : str or None, default: None +# Path for storing model stats during training. Used only if is not +# None. WARNING: If such directory already exists, it will be +# removed! You can use TensorBoard to visualize the stats: +# `tensorboard --logdir={log_dir}` +# session_config : tf.ConfigProto or None, default: None +# Additional setting passed to tf.Session object. +# Useful for CPU/GPU switching, setting number of threads and so on, +# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if +# enabled). +# random_state : int or None, default: None +# Random seed used at graph creating time +# verbose : int, default: False +# Level of verbosity. +# Set 1 for tensorboard info only and 2 for additional stats every +# epoch. +# """ + +# def __init__(self, order=2, n_factors=2, loss_function='mse', +# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), +# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, +# batch_size=-1, n_epochs=100, log_dir=None, +# session_config=None, random_state=None, verbose=False, +# **kwargs): + +# input_type = 'sparse' + +# FMAlgo.__init__(self, order=order, n_factors=n_factors, +# input_type=input_type, loss_function=loss_function, +# optimizer=optimizer, reg_all=reg_all, +# use_diag=use_diag, reweight_reg=reweight_reg, +# init_std=init_std, batch_size=batch_size, +# n_epochs=n_epochs, log_dir=log_dir, +# session_config=session_config, +# random_state=random_state, verbose=verbose, **kwargs) + +# def fit(self, trainset): + +# FMAlgo.fit(self, trainset) +# self.fm(trainset) + +# return self + +# def fm(self, trainset): + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# # Construct sparse X and y +# row_ind = [] +# col_ind = [] +# data = [] +# y_train = np.empty(n_ratings, dtype=np.float32) +# rating_counter = 0 +# for uid, iid, rating in self.trainset.all_ratings(): +# # Add user +# row_ind.append(rating_counter) +# col_ind.append(uid) +# data.append(1.) +# # Add item +# row_ind.append(rating_counter) +# col_ind.append(n_users + iid) +# data.append(1.) +# # Add implicit ratings +# sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) +# for iid_imp, _ in self.trainset.ur[uid]: +# row_ind.append(rating_counter) +# col_ind.append(n_users + n_items + iid_imp) +# data.append(1 / sqrt_Iu) +# # Add rating +# y_train[rating_counter] = rating +# rating_counter += 1 +# X_train = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(n_ratings, n_users + 2 * n_items), +# dtype=np.float32) + +# # `Dataset` and `Trainset` do not support sample_weight at the moment. +# self.model.fit(X_train, y_train, sample_weight=None, +# show_progress=self.show_progress) +# self.X_train = X_train +# self.y_train = y_train + +# def estimate(self, u, i, *_): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# data = [1., 1.] +# row_ind = [0, 0] +# col_ind = [u, n_users + i] +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# data = [1.] +# row_ind = [0] +# col_ind = [u] +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# data = [1.] +# row_ind = [0] +# col_ind = [n_users + i] +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# data = [] +# row_ind = [] +# col_ind = [] +# details['knows_user'] = False +# details['knows_item'] = False + +# if self.trainset.knows_user(u): +# sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) +# for iid_imp, _ in self.trainset.ur[u]: +# row_ind.append(0) +# col_ind.append(n_users + n_items + iid_imp) +# data.append(1 / sqrt_Iu) + +# if data: +# X_test = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(1, n_users + 2 * n_items), +# dtype=np.float32) +# else: +# X_test = sps.csr_matrix((1, n_users + 2 * n_items), +# dtype=np.float32) + +# est = self.model.predict(X_test)[0] + +# return est, details + + +# class FMExplicit(FMAlgo): +# """A factorization machine algorithm that uses explicit ratings. + +# This code is an interface to the `tffm` library. + +# Args: +# order : int, default: 2 +# Order of corresponding polynomial model. +# All interaction from bias and linear to order will be included. +# n_factors : int, default: 2 +# Number of factors in low-rank appoximation. +# This value is shared across different orders of interaction. +# loss_function : str, default: 'mse' +# 'mse' is the only supported loss_function at the moment. +# optimizer : tf.train.Optimizer, +# default: AdamOptimizer(learning_rate=0.01) +# Optimization method used for training +# reg_all : float, default: 0 +# Strength of L2 regularization +# use_diag : bool, default: False +# Use diagonal elements of weights matrix or not. +# In the other words, should terms like x^2 be included. +# Often reffered as a "Polynomial Network". +# Default value (False) corresponds to FM. +# reweight_reg : bool, default: False +# Use frequency of features as weights for regularization or not. +# Should be useful for very sparse data and/or small batches +# init_std : float, default: 0.01 +# Amplitude of random initialization +# batch_size : int, default: -1 +# Number of samples in mini-batches. Shuffled every epoch. +# Use -1 for full gradient (whole training set in each batch). +# n_epoch : int, default: 100 +# Default number of epoches. +# It can be overrived by explicitly provided value in fit() method. +# log_dir : str or None, default: None +# Path for storing model stats during training. Used only if is not +# None. WARNING: If such directory already exists, it will be +# removed! You can use TensorBoard to visualize the stats: +# `tensorboard --logdir={log_dir}` +# session_config : tf.ConfigProto or None, default: None +# Additional setting passed to tf.Session object. +# Useful for CPU/GPU switching, setting number of threads and so on, +# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if +# enabled). +# random_state : int or None, default: None +# Random seed used at graph creating time +# verbose : int, default: False +# Level of verbosity. +# Set 1 for tensorboard info only and 2 for additional stats every +# epoch. +# """ + +# def __init__(self, order=2, n_factors=2, loss_function='mse', +# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), +# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, +# batch_size=-1, n_epochs=100, log_dir=None, +# session_config=None, random_state=None, verbose=False, +# **kwargs): + +# input_type = 'sparse' + +# FMAlgo.__init__(self, order=order, n_factors=n_factors, +# input_type=input_type, loss_function=loss_function, +# optimizer=optimizer, reg_all=reg_all, +# use_diag=use_diag, reweight_reg=reweight_reg, +# init_std=init_std, batch_size=batch_size, +# n_epochs=n_epochs, log_dir=log_dir, +# session_config=session_config, +# random_state=random_state, verbose=verbose, **kwargs) + +# def fit(self, trainset): + +# FMAlgo.fit(self, trainset) +# self.fm(trainset) + +# return self + +# def fm(self, trainset): + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# # Construct sparse X and y +# row_ind = [] +# col_ind = [] +# data = [] +# y_train = np.empty(n_ratings, dtype=np.float32) +# max_value = self.trainset.rating_scale[1] + self.trainset.offset +# rating_counter = 0 +# for uid, iid, rating in self.trainset.all_ratings(): +# # Add user +# row_ind.append(rating_counter) +# col_ind.append(uid) +# data.append(1.) +# # Add item +# row_ind.append(rating_counter) +# col_ind.append(n_users + iid) +# data.append(1.) +# # Add explicit ratings +# sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) +# for iid_exp, rating_exp in self.trainset.ur[uid]: +# row_ind.append(rating_counter) +# col_ind.append(n_users + n_items + iid_exp) +# data.append(rating_exp / (max_value * sqrt_Iu)) +# # Add rating +# y_train[rating_counter] = rating +# rating_counter += 1 +# X_train = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(n_ratings, n_users + 2 * n_items), +# dtype=np.float32) + +# # `Dataset` and `Trainset` do not support sample_weight at the moment. +# self.model.fit(X_train, y_train, sample_weight=None, +# show_progress=self.show_progress) +# self.X_train = X_train +# self.y_train = y_train + +# def estimate(self, u, i, *_): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# data = [1., 1.] +# row_ind = [0, 0] +# col_ind = [u, n_users + i] +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# data = [1.] +# row_ind = [0] +# col_ind = [u] +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# data = [1.] +# row_ind = [0] +# col_ind = [n_users + i] +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# data = [] +# row_ind = [] +# col_ind = [] +# details['knows_user'] = False +# details['knows_item'] = False + +# if self.trainset.knows_user(u): +# sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) +# max_value = self.trainset.rating_scale[1] + self.trainset.offset +# for iid_exp, rating_exp in self.trainset.ur[u]: +# row_ind.append(0) +# col_ind.append(n_users + n_items + iid_exp) +# data.append(rating_exp / (max_value * sqrt_Iu)) + +# if data: +# X_test = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(1, n_users + 2 * n_items), +# dtype=np.float32) +# else: +# X_test = sps.csr_matrix((1, n_users + 2 * n_items), +# dtype=np.float32) + +# est = self.model.predict(X_test)[0] + +# return est, details + + +# class FMFeatures(FMAlgo): +# """A factorization machine algorithm that uses available features. + +# WARNING: Features should be pre-scaled to an absolute value less or equal +# to 1 if using a high value for `order`. + +# This code is an interface to the `tffm` library. + +# Args: +# order : int, default: 2 +# Order of corresponding polynomial model. +# All interaction from bias and linear to order will be included. +# n_factors : int, default: 2 +# Number of factors in low-rank appoximation. +# This value is shared across different orders of interaction. +# loss_function : str, default: 'mse' +# 'mse' is the only supported loss_function at the moment. +# optimizer : tf.train.Optimizer, +# default: AdamOptimizer(learning_rate=0.01) +# Optimization method used for training +# reg_all : float, default: 0 +# Strength of L2 regularization +# use_diag : bool, default: False +# Use diagonal elements of weights matrix or not. +# In the other words, should terms like x^2 be included. +# Often reffered as a "Polynomial Network". +# Default value (False) corresponds to FM. +# reweight_reg : bool, default: False +# Use frequency of features as weights for regularization or not. +# Should be useful for very sparse data and/or small batches +# init_std : float, default: 0.01 +# Amplitude of random initialization +# batch_size : int, default: -1 +# Number of samples in mini-batches. Shuffled every epoch. +# Use -1 for full gradient (whole training set in each batch). +# n_epoch : int, default: 100 +# Default number of epoches. +# It can be overrived by explicitly provided value in fit() method. +# log_dir : str or None, default: None +# Path for storing model stats during training. Used only if is not +# None. WARNING: If such directory already exists, it will be +# removed! You can use TensorBoard to visualize the stats: +# `tensorboard --logdir={log_dir}` +# session_config : tf.ConfigProto or None, default: None +# Additional setting passed to tf.Session object. +# Useful for CPU/GPU switching, setting number of threads and so on, +# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if +# enabled). +# random_state : int or None, default: None +# Random seed used at graph creating time +# verbose : int, default: False +# Level of verbosity. +# Set 1 for tensorboard info only and 2 for additional stats every +# epoch. +# """ + +# def __init__(self, order=2, n_factors=2, loss_function='mse', +# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), +# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, +# batch_size=-1, n_epochs=100, log_dir=None, +# session_config=None, random_state=None, verbose=False, +# **kwargs): + +# input_type = 'sparse' + +# FMAlgo.__init__(self, order=order, n_factors=n_factors, +# input_type=input_type, loss_function=loss_function, +# optimizer=optimizer, reg_all=reg_all, +# use_diag=use_diag, reweight_reg=reweight_reg, +# init_std=init_std, batch_size=batch_size, +# n_epochs=n_epochs, log_dir=log_dir, +# session_config=session_config, +# random_state=random_state, verbose=verbose, **kwargs) + +# def fit(self, trainset): + +# FMAlgo.fit(self, trainset) +# self.fm(trainset) + +# return self + +# def fm(self, trainset): + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items +# n_user_features = self.trainset.n_user_features +# n_item_features = self.trainset.n_item_features + +# # Construct sparse X and y +# row_ind = [] +# col_ind = [] +# data = [] +# y_train = np.empty(n_ratings, dtype=np.float32) +# rating_counter = 0 +# for uid, iid, rating in self.trainset.all_ratings(): +# # Add user +# row_ind.append(rating_counter) +# col_ind.append(uid) +# data.append(1.) +# # Add item +# row_ind.append(rating_counter) +# col_ind.append(n_users + iid) +# data.append(1.) +# # Add user features (if any) +# if n_user_features > 0: +# if self.trainset.has_user_features(uid): +# for n, value in enumerate(self.trainset.u_features[uid]): +# if value != 0: +# row_ind.append(rating_counter) +# col_ind.append(n_users + n_items + n) +# data.append(value) +# else: +# raise ValueError('No features for user ' + +# str(self.trainset.to_raw_uid(uid))) +# # Add item features (if any) +# if n_item_features > 0: +# if self.trainset.has_item_features(iid): +# for n, value in enumerate(self.trainset.i_features[iid]): +# if value != 0: +# row_ind.append(rating_counter) +# col_ind.append(n_users + n_items + +# n_user_features + n) +# data.append(value) +# else: +# raise ValueError('No features for item ' + +# str(self.trainset.to_raw_iid(iid))) +# # Add rating +# y_train[rating_counter] = rating +# rating_counter += 1 +# X_train = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(n_ratings, n_users + n_items + +# n_user_features + n_item_features), +# dtype=np.float32) + +# # `Dataset` and `Trainset` do not support sample_weight at the moment. +# self.model.fit(X_train, y_train, sample_weight=None, +# show_progress=self.show_progress) +# self.X_train = X_train +# self.y_train = y_train + +# def estimate(self, u, i, u_features, i_features): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items +# n_user_features = self.trainset.n_user_features +# n_item_features = self.trainset.n_item_features + +# if (len(u_features) != n_user_features or +# len(i_features) != n_item_features): +# raise PredictionImpossible( +# 'User and/or item features are missing.') + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# data = [1., 1.] +# row_ind = [0, 0] +# col_ind = [u, n_users + i] +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# data = [1.] +# row_ind = [0] +# col_ind = [u] +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# data = [1.] +# row_ind = [0] +# col_ind = [n_users + i] +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# data = [] +# row_ind = [] +# col_ind = [] +# details['knows_user'] = False +# details['knows_item'] = False + +# # Add user features (if any) +# if n_user_features > 0: +# for n, value in enumerate(u_features): +# if value != 0: +# row_ind.append(0) +# col_ind.append(n_users + n_items + n) +# data.append(value) + +# # Add item features (if any) +# if n_item_features > 0: +# for n, value in enumerate(i_features): +# if value != 0: +# row_ind.append(0) +# col_ind.append(n_users + n_items + +# n_user_features + n) +# data.append(value) + +# if data: +# X_test = sps.csr_matrix((data, (row_ind, col_ind)), +# shape=(1, n_users + n_items + +# n_user_features + n_item_features), +# dtype=np.float32) +# else: +# X_test = sps.csr_matrix((1, n_users + n_items + n_user_features + +# n_item_features), +# dtype=np.float32) + +# est = self.model.predict(X_test)[0] + +# return est, details + + +# class FMBasicPL(AlgoBase): +# """A basic factorization machine algorithm. With order 2, this algorithm +# is equivalent to the biased SVD algorithm. + +# This code is an interface to the `polylearn` library. + +# Args: +# degree : int, default: 2 +# Degree of the polynomial. Corresponds to the order of feature +# interactions captured by the model. Currently only supports +# degrees up to 3. +# n_factors : int, default: 2 +# Number of basis vectors to learn, a.k.a. the dimension of the +# low-rank parametrization. +# reg_alpha : float, default: 1 +# Regularization amount for linear term (if ``fit_linear=True``). +# reg_beta : float, default: 1 +# Regularization amount for higher-order weights. +# tol : float, default: 1e-6 +# Tolerance for the stopping condition. +# fit_lower : {'explicit'|'augment'|None}, default: 'explicit' +# Whether and how to fit lower-order, non-homogeneous terms. +# - 'explicit': fits a separate P directly for each lower order. +# - 'augment': adds the required number of dummy columns (columns +# that are 1 everywhere) in order to capture lower-order terms. +# Adds ``degree - 2`` columns if ``fit_linear`` is true, or +# ``degree - 1`` columns otherwise, to account for the linear +# term. +# - None: only learns weights for the degree given. If +# ``degree == 3``, for example, the model will only have weights +# for third-order feature interactions. +# fit_linear : {True|False}, default: True +# Whether to fit an explicit linear term to the model, using +# coordinate descent. If False, the model can still capture linear +# effects if ``fit_lower == 'augment'``. +# warm_start : boolean, optional, default: False +# Whether to use the existing solution, if available. Useful for +# computing regularization paths or pre-initializing the model. +# init_lambdas : {'ones'|'random_signs'}, default: 'ones' +# How to initialize the predictive weights of each learned basis. The +# lambdas are not trained; using alternate signs can theoretically +# improve performance if the kernel degree is even. The default +# value of 'ones' matches the original formulation of factorization +# machines (Rendle, 2010). +# To use custom values for the lambdas, ``warm_start`` may be used. +# max_iter : int, optional, default: 10000 +# Maximum number of passes over the dataset to perform. +# random_state : int seed, RandomState instance, or None (default) +# The seed of the pseudo random number generator to use for +# initializing the parameters. +# verbose : boolean, optional, default: False +# Whether to print debugging information. +# """ + +# def __init__(self, degree=2, n_factors=2, reg_alpha=1., reg_beta=1., +# tol=1e-6, fit_lower='explicit', fit_linear=True, +# warm_start=False, init_lambdas='ones', max_iter=10000, +# random_state=None, verbose=False, **kwargs): + +# self.degree = degree +# self.n_factors = n_factors # n_components in `polylearn` +# self.reg_alpha = reg_alpha # alpha in `polylearn` +# self.reg_beta = reg_beta # beta in `polylearn` +# self.tol = tol +# self.fit_lower = fit_lower +# self.fit_linear = fit_linear +# self.warm_start = warm_start +# self.init_lambdas = init_lambdas +# self.max_iter = max_iter +# self.random_state = random_state +# self.verbose = verbose + +# self.model = FactorizationMachineRegressor( +# degree=self.degree, n_components=self.n_factors, +# alpha=self.reg_alpha, beta=self.reg_beta, tol=self.tol, +# fit_lower=self.fit_lower, fit_linear=self.fit_linear, +# warm_start=self.warm_start, init_lambdas=self.init_lambdas, +# max_iter=self.max_iter, random_state=self.random_state, +# verbose=self.verbose) + +# def fit(self, trainset): + +# AlgoBase.fit(self, trainset) + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# # Construct sparse X and y +# row_ind = np.empty(2 * n_ratings, dtype=int) +# col_ind = np.empty(2 * n_ratings, dtype=int) +# data = np.ones(2 * n_ratings, dtype=bool) +# y_train = np.empty(n_ratings, dtype=float) +# nonzero_counter = 0 +# rating_counter = 0 +# for uid, iid, rating in self.trainset.all_ratings(): +# # Add user +# row_ind[nonzero_counter] = rating_counter +# col_ind[nonzero_counter] = uid +# nonzero_counter += 1 +# # Add item +# row_ind[nonzero_counter] = rating_counter +# col_ind[nonzero_counter] = n_users + iid +# nonzero_counter += 1 +# # Add rating +# y_train[rating_counter] = rating +# rating_counter += 1 +# X_train = sps.csc_matrix((data, (row_ind, col_ind)), +# shape=(n_ratings, n_users + n_items), +# dtype=bool) + +# self.model.fit(X_train, y_train) +# self.X_train = X_train +# self.y_train = y_train + +# return self + +# def estimate(self, u, i, *_): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# X_test = sps.csc_matrix(([1., 1.], ([0, 0], [u, n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# X_test = sps.csc_matrix(([1.], ([0], [u])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# X_test = sps.csc_matrix(([1.], ([0], [n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# X_test = sps.csc_matrix((1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = False + +# est = self.model.predict(X_test)[0] + +# return est, details From 011105c3ce48362ea696cdfdc51ac9669ccdafee Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 29 May 2019 13:13:02 -0400 Subject: [PATCH 55/60] Implemented FM with Pytorch --- .../factorization_machines.py | 714 ++++++++++++++---- 1 file changed, 559 insertions(+), 155 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 04d84d8b..52096281 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -9,172 +9,220 @@ import pandas as pd # import tensorflow as tf import scipy.sparse as sps -import xlearn as xl +# import xlearn as xl # from tffm import TFFMRegressor # from polylearn import FactorizationMachineRegressor +import torch +from torch import nn from .predictions import PredictionImpossible from .algo_base import AlgoBase -class FM(AlgoBase): - """A factorization machine algorithm. +class FMtorchNN(nn.Module): + """ The PyTorch model for factorization machine. This class is used by + `FM`. The initilization is done as in Rendle (2012). + + Args: + n_features: int + Defines the number of features in x. + n_factors: int, default: 20 + Defines the number of factors in the interaction terms. + init_std: float, default: 0.01 + The standard deviation of the normal distribution for + initialization. + """ + + def __init__(self, n_features, n_factors=20, init_std=0.01): + super(FMtorchNN, self).__init__() + self.n_features = n_features + self.n_factors = n_factors + self.init_std = init_std + + # Initialize bias term + self.b = nn.Parameter(torch.Tensor(1), + requires_grad=True) + self.b.data.fill_(0.) + # self.b.data.normal_(init_mean, init_std) + # self.b.data.uniform_(-0.01, 0.01) + + # Initialize linear terms + self.w = nn.Parameter(torch.Tensor(self.n_features, 1), + requires_grad=True) + self.w.data.fill_(0.) + # self.w.data.normal_(init_mean, init_std) + # self.w.data.uniform_(-0.01, 0.01) + + # Initialize interaction terms + self.V = nn.Parameter(torch.Tensor(self.n_features, self.n_factors), + requires_grad=True) + self.V.data.normal_(0., self.init_std) + # self.V.data.uniform_(-0.01, 0.01) - This code is an interface to the `xlearn` library. + def forward(self, x): + + # The linear part + total_linear = torch.sum(torch.mm(x, self.w), dim=1) + + # The interaction part + # O(kn) formulation from Steffen Rendle + total_inter_1 = torch.mm(x, self.V) ** 2 + total_inter_2 = torch.mm(x ** 2, self.V ** 2) + total_inter = 0.5 * torch.sum(total_inter_1 - total_inter_2, dim=1) + + # Compute predictions + y_pred = self.b + total_linear + total_inter + + return y_pred + + +class FM(AlgoBase): + """A factorization machine algorithm implemented using pytorch. Args: - rating_lst (list of str or `None`): This list specifies what - information from the `raw_ratings` to put in the `x` vector. - Accepted list values are 'userID', 'itemID', 'imp_u_rating' and - 'exp_u_rating'. Implicit and explicit user rating values are scaled - by the number of values. If `None`, no info is added. - user_lst (list of str or `None`): This list specifies what - information from the `user_features` to put in the `x` vector. - Accepted list values consist of the names of features. If `None`, - no info is added. - item_lst (list of str or `None`): This list specifies what - information from the `item_features` to put in the `x` vector. - Accepted list values consist of the names of features. If `None`, - no info is added. - task : str, default: 'reg' - 'binary' or 'reg'. - metric : str, default: 'rmse' - 'acc', 'prec', 'recall', 'f1', 'auc' for classification. - 'mae', 'mape', 'rmse', 'rmsd' for regression. - lr : float, default: 0.2 - Learning rate for optimization method. If you choose 'adagrad' - method, the learning rate will be changed adaptively. - reg : float, default: 0.00002 + rating_lst : list of str or `None`, default : ['userID', 'itemID'] + This list specifies what information from the `raw_ratings` to put + in the `x` vector. Accepted list values are 'userID', 'itemID', + 'imp_u_rating' and 'exp_u_rating'. Implicit and explicit user + rating values are scaled by the number of values. If `None`, no + info is added. + user_lst : list of str or `None`, default : `None` + This list specifies what information from the `user_features` to + put in the `x` vector. Accepted list values consist of the names of + features. If `None`, no info is added. + item_lst : list of str or `None`, default : `None` + This list specifies what information from the `item_features` to + put in the `x` vector. Accepted list values consist of the names of + features. If `None`, no info is added. + n_factors : int, default: 20 + Number of latent factors in low-rank appoximation. + n_epochs : int, default : 30 + Number of epochs. + init_std: float, default : 0.01 + The standard deviation of the normal distribution for + initialization. + lr : float, default: 0.001 + Learning rate for optimization method. + reg : float, default: 0.02 Strength of L2 regularization. It can be disabled by setting it to zero. - k : int, default: 4 - Number of latent factors in low-rank appoximation. - init : float, default: 0.66 - Used to initialize model. - alpha : float, default: 0.3 - Hyper parameter for 'ftrl'. - beta : float, default: 1.0 - Hyper parameter for 'ftrl'. - lambda_1 : float, default : 0.00001 - Hyper parameter for 'ftrl'. - lambda_2 : float, default : 0.00002 - Hyper parameter for 'ftrl'. - nthread : int, default : 1 - Number of CPU cores. - epoch : int, default : 10 - Number of epochs. This value could be changed in early-stop. - opt : str, default : 'adagrad' - 'sgd', 'adagrad' and 'ftrl' are accepted values for the - optimization method. - stop_window : int, default : 2 - Size of the stop window for early-stopping. - use_bin : bool, default : True - Generate bin file for training and testing. - use_norm : bool, default : True - Use instance-wise normalization. - use_lock_free : bool, default : True - Use lock-free training. This does not allow reproducible results. - use_early_stop : bool, default : True - Use early stopping. - random_state : int, default: 1 - Random seed used to shuffle data set. + random_state : int, default: `None` + Determines the RNG that will be used for initialization. If + int, ``random_state`` will be used as a seed for a new RNG. This is + useful to get the same initialization over multiple calls to + ``fit()``. If ``None``, the current RNG from torch is used. verbose : int, default: False Level of verbosity. - modeltxtpath : 'str', default: 'model.txt' - Path and filename of model in text format. - modelpath : 'str', default: 'model.out' - Path and filename of model in binary format. """ def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, - item_lst=None, task='reg', metric='rmse', lr=0.2, reg=0.00002, - k=4, init=0.66, alpha=0.3, beta=1.0, lambda_1=0.00001, - lambda_2=0.00002, nthread=1, epoch=10, opt='adagrad', - stop_window=2, use_bin=True, use_norm=True, - use_lock_free=True, use_early_stop=True, - random_state=1, verbose=False, modeltxtpath='model.txt', - modelpath='model.out', **kwargs): + item_lst=None, n_factors=20, n_epochs=30, init_std=0.01, + lr=0.001, reg=0.02, random_state=None, verbose=False, + **kwargs): AlgoBase.__init__(self, **kwargs) self.rating_lst = rating_lst self.user_lst = user_lst self.item_lst = item_lst - self.task = task - self.metric = metric + self.n_factors = n_factors + self.n_epochs = n_epochs + self.init_std = init_std self.lr = lr self.reg = reg - self.k = k - self.init = init - self.alpha = alpha - self.beta = beta - self.lambda_1 = lambda_1 - self.lambda_2 = lambda_2 - self.nthread = nthread - self.epoch = epoch - self.opt = opt - self.stop_window = stop_window - self.use_bin = use_bin - self.use_norm = use_norm - self.use_lock_free = use_lock_free - self.use_early_stop = use_early_stop self.random_state = random_state self.verbose = verbose - self.modeltxtpath = modeltxtpath - self.modelpath = modelpath - - self.model = xl.create_fm() - self.param = {'task': self.task, - 'metric': self.metric, - 'lr': self.lr, - 'lambda': self.reg, - 'k': self.k, - 'init': self.init, - 'alpha': self.alpha, - 'beta': self.beta, - 'lambda_1': self.lambda_1, - 'lambda_2': self.lambda_2, - 'nthread': self.nthread, - 'epoch': self.epoch, - 'opt': self.opt, - 'stop_window': self.stop_window} - if not self.use_bin: - self.model.setNoBin() - if not self.use_norm: - self.model.disableNorm() - if not self.use_lock_free: - self.model.disableLockFree() - if not self.use_early_stop: - self.model.disableEarlyStop() - if not verbose: - self.model.setQuiet() + + torch.set_default_dtype(torch.float64) # use float64 def fit(self, trainset): AlgoBase.fit(self, trainset) - self._construct_libsvm() - # Fit without validation data set - xdm_train = xl.DMatrix( - self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'], - self.libsvm_df.loc[:, 'rating']) - self.model.setTrain(xdm_train) - self.model.setTXTModel(self.modeltxtpath) - self.model.fit(self.param, self.modelpath) + # Construct data and initialize model + # Initialization needs to be done in fit() since it depends on the + # trainset + if self.random_state: + # random.seed(random_state) # just in case + np.random.seed(self.random_state) # just in case + torch.manual_seed(self.random_state) + self._construct_FM_data() + self.model = FMtorchNN(self.n_features, self.n_factors, self.init_std) + params = FM._add_weight_decay(self.model, self.reg) + self.optimizer = torch.optim.Adam(params, lr=self.lr) + + # Define training data and sample_weights (TODO) + x_train = torch.Tensor( + self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'].values) + y_train = torch.Tensor( + self.libsvm_df.loc[:, 'rating'].values) + sample_weights = None + + # Define development data (TODO) + x_dev = torch.Tensor(x_train.shape) + y_dev = torch.Tensor(y_train.shape) + + for epoch in range(self.n_epochs): + # Switch to training mode, clear gradient accumulators + self.model.train() + self.optimizer.zero_grad() + # Forward pass + y_pred = self.model(x_train) + # Compute loss + self.train_loss = self._compute_loss( + y_pred, y_train, sample_weights) + # Backward pass and update weights + self.train_loss.backward() + self.optimizer.step() + + # Switch to eval mode and evaluate with development data (TODO) + # Also keep best model in memory (TODO) + # See https://github.com/pytorch/examples/blob/master/snli/train.py + self.model.eval() + y_pred = self.model(x_dev) + # Do we need sample_weights? (TODO) + self.dev_loss = self._compute_loss(y_pred, y_dev) + + if self.verbose: + print(epoch, self.train_loss.item(), self.dev_loss.item()) + + return self - # Fit with validation data set (TODO) + def _add_weight_decay(model, reg, skip_list=[]): + """ Add weight_decay with no regularization for bias. + """ - # Load text model - # This is used to define `self.bias`, `self.linear_coefs` and - # `self.inter_coefs` - self._load_txt_model() + decay, no_decay = [], [] + for name, param in model.named_parameters(): + if not param.requires_grad: + continue # frozen weights + if ((len(param.shape) == 1) or name.endswith(".bias") or + (name in skip_list)): + no_decay.append(param) + else: + decay.append(param) + + return [{'params': no_decay, 'weight_decay': 0.}, + {'params': decay, 'weight_decay': reg}] + + def _compute_loss(self, y_pred, y, sample_weights=None): + """ Computes a different loss depending on whether `sample_weights` are + defined. + """ - # Delete model files (TODO) + if sample_weights is not None: + criterion = nn.MSELoss(reduction='none') + loss = criterion(y_pred, y) + loss = torch.dot(sample_weights, loss) / y.shape[0] + else: + criterion = nn.MSELoss() + loss = criterion(y_pred, y) - return self + return loss - def _construct_libsvm(self): - """ Outputs the data in a libsvm format. This format is used by FM - algorithms. It is assumed that these features are correctly encoded. + def _construct_FM_data(self): + """ Construct the data needed by `FM`. + + It is assumed that the user and item features are correctly encoded. These dummies are created (if needed) using only the info in the trainset. """ @@ -269,50 +317,406 @@ def _construct_libsvm(self): '{} is not part of item_features'.format(feature)) self.libsvm_df = libsvm_df - self.libsvm_feature_nb = self.libsvm_df.shape[1] - 1 - - def _load_txt_model(self): + self.n_features = libsvm_df.shape[1] - 1 - temp = pd.read_csv(self.modeltxtpath, sep=':', header=None) - coefs = temp.loc[:, 1].tolist() - coefs = " ".join(coefs) - coefs = np.array(list(map(float, coefs.split()))) + def estimate(self, u, i, u_features, i_features): - self.bias = coefs[0] - self.linear_coefs = coefs[1:self.libsvm_feature_nb + 1] - self.inter_coefs = coefs[self.libsvm_feature_nb + 1:].reshape( - self.libsvm_feature_nb, self.k) - - def estimate(self, u, i, *_): - - n_users = self.trainset.n_users - n_items = self.trainset.n_items + # Estimate rating + x = self._construct_estimate_input(u, i, u_features, i_features) + x = torch.Tensor(x[None, :]) # add dimension + est = float(self.model(x)) + # Construct details details = {} if self.trainset.knows_user(u) and self.trainset.knows_item(i): - X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), - shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = True details['knows_item'] = True elif self.trainset.knows_user(u): - X_test = sps.csr_matrix(([1.], ([0], [u])), - shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = True details['knows_item'] = False elif self.trainset.knows_item(i): - X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), - shape=(1, n_users + n_items), dtype=bool) details['knows_user'] = False details['knows_item'] = True else: - X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) details['knows_user'] = False details['knows_item'] = False - est = self.model.predict(X_test)[0] - return est, details + def _construct_estimate_input(self, u, i, u_features, i_features): + """ Construct the input for the model. + + It is assumed that if features are given in u_features or i_features, + they are all given and in the same order as in the trainset. + """ + + n_users = self.trainset.n_users + n_items = self.trainset.n_items + + x = [] + + # Add rating features + if self.rating_lst: + for feature in self.rating_lst: + if feature == 'userID': + temp = [0.] * n_users + if self.trainset.knows_user(u): + temp[u] = 1. + x.extend(temp) + elif feature == 'itemID': + temp = [0.] * n_items + if self.trainset.knows_item(i): + temp[i] = 1. + x.extend(temp) + elif feature == 'imp_u_rating': + temp = [0.] * n_items + if self.trainset.knows_user(u): + all_u_ratings = self.trainset.ur[u] + for other_i, rating in all_u_ratings: + if other_i != i: # only the other ratings + temp[other_i] = 1. + temp = np.array(temp) + count = np.count_nonzero(temp) + if count == 0: + count = 1 + temp = list(temp / count) + x.extend(temp) + elif feature == 'exp_u_rating': + # a rating is at least 1 with the offset + temp = [0.] * n_items + if self.trainset.knows_user(u): + all_u_ratings = self.trainset.ur[u] + for other_i, rating in all_u_ratings: + if other_i != i: # only the other ratings + temp[other_i] = rating + temp = np.array(temp) + count = np.count_nonzero(temp) + if count == 0: + count = 1 + temp = list(temp / count) + x.extend(temp) + + # Add user features + if self.user_lst: + temp = [0.] * len(self.user_lst) + if u_features: + # It is assumed that if features are given, they are all given. + temp_df = pd.Series( + u_features, index=self.trainset.user_features_labels) + for idx, feature in enumerate(self.user_lst): + temp[idx] = temp_df[feature] + x.extend(temp) + + # Add item features + if self.item_lst: + temp = [0.] * len(self.item_lst) + if u_features: + # It is assumed that if features are given, they are all given. + temp_df = pd.Series( + i_features, index=self.trainset.item_features_labels) + for idx, feature in enumerate(self.item_lst): + temp[idx] = temp_df[feature] + x.extend(temp) + + return np.array(x) + + +# class FM(AlgoBase): +# """A factorization machine algorithm. + +# This code is an interface to the `xlearn` library. + +# Args: +# rating_lst (list of str or `None`): This list specifies what +# information from the `raw_ratings` to put in the `x` vector. +# Accepted list values are 'userID', 'itemID', 'imp_u_rating' and +# 'exp_u_rating'. Implicit and explicit user rating values are scaled +# by the number of values. If `None`, no info is added. +# user_lst (list of str or `None`): This list specifies what +# information from the `user_features` to put in the `x` vector. +# Accepted list values consist of the names of features. If `None`, +# no info is added. +# item_lst (list of str or `None`): This list specifies what +# information from the `item_features` to put in the `x` vector. +# Accepted list values consist of the names of features. If `None`, +# no info is added. +# task : str, default: 'reg' +# 'binary' or 'reg'. +# metric : str, default: 'rmse' +# 'acc', 'prec', 'recall', 'f1', 'auc' for classification. +# 'mae', 'mape', 'rmse', 'rmsd' for regression. +# lr : float, default: 0.2 +# Learning rate for optimization method. If you choose 'adagrad' +# method, the learning rate will be changed adaptively. +# reg : float, default: 0.00002 +# Strength of L2 regularization. It can be disabled by setting it to +# zero. +# k : int, default: 4 +# Number of latent factors in low-rank appoximation. +# init : float, default: 0.66 +# Used to initialize model. +# alpha : float, default: 0.3 +# Hyper parameter for 'ftrl'. +# beta : float, default: 1.0 +# Hyper parameter for 'ftrl'. +# lambda_1 : float, default : 0.00001 +# Hyper parameter for 'ftrl'. +# lambda_2 : float, default : 0.00002 +# Hyper parameter for 'ftrl'. +# nthread : int, default : 1 +# Number of CPU cores. +# epoch : int, default : 10 +# Number of epochs. This value could be changed in early-stop. +# opt : str, default : 'adagrad' +# 'sgd', 'adagrad' and 'ftrl' are accepted values for the +# optimization method. +# stop_window : int, default : 2 +# Size of the stop window for early-stopping. +# use_bin : bool, default : True +# Generate bin file for training and testing. +# use_norm : bool, default : True +# Use instance-wise normalization. +# use_lock_free : bool, default : True +# Use lock-free training. This does not allow reproducible results. +# use_early_stop : bool, default : True +# Use early stopping. +# random_state : int, default: 1 +# Random seed used to shuffle data set. +# verbose : int, default: False +# Level of verbosity. +# modeltxtpath : 'str', default: 'model.txt' +# Path and filename of model in text format. +# modelpath : 'str', default: 'model.out' +# Path and filename of model in binary format. +# """ + +# def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, +# item_lst=None, task='reg', metric='rmse', lr=0.2, reg=0.00002, +# k=4, init=0.66, alpha=0.3, beta=1.0, lambda_1=0.00001, +# lambda_2=0.00002, nthread=1, epoch=10, opt='adagrad', +# stop_window=2, use_bin=True, use_norm=True, +# use_lock_free=True, use_early_stop=True, +# random_state=1, verbose=False, modeltxtpath='model.txt', +# modelpath='model.out', **kwargs): + +# AlgoBase.__init__(self, **kwargs) +# self.rating_lst = rating_lst +# self.user_lst = user_lst +# self.item_lst = item_lst +# self.task = task +# self.metric = metric +# self.lr = lr +# self.reg = reg +# self.k = k +# self.init = init +# self.alpha = alpha +# self.beta = beta +# self.lambda_1 = lambda_1 +# self.lambda_2 = lambda_2 +# self.nthread = nthread +# self.epoch = epoch +# self.opt = opt +# self.stop_window = stop_window +# self.use_bin = use_bin +# self.use_norm = use_norm +# self.use_lock_free = use_lock_free +# self.use_early_stop = use_early_stop +# self.random_state = random_state +# self.verbose = verbose +# self.modeltxtpath = modeltxtpath +# self.modelpath = modelpath + +# self.model = xl.create_fm() +# self.param = {'task': self.task, +# 'metric': self.metric, +# 'lr': self.lr, +# 'lambda': self.reg, +# 'k': self.k, +# 'init': self.init, +# 'alpha': self.alpha, +# 'beta': self.beta, +# 'lambda_1': self.lambda_1, +# 'lambda_2': self.lambda_2, +# 'nthread': self.nthread, +# 'epoch': self.epoch, +# 'opt': self.opt, +# 'stop_window': self.stop_window} +# if not self.use_bin: +# self.model.setNoBin() +# if not self.use_norm: +# self.model.disableNorm() +# if not self.use_lock_free: +# self.model.disableLockFree() +# if not self.use_early_stop: +# self.model.disableEarlyStop() +# if not verbose: +# self.model.setQuiet() + +# def fit(self, trainset): + +# AlgoBase.fit(self, trainset) +# self._construct_libsvm() + +# # Fit without validation data set +# xdm_train = xl.DMatrix( +# self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'], +# self.libsvm_df.loc[:, 'rating']) +# self.model.setTrain(xdm_train) +# self.model.setTXTModel(self.modeltxtpath) +# self.model.fit(self.param, self.modelpath) + +# # Fit with validation data set (TODO) + +# # Load text model +# # This is used to define `self.bias`, `self.linear_coefs` and +# # `self.inter_coefs` +# self._load_txt_model() + +# # Delete model files (TODO) + +# return self + +# def _construct_libsvm(self): +# """ Outputs the data in a libsvm format. This format is used by FM +# algorithms. It is assumed that these features are correctly encoded. +# These dummies are created (if needed) using only the info in the +# trainset. +# """ + +# if self.user_lst and (self.trainset.n_user_features == 0): +# raise ValueError('user_lst cannot be used since ' +# 'there are no user_features') +# if self.item_lst and (self.trainset.n_item_features == 0): +# raise ValueError('item_lst cannot be used since ' +# 'there are no item_features') + +# n_ratings = self.trainset.n_ratings +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# # Construct ratings_df from trainset +# # The IDs are unique and start at 0 +# ratings_df = pd.DataFrame([tup for tup in self.trainset.all_ratings()], +# columns=['userID', 'itemID', 'rating']) + +# # Initialize df with rating values +# libsvm_df = pd.DataFrame(ratings_df['rating']) + +# # Add rating features +# if self.rating_lst: +# for feature in self.rating_lst: +# if feature == 'userID': +# libsvm_df = pd.concat([libsvm_df, pd.get_dummies( +# ratings_df['userID'], prefix='userID')], axis=1) +# elif feature == 'itemID': +# libsvm_df = pd.concat([libsvm_df, pd.get_dummies( +# ratings_df['itemID'], prefix='itemID')], axis=1) +# elif feature == 'imp_u_rating': +# temp = np.zeros((n_ratings, n_items)) +# for row in ratings_df.itertuples(): +# iid = row.itemID +# all_u_ratings = self.trainset.ur[row.userID] +# for other_iid, rating in all_u_ratings: +# if other_iid != iid: # only the other ratings +# temp[row.Index, other_iid] = 1 +# count = np.count_nonzero(temp, axis=1)[:, None] +# count[count == 0] = 1 # remove zeros for division +# temp = temp / count +# cols = ['imp_u_rating_{}'.format(i) +# for i in range(n_items)] +# libsvm_df = pd.concat([libsvm_df, pd.DataFrame( +# temp, columns=cols)], axis=1) +# elif feature == 'exp_u_rating': +# # a rating is at least 1 with the offset +# temp = np.zeros((n_ratings, n_items)) +# for row in ratings_df.itertuples(): +# iid = row.itemID +# all_u_ratings = self.trainset.ur[row.userID] +# for other_iid, rating in all_u_ratings: +# if other_iid != iid: # only the other ratings +# temp[row.Index, other_iid] = rating +# count = np.count_nonzero(temp, axis=1)[:, None] +# count[count == 0] = 1 # remove zeros for division +# temp = temp / count +# cols = ['exp_u_rating_{}'.format(i) +# for i in range(n_items)] +# libsvm_df = pd.concat([libsvm_df, pd.DataFrame( +# temp, columns=cols)], axis=1) +# else: +# raise ValueError('{} is not an accepted value ' +# 'for rating_lst'.format(feature)) + +# # Add user features +# if self.user_lst: +# temp = pd.DataFrame( +# [self.trainset.u_features[uid] +# for uid in ratings_df['userID']], +# columns=self.trainset.user_features_labels) +# for feature in self.user_lst: +# if feature in self.trainset.user_features_labels: +# libsvm_df[feature] = temp[feature] +# else: +# raise ValueError( +# '{} is not part of user_features'.format(feature)) + +# # Add item features +# if self.item_lst: +# temp = pd.DataFrame( +# [self.trainset.i_features[iid] +# for iid in ratings_df['itemID']], +# columns=self.trainset.item_features_labels) +# for feature in self.item_lst: +# if feature in self.trainset.item_features_labels: +# libsvm_df[feature] = temp[feature] +# else: +# raise ValueError( +# '{} is not part of item_features'.format(feature)) + +# self.libsvm_df = libsvm_df +# self.libsvm_feature_nb = self.libsvm_df.shape[1] - 1 + +# def _load_txt_model(self): + +# temp = pd.read_csv(self.modeltxtpath, sep=':', header=None) +# coefs = temp.loc[:, 1].tolist() +# coefs = " ".join(coefs) +# coefs = np.array(list(map(float, coefs.split()))) + +# self.bias = coefs[0] +# self.linear_coefs = coefs[1:self.libsvm_feature_nb + 1] +# self.inter_coefs = coefs[self.libsvm_feature_nb + 1:].reshape( +# self.libsvm_feature_nb, self.k) + +# def estimate(self, u, i, *_): + +# n_users = self.trainset.n_users +# n_items = self.trainset.n_items + +# details = {} +# if self.trainset.knows_user(u) and self.trainset.knows_item(i): +# X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = True +# elif self.trainset.knows_user(u): +# X_test = sps.csr_matrix(([1.], ([0], [u])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = True +# details['knows_item'] = False +# elif self.trainset.knows_item(i): +# X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), +# shape=(1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = True +# else: +# X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) +# details['knows_user'] = False +# details['knows_item'] = False + +# est = self.model.predict(X_test)[0] + +# return est, details + # class FMAlgo(AlgoBase): # """This is an abstract class aimed to reduce code redundancy for From c808cdec1742dadfb587c7eeb986c9f0483145de Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 29 May 2019 13:15:07 -0400 Subject: [PATCH 56/60] Removed FM deprecated implementations --- surprise/__init__.py | 7 - surprise/prediction_algorithms/__init__.py | 7 - .../factorization_machines.py | 1206 ----------------- 3 files changed, 1220 deletions(-) diff --git a/surprise/__init__.py b/surprise/__init__.py index 5c8326c8..33d958a3 100644 --- a/surprise/__init__.py +++ b/surprise/__init__.py @@ -14,11 +14,6 @@ from .prediction_algorithms import CoClustering from .prediction_algorithms import Lasso from .prediction_algorithms import FM -# from .prediction_algorithms import FMBasic -# from .prediction_algorithms import FMImplicit -# from .prediction_algorithms import FMExplicit -# from .prediction_algorithms import FMFeatures -# from .prediction_algorithms import FMBasicPL from .prediction_algorithms import PredictionImpossible from .prediction_algorithms import Prediction @@ -39,7 +34,5 @@ 'Reader', 'Trainset', 'evaluate', 'print_perf', 'GridSearch', 'dump', 'KNNWithZScore', 'get_dataset_dir', 'model_selection', 'Lasso', 'FM'] - # 'FMBasic', 'FMImplicit', 'FMExplicit', 'FMFeatures'] - # 'FMBasicPL' __version__ = get_distribution('scikit-surprise').version diff --git a/surprise/prediction_algorithms/__init__.py b/surprise/prediction_algorithms/__init__.py index 5c8d4bdd..5005c581 100644 --- a/surprise/prediction_algorithms/__init__.py +++ b/surprise/prediction_algorithms/__init__.py @@ -34,11 +34,6 @@ from .co_clustering import CoClustering from .linear import Lasso from .factorization_machines import FM -# from .factorization_machines import FMBasic -# from .factorization_machines import FMImplicit -# from .factorization_machines import FMExplicit -# from .factorization_machines import FMFeatures -# from .factorization_machines import FMBasicPL from .predictions import PredictionImpossible from .predictions import Prediction @@ -47,5 +42,3 @@ 'KNNBaseline', 'KNNWithMeans', 'SVD', 'SVDpp', 'NMF', 'SlopeOne', 'CoClustering', 'PredictionImpossible', 'Prediction', 'KNNWithZScore', 'Lasso', 'FM'] - # 'FMBasic', 'FMImplicit', 'FMExplicit', - # 'FMFeatures'] #, 'FMBasicPL'] diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 52096281..5f3fb21f 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -7,11 +7,6 @@ import numpy as np import pandas as pd -# import tensorflow as tf -import scipy.sparse as sps -# import xlearn as xl -# from tffm import TFFMRegressor -# from polylearn import FactorizationMachineRegressor import torch from torch import nn @@ -419,1204 +414,3 @@ def _construct_estimate_input(self, u, i, u_features, i_features): x.extend(temp) return np.array(x) - - -# class FM(AlgoBase): -# """A factorization machine algorithm. - -# This code is an interface to the `xlearn` library. - -# Args: -# rating_lst (list of str or `None`): This list specifies what -# information from the `raw_ratings` to put in the `x` vector. -# Accepted list values are 'userID', 'itemID', 'imp_u_rating' and -# 'exp_u_rating'. Implicit and explicit user rating values are scaled -# by the number of values. If `None`, no info is added. -# user_lst (list of str or `None`): This list specifies what -# information from the `user_features` to put in the `x` vector. -# Accepted list values consist of the names of features. If `None`, -# no info is added. -# item_lst (list of str or `None`): This list specifies what -# information from the `item_features` to put in the `x` vector. -# Accepted list values consist of the names of features. If `None`, -# no info is added. -# task : str, default: 'reg' -# 'binary' or 'reg'. -# metric : str, default: 'rmse' -# 'acc', 'prec', 'recall', 'f1', 'auc' for classification. -# 'mae', 'mape', 'rmse', 'rmsd' for regression. -# lr : float, default: 0.2 -# Learning rate for optimization method. If you choose 'adagrad' -# method, the learning rate will be changed adaptively. -# reg : float, default: 0.00002 -# Strength of L2 regularization. It can be disabled by setting it to -# zero. -# k : int, default: 4 -# Number of latent factors in low-rank appoximation. -# init : float, default: 0.66 -# Used to initialize model. -# alpha : float, default: 0.3 -# Hyper parameter for 'ftrl'. -# beta : float, default: 1.0 -# Hyper parameter for 'ftrl'. -# lambda_1 : float, default : 0.00001 -# Hyper parameter for 'ftrl'. -# lambda_2 : float, default : 0.00002 -# Hyper parameter for 'ftrl'. -# nthread : int, default : 1 -# Number of CPU cores. -# epoch : int, default : 10 -# Number of epochs. This value could be changed in early-stop. -# opt : str, default : 'adagrad' -# 'sgd', 'adagrad' and 'ftrl' are accepted values for the -# optimization method. -# stop_window : int, default : 2 -# Size of the stop window for early-stopping. -# use_bin : bool, default : True -# Generate bin file for training and testing. -# use_norm : bool, default : True -# Use instance-wise normalization. -# use_lock_free : bool, default : True -# Use lock-free training. This does not allow reproducible results. -# use_early_stop : bool, default : True -# Use early stopping. -# random_state : int, default: 1 -# Random seed used to shuffle data set. -# verbose : int, default: False -# Level of verbosity. -# modeltxtpath : 'str', default: 'model.txt' -# Path and filename of model in text format. -# modelpath : 'str', default: 'model.out' -# Path and filename of model in binary format. -# """ - -# def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, -# item_lst=None, task='reg', metric='rmse', lr=0.2, reg=0.00002, -# k=4, init=0.66, alpha=0.3, beta=1.0, lambda_1=0.00001, -# lambda_2=0.00002, nthread=1, epoch=10, opt='adagrad', -# stop_window=2, use_bin=True, use_norm=True, -# use_lock_free=True, use_early_stop=True, -# random_state=1, verbose=False, modeltxtpath='model.txt', -# modelpath='model.out', **kwargs): - -# AlgoBase.__init__(self, **kwargs) -# self.rating_lst = rating_lst -# self.user_lst = user_lst -# self.item_lst = item_lst -# self.task = task -# self.metric = metric -# self.lr = lr -# self.reg = reg -# self.k = k -# self.init = init -# self.alpha = alpha -# self.beta = beta -# self.lambda_1 = lambda_1 -# self.lambda_2 = lambda_2 -# self.nthread = nthread -# self.epoch = epoch -# self.opt = opt -# self.stop_window = stop_window -# self.use_bin = use_bin -# self.use_norm = use_norm -# self.use_lock_free = use_lock_free -# self.use_early_stop = use_early_stop -# self.random_state = random_state -# self.verbose = verbose -# self.modeltxtpath = modeltxtpath -# self.modelpath = modelpath - -# self.model = xl.create_fm() -# self.param = {'task': self.task, -# 'metric': self.metric, -# 'lr': self.lr, -# 'lambda': self.reg, -# 'k': self.k, -# 'init': self.init, -# 'alpha': self.alpha, -# 'beta': self.beta, -# 'lambda_1': self.lambda_1, -# 'lambda_2': self.lambda_2, -# 'nthread': self.nthread, -# 'epoch': self.epoch, -# 'opt': self.opt, -# 'stop_window': self.stop_window} -# if not self.use_bin: -# self.model.setNoBin() -# if not self.use_norm: -# self.model.disableNorm() -# if not self.use_lock_free: -# self.model.disableLockFree() -# if not self.use_early_stop: -# self.model.disableEarlyStop() -# if not verbose: -# self.model.setQuiet() - -# def fit(self, trainset): - -# AlgoBase.fit(self, trainset) -# self._construct_libsvm() - -# # Fit without validation data set -# xdm_train = xl.DMatrix( -# self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'], -# self.libsvm_df.loc[:, 'rating']) -# self.model.setTrain(xdm_train) -# self.model.setTXTModel(self.modeltxtpath) -# self.model.fit(self.param, self.modelpath) - -# # Fit with validation data set (TODO) - -# # Load text model -# # This is used to define `self.bias`, `self.linear_coefs` and -# # `self.inter_coefs` -# self._load_txt_model() - -# # Delete model files (TODO) - -# return self - -# def _construct_libsvm(self): -# """ Outputs the data in a libsvm format. This format is used by FM -# algorithms. It is assumed that these features are correctly encoded. -# These dummies are created (if needed) using only the info in the -# trainset. -# """ - -# if self.user_lst and (self.trainset.n_user_features == 0): -# raise ValueError('user_lst cannot be used since ' -# 'there are no user_features') -# if self.item_lst and (self.trainset.n_item_features == 0): -# raise ValueError('item_lst cannot be used since ' -# 'there are no item_features') - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# # Construct ratings_df from trainset -# # The IDs are unique and start at 0 -# ratings_df = pd.DataFrame([tup for tup in self.trainset.all_ratings()], -# columns=['userID', 'itemID', 'rating']) - -# # Initialize df with rating values -# libsvm_df = pd.DataFrame(ratings_df['rating']) - -# # Add rating features -# if self.rating_lst: -# for feature in self.rating_lst: -# if feature == 'userID': -# libsvm_df = pd.concat([libsvm_df, pd.get_dummies( -# ratings_df['userID'], prefix='userID')], axis=1) -# elif feature == 'itemID': -# libsvm_df = pd.concat([libsvm_df, pd.get_dummies( -# ratings_df['itemID'], prefix='itemID')], axis=1) -# elif feature == 'imp_u_rating': -# temp = np.zeros((n_ratings, n_items)) -# for row in ratings_df.itertuples(): -# iid = row.itemID -# all_u_ratings = self.trainset.ur[row.userID] -# for other_iid, rating in all_u_ratings: -# if other_iid != iid: # only the other ratings -# temp[row.Index, other_iid] = 1 -# count = np.count_nonzero(temp, axis=1)[:, None] -# count[count == 0] = 1 # remove zeros for division -# temp = temp / count -# cols = ['imp_u_rating_{}'.format(i) -# for i in range(n_items)] -# libsvm_df = pd.concat([libsvm_df, pd.DataFrame( -# temp, columns=cols)], axis=1) -# elif feature == 'exp_u_rating': -# # a rating is at least 1 with the offset -# temp = np.zeros((n_ratings, n_items)) -# for row in ratings_df.itertuples(): -# iid = row.itemID -# all_u_ratings = self.trainset.ur[row.userID] -# for other_iid, rating in all_u_ratings: -# if other_iid != iid: # only the other ratings -# temp[row.Index, other_iid] = rating -# count = np.count_nonzero(temp, axis=1)[:, None] -# count[count == 0] = 1 # remove zeros for division -# temp = temp / count -# cols = ['exp_u_rating_{}'.format(i) -# for i in range(n_items)] -# libsvm_df = pd.concat([libsvm_df, pd.DataFrame( -# temp, columns=cols)], axis=1) -# else: -# raise ValueError('{} is not an accepted value ' -# 'for rating_lst'.format(feature)) - -# # Add user features -# if self.user_lst: -# temp = pd.DataFrame( -# [self.trainset.u_features[uid] -# for uid in ratings_df['userID']], -# columns=self.trainset.user_features_labels) -# for feature in self.user_lst: -# if feature in self.trainset.user_features_labels: -# libsvm_df[feature] = temp[feature] -# else: -# raise ValueError( -# '{} is not part of user_features'.format(feature)) - -# # Add item features -# if self.item_lst: -# temp = pd.DataFrame( -# [self.trainset.i_features[iid] -# for iid in ratings_df['itemID']], -# columns=self.trainset.item_features_labels) -# for feature in self.item_lst: -# if feature in self.trainset.item_features_labels: -# libsvm_df[feature] = temp[feature] -# else: -# raise ValueError( -# '{} is not part of item_features'.format(feature)) - -# self.libsvm_df = libsvm_df -# self.libsvm_feature_nb = self.libsvm_df.shape[1] - 1 - -# def _load_txt_model(self): - -# temp = pd.read_csv(self.modeltxtpath, sep=':', header=None) -# coefs = temp.loc[:, 1].tolist() -# coefs = " ".join(coefs) -# coefs = np.array(list(map(float, coefs.split()))) - -# self.bias = coefs[0] -# self.linear_coefs = coefs[1:self.libsvm_feature_nb + 1] -# self.inter_coefs = coefs[self.libsvm_feature_nb + 1:].reshape( -# self.libsvm_feature_nb, self.k) - -# def estimate(self, u, i, *_): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# X_test = sps.csr_matrix(([1.], ([0], [u])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = False - -# est = self.model.predict(X_test)[0] - -# return est, details - - -# class FMAlgo(AlgoBase): -# """This is an abstract class aimed to reduce code redundancy for -# factoration machines. -# """ - -# def __init__(self, order=2, n_factors=2, input_type='dense', -# loss_function='mse', -# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), -# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, -# batch_size=-1, n_epochs=100, log_dir=None, -# session_config=None, random_state=None, verbose=False, -# **kwargs): - -# AlgoBase.__init__(self, **kwargs) -# self.order = order -# self.n_factors = n_factors # rank in `tffm` -# self.input_type = input_type -# self.loss_function = loss_function # {'mse', 'loss_logistic'} -# # https://www.tensorflow.org/api_guides/python/train#Optimizers -# self.optimizer = optimizer -# self.reg_all = reg_all # reg in `tffm` -# self.use_diag = use_diag -# self.reweight_reg = reweight_reg -# self.init_std = np.float32(init_std) -# self.batch_size = batch_size -# self.n_epochs = n_epochs -# self.log_dir = log_dir -# self.session_config = session_config -# self.random_state = random_state # seed in `tffm` -# self.verbose = verbose - -# if self.loss_function == 'mse': -# self.model = TFFMRegressor( -# order=self.order, rank=self.n_factors, -# input_type=self.input_type, optimizer=self.optimizer, -# reg=self.reg_all, use_diag=self.use_diag, -# reweight_reg=self.reweight_reg, init_std=self.init_std, -# batch_size=self.batch_size, n_epochs=self.n_epochs, -# log_dir=self.log_dir, session_config=self.session_config, -# seed=self.random_state, verbose=self.verbose) -# elif self.loss_function == 'loss_logistic': -# # See issue #157 of Surprise -# raise ValueError('loss_logistic is not supported at the moment') -# else: -# raise ValueError(('Unknown value {} for parameter' -# 'loss_function').format(self.loss_function)) - -# def fit(self, trainset): - -# AlgoBase.fit(self, trainset) - -# if self.verbose > 0: -# self.show_progress = True -# else: -# self.show_progress = False - -# return self - - -# class FMBasic(FMAlgo): -# """A basic factorization machine algorithm. With order 2, this algorithm -# is equivalent to the biased SVD algorithm. - -# This code is an interface to the `tffm` library. - -# Args: -# order : int, default: 2 -# Order of corresponding polynomial model. -# All interaction from bias and linear to order will be included. -# n_factors : int, default: 2 -# Number of factors in low-rank appoximation. -# This value is shared across different orders of interaction. -# loss_function : str, default: 'mse' -# 'mse' is the only supported loss_function at the moment. -# optimizer : tf.train.Optimizer, -# default: AdamOptimizer(learning_rate=0.01) -# Optimization method used for training -# reg_all : float, default: 0 -# Strength of L2 regularization -# use_diag : bool, default: False -# Use diagonal elements of weights matrix or not. -# In the other words, should terms like x^2 be included. -# Often reffered as a "Polynomial Network". -# Default value (False) corresponds to FM. -# reweight_reg : bool, default: False -# Use frequency of features as weights for regularization or not. -# Should be useful for very sparse data and/or small batches -# init_std : float, default: 0.01 -# Amplitude of random initialization -# batch_size : int, default: -1 -# Number of samples in mini-batches. Shuffled every epoch. -# Use -1 for full gradient (whole training set in each batch). -# n_epoch : int, default: 100 -# Default number of epoches. -# It can be overrived by explicitly provided value in fit() method. -# log_dir : str or None, default: None -# Path for storing model stats during training. Used only if is not -# None. WARNING: If such directory already exists, it will be -# removed! You can use TensorBoard to visualize the stats: -# `tensorboard --logdir={log_dir}` -# session_config : tf.ConfigProto or None, default: None -# Additional setting passed to tf.Session object. -# Useful for CPU/GPU switching, setting number of threads and so on, -# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if -# enabled). -# random_state : int or None, default: None -# Random seed used at graph creating time -# verbose : int, default: False -# Level of verbosity. -# Set 1 for tensorboard info only and 2 for additional stats every -# epoch. -# """ - -# def __init__(self, order=2, n_factors=2, loss_function='mse', -# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), -# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, -# batch_size=-1, n_epochs=100, log_dir=None, -# session_config=None, random_state=None, verbose=False, -# **kwargs): - -# input_type = 'sparse' - -# FMAlgo.__init__(self, order=order, n_factors=n_factors, -# input_type=input_type, loss_function=loss_function, -# optimizer=optimizer, reg_all=reg_all, -# use_diag=use_diag, reweight_reg=reweight_reg, -# init_std=init_std, batch_size=batch_size, -# n_epochs=n_epochs, log_dir=log_dir, -# session_config=session_config, -# random_state=random_state, verbose=verbose, **kwargs) - -# def fit(self, trainset): - -# FMAlgo.fit(self, trainset) -# self.fm(trainset) - -# return self - -# def fm(self, trainset): - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# # Construct sparse X and y -# row_ind = np.empty(2 * n_ratings, dtype=int) -# col_ind = np.empty(2 * n_ratings, dtype=int) -# data = np.ones(2 * n_ratings, dtype=bool) -# y_train = np.empty(n_ratings, dtype=float) -# nonzero_counter = 0 -# rating_counter = 0 -# for uid, iid, rating in self.trainset.all_ratings(): -# # Add user -# row_ind[nonzero_counter] = rating_counter -# col_ind[nonzero_counter] = uid -# nonzero_counter += 1 -# # Add item -# row_ind[nonzero_counter] = rating_counter -# col_ind[nonzero_counter] = n_users + iid -# nonzero_counter += 1 -# # Add rating -# y_train[rating_counter] = rating -# rating_counter += 1 -# X_train = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(n_ratings, n_users + n_items), -# dtype=bool) - -# # `Dataset` and `Trainset` do not support sample_weight at the moment. -# self.model.fit(X_train, y_train, sample_weight=None, -# show_progress=self.show_progress) -# self.X_train = X_train -# self.y_train = y_train - -# def estimate(self, u, i, *_): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# X_test = sps.csr_matrix(([1., 1.], ([0, 0], [u, n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# X_test = sps.csr_matrix(([1.], ([0], [u])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# X_test = sps.csr_matrix(([1.], ([0], [n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# X_test = sps.csr_matrix((1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = False - -# est = self.model.predict(X_test)[0] - -# return est, details - - -# class FMImplicit(FMAlgo): -# """A factorization machine algorithm that uses implicit ratings. With order -# 2, this algorithm correspond to an extension of the biased SVD++ algorithm. - -# This code is an interface to the `tffm` library. - -# Args: -# order : int, default: 2 -# Order of corresponding polynomial model. -# All interaction from bias and linear to order will be included. -# n_factors : int, default: 2 -# Number of factors in low-rank appoximation. -# This value is shared across different orders of interaction. -# loss_function : str, default: 'mse' -# 'mse' is the only supported loss_function at the moment. -# optimizer : tf.train.Optimizer, -# default: AdamOptimizer(learning_rate=0.01) -# Optimization method used for training -# reg_all : float, default: 0 -# Strength of L2 regularization -# use_diag : bool, default: False -# Use diagonal elements of weights matrix or not. -# In the other words, should terms like x^2 be included. -# Often reffered as a "Polynomial Network". -# Default value (False) corresponds to FM. -# reweight_reg : bool, default: False -# Use frequency of features as weights for regularization or not. -# Should be useful for very sparse data and/or small batches -# init_std : float, default: 0.01 -# Amplitude of random initialization -# batch_size : int, default: -1 -# Number of samples in mini-batches. Shuffled every epoch. -# Use -1 for full gradient (whole training set in each batch). -# n_epoch : int, default: 100 -# Default number of epoches. -# It can be overrived by explicitly provided value in fit() method. -# log_dir : str or None, default: None -# Path for storing model stats during training. Used only if is not -# None. WARNING: If such directory already exists, it will be -# removed! You can use TensorBoard to visualize the stats: -# `tensorboard --logdir={log_dir}` -# session_config : tf.ConfigProto or None, default: None -# Additional setting passed to tf.Session object. -# Useful for CPU/GPU switching, setting number of threads and so on, -# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if -# enabled). -# random_state : int or None, default: None -# Random seed used at graph creating time -# verbose : int, default: False -# Level of verbosity. -# Set 1 for tensorboard info only and 2 for additional stats every -# epoch. -# """ - -# def __init__(self, order=2, n_factors=2, loss_function='mse', -# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), -# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, -# batch_size=-1, n_epochs=100, log_dir=None, -# session_config=None, random_state=None, verbose=False, -# **kwargs): - -# input_type = 'sparse' - -# FMAlgo.__init__(self, order=order, n_factors=n_factors, -# input_type=input_type, loss_function=loss_function, -# optimizer=optimizer, reg_all=reg_all, -# use_diag=use_diag, reweight_reg=reweight_reg, -# init_std=init_std, batch_size=batch_size, -# n_epochs=n_epochs, log_dir=log_dir, -# session_config=session_config, -# random_state=random_state, verbose=verbose, **kwargs) - -# def fit(self, trainset): - -# FMAlgo.fit(self, trainset) -# self.fm(trainset) - -# return self - -# def fm(self, trainset): - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# # Construct sparse X and y -# row_ind = [] -# col_ind = [] -# data = [] -# y_train = np.empty(n_ratings, dtype=np.float32) -# rating_counter = 0 -# for uid, iid, rating in self.trainset.all_ratings(): -# # Add user -# row_ind.append(rating_counter) -# col_ind.append(uid) -# data.append(1.) -# # Add item -# row_ind.append(rating_counter) -# col_ind.append(n_users + iid) -# data.append(1.) -# # Add implicit ratings -# sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) -# for iid_imp, _ in self.trainset.ur[uid]: -# row_ind.append(rating_counter) -# col_ind.append(n_users + n_items + iid_imp) -# data.append(1 / sqrt_Iu) -# # Add rating -# y_train[rating_counter] = rating -# rating_counter += 1 -# X_train = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(n_ratings, n_users + 2 * n_items), -# dtype=np.float32) - -# # `Dataset` and `Trainset` do not support sample_weight at the moment. -# self.model.fit(X_train, y_train, sample_weight=None, -# show_progress=self.show_progress) -# self.X_train = X_train -# self.y_train = y_train - -# def estimate(self, u, i, *_): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# data = [1., 1.] -# row_ind = [0, 0] -# col_ind = [u, n_users + i] -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# data = [1.] -# row_ind = [0] -# col_ind = [u] -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# data = [1.] -# row_ind = [0] -# col_ind = [n_users + i] -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# data = [] -# row_ind = [] -# col_ind = [] -# details['knows_user'] = False -# details['knows_item'] = False - -# if self.trainset.knows_user(u): -# sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) -# for iid_imp, _ in self.trainset.ur[u]: -# row_ind.append(0) -# col_ind.append(n_users + n_items + iid_imp) -# data.append(1 / sqrt_Iu) - -# if data: -# X_test = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(1, n_users + 2 * n_items), -# dtype=np.float32) -# else: -# X_test = sps.csr_matrix((1, n_users + 2 * n_items), -# dtype=np.float32) - -# est = self.model.predict(X_test)[0] - -# return est, details - - -# class FMExplicit(FMAlgo): -# """A factorization machine algorithm that uses explicit ratings. - -# This code is an interface to the `tffm` library. - -# Args: -# order : int, default: 2 -# Order of corresponding polynomial model. -# All interaction from bias and linear to order will be included. -# n_factors : int, default: 2 -# Number of factors in low-rank appoximation. -# This value is shared across different orders of interaction. -# loss_function : str, default: 'mse' -# 'mse' is the only supported loss_function at the moment. -# optimizer : tf.train.Optimizer, -# default: AdamOptimizer(learning_rate=0.01) -# Optimization method used for training -# reg_all : float, default: 0 -# Strength of L2 regularization -# use_diag : bool, default: False -# Use diagonal elements of weights matrix or not. -# In the other words, should terms like x^2 be included. -# Often reffered as a "Polynomial Network". -# Default value (False) corresponds to FM. -# reweight_reg : bool, default: False -# Use frequency of features as weights for regularization or not. -# Should be useful for very sparse data and/or small batches -# init_std : float, default: 0.01 -# Amplitude of random initialization -# batch_size : int, default: -1 -# Number of samples in mini-batches. Shuffled every epoch. -# Use -1 for full gradient (whole training set in each batch). -# n_epoch : int, default: 100 -# Default number of epoches. -# It can be overrived by explicitly provided value in fit() method. -# log_dir : str or None, default: None -# Path for storing model stats during training. Used only if is not -# None. WARNING: If such directory already exists, it will be -# removed! You can use TensorBoard to visualize the stats: -# `tensorboard --logdir={log_dir}` -# session_config : tf.ConfigProto or None, default: None -# Additional setting passed to tf.Session object. -# Useful for CPU/GPU switching, setting number of threads and so on, -# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if -# enabled). -# random_state : int or None, default: None -# Random seed used at graph creating time -# verbose : int, default: False -# Level of verbosity. -# Set 1 for tensorboard info only and 2 for additional stats every -# epoch. -# """ - -# def __init__(self, order=2, n_factors=2, loss_function='mse', -# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), -# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, -# batch_size=-1, n_epochs=100, log_dir=None, -# session_config=None, random_state=None, verbose=False, -# **kwargs): - -# input_type = 'sparse' - -# FMAlgo.__init__(self, order=order, n_factors=n_factors, -# input_type=input_type, loss_function=loss_function, -# optimizer=optimizer, reg_all=reg_all, -# use_diag=use_diag, reweight_reg=reweight_reg, -# init_std=init_std, batch_size=batch_size, -# n_epochs=n_epochs, log_dir=log_dir, -# session_config=session_config, -# random_state=random_state, verbose=verbose, **kwargs) - -# def fit(self, trainset): - -# FMAlgo.fit(self, trainset) -# self.fm(trainset) - -# return self - -# def fm(self, trainset): - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# # Construct sparse X and y -# row_ind = [] -# col_ind = [] -# data = [] -# y_train = np.empty(n_ratings, dtype=np.float32) -# max_value = self.trainset.rating_scale[1] + self.trainset.offset -# rating_counter = 0 -# for uid, iid, rating in self.trainset.all_ratings(): -# # Add user -# row_ind.append(rating_counter) -# col_ind.append(uid) -# data.append(1.) -# # Add item -# row_ind.append(rating_counter) -# col_ind.append(n_users + iid) -# data.append(1.) -# # Add explicit ratings -# sqrt_Iu = np.sqrt(len(self.trainset.ur[uid])) -# for iid_exp, rating_exp in self.trainset.ur[uid]: -# row_ind.append(rating_counter) -# col_ind.append(n_users + n_items + iid_exp) -# data.append(rating_exp / (max_value * sqrt_Iu)) -# # Add rating -# y_train[rating_counter] = rating -# rating_counter += 1 -# X_train = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(n_ratings, n_users + 2 * n_items), -# dtype=np.float32) - -# # `Dataset` and `Trainset` do not support sample_weight at the moment. -# self.model.fit(X_train, y_train, sample_weight=None, -# show_progress=self.show_progress) -# self.X_train = X_train -# self.y_train = y_train - -# def estimate(self, u, i, *_): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# data = [1., 1.] -# row_ind = [0, 0] -# col_ind = [u, n_users + i] -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# data = [1.] -# row_ind = [0] -# col_ind = [u] -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# data = [1.] -# row_ind = [0] -# col_ind = [n_users + i] -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# data = [] -# row_ind = [] -# col_ind = [] -# details['knows_user'] = False -# details['knows_item'] = False - -# if self.trainset.knows_user(u): -# sqrt_Iu = np.sqrt(len(self.trainset.ur[u])) -# max_value = self.trainset.rating_scale[1] + self.trainset.offset -# for iid_exp, rating_exp in self.trainset.ur[u]: -# row_ind.append(0) -# col_ind.append(n_users + n_items + iid_exp) -# data.append(rating_exp / (max_value * sqrt_Iu)) - -# if data: -# X_test = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(1, n_users + 2 * n_items), -# dtype=np.float32) -# else: -# X_test = sps.csr_matrix((1, n_users + 2 * n_items), -# dtype=np.float32) - -# est = self.model.predict(X_test)[0] - -# return est, details - - -# class FMFeatures(FMAlgo): -# """A factorization machine algorithm that uses available features. - -# WARNING: Features should be pre-scaled to an absolute value less or equal -# to 1 if using a high value for `order`. - -# This code is an interface to the `tffm` library. - -# Args: -# order : int, default: 2 -# Order of corresponding polynomial model. -# All interaction from bias and linear to order will be included. -# n_factors : int, default: 2 -# Number of factors in low-rank appoximation. -# This value is shared across different orders of interaction. -# loss_function : str, default: 'mse' -# 'mse' is the only supported loss_function at the moment. -# optimizer : tf.train.Optimizer, -# default: AdamOptimizer(learning_rate=0.01) -# Optimization method used for training -# reg_all : float, default: 0 -# Strength of L2 regularization -# use_diag : bool, default: False -# Use diagonal elements of weights matrix or not. -# In the other words, should terms like x^2 be included. -# Often reffered as a "Polynomial Network". -# Default value (False) corresponds to FM. -# reweight_reg : bool, default: False -# Use frequency of features as weights for regularization or not. -# Should be useful for very sparse data and/or small batches -# init_std : float, default: 0.01 -# Amplitude of random initialization -# batch_size : int, default: -1 -# Number of samples in mini-batches. Shuffled every epoch. -# Use -1 for full gradient (whole training set in each batch). -# n_epoch : int, default: 100 -# Default number of epoches. -# It can be overrived by explicitly provided value in fit() method. -# log_dir : str or None, default: None -# Path for storing model stats during training. Used only if is not -# None. WARNING: If such directory already exists, it will be -# removed! You can use TensorBoard to visualize the stats: -# `tensorboard --logdir={log_dir}` -# session_config : tf.ConfigProto or None, default: None -# Additional setting passed to tf.Session object. -# Useful for CPU/GPU switching, setting number of threads and so on, -# `tf.ConfigProto(device_count={'GPU': 0})` will disable GPU (if -# enabled). -# random_state : int or None, default: None -# Random seed used at graph creating time -# verbose : int, default: False -# Level of verbosity. -# Set 1 for tensorboard info only and 2 for additional stats every -# epoch. -# """ - -# def __init__(self, order=2, n_factors=2, loss_function='mse', -# optimizer=tf.train.AdamOptimizer(learning_rate=0.01), -# reg_all=0, use_diag=False, reweight_reg=False, init_std=0.01, -# batch_size=-1, n_epochs=100, log_dir=None, -# session_config=None, random_state=None, verbose=False, -# **kwargs): - -# input_type = 'sparse' - -# FMAlgo.__init__(self, order=order, n_factors=n_factors, -# input_type=input_type, loss_function=loss_function, -# optimizer=optimizer, reg_all=reg_all, -# use_diag=use_diag, reweight_reg=reweight_reg, -# init_std=init_std, batch_size=batch_size, -# n_epochs=n_epochs, log_dir=log_dir, -# session_config=session_config, -# random_state=random_state, verbose=verbose, **kwargs) - -# def fit(self, trainset): - -# FMAlgo.fit(self, trainset) -# self.fm(trainset) - -# return self - -# def fm(self, trainset): - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items -# n_user_features = self.trainset.n_user_features -# n_item_features = self.trainset.n_item_features - -# # Construct sparse X and y -# row_ind = [] -# col_ind = [] -# data = [] -# y_train = np.empty(n_ratings, dtype=np.float32) -# rating_counter = 0 -# for uid, iid, rating in self.trainset.all_ratings(): -# # Add user -# row_ind.append(rating_counter) -# col_ind.append(uid) -# data.append(1.) -# # Add item -# row_ind.append(rating_counter) -# col_ind.append(n_users + iid) -# data.append(1.) -# # Add user features (if any) -# if n_user_features > 0: -# if self.trainset.has_user_features(uid): -# for n, value in enumerate(self.trainset.u_features[uid]): -# if value != 0: -# row_ind.append(rating_counter) -# col_ind.append(n_users + n_items + n) -# data.append(value) -# else: -# raise ValueError('No features for user ' + -# str(self.trainset.to_raw_uid(uid))) -# # Add item features (if any) -# if n_item_features > 0: -# if self.trainset.has_item_features(iid): -# for n, value in enumerate(self.trainset.i_features[iid]): -# if value != 0: -# row_ind.append(rating_counter) -# col_ind.append(n_users + n_items + -# n_user_features + n) -# data.append(value) -# else: -# raise ValueError('No features for item ' + -# str(self.trainset.to_raw_iid(iid))) -# # Add rating -# y_train[rating_counter] = rating -# rating_counter += 1 -# X_train = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(n_ratings, n_users + n_items + -# n_user_features + n_item_features), -# dtype=np.float32) - -# # `Dataset` and `Trainset` do not support sample_weight at the moment. -# self.model.fit(X_train, y_train, sample_weight=None, -# show_progress=self.show_progress) -# self.X_train = X_train -# self.y_train = y_train - -# def estimate(self, u, i, u_features, i_features): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items -# n_user_features = self.trainset.n_user_features -# n_item_features = self.trainset.n_item_features - -# if (len(u_features) != n_user_features or -# len(i_features) != n_item_features): -# raise PredictionImpossible( -# 'User and/or item features are missing.') - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# data = [1., 1.] -# row_ind = [0, 0] -# col_ind = [u, n_users + i] -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# data = [1.] -# row_ind = [0] -# col_ind = [u] -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# data = [1.] -# row_ind = [0] -# col_ind = [n_users + i] -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# data = [] -# row_ind = [] -# col_ind = [] -# details['knows_user'] = False -# details['knows_item'] = False - -# # Add user features (if any) -# if n_user_features > 0: -# for n, value in enumerate(u_features): -# if value != 0: -# row_ind.append(0) -# col_ind.append(n_users + n_items + n) -# data.append(value) - -# # Add item features (if any) -# if n_item_features > 0: -# for n, value in enumerate(i_features): -# if value != 0: -# row_ind.append(0) -# col_ind.append(n_users + n_items + -# n_user_features + n) -# data.append(value) - -# if data: -# X_test = sps.csr_matrix((data, (row_ind, col_ind)), -# shape=(1, n_users + n_items + -# n_user_features + n_item_features), -# dtype=np.float32) -# else: -# X_test = sps.csr_matrix((1, n_users + n_items + n_user_features + -# n_item_features), -# dtype=np.float32) - -# est = self.model.predict(X_test)[0] - -# return est, details - - -# class FMBasicPL(AlgoBase): -# """A basic factorization machine algorithm. With order 2, this algorithm -# is equivalent to the biased SVD algorithm. - -# This code is an interface to the `polylearn` library. - -# Args: -# degree : int, default: 2 -# Degree of the polynomial. Corresponds to the order of feature -# interactions captured by the model. Currently only supports -# degrees up to 3. -# n_factors : int, default: 2 -# Number of basis vectors to learn, a.k.a. the dimension of the -# low-rank parametrization. -# reg_alpha : float, default: 1 -# Regularization amount for linear term (if ``fit_linear=True``). -# reg_beta : float, default: 1 -# Regularization amount for higher-order weights. -# tol : float, default: 1e-6 -# Tolerance for the stopping condition. -# fit_lower : {'explicit'|'augment'|None}, default: 'explicit' -# Whether and how to fit lower-order, non-homogeneous terms. -# - 'explicit': fits a separate P directly for each lower order. -# - 'augment': adds the required number of dummy columns (columns -# that are 1 everywhere) in order to capture lower-order terms. -# Adds ``degree - 2`` columns if ``fit_linear`` is true, or -# ``degree - 1`` columns otherwise, to account for the linear -# term. -# - None: only learns weights for the degree given. If -# ``degree == 3``, for example, the model will only have weights -# for third-order feature interactions. -# fit_linear : {True|False}, default: True -# Whether to fit an explicit linear term to the model, using -# coordinate descent. If False, the model can still capture linear -# effects if ``fit_lower == 'augment'``. -# warm_start : boolean, optional, default: False -# Whether to use the existing solution, if available. Useful for -# computing regularization paths or pre-initializing the model. -# init_lambdas : {'ones'|'random_signs'}, default: 'ones' -# How to initialize the predictive weights of each learned basis. The -# lambdas are not trained; using alternate signs can theoretically -# improve performance if the kernel degree is even. The default -# value of 'ones' matches the original formulation of factorization -# machines (Rendle, 2010). -# To use custom values for the lambdas, ``warm_start`` may be used. -# max_iter : int, optional, default: 10000 -# Maximum number of passes over the dataset to perform. -# random_state : int seed, RandomState instance, or None (default) -# The seed of the pseudo random number generator to use for -# initializing the parameters. -# verbose : boolean, optional, default: False -# Whether to print debugging information. -# """ - -# def __init__(self, degree=2, n_factors=2, reg_alpha=1., reg_beta=1., -# tol=1e-6, fit_lower='explicit', fit_linear=True, -# warm_start=False, init_lambdas='ones', max_iter=10000, -# random_state=None, verbose=False, **kwargs): - -# self.degree = degree -# self.n_factors = n_factors # n_components in `polylearn` -# self.reg_alpha = reg_alpha # alpha in `polylearn` -# self.reg_beta = reg_beta # beta in `polylearn` -# self.tol = tol -# self.fit_lower = fit_lower -# self.fit_linear = fit_linear -# self.warm_start = warm_start -# self.init_lambdas = init_lambdas -# self.max_iter = max_iter -# self.random_state = random_state -# self.verbose = verbose - -# self.model = FactorizationMachineRegressor( -# degree=self.degree, n_components=self.n_factors, -# alpha=self.reg_alpha, beta=self.reg_beta, tol=self.tol, -# fit_lower=self.fit_lower, fit_linear=self.fit_linear, -# warm_start=self.warm_start, init_lambdas=self.init_lambdas, -# max_iter=self.max_iter, random_state=self.random_state, -# verbose=self.verbose) - -# def fit(self, trainset): - -# AlgoBase.fit(self, trainset) - -# n_ratings = self.trainset.n_ratings -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# # Construct sparse X and y -# row_ind = np.empty(2 * n_ratings, dtype=int) -# col_ind = np.empty(2 * n_ratings, dtype=int) -# data = np.ones(2 * n_ratings, dtype=bool) -# y_train = np.empty(n_ratings, dtype=float) -# nonzero_counter = 0 -# rating_counter = 0 -# for uid, iid, rating in self.trainset.all_ratings(): -# # Add user -# row_ind[nonzero_counter] = rating_counter -# col_ind[nonzero_counter] = uid -# nonzero_counter += 1 -# # Add item -# row_ind[nonzero_counter] = rating_counter -# col_ind[nonzero_counter] = n_users + iid -# nonzero_counter += 1 -# # Add rating -# y_train[rating_counter] = rating -# rating_counter += 1 -# X_train = sps.csc_matrix((data, (row_ind, col_ind)), -# shape=(n_ratings, n_users + n_items), -# dtype=bool) - -# self.model.fit(X_train, y_train) -# self.X_train = X_train -# self.y_train = y_train - -# return self - -# def estimate(self, u, i, *_): - -# n_users = self.trainset.n_users -# n_items = self.trainset.n_items - -# details = {} -# if self.trainset.knows_user(u) and self.trainset.knows_item(i): -# X_test = sps.csc_matrix(([1., 1.], ([0, 0], [u, n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = True -# elif self.trainset.knows_user(u): -# X_test = sps.csc_matrix(([1.], ([0], [u])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = True -# details['knows_item'] = False -# elif self.trainset.knows_item(i): -# X_test = sps.csc_matrix(([1.], ([0], [n_users + i])), -# shape=(1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = True -# else: -# X_test = sps.csc_matrix((1, n_users + n_items), dtype=bool) -# details['knows_user'] = False -# details['knows_item'] = False - -# est = self.model.predict(X_test)[0] - -# return est, details From efe2b859f54ccee02d1541636c4eab6ed0701c99 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 29 May 2019 14:22:45 -0400 Subject: [PATCH 57/60] Added dev set functionnality to FM --- .../factorization_machines.py | 67 +++++++++++++------ 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 5f3fb21f..76de7946 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -4,9 +4,11 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +import copy import numpy as np import pandas as pd +from sklearn.model_selection import train_test_split import torch from torch import nn @@ -92,7 +94,12 @@ class FM(AlgoBase): n_factors : int, default: 20 Number of latent factors in low-rank appoximation. n_epochs : int, default : 30 - Number of epochs. + Number of epochs. All epochs are ran but only the best model out of + all epochs is kept. + dev_ratio : float, default : 0.3 + Ratio of `trainset` to dedicate to development data set to identify + best model. Should be either positive and smaller than the number + of samples or a float in the (0, 1) range. init_std: float, default : 0.01 The standard deviation of the normal distribution for initialization. @@ -111,9 +118,9 @@ class FM(AlgoBase): """ def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, - item_lst=None, n_factors=20, n_epochs=30, init_std=0.01, - lr=0.001, reg=0.02, random_state=None, verbose=False, - **kwargs): + item_lst=None, n_factors=20, n_epochs=30, dev_ratio=0.3, + init_std=0.01, lr=0.001, reg=0.02, random_state=None, + verbose=False, **kwargs): AlgoBase.__init__(self, **kwargs) self.rating_lst = rating_lst @@ -121,6 +128,7 @@ def __init__(self, rating_lst=['userID', 'itemID'], user_lst=None, self.item_lst = item_lst self.n_factors = n_factors self.n_epochs = n_epochs + self.dev_ratio = dev_ratio self.init_std = init_std self.lr = lr self.reg = reg @@ -137,25 +145,35 @@ def fit(self, trainset): # Initialization needs to be done in fit() since it depends on the # trainset if self.random_state: - # random.seed(random_state) # just in case - np.random.seed(self.random_state) # just in case + np.random.seed(self.random_state) torch.manual_seed(self.random_state) self._construct_FM_data() self.model = FMtorchNN(self.n_features, self.n_factors, self.init_std) params = FM._add_weight_decay(self.model, self.reg) self.optimizer = torch.optim.Adam(params, lr=self.lr) - # Define training data and sample_weights (TODO) - x_train = torch.Tensor( - self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'].values) - y_train = torch.Tensor( - self.libsvm_df.loc[:, 'rating'].values) + # Define data (TODO : sample_weights) + x = self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'].values + y = self.libsvm_df.loc[:, 'rating'].values sample_weights = None - - # Define development data (TODO) - x_dev = torch.Tensor(x_train.shape) - y_dev = torch.Tensor(y_train.shape) - + if sample_weights: + x_train, x_dev, y_train, y_dev, w_train, w_dev = train_test_split( + x, y, sample_weights, test_size=self.dev_ratio, + random_state=self.random_state) + w_train = torch.Tensor(w_train) + w_dev = torch.Tensor(w_dev) + else: + x_train, x_dev, y_train, y_dev = train_test_split( + x, y, test_size=self.dev_ratio, random_state=self.random_state) + w_train = None + w_dev = None + x_train = torch.Tensor(x_train) + y_train = torch.Tensor(y_train) + x_dev = torch.Tensor(x_dev) + y_dev = torch.Tensor(y_dev) + + best_loss = np.inf + best_model = None for epoch in range(self.n_epochs): # Switch to training mode, clear gradient accumulators self.model.train() @@ -163,23 +181,28 @@ def fit(self, trainset): # Forward pass y_pred = self.model(x_train) # Compute loss - self.train_loss = self._compute_loss( - y_pred, y_train, sample_weights) + self.train_loss = self._compute_loss(y_pred, y_train, w_train) # Backward pass and update weights self.train_loss.backward() self.optimizer.step() - # Switch to eval mode and evaluate with development data (TODO) - # Also keep best model in memory (TODO) + # Switch to eval mode and evaluate with development data # See https://github.com/pytorch/examples/blob/master/snli/train.py self.model.eval() y_pred = self.model(x_dev) - # Do we need sample_weights? (TODO) - self.dev_loss = self._compute_loss(y_pred, y_dev) + self.dev_loss = self._compute_loss(y_pred, y_dev, w_dev) if self.verbose: print(epoch, self.train_loss.item(), self.dev_loss.item()) + if self.dev_loss.item() < best_loss: + best_model = copy.deepcopy(self.model) + best_loss = self.dev_loss.item() + if self.verbose: + print('A new best model have been found!') + + self.model = best_model + return self def _add_weight_decay(model, reg, skip_list=[]): From 1053a606a9a0466042d51dd128e08b9f5cf7174b Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 29 May 2019 14:43:08 -0400 Subject: [PATCH 58/60] Update linear.py --- surprise/prediction_algorithms/linear.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/surprise/prediction_algorithms/linear.py b/surprise/prediction_algorithms/linear.py index 737324e9..03b7109b 100644 --- a/surprise/prediction_algorithms/linear.py +++ b/surprise/prediction_algorithms/linear.py @@ -75,17 +75,15 @@ def lasso(self, trainset): for k, (uid, iid, rating) in enumerate(self.trainset.all_ratings()): y[k] = rating - temp = u_features[uid] - if temp: - X[k, :n_uf] = temp - else: + try: + X[k, :n_uf] = u_features[uid] + except KeyError: raise ValueError('No features for user ' + str(self.trainset.to_raw_uid(uid))) - temp = i_features[iid] - if temp: - X[k, n_uf:] = temp - else: + try: + X[k, n_uf:] = i_features[iid] + except KeyError: raise ValueError('No features for item ' + str(self.trainset.to_raw_iid(iid))) From cc09a6d809f08f503b90cc4a73ac77bf4fc493a6 Mon Sep 17 00:00:00 2001 From: martincousi Date: Wed, 29 May 2019 15:01:43 -0400 Subject: [PATCH 59/60] Change Baseline_only default verbose value --- surprise/prediction_algorithms/baseline_only.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surprise/prediction_algorithms/baseline_only.py b/surprise/prediction_algorithms/baseline_only.py index a6eb707f..25b22103 100644 --- a/surprise/prediction_algorithms/baseline_only.py +++ b/surprise/prediction_algorithms/baseline_only.py @@ -22,10 +22,10 @@ class BaselineOnly(AlgoBase): computation. See :ref:`baseline_estimates_configuration` for accepted options. verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. + similarity, etc. Default is False. """ - def __init__(self, bsl_options={}, verbose=True): + def __init__(self, bsl_options={}, verbose=False): AlgoBase.__init__(self, bsl_options=bsl_options) self.verbose = verbose From 723afd778d25c4aed947ee4fc9cf40928db67691 Mon Sep 17 00:00:00 2001 From: martincousi Date: Mon, 3 Jun 2019 13:19:40 -0400 Subject: [PATCH 60/60] Updated tests --- surprise/dataset.py | 28 +++++++-- .../factorization_machines.py | 21 +++++-- surprise/prediction_algorithms/knns.py | 18 +++--- surprise/trainset.py | 8 +-- tests/test_algorithms.py | 59 +++++++++++++++---- tests/test_dataset.py | 29 +++++---- tests/test_dump.py | 6 +- tests/test_grid_search.py | 10 ++-- 8 files changed, 126 insertions(+), 53 deletions(-) diff --git a/surprise/dataset.py b/surprise/dataset.py index e6f46acf..e31d08bc 100644 --- a/surprise/dataset.py +++ b/surprise/dataset.py @@ -262,7 +262,11 @@ def construct_trainset(self, raw_trainset): raw2inner_id_users[urid] = current_u_index current_u_index += 1 if self.user_features_nb > 0: - u_features[uid] = self.user_features[urid] + try: + u_features[uid] = self.user_features[urid] + except KeyError: + raise ValueError('Features are defined for all users' + 'but user {}'.format(urid)) try: iid = raw2inner_id_items[irid] @@ -271,7 +275,11 @@ def construct_trainset(self, raw_trainset): raw2inner_id_items[irid] = current_i_index current_i_index += 1 if self.item_features_nb > 0: - i_features[iid] = self.item_features[irid] + try: + i_features[iid] = self.item_features[irid] + except KeyError: + raise ValueError('Features are defined for all items' + 'but item {}'.format(irid)) ur[uid].append((iid, r)) ir[iid].append((uid, r)) @@ -302,8 +310,20 @@ def construct_testset(self, raw_testset): testset = [] for (ruid, riid, r_ui_trans, _) in raw_testset: - u_features = self.user_features[ruid] - i_features = self.item_features[riid] + if self.user_features_nb > 0: + try: # add features if available + u_features = self.user_features[ruid] + except KeyError: + u_features = [] + else: + u_features = [] + if self.item_features_nb > 0: + try: # add features if available + i_features = self.item_features[riid] + except KeyError: + i_features = [] + else: + i_features = [] testset.append((ruid, riid, u_features, i_features, r_ui_trans)) return testset diff --git a/surprise/prediction_algorithms/factorization_machines.py b/surprise/prediction_algorithms/factorization_machines.py index 76de7946..9752cf40 100644 --- a/surprise/prediction_algorithms/factorization_machines.py +++ b/surprise/prediction_algorithms/factorization_machines.py @@ -12,7 +12,7 @@ import torch from torch import nn -from .predictions import PredictionImpossible +# from .predictions import PredictionImpossible from .algo_base import AlgoBase @@ -153,8 +153,10 @@ def fit(self, trainset): self.optimizer = torch.optim.Adam(params, lr=self.lr) # Define data (TODO : sample_weights) - x = self.libsvm_df.loc[:, self.libsvm_df.columns != 'rating'].values - y = self.libsvm_df.loc[:, 'rating'].values + x = self.libsvm_df.loc[ + :, self.libsvm_df.columns != 'rating'].values.astype('float64') + y = self.libsvm_df.loc[ + :, 'rating'].values.astype('float64') sample_weights = None if sample_weights: x_train, x_dev, y_train, y_dev, w_train, w_dev = train_test_split( @@ -253,7 +255,7 @@ def _construct_FM_data(self): 'there are no item_features') n_ratings = self.trainset.n_ratings - n_users = self.trainset.n_users + # n_users = self.trainset.n_users n_items = self.trainset.n_items # Construct ratings_df from trainset @@ -368,6 +370,15 @@ def _construct_estimate_input(self, u, i, u_features, i_features): they are all given and in the same order as in the trainset. """ + if (self.user_lst and u_features and ( + len(u_features) != len(self.trainset.user_features_labels))): + raise ValueError('If u_features are provided for predict(), they' + 'should all be provided as in trainset') + if (self.item_lst and i_features and ( + len(i_features) != len(self.trainset.item_features_labels))): + raise ValueError('If i_features are provided for predict(), they' + 'should all be provided as in trainset') + n_users = self.trainset.n_users n_items = self.trainset.n_items @@ -428,7 +439,7 @@ def _construct_estimate_input(self, u, i, u_features, i_features): # Add item features if self.item_lst: temp = [0.] * len(self.item_lst) - if u_features: + if i_features: # It is assumed that if features are given, they are all given. temp_df = pd.Series( i_features, index=self.trainset.item_features_labels) diff --git a/surprise/prediction_algorithms/knns.py b/surprise/prediction_algorithms/knns.py index 245a83dc..9307db18 100644 --- a/surprise/prediction_algorithms/knns.py +++ b/surprise/prediction_algorithms/knns.py @@ -27,7 +27,7 @@ class SymmetricAlgo(AlgoBase): reversed. """ - def __init__(self, sim_options={}, verbose=True, **kwargs): + def __init__(self, sim_options={}, verbose=False, **kwargs): AlgoBase.__init__(self, sim_options=sim_options, **kwargs) self.verbose = verbose @@ -83,10 +83,10 @@ class KNNBasic(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. + similarity, etc. Default is False. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=False, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, **kwargs) @@ -161,10 +161,10 @@ class KNNWithMeans(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. + similarity, etc. Default is False. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=False, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, **kwargs) @@ -258,11 +258,11 @@ class KNNBaseline(SymmetricAlgo): computation. See :ref:`baseline_estimates_configuration` for accepted options. verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. + similarity, etc. Default is False. """ def __init__(self, k=40, min_k=1, sim_options={}, bsl_options={}, - verbose=True, **kwargs): + verbose=False, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, bsl_options=bsl_options, verbose=verbose, @@ -352,10 +352,10 @@ class KNNWithZScore(SymmetricAlgo): measure. See :ref:`similarity_measures_configuration` for accepted options. verbose(bool): Whether to print trace messages of bias estimation, - similarity, etc. Default is True. + similarity, etc. Default is False. """ - def __init__(self, k=40, min_k=1, sim_options={}, verbose=True, **kwargs): + def __init__(self, k=40, min_k=1, sim_options={}, verbose=False, **kwargs): SymmetricAlgo.__init__(self, sim_options=sim_options, verbose=verbose, **kwargs) diff --git a/surprise/trainset.py b/surprise/trainset.py index c7d091f6..bf69c850 100644 --- a/surprise/trainset.py +++ b/surprise/trainset.py @@ -242,8 +242,8 @@ def build_testset(self): testset = [] for (u, i, r) in self.all_ratings(): - u_features = self.u_features.get(u, None) - i_features = self.i_features.get(i, None) + u_features = self.u_features.get(u, []) + i_features = self.i_features.get(i, []) testset.append((self.to_raw_uid(u), self.to_raw_iid(i), u_features, i_features, r)) @@ -276,8 +276,8 @@ def build_anti_testset(self, fill=None): user_items = set([j for (j, _) in self.ur[u]]) anti_testset += [(self.to_raw_uid(u), self.to_raw_iid(i), - self.u_features.get(u, None), - self.i_features.get(i, None), + self.u_features.get(u, []), + self.i_features.get(i, []), fill) for i in self.all_items() if i not in user_items] diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index c1504173..50cfa77a 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -21,6 +21,7 @@ from surprise import CoClustering from surprise import KNNWithZScore from surprise import Lasso +from surprise import FM from surprise import Dataset from surprise import Reader from surprise.model_selection import PredefinedKFold @@ -86,12 +87,14 @@ def test_unknown_user_or_item(): data_ui_mu = Dataset.load_from_file(file_path=file_path, reader=reader) data_ui_mu.load_features_df(u_features_m_df, user_features=True) data_ui_mu.load_features_df(i_features_df, user_features=False) - trainset_ui_mu = data_ui_mu.build_full_trainset() + with pytest.raises(ValueError): + data_ui_mu.build_full_trainset() data_ui_mi = Dataset.load_from_file(file_path=file_path, reader=reader) data_ui_mi.load_features_df(u_features_df, user_features=True) data_ui_mi.load_features_df(i_features_m_df, user_features=False) - trainset_ui_mi = data_ui_mi.build_full_trainset() + with pytest.raises(ValueError): + data_ui_mi.build_full_trainset() # algos not using features klasses = (NormalPredictor, BaselineOnly, KNNBasic, KNNWithMeans, @@ -102,8 +105,6 @@ def test_unknown_user_or_item(): algo.fit(trainset) algo.fit(trainset_u) algo.fit(trainset_i) - algo.fit(trainset_ui_mu) - algo.fit(trainset_ui_mi) algo.fit(trainset_ui) algo.predict('user0', 'unknown_item') algo.predict('unkown_user', 'item0') @@ -131,10 +132,6 @@ def test_unknown_user_or_item(): algo.fit(trainset_u) with pytest.raises(ValueError): algo.fit(trainset_i) - with pytest.raises(ValueError): - algo.fit(trainset_ui_mu) - with pytest.raises(ValueError): - algo.fit(trainset_ui_mi) algo.fit(trainset_ui) algo.predict('user0', 'unknown_item') algo.predict('unkown_user', 'item0') @@ -152,9 +149,51 @@ def test_unknown_user_or_item(): algo.predict('unkown_user', 'item0', [False], [False, 4, True]) algo.predict('unkown_user', 'unknown_item', [False], [False, 4, True]) + # FM using (optionally) user and item features + rating_lst_opt = ['userID', 'itemID', 'imp_u_rating', 'exp_u_rating'] + user_lst_opt = ['isMale'] + item_lst_opt = ['isNew', 'webRating', 'isComedy'] + algos = (FM(rating_lst=rating_lst_opt), FM(user_lst=user_lst_opt), + FM(item_lst=item_lst_opt), + FM(rating_lst=rating_lst_opt, user_lst=user_lst_opt, + item_lst=item_lst_opt)) + for algo in algos: + if algo.user_lst: + with pytest.raises(ValueError): + algo.fit(trainset) + with pytest.raises(ValueError): + algo.fit(trainset_i) + if algo.item_lst: + with pytest.raises(ValueError): + algo.fit(trainset) + with pytest.raises(ValueError): + algo.fit(trainset_u) + algo.fit(trainset_ui) + if algo.user_lst: + with pytest.raises(ValueError): + algo.predict('user0', 'item0', [False, 'AddedFeature'], []) + if algo.item_lst: + with pytest.raises(ValueError): + algo.predict('user0', 'item0', [], ['NotEnoughFeatures']) + algo.predict('user0', 'unknown_item') + algo.predict('unkown_user', 'item0') + algo.predict('unkown_user', 'unknown_item') + algo.predict('user0', 'unknown_item', [], []) + algo.predict('unkown_user', 'item0', [], []) + algo.predict('unkown_user', 'unknown_item', [], []) + algo.predict('user0', 'unknown_item', [False], []) + algo.predict('unkown_user', 'item0', [False], []) + algo.predict('unkown_user', 'unknown_item', [False], []) + algo.predict('user0', 'unknown_item', [], [False, 4, True]) + algo.predict('unkown_user', 'item0', [], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [], [False, 4, True]) + algo.predict('user0', 'unknown_item', [False], [False, 4, True]) + algo.predict('unkown_user', 'item0', [False], [False, 4, True]) + algo.predict('unkown_user', 'unknown_item', [False], [False, 4, True]) + # unrelated, but test the fit().test() one-liner: - trainset, testset = train_test_split(data, test_size=2) - for klass in klasses: + trainset, testset = train_test_split(data_ui, test_size=2) + for klass in (klasses + klasses_ui + (FM,)): algo = klass() algo.fit(trainset).test(testset) with pytest.warns(UserWarning): diff --git a/tests/test_dataset.py b/tests/test_dataset.py index e98e5640..c29c989f 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -280,16 +280,16 @@ def test_trainset_testset_ui_features(): data = Dataset.load_from_folds(folds_files=folds_files, reader=reader) u_features_df = pd.DataFrame( - {'urid': ['user0', 'user2', 'user1'], # 'user3' is missing - 'isMale': [False, True, True]}, + {'urid': ['user0', 'user2', 'user3', 'user1'], + 'isMale': [False, True, False, True]}, columns=['urid', 'isMale']) data = data.load_features_df(u_features_df, user_features=True) i_features_df = pd.DataFrame( - {'irid': ['item0'], - 'isNew': [False], - 'webRating': [4], - 'isComedy': [True]}, + {'irid': ['item0', 'item1'], + 'isNew': [False, True], + 'webRating': [4, 3], + 'isComedy': [True, False]}, columns=['irid', 'isNew', 'webRating', 'isComedy']) data = data.load_features_df(i_features_df, user_features=False) @@ -317,8 +317,6 @@ def test_trainset_testset_ui_features(): # test user features u_features = trainset.u_features assert u_features[0] == [False] - assert u_features[1] == [True] - assert u_features[3] == [] # not in u_features_df assert u_features[40] == [] # not in trainset and u_features_df assert trainset.user_features_labels == ['isMale'] assert trainset.n_user_features == 1 @@ -326,7 +324,6 @@ def test_trainset_testset_ui_features(): # test item features i_features = trainset.i_features assert i_features[0] == [False, 4, True] - assert i_features[1] == [] # not in i_features_df assert i_features[20000] == [] # not in trainset and i_features_df assert trainset.item_features_labels == ['isNew', 'webRating', 'isComedy'] assert trainset.n_item_features == 3 @@ -358,9 +355,9 @@ def test_trainset_testset_ui_features(): testset = trainset.build_testset() algo.test(testset) # ensure an algorithm can manage the data assert ('user0', 'item0', [False], [False, 4, True], 4) in testset - assert ('user2', 'item1', [True], [], 1) in testset - assert ('user3', 'item1', [], [], 5) in testset - assert ('user3', 'item1', [], [], 0) not in testset + assert ('user2', 'item1', [True], [True, 3, False], 1) in testset + assert ('user3', 'item1', [False], [True, 3, False], 5) in testset + assert ('user3', 'item1', [False], [True, 3, False], 0) not in testset # Test the build_anti_testset() method algo = BaselineOnly() @@ -369,9 +366,11 @@ def test_trainset_testset_ui_features(): algo.test(testset) # ensure an algorithm can manage the data assert (('user0', 'item0', [False], [False, 4, True], trainset.global_mean) not in testset) - assert ('user3', 'item1', [], [], trainset.global_mean) not in testset - assert ('user0', 'item1', [False], [], trainset.global_mean) in testset - assert (('user3', 'item0', [], [False, 4, True], trainset.global_mean) + assert (('user3', 'item1', [False], [True, 3, False], trainset.global_mean) + not in testset) + assert (('user0', 'item1', [False], [True, 3, False], trainset.global_mean) + in testset) + assert (('user3', 'item0', [False], [False, 4, True], trainset.global_mean) in testset) diff --git a/tests/test_dump.py b/tests/test_dump.py index 128442f9..a6be8522 100644 --- a/tests/test_dump.py +++ b/tests/test_dump.py @@ -34,19 +34,21 @@ def test_dump(): algo.fit(trainset) predictions = algo.test(testset) - with tempfile.NamedTemporaryFile() as tmp_file: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: dump.dump(tmp_file.name, predictions, algo) predictions_dumped, algo_dumped = dump.load(tmp_file.name) predictions_algo_dumped = algo_dumped.test(testset) assert predictions == predictions_dumped assert predictions == predictions_algo_dumped + os.remove(tmp_file.name) def test_dump_nothing(): """Ensure that by default None objects are dumped.""" - with tempfile.NamedTemporaryFile() as tmp_file: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: dump.dump(tmp_file.name) predictions, algo = dump.load(tmp_file.name) assert predictions is None assert algo is None + os.remove(tmp_file.name) diff --git a/tests/test_grid_search.py b/tests/test_grid_search.py index e54eeff3..1ba4cb68 100644 --- a/tests/test_grid_search.py +++ b/tests/test_grid_search.py @@ -99,15 +99,17 @@ def test_same_splits(): data.split(3) # all RMSE should be the same (as param combinations are the same) - param_grid = {'n_epochs': [1, 1], 'lr_all': [.5, .5]} + param_grid = {'n_epochs': [1, 1], 'lr_all': [.5, .5], 'random_state': [0]} with pytest.warns(UserWarning): - grid_search = GridSearch(SVD, param_grid, measures=['RMSE'], n_jobs=-1) - grid_search.evaluate(data) + grid_search = GridSearch(SVD, param_grid, measures=['RMSE'], n_jobs=1) + with pytest.warns(UserWarning): + grid_search.evaluate(data) rmse_scores = [s['RMSE'] for s in grid_search.cv_results['scores']] assert len(set(rmse_scores)) == 1 # assert rmse_scores are all equal # evaluate grid search again, to make sure that splits are still the same. - grid_search.evaluate(data) + with pytest.warns(UserWarning): + grid_search.evaluate(data) rmse_scores += [s['RMSE'] for s in grid_search.cv_results['scores']] assert len(set(rmse_scores)) == 1