Module reclab.recommenders.recommender
Defines a set of base classes from which recommenders can inherit.
Recommenders do not need to inherit from any of the classes here to interact with the environments. However, a recommender that is used in our experiment framework must be a descendent of the Recommender base class. All the other classes represent specific recommender variants that occur often enough to be an abstract classes to inherit from.
Expand source code
"""Defines a set of base classes from which recommenders can inherit.
Recommenders do not need to inherit from any of the classes here to interact with the environments.
However, a recommender that is used in our experiment framework must be a descendent of the
Recommender base class. All the other classes represent specific recommender variants that occur
often enough to be an abstract classes to inherit from.
"""
import abc
import collections
import itertools
import numpy as np
import scipy
class Recommender(abc.ABC):
"""The interface for recommenders."""
@property
@abc.abstractmethod
def name(self):
"""Get the name of the recommender."""
raise NotImplementedError
@property
@abc.abstractmethod
def hyperparameters(self):
"""Get a dict of all the recommender's hyperparameters."""
raise NotImplementedError
@abc.abstractmethod
def reset(self, users=None, items=None, ratings=None):
"""Reset the recommender with optional starting user, item, and rating data.
Parameters
----------
users : iterable, optional
The starting users.
items : iterable, optional
The starting items.
ratings : iterable, optional
The starting ratings and the associated contexts in which each rating was made.
"""
raise NotImplementedError
@abc.abstractmethod
def update(self, users=None, items=None, ratings=None):
"""Update the recommender with new user, item, and rating data.
Parameters
----------
users : iterable, optional
The new users.
items : iterable, optional
The new items.
ratings : iterable, optional
The new ratings and the associated context in which each rating was made.
"""
raise NotImplementedError
@abc.abstractmethod
def recommend(self, user_contexts, num_recommendations):
"""Recommend items to users.
Parameters
----------
user_contexts : iterable
The setting each user is going to be recommended items in.
num_recommendations : int
The number of items to recommend to each user.
Returns
-------
recs : iterable
The recommendations made to each user. recs[i] represents the recommendations
made to the i-th user in the user_contexts variable.
predicted_ratings : iterable or None
The predicted rating for each item recommended to each user. Where predicted_ratings[i]
represents the predictions of recommended items on the i-th user in the user_contexts
variable. Derived classes may simply return None if they do not directly estimate
ratings when making recommendations.
"""
raise NotImplementedError
class PredictRecommender(Recommender):
"""A recommender that makes recommendations based on its rating predictions.
Data is primarily passed around through dicts for any recommenders derived from this class.
Each user is assumed to have a unique hashable id, likewise for all items. User and item
features as well as rating contexts are assumed to be dense arrays.
Parameters
----------
strategy : str, optional
The item selection strategy to use.
Valid strategies are:
'greedy': chooses the unseen item with largest predicted rating
'eps_greedy': with probability 1-eps chooses the unseen item with largest
predicted rating, with probability eps chooses a random unseen item
'thompson': picks an item with probability proportional to the expected rating
"""
def __init__(self, strategy='greedy'):
"""Create a new PredictRecommender object."""
# The features associated with each user.
self._users = []
# The features associated with each item.
self._items = []
# The matrix of all numerical ratings.
self._ratings = scipy.sparse.csr_matrix((0, 0))
# Keeps track of the history of contexts in which a user-item rating was made.
self._rating_contexts = collections.defaultdict(list)
# Since outer ids passed to the recommender can be arbitrary hashable objects we
# use these four variables to keep track of which index (AKA inner ids)
# correspond to each outer id.
self._outer_to_inner_uid = {}
self._inner_to_outer_uid = []
self._outer_to_inner_iid = {}
self._inner_to_outer_iid = []
# The sampling strategy to use.
self._strategy = strategy
# A dict of all the recommender's hyperparameters.
self._hyperparameters = {'strategy': strategy}
# The cached dense predictions, reset to None each time update is called.
self._dense_predictions = None
# Check that the strategy is of valid type.
assert self._strategy.split(',')[0] in ['greedy', 'eps_greedy', 'thompson']
@property
def hyperparameters(self):
"""Get a dict of all the recommender's hyperparameters."""
return self._hyperparameters
def reset(self, users=None, items=None, ratings=None):
"""Reset the recommender with optional starting user, item, and rating data.
Parameters
----------
users : dict, optional
The starting users where the key is the user id while the value is the
user features.
items : dict, optional
The starting items where the key is the user id while the value is the
item features.
ratings : dict, optional
The starting ratings where the key is a double whose first index is the
id of the user making the rating and the second index is the id of the item being
rated. The value is a double whose first index is the rating value and the second
index is a numpy array that represents the context in which the rating was made.
"""
self._users = []
self._items = []
self._ratings = scipy.sparse.dok_matrix((0, 0))
self._rating_contexts = collections.defaultdict(list)
self._outer_to_inner_uid = {}
self._inner_to_outer_uid = []
self._outer_to_inner_iid = {}
self._inner_to_outer_iid = []
self._dense_predictions = None
self.update(users, items, ratings)
def update(self, users=None, items=None, ratings=None):
"""Update the recommender with new user, item, and rating data.
Parameters
----------
users : dict, optional
The new users where the key is the user id while the value is the
user features.
items : dict, optional
The new items where the key is the user id while the value is the
item features.
ratings : dict, optional
The new ratings where the key is a double whose first index is the
id of the user making the rating and the second index is the id of the item being
rated. The value is a double whose first index is the rating value and the second
index is a numpy array that represents the context in which the rating was made.
"""
self._dense_predictions = None
# Update the user info.
if users is not None:
for user_id, features in users.items():
if user_id not in self._outer_to_inner_uid:
self._outer_to_inner_uid[user_id] = len(self._users)
self._inner_to_outer_uid.append(user_id)
self._ratings.resize((self._ratings.shape[0] + 1, self._ratings.shape[1]))
self._users.append(features)
else:
inner_id = self._outer_to_inner_uid[user_id]
self._users[inner_id] = features
# Update the item info.
if items is not None:
for item_id, features in items.items():
if item_id not in self._outer_to_inner_iid:
self._outer_to_inner_iid[item_id] = len(self._items)
self._inner_to_outer_iid.append(item_id)
self._ratings.resize((self._ratings.shape[0], self._ratings.shape[1] + 1))
self._items.append(features)
else:
inner_id = self._outer_to_inner_iid[item_id]
self._items[inner_id] = features
# Update the rating info.
if ratings is not None:
for (user_id, item_id), (rating, context) in ratings.items():
inner_uid = self._outer_to_inner_uid[user_id]
inner_iid = self._outer_to_inner_iid[item_id]
self._ratings[inner_uid, inner_iid] = rating
self._rating_contexts[inner_uid, inner_iid].append(context)
assert inner_uid < len(self._users)
assert inner_iid < len(self._items)
def recommend(self, user_contexts, num_recommendations):
"""Recommend items to users.
Parameters
----------
user_contexts : ordered dict
The setting each user is going to be recommended items in. The key is the user id and
the value is the rating features.
num_recommendations : int
The number of items to recommend to each user.
Returns
-------
recs : list of list
The recommendations made to each user. recs[i] is the list of item ids recommended
to the i-th user.
predicted_ratings : list of list
The predicted ratings of the recommended items. recs[i] is the list of predicted
ratings for the items recommended to the i-th user.
"""
# Format the arrays to be passed to the prediction function. We need to predict all
# items that have not been rated for each user.
ratings_to_predict = []
all_item_ids = []
# TODO: We need to figure out what to do when the number of items left to recommend
# runs out.
for user_id in user_contexts:
inner_uid = self._outer_to_inner_uid[user_id]
item_ids = self._ratings[inner_uid].nonzero()[1]
item_ids = np.setdiff1d(np.arange(len(self._items)), item_ids)
user_ids = inner_uid * np.ones(len(item_ids), dtype=np.int)
contexts = len(item_ids) * [user_contexts[user_id]]
ratings_to_predict += list(zip(user_ids, item_ids, contexts))
all_item_ids.append(item_ids)
# Predict the ratings and convert predictions into a list of arrays indexed by user.
if self._dense_predictions is None:
all_predictions = self._predict(ratings_to_predict)
else:
all_predictions = []
for user_id, item_id, _ in ratings_to_predict:
all_predictions.append(self._dense_predictions[user_id, item_id])
item_lens = map(len, all_item_ids)
all_predictions = np.split(all_predictions,
list(itertools.accumulate(item_lens)))
# Pick items according to the strategy, along with their predicted ratings.
all_recs = []
all_predicted_ratings = []
# TODO: Right now items with the same ratings will be sorted in a deterministic order.
# This probably shouldn't be the case.
for item_ids, predictions in zip(all_item_ids, all_predictions):
recs, predicted_ratings = self._select_item(item_ids, predictions,
num_recommendations)
# Convert the recommendations to outer item ids.
all_recs.append([self._inner_to_outer_iid[rec] for rec in recs])
all_predicted_ratings.append(predicted_ratings)
return np.array(all_recs), np.array(all_predicted_ratings)
@property
def dense_predictions(self):
"""Get the predictions on all user-item pairs.
This method should be overwritten if there is a more efficient way to compute dense
predictions than calling _predict on all user-item pairs.
"""
if self._dense_predictions is None:
user_item = []
for i in range(len(self._users)):
for j in range(len(self._items)):
user_item.append((i, j, np.zeros(0)))
self._dense_predictions = self._predict(user_item)
self._dense_predictions = self._dense_predictions.reshape((len(self._users),
len(self._items)))
return self._dense_predictions
def predict(self, user_item):
"""Predict the ratings of user-item pairs.
Parameters
----------
user_item : list of tuple
The list of all user-item pairs along with the rating context.
Each element is a triple where the first element in the tuple is
the user id, the second element is the item id and the third element
is the context in which the item will be rated.
Returns
-------
predictions : np.ndarray
The rating predictions where predictions[i] is the prediction of the i-th pair.
"""
inner_user_item = []
for user_id, item_id, context in user_item:
inner_uid = self._outer_to_inner_uid[user_id]
inner_iid = self._outer_to_inner_iid[item_id]
inner_user_item.append((inner_uid, inner_iid, context))
return self._predict(inner_user_item)
def _select_item(self, item_ids, predictions, num_recommendations):
"""Select items given a strategy.
Parameters
----------
item_ids : np.ndarray of int
ids of the items available for recommendation at this time step
predictions : np.ndarray
corresponding predicted ratings for these items
num_recommendations : int
number of items to select
Returns
-------
recs : np.ndarray of int
the indices of the items to be recommended
predicted_ratings : np.ndarray
predicted ratings for the selected items
"""
assert len(item_ids) == len(predictions)
num_items = len(item_ids)
strategy_name = self._strategy.split(',')[0]
# TODO: clean up this method of parameter specification
if len(self._strategy.split(',')) > 1:
strategy_param = self._strategy.split(',')[1]
else:
strategy_param = None
if strategy_name == 'greedy':
selected_indices = np.argsort(predictions)[-num_recommendations:]
elif strategy_name == 'eps_greedy':
if strategy_param is None:
eps = 0.1
else:
eps = float(strategy_param)
num_explore = np.random.binomial(num_recommendations, eps)
num_exploit = num_recommendations - num_explore
if num_exploit > 0:
exploit_indices = np.argsort(predictions)[-num_exploit:]
else:
exploit_indices = []
explore_indices = np.random.choice([x for x in range(0, num_items)
if x not in exploit_indices], num_explore)
selected_indices = np.concatenate((exploit_indices, explore_indices))
elif strategy_name == 'thompson':
if strategy_param is None:
# artificial parameter to boost the probability of the more likely items
power = np.ceil(np.log(len(predictions)))
else:
power = int(float(strategy_param))
selection_probs = np.power(predictions/sum(predictions), power)
selection_probs = selection_probs/sum(selection_probs)
selected_indices = np.random.choice(range(0, num_items),
num_recommendations, p=selection_probs)
selected_indices = selected_indices.astype('int')
predicted_ratings = predictions[selected_indices]
recs = item_ids[selected_indices]
return recs, predicted_ratings
@abc.abstractmethod
def _predict(self, user_item):
"""Predict the ratings of user-item pairs. This internal version assumes inner ids.
Parameters
----------
user_item : list of tuple
The list of all user-item pairs along with the rating context.
Each element is a triple where the first element in the tuple is
the inner user id, the second element is the inner item id and the third element
is the context in which the item will be rated.
Returns
-------
predictions : np.ndarray
The rating predictions where predictions[i] is the prediction of the i-th pair.
"""
raise NotImplementedError
Classes
class PredictRecommender (strategy='greedy')
-
A recommender that makes recommendations based on its rating predictions.
Data is primarily passed around through dicts for any recommenders derived from this class. Each user is assumed to have a unique hashable id, likewise for all items. User and item features as well as rating contexts are assumed to be dense arrays.
Parameters
strategy
:str
, optional- The item selection strategy to use. Valid strategies are: 'greedy': chooses the unseen item with largest predicted rating 'eps_greedy': with probability 1-eps chooses the unseen item with largest predicted rating, with probability eps chooses a random unseen item 'thompson': picks an item with probability proportional to the expected rating
Create a new PredictRecommender object.
Expand source code
class PredictRecommender(Recommender): """A recommender that makes recommendations based on its rating predictions. Data is primarily passed around through dicts for any recommenders derived from this class. Each user is assumed to have a unique hashable id, likewise for all items. User and item features as well as rating contexts are assumed to be dense arrays. Parameters ---------- strategy : str, optional The item selection strategy to use. Valid strategies are: 'greedy': chooses the unseen item with largest predicted rating 'eps_greedy': with probability 1-eps chooses the unseen item with largest predicted rating, with probability eps chooses a random unseen item 'thompson': picks an item with probability proportional to the expected rating """ def __init__(self, strategy='greedy'): """Create a new PredictRecommender object.""" # The features associated with each user. self._users = [] # The features associated with each item. self._items = [] # The matrix of all numerical ratings. self._ratings = scipy.sparse.csr_matrix((0, 0)) # Keeps track of the history of contexts in which a user-item rating was made. self._rating_contexts = collections.defaultdict(list) # Since outer ids passed to the recommender can be arbitrary hashable objects we # use these four variables to keep track of which index (AKA inner ids) # correspond to each outer id. self._outer_to_inner_uid = {} self._inner_to_outer_uid = [] self._outer_to_inner_iid = {} self._inner_to_outer_iid = [] # The sampling strategy to use. self._strategy = strategy # A dict of all the recommender's hyperparameters. self._hyperparameters = {'strategy': strategy} # The cached dense predictions, reset to None each time update is called. self._dense_predictions = None # Check that the strategy is of valid type. assert self._strategy.split(',')[0] in ['greedy', 'eps_greedy', 'thompson'] @property def hyperparameters(self): """Get a dict of all the recommender's hyperparameters.""" return self._hyperparameters def reset(self, users=None, items=None, ratings=None): """Reset the recommender with optional starting user, item, and rating data. Parameters ---------- users : dict, optional The starting users where the key is the user id while the value is the user features. items : dict, optional The starting items where the key is the user id while the value is the item features. ratings : dict, optional The starting ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made. """ self._users = [] self._items = [] self._ratings = scipy.sparse.dok_matrix((0, 0)) self._rating_contexts = collections.defaultdict(list) self._outer_to_inner_uid = {} self._inner_to_outer_uid = [] self._outer_to_inner_iid = {} self._inner_to_outer_iid = [] self._dense_predictions = None self.update(users, items, ratings) def update(self, users=None, items=None, ratings=None): """Update the recommender with new user, item, and rating data. Parameters ---------- users : dict, optional The new users where the key is the user id while the value is the user features. items : dict, optional The new items where the key is the user id while the value is the item features. ratings : dict, optional The new ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made. """ self._dense_predictions = None # Update the user info. if users is not None: for user_id, features in users.items(): if user_id not in self._outer_to_inner_uid: self._outer_to_inner_uid[user_id] = len(self._users) self._inner_to_outer_uid.append(user_id) self._ratings.resize((self._ratings.shape[0] + 1, self._ratings.shape[1])) self._users.append(features) else: inner_id = self._outer_to_inner_uid[user_id] self._users[inner_id] = features # Update the item info. if items is not None: for item_id, features in items.items(): if item_id not in self._outer_to_inner_iid: self._outer_to_inner_iid[item_id] = len(self._items) self._inner_to_outer_iid.append(item_id) self._ratings.resize((self._ratings.shape[0], self._ratings.shape[1] + 1)) self._items.append(features) else: inner_id = self._outer_to_inner_iid[item_id] self._items[inner_id] = features # Update the rating info. if ratings is not None: for (user_id, item_id), (rating, context) in ratings.items(): inner_uid = self._outer_to_inner_uid[user_id] inner_iid = self._outer_to_inner_iid[item_id] self._ratings[inner_uid, inner_iid] = rating self._rating_contexts[inner_uid, inner_iid].append(context) assert inner_uid < len(self._users) assert inner_iid < len(self._items) def recommend(self, user_contexts, num_recommendations): """Recommend items to users. Parameters ---------- user_contexts : ordered dict The setting each user is going to be recommended items in. The key is the user id and the value is the rating features. num_recommendations : int The number of items to recommend to each user. Returns ------- recs : list of list The recommendations made to each user. recs[i] is the list of item ids recommended to the i-th user. predicted_ratings : list of list The predicted ratings of the recommended items. recs[i] is the list of predicted ratings for the items recommended to the i-th user. """ # Format the arrays to be passed to the prediction function. We need to predict all # items that have not been rated for each user. ratings_to_predict = [] all_item_ids = [] # TODO: We need to figure out what to do when the number of items left to recommend # runs out. for user_id in user_contexts: inner_uid = self._outer_to_inner_uid[user_id] item_ids = self._ratings[inner_uid].nonzero()[1] item_ids = np.setdiff1d(np.arange(len(self._items)), item_ids) user_ids = inner_uid * np.ones(len(item_ids), dtype=np.int) contexts = len(item_ids) * [user_contexts[user_id]] ratings_to_predict += list(zip(user_ids, item_ids, contexts)) all_item_ids.append(item_ids) # Predict the ratings and convert predictions into a list of arrays indexed by user. if self._dense_predictions is None: all_predictions = self._predict(ratings_to_predict) else: all_predictions = [] for user_id, item_id, _ in ratings_to_predict: all_predictions.append(self._dense_predictions[user_id, item_id]) item_lens = map(len, all_item_ids) all_predictions = np.split(all_predictions, list(itertools.accumulate(item_lens))) # Pick items according to the strategy, along with their predicted ratings. all_recs = [] all_predicted_ratings = [] # TODO: Right now items with the same ratings will be sorted in a deterministic order. # This probably shouldn't be the case. for item_ids, predictions in zip(all_item_ids, all_predictions): recs, predicted_ratings = self._select_item(item_ids, predictions, num_recommendations) # Convert the recommendations to outer item ids. all_recs.append([self._inner_to_outer_iid[rec] for rec in recs]) all_predicted_ratings.append(predicted_ratings) return np.array(all_recs), np.array(all_predicted_ratings) @property def dense_predictions(self): """Get the predictions on all user-item pairs. This method should be overwritten if there is a more efficient way to compute dense predictions than calling _predict on all user-item pairs. """ if self._dense_predictions is None: user_item = [] for i in range(len(self._users)): for j in range(len(self._items)): user_item.append((i, j, np.zeros(0))) self._dense_predictions = self._predict(user_item) self._dense_predictions = self._dense_predictions.reshape((len(self._users), len(self._items))) return self._dense_predictions def predict(self, user_item): """Predict the ratings of user-item pairs. Parameters ---------- user_item : list of tuple The list of all user-item pairs along with the rating context. Each element is a triple where the first element in the tuple is the user id, the second element is the item id and the third element is the context in which the item will be rated. Returns ------- predictions : np.ndarray The rating predictions where predictions[i] is the prediction of the i-th pair. """ inner_user_item = [] for user_id, item_id, context in user_item: inner_uid = self._outer_to_inner_uid[user_id] inner_iid = self._outer_to_inner_iid[item_id] inner_user_item.append((inner_uid, inner_iid, context)) return self._predict(inner_user_item) def _select_item(self, item_ids, predictions, num_recommendations): """Select items given a strategy. Parameters ---------- item_ids : np.ndarray of int ids of the items available for recommendation at this time step predictions : np.ndarray corresponding predicted ratings for these items num_recommendations : int number of items to select Returns ------- recs : np.ndarray of int the indices of the items to be recommended predicted_ratings : np.ndarray predicted ratings for the selected items """ assert len(item_ids) == len(predictions) num_items = len(item_ids) strategy_name = self._strategy.split(',')[0] # TODO: clean up this method of parameter specification if len(self._strategy.split(',')) > 1: strategy_param = self._strategy.split(',')[1] else: strategy_param = None if strategy_name == 'greedy': selected_indices = np.argsort(predictions)[-num_recommendations:] elif strategy_name == 'eps_greedy': if strategy_param is None: eps = 0.1 else: eps = float(strategy_param) num_explore = np.random.binomial(num_recommendations, eps) num_exploit = num_recommendations - num_explore if num_exploit > 0: exploit_indices = np.argsort(predictions)[-num_exploit:] else: exploit_indices = [] explore_indices = np.random.choice([x for x in range(0, num_items) if x not in exploit_indices], num_explore) selected_indices = np.concatenate((exploit_indices, explore_indices)) elif strategy_name == 'thompson': if strategy_param is None: # artificial parameter to boost the probability of the more likely items power = np.ceil(np.log(len(predictions))) else: power = int(float(strategy_param)) selection_probs = np.power(predictions/sum(predictions), power) selection_probs = selection_probs/sum(selection_probs) selected_indices = np.random.choice(range(0, num_items), num_recommendations, p=selection_probs) selected_indices = selected_indices.astype('int') predicted_ratings = predictions[selected_indices] recs = item_ids[selected_indices] return recs, predicted_ratings @abc.abstractmethod def _predict(self, user_item): """Predict the ratings of user-item pairs. This internal version assumes inner ids. Parameters ---------- user_item : list of tuple The list of all user-item pairs along with the rating context. Each element is a triple where the first element in the tuple is the inner user id, the second element is the inner item id and the third element is the context in which the item will be rated. Returns ------- predictions : np.ndarray The rating predictions where predictions[i] is the prediction of the i-th pair. """ raise NotImplementedError
Ancestors
- Recommender
- abc.ABC
Subclasses
Instance variables
var dense_predictions
-
Get the predictions on all user-item pairs.
This method should be overwritten if there is a more efficient way to compute dense predictions than calling _predict on all user-item pairs.
Expand source code
@property def dense_predictions(self): """Get the predictions on all user-item pairs. This method should be overwritten if there is a more efficient way to compute dense predictions than calling _predict on all user-item pairs. """ if self._dense_predictions is None: user_item = [] for i in range(len(self._users)): for j in range(len(self._items)): user_item.append((i, j, np.zeros(0))) self._dense_predictions = self._predict(user_item) self._dense_predictions = self._dense_predictions.reshape((len(self._users), len(self._items))) return self._dense_predictions
Methods
def predict(self, user_item)
-
Predict the ratings of user-item pairs.
Parameters
user_item
:list
oftuple
- The list of all user-item pairs along with the rating context. Each element is a triple where the first element in the tuple is the user id, the second element is the item id and the third element is the context in which the item will be rated.
Returns
predictions
:np.ndarray
- The rating predictions where predictions[i] is the prediction of the i-th pair.
Expand source code
def predict(self, user_item): """Predict the ratings of user-item pairs. Parameters ---------- user_item : list of tuple The list of all user-item pairs along with the rating context. Each element is a triple where the first element in the tuple is the user id, the second element is the item id and the third element is the context in which the item will be rated. Returns ------- predictions : np.ndarray The rating predictions where predictions[i] is the prediction of the i-th pair. """ inner_user_item = [] for user_id, item_id, context in user_item: inner_uid = self._outer_to_inner_uid[user_id] inner_iid = self._outer_to_inner_iid[item_id] inner_user_item.append((inner_uid, inner_iid, context)) return self._predict(inner_user_item)
def recommend(self, user_contexts, num_recommendations)
-
Recommend items to users.
Parameters
user_contexts
:ordered dict
- The setting each user is going to be recommended items in. The key is the user id and the value is the rating features.
num_recommendations
:int
- The number of items to recommend to each user.
Returns
recs
:list
oflist
- The recommendations made to each user. recs[i] is the list of item ids recommended to the i-th user.
predicted_ratings
:list
oflist
- The predicted ratings of the recommended items. recs[i] is the list of predicted ratings for the items recommended to the i-th user.
Expand source code
def recommend(self, user_contexts, num_recommendations): """Recommend items to users. Parameters ---------- user_contexts : ordered dict The setting each user is going to be recommended items in. The key is the user id and the value is the rating features. num_recommendations : int The number of items to recommend to each user. Returns ------- recs : list of list The recommendations made to each user. recs[i] is the list of item ids recommended to the i-th user. predicted_ratings : list of list The predicted ratings of the recommended items. recs[i] is the list of predicted ratings for the items recommended to the i-th user. """ # Format the arrays to be passed to the prediction function. We need to predict all # items that have not been rated for each user. ratings_to_predict = [] all_item_ids = [] # TODO: We need to figure out what to do when the number of items left to recommend # runs out. for user_id in user_contexts: inner_uid = self._outer_to_inner_uid[user_id] item_ids = self._ratings[inner_uid].nonzero()[1] item_ids = np.setdiff1d(np.arange(len(self._items)), item_ids) user_ids = inner_uid * np.ones(len(item_ids), dtype=np.int) contexts = len(item_ids) * [user_contexts[user_id]] ratings_to_predict += list(zip(user_ids, item_ids, contexts)) all_item_ids.append(item_ids) # Predict the ratings and convert predictions into a list of arrays indexed by user. if self._dense_predictions is None: all_predictions = self._predict(ratings_to_predict) else: all_predictions = [] for user_id, item_id, _ in ratings_to_predict: all_predictions.append(self._dense_predictions[user_id, item_id]) item_lens = map(len, all_item_ids) all_predictions = np.split(all_predictions, list(itertools.accumulate(item_lens))) # Pick items according to the strategy, along with their predicted ratings. all_recs = [] all_predicted_ratings = [] # TODO: Right now items with the same ratings will be sorted in a deterministic order. # This probably shouldn't be the case. for item_ids, predictions in zip(all_item_ids, all_predictions): recs, predicted_ratings = self._select_item(item_ids, predictions, num_recommendations) # Convert the recommendations to outer item ids. all_recs.append([self._inner_to_outer_iid[rec] for rec in recs]) all_predicted_ratings.append(predicted_ratings) return np.array(all_recs), np.array(all_predicted_ratings)
def reset(self, users=None, items=None, ratings=None)
-
Reset the recommender with optional starting user, item, and rating data.
Parameters
users
:dict
, optional- The starting users where the key is the user id while the value is the user features.
items
:dict
, optional- The starting items where the key is the user id while the value is the item features.
ratings
:dict
, optional- The starting ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made.
Expand source code
def reset(self, users=None, items=None, ratings=None): """Reset the recommender with optional starting user, item, and rating data. Parameters ---------- users : dict, optional The starting users where the key is the user id while the value is the user features. items : dict, optional The starting items where the key is the user id while the value is the item features. ratings : dict, optional The starting ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made. """ self._users = [] self._items = [] self._ratings = scipy.sparse.dok_matrix((0, 0)) self._rating_contexts = collections.defaultdict(list) self._outer_to_inner_uid = {} self._inner_to_outer_uid = [] self._outer_to_inner_iid = {} self._inner_to_outer_iid = [] self._dense_predictions = None self.update(users, items, ratings)
def update(self, users=None, items=None, ratings=None)
-
Update the recommender with new user, item, and rating data.
Parameters
users
:dict
, optional- The new users where the key is the user id while the value is the user features.
items
:dict
, optional- The new items where the key is the user id while the value is the item features.
ratings
:dict
, optional- The new ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made.
Expand source code
def update(self, users=None, items=None, ratings=None): """Update the recommender with new user, item, and rating data. Parameters ---------- users : dict, optional The new users where the key is the user id while the value is the user features. items : dict, optional The new items where the key is the user id while the value is the item features. ratings : dict, optional The new ratings where the key is a double whose first index is the id of the user making the rating and the second index is the id of the item being rated. The value is a double whose first index is the rating value and the second index is a numpy array that represents the context in which the rating was made. """ self._dense_predictions = None # Update the user info. if users is not None: for user_id, features in users.items(): if user_id not in self._outer_to_inner_uid: self._outer_to_inner_uid[user_id] = len(self._users) self._inner_to_outer_uid.append(user_id) self._ratings.resize((self._ratings.shape[0] + 1, self._ratings.shape[1])) self._users.append(features) else: inner_id = self._outer_to_inner_uid[user_id] self._users[inner_id] = features # Update the item info. if items is not None: for item_id, features in items.items(): if item_id not in self._outer_to_inner_iid: self._outer_to_inner_iid[item_id] = len(self._items) self._inner_to_outer_iid.append(item_id) self._ratings.resize((self._ratings.shape[0], self._ratings.shape[1] + 1)) self._items.append(features) else: inner_id = self._outer_to_inner_iid[item_id] self._items[inner_id] = features # Update the rating info. if ratings is not None: for (user_id, item_id), (rating, context) in ratings.items(): inner_uid = self._outer_to_inner_uid[user_id] inner_iid = self._outer_to_inner_iid[item_id] self._ratings[inner_uid, inner_iid] = rating self._rating_contexts[inner_uid, inner_iid].append(context) assert inner_uid < len(self._users) assert inner_iid < len(self._items)
Inherited members
class Recommender
-
The interface for recommenders.
Expand source code
class Recommender(abc.ABC): """The interface for recommenders.""" @property @abc.abstractmethod def name(self): """Get the name of the recommender.""" raise NotImplementedError @property @abc.abstractmethod def hyperparameters(self): """Get a dict of all the recommender's hyperparameters.""" raise NotImplementedError @abc.abstractmethod def reset(self, users=None, items=None, ratings=None): """Reset the recommender with optional starting user, item, and rating data. Parameters ---------- users : iterable, optional The starting users. items : iterable, optional The starting items. ratings : iterable, optional The starting ratings and the associated contexts in which each rating was made. """ raise NotImplementedError @abc.abstractmethod def update(self, users=None, items=None, ratings=None): """Update the recommender with new user, item, and rating data. Parameters ---------- users : iterable, optional The new users. items : iterable, optional The new items. ratings : iterable, optional The new ratings and the associated context in which each rating was made. """ raise NotImplementedError @abc.abstractmethod def recommend(self, user_contexts, num_recommendations): """Recommend items to users. Parameters ---------- user_contexts : iterable The setting each user is going to be recommended items in. num_recommendations : int The number of items to recommend to each user. Returns ------- recs : iterable The recommendations made to each user. recs[i] represents the recommendations made to the i-th user in the user_contexts variable. predicted_ratings : iterable or None The predicted rating for each item recommended to each user. Where predicted_ratings[i] represents the predictions of recommended items on the i-th user in the user_contexts variable. Derived classes may simply return None if they do not directly estimate ratings when making recommendations. """ raise NotImplementedError
Ancestors
- abc.ABC
Subclasses
Instance variables
var hyperparameters
-
Get a dict of all the recommender's hyperparameters.
Expand source code
@property @abc.abstractmethod def hyperparameters(self): """Get a dict of all the recommender's hyperparameters.""" raise NotImplementedError
var name
-
Get the name of the recommender.
Expand source code
@property @abc.abstractmethod def name(self): """Get the name of the recommender.""" raise NotImplementedError
Methods
def recommend(self, user_contexts, num_recommendations)
-
Recommend items to users.
Parameters
user_contexts
:iterable
- The setting each user is going to be recommended items in.
num_recommendations
:int
- The number of items to recommend to each user.
Returns
recs
:iterable
- The recommendations made to each user. recs[i] represents the recommendations made to the i-th user in the user_contexts variable.
predicted_ratings
:iterable
orNone
- The predicted rating for each item recommended to each user. Where predicted_ratings[i] represents the predictions of recommended items on the i-th user in the user_contexts variable. Derived classes may simply return None if they do not directly estimate ratings when making recommendations.
Expand source code
@abc.abstractmethod def recommend(self, user_contexts, num_recommendations): """Recommend items to users. Parameters ---------- user_contexts : iterable The setting each user is going to be recommended items in. num_recommendations : int The number of items to recommend to each user. Returns ------- recs : iterable The recommendations made to each user. recs[i] represents the recommendations made to the i-th user in the user_contexts variable. predicted_ratings : iterable or None The predicted rating for each item recommended to each user. Where predicted_ratings[i] represents the predictions of recommended items on the i-th user in the user_contexts variable. Derived classes may simply return None if they do not directly estimate ratings when making recommendations. """ raise NotImplementedError
def reset(self, users=None, items=None, ratings=None)
-
Reset the recommender with optional starting user, item, and rating data.
Parameters
users
:iterable
, optional- The starting users.
items
:iterable
, optional- The starting items.
ratings
:iterable
, optional- The starting ratings and the associated contexts in which each rating was made.
Expand source code
@abc.abstractmethod def reset(self, users=None, items=None, ratings=None): """Reset the recommender with optional starting user, item, and rating data. Parameters ---------- users : iterable, optional The starting users. items : iterable, optional The starting items. ratings : iterable, optional The starting ratings and the associated contexts in which each rating was made. """ raise NotImplementedError
def update(self, users=None, items=None, ratings=None)
-
Update the recommender with new user, item, and rating data.
Parameters
users
:iterable
, optional- The new users.
items
:iterable
, optional- The new items.
ratings
:iterable
, optional- The new ratings and the associated context in which each rating was made.
Expand source code
@abc.abstractmethod def update(self, users=None, items=None, ratings=None): """Update the recommender with new user, item, and rating data. Parameters ---------- users : iterable, optional The new users. items : iterable, optional The new items. ratings : iterable, optional The new ratings and the associated context in which each rating was made. """ raise NotImplementedError