Module reclab.environments.latent_factors

Contains the implementation for the Latent Behavior environment.

In this environment users and items both have latent vectors, and the rating is determined by the inner product. Users and item both have bias terms, and there is an underlying bias as well.

Expand source code
"""Contains the implementation for the Latent Behavior environment.

In this environment users and items both have latent vectors, and
the rating is determined by the inner product. Users and item both
have bias terms, and there is an underlying bias as well.
"""
import collections
import json
import os

import numpy as np

from . import environment
from .. import data_utils
from ..recommenders import LibFM


class LatentFactorBehavior(environment.DictEnvironment):
    """An environment where users and items have latent factors and biases.

    Ratings are generated as
    r = clip( <p_u, q_i> + b_u + b_i + b_0 )
    where p_u is a user's latent factor, q_i is an item's latent factor,
    b_u is a user bias, b_i is an item bias, and b_0 is a global bias.

    Parameters
    ----------
    latent_dim : int
        Size of latent factors p, q.
    num_users : int
        The number of users in the environment.
    num_items : int
        The number of items in the environment.
    rating_frequency : float
        The proportion of users that will need a recommendation at each step.
        Must be between 0 and 1.
    num_init_ratings : int
        The number of ratings available from the start. User-item pairs are randomly selected.
    noise : float
        The standard deviation of the noise added to ratings.
    affinity_change : float
        How much the user's latent factor is shifted towards that of an item.
    memory_length : int
        The number of recent items a user remembers which affect the rating.
    boredom_threshold : int
        The size of the inner product between a new item and an item in the
        user's history to trigger a boredom response.
    boredom_penalty : float
        The factor on the penalty on the rating when a user is bored. The penalty
        is the average of the values which exceed the boredom_threshold, and the decrease
        in rating is the penalty multiplied by this factor.
    user_dist_choice : str
        The choice of user distribution for selecting online users. By default, the subset of
        online users is chosen from a uniform distribution. Currently supports normal and lognormal.

    """

    def __init__(self, latent_dim, num_users, num_items,
                 rating_frequency=0.02, num_init_ratings=0,
                 noise=0.0, memory_length=0, affinity_change=0.0,
                 boredom_threshold=0, boredom_penalty=0.0, user_dist_choice='uniform'):
        """Create a Latent Factor environment."""
        super().__init__(rating_frequency, num_init_ratings, memory_length, user_dist_choice)
        self._latent_dim = latent_dim
        self._num_users = num_users
        self._num_items = num_items
        self._noise = noise
        self._affinity_change = affinity_change
        self._boredom_threshold = boredom_threshold
        self._boredom_penalty = boredom_penalty
        if self._memory_length > 0:
            self._boredom_penalty /= self._memory_length
        self._user_factors = None
        self._user_biases = None
        self._item_factors = None
        self._item_biases = None
        self._offset = None

    @property
    def name(self):
        """Name of environment, used for saving."""
        return 'latent'

    def _get_dense_ratings(self):  # noqa: D102
        ratings = (self._user_factors @ self._item_factors.T + self._user_biases[:, np.newaxis] +
                   self._item_biases[np.newaxis, :] + self._offset)
        # Compute the boredom penalties.
        item_norms = np.linalg.norm(self._item_factors, axis=1)
        normalized_items = self._item_factors / item_norms[:, np.newaxis]
        similarities = normalized_items @ normalized_items.T
        similarities -= self._boredom_threshold
        similarities[similarities < 0] = 0
        penalties = self._boredom_penalty * similarities
        for user_id in range(self._num_users):
            for item_id in self._user_histories[user_id]:
                if item_id is not None:
                    ratings[user_id] -= penalties[item_id]

        return ratings

    def _get_rating(self, user_id, item_id):
        """Compute user's rating of item based on model.

        Parameters
        ----------
        user_id : int
            The id of the user making the rating.
        item_id : int
            The id of the item being rated.

        Returns
        -------
        rating : int
            The rating the item was given by the user.

        """
        raw_rating = (self._user_factors[user_id] @ self._item_factors[item_id]
                      + self._user_biases[user_id] + self._item_biases[item_id] + self._offset)

        # Compute the boredom penalty.
        boredom_penalty = 0
        for item_id_hist in self._user_histories[user_id]:
            item_factor = self._item_factors[item_id_hist]
            if item_factor is not None:
                similarity = ((self._item_factors[item_id] @ item_factor)
                              / np.linalg.norm(item_factor)
                              / np.linalg.norm(self._item_factors[item_id]))
                if similarity > self._boredom_threshold:
                    boredom_penalty += (similarity - self._boredom_threshold)
        boredom_penalty *= self._boredom_penalty
        rating = np.clip(raw_rating - boredom_penalty + self._dynamics_random.randn() *
                         self._noise, 1, 5)

        return rating

    def _rate_item(self, user_id, item_id):
        """Get a user to rate an item and update the internal rating state.

        Parameters
        ----------
        user_id : int
            The id of the user making the rating.
        item_id : int
            The id of the item being rated.

        Returns
        -------
        rating : int
            The rating the item was given by the user.

        """
        rating = self._get_rating(user_id, item_id)

        # Updating underlying affinity
        self._user_factors[user_id] = ((1.0 - self._affinity_change) * self._user_factors[user_id]
                                       + self._affinity_change * self._item_factors[item_id])
        return rating

    def _reset_state(self):
        """Reset the state of the environment."""
        user_factors, user_bias, item_factors, item_bias, offset = self._generate_latent_factors()
        self._user_factors = user_factors
        self._user_biases = user_bias
        self._item_factors = item_factors
        self._item_biases = item_bias
        self._offset = offset

        self._users = collections.OrderedDict((user_id, np.zeros(0))
                                              for user_id in range(self._num_users))
        self._items = collections.OrderedDict((item_id, np.zeros(0))
                                              for item_id in range(self._num_items))

    def _generate_latent_factors(self):
        """Generate random latent factors."""
        # Initialization size determined such that ratings generally fall in 0-5 range
        factor_sd = np.sqrt(np.sqrt(0.5 / self._latent_dim))
        # User latent factors are normally distributed
        user_bias = self._init_random.normal(loc=0., scale=0.5, size=self._num_users)
        user_factors = self._init_random.normal(loc=0., scale=factor_sd,
                                                size=(self._num_users, self._latent_dim))
        # Item latent factors are normally distributed
        item_bias = self._init_random.normal(loc=0., scale=0.5, size=self._num_items)
        item_factors = self._init_random.normal(loc=0., scale=factor_sd,
                                                size=(self._num_items, self._latent_dim))
        # Shift up the mean
        offset = 3.0
        return user_factors, user_bias, item_factors, item_bias, offset


class DatasetLatentFactor(LatentFactorBehavior):
    """An environment where user behavior is based on a dataset.

    Latent factor model of behavior with parameters fit directly from full dataset.

    Parameters
    ----------
    name : str
        The name of the dataset. Must be one of: 'ml-100k', 'ml-10m', 'lastfm'.
    latent_dim : int
        Size of latent factors p, q.
    datapath : str
        The path to the directory containing datafiles
    force_retrain : bool
        Forces retraining the latent factor model
    max_num_users : int
        The maximum number of users for the environment, if not the number in the dataset.
    max_num_items : int
        The maximum number of items for the environment, if not the number in the dataset.

    """

    def __init__(self, name, latent_dim=128, datapath=data_utils.DATA_DIR, force_retrain=False,
                 max_num_users=np.inf, max_num_items=np.inf, **kwargs):
        """Create a ML100K Latent Factor environment."""
        self.dataset_name = name
        if name == 'ml-100k':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'ml-100k'))
            latent_dim = 100 if latent_dim is None else latent_dim
            self._full_num_users = 943
            self._full_num_items = 1682
            # These parameters are the result of tuning.
            reg = 0.1
            learn_rate = 0.005
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=100)
        elif name == 'ml-10m':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'ml-10M100K'))
            latent_dim = 128 if latent_dim is None else latent_dim
            self._full_num_users = 69878
            self._full_num_items = 10677
            # these parameters are presented in "On the Difficulty of Baselines" by Rendle et al.
            reg = 0.04
            learn_rate = 0.003
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=128)
        elif name == 'lastfm':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'lastfm-dataset-1K'))
            latent_dim = 128 if latent_dim is None else latent_dim
            self._full_num_users = 992
            self._full_num_items = 177023
            # These parameters are presented in "Recommendations and User Agency" by Dean et al.
            reg = 0.08
            learn_rate = 0.001
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=128)
        else:
            raise ValueError('dataset name not recognized')
        self._force_retrain = force_retrain

        num_users = min(self._full_num_users, max_num_users)
        num_items = min(self._full_num_items, max_num_items)

        super().__init__(latent_dim, num_users, num_items, **kwargs)

    @property
    def name(self):
        """Name of environment, used for saving."""
        return 'latent-{}'.format(self.dataset_name)

    def _generate_latent_factors(self):
        full_model_params = dict(num_user_features=0, num_item_features=0, num_rating_features=0,
                                 max_num_users=self._full_num_users,
                                 max_num_items=self._full_num_items,
                                 num_two_way_factors=self._latent_dim, **self.train_params)
        if self._num_users < self._full_num_users or self._num_items < self._full_num_items:
            reduced_num_users_items = (min(self._num_users, self._full_num_users),
                                       min(self._num_items, self._full_num_items))
        else:
            reduced_num_users_items = None
        # TODO: This is another source of randomness that isn't accounted for by our seeds.
        # We probably want to make it more obvious that we need to snapshot the output when
        # we want consistency across experiments.
        return generate_latent_factors_from_data(self.dataset_name, self.datapath,
                                                 full_model_params, self._init_random,
                                                 force_retrain=self._force_retrain,
                                                 reduced_num_users_items=reduced_num_users_items)


def generate_latent_factors_from_data(dataset_name, datapath, params, random,
                                      force_retrain=False, reduced_num_users_items=None):
    """Create latent factors based on a dataset."""
    model_file = os.path.join(datapath, 'fm_model.npz')
    if not os.path.isfile(model_file) or force_retrain:
        print('Did not find model file at {}, loading data for training'.format(model_file))

        users, items, ratings = data_utils.read_dataset(dataset_name)
        print('Initializing latent factor model')
        recommender = LibFM(**params)
        recommender.reset(users, items, ratings)
        print('Training latent factor model with parameters: {}'.format(params))

        global_bias, weights, pairwise_interactions = recommender.model_parameters()
        if len(weights) == 0:
            weights = np.zeros(pairwise_interactions.shape[0])

        # TODO: this logic is only correct if there are no additional user/item/rating features
        # Note that we discard the original data's user_ids and item_ids at this step
        user_indices = np.arange(params['max_num_users'])
        item_indices = np.arange(params['max_num_users'],
                                 params['max_num_users'] + params['max_num_items'])

        user_factors = pairwise_interactions[user_indices]
        user_bias = weights[user_indices]
        item_factors = pairwise_interactions[item_indices]
        item_bias = weights[item_indices]
        offset = global_bias
        params = json.dumps(recommender.hyperparameters)

        np.savez(model_file, user_factors=user_factors, user_bias=user_bias,
                 item_factors=item_factors, item_bias=item_bias, offset=offset,
                 params=params)

    else:
        model = np.load(model_file)
        print('Loading model from {} trained via:\n{}.'.format(model_file, model['params']))

        user_factors = model['user_factors']
        user_bias = model['user_bias']
        item_factors = model['item_factors']
        item_bias = model['item_bias']
        offset = model['offset']

    if reduced_num_users_items is not None:
        num_users, num_items = reduced_num_users_items
        # TODO: may want to reduce the number in some other way
        # e.g. related to popularity
        user_indices = random.choice(user_factors.shape[0], size=num_users,
                                     replace=False)
        item_indices = random.choice(item_factors.shape[0], size=num_items,
                                     replace=False)
        user_factors = user_factors[user_indices]
        user_bias = user_bias[user_indices]
        item_factors = item_factors[item_indices]
        item_bias = item_bias[item_indices]

    return user_factors, user_bias, item_factors, item_bias, offset

Functions

def generate_latent_factors_from_data(dataset_name, datapath, params, random, force_retrain=False, reduced_num_users_items=None)

Create latent factors based on a dataset.

Expand source code
def generate_latent_factors_from_data(dataset_name, datapath, params, random,
                                      force_retrain=False, reduced_num_users_items=None):
    """Create latent factors based on a dataset."""
    model_file = os.path.join(datapath, 'fm_model.npz')
    if not os.path.isfile(model_file) or force_retrain:
        print('Did not find model file at {}, loading data for training'.format(model_file))

        users, items, ratings = data_utils.read_dataset(dataset_name)
        print('Initializing latent factor model')
        recommender = LibFM(**params)
        recommender.reset(users, items, ratings)
        print('Training latent factor model with parameters: {}'.format(params))

        global_bias, weights, pairwise_interactions = recommender.model_parameters()
        if len(weights) == 0:
            weights = np.zeros(pairwise_interactions.shape[0])

        # TODO: this logic is only correct if there are no additional user/item/rating features
        # Note that we discard the original data's user_ids and item_ids at this step
        user_indices = np.arange(params['max_num_users'])
        item_indices = np.arange(params['max_num_users'],
                                 params['max_num_users'] + params['max_num_items'])

        user_factors = pairwise_interactions[user_indices]
        user_bias = weights[user_indices]
        item_factors = pairwise_interactions[item_indices]
        item_bias = weights[item_indices]
        offset = global_bias
        params = json.dumps(recommender.hyperparameters)

        np.savez(model_file, user_factors=user_factors, user_bias=user_bias,
                 item_factors=item_factors, item_bias=item_bias, offset=offset,
                 params=params)

    else:
        model = np.load(model_file)
        print('Loading model from {} trained via:\n{}.'.format(model_file, model['params']))

        user_factors = model['user_factors']
        user_bias = model['user_bias']
        item_factors = model['item_factors']
        item_bias = model['item_bias']
        offset = model['offset']

    if reduced_num_users_items is not None:
        num_users, num_items = reduced_num_users_items
        # TODO: may want to reduce the number in some other way
        # e.g. related to popularity
        user_indices = random.choice(user_factors.shape[0], size=num_users,
                                     replace=False)
        item_indices = random.choice(item_factors.shape[0], size=num_items,
                                     replace=False)
        user_factors = user_factors[user_indices]
        user_bias = user_bias[user_indices]
        item_factors = item_factors[item_indices]
        item_bias = item_bias[item_indices]

    return user_factors, user_bias, item_factors, item_bias, offset

Classes

class DatasetLatentFactor (name, latent_dim=128, datapath='/home/sarah/recsys/recsys-eval/reclab/../data', force_retrain=False, max_num_users=inf, max_num_items=inf, **kwargs)

An environment where user behavior is based on a dataset.

Latent factor model of behavior with parameters fit directly from full dataset.

Parameters

name : str
The name of the dataset. Must be one of: 'ml-100k', 'ml-10m', 'lastfm'.
latent_dim : int
Size of latent factors p, q.
datapath : str
The path to the directory containing datafiles
force_retrain : bool
Forces retraining the latent factor model
max_num_users : int
The maximum number of users for the environment, if not the number in the dataset.
max_num_items : int
The maximum number of items for the environment, if not the number in the dataset.

Create a ML100K Latent Factor environment.

Expand source code
class DatasetLatentFactor(LatentFactorBehavior):
    """An environment where user behavior is based on a dataset.

    Latent factor model of behavior with parameters fit directly from full dataset.

    Parameters
    ----------
    name : str
        The name of the dataset. Must be one of: 'ml-100k', 'ml-10m', 'lastfm'.
    latent_dim : int
        Size of latent factors p, q.
    datapath : str
        The path to the directory containing datafiles
    force_retrain : bool
        Forces retraining the latent factor model
    max_num_users : int
        The maximum number of users for the environment, if not the number in the dataset.
    max_num_items : int
        The maximum number of items for the environment, if not the number in the dataset.

    """

    def __init__(self, name, latent_dim=128, datapath=data_utils.DATA_DIR, force_retrain=False,
                 max_num_users=np.inf, max_num_items=np.inf, **kwargs):
        """Create a ML100K Latent Factor environment."""
        self.dataset_name = name
        if name == 'ml-100k':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'ml-100k'))
            latent_dim = 100 if latent_dim is None else latent_dim
            self._full_num_users = 943
            self._full_num_items = 1682
            # These parameters are the result of tuning.
            reg = 0.1
            learn_rate = 0.005
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=100)
        elif name == 'ml-10m':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'ml-10M100K'))
            latent_dim = 128 if latent_dim is None else latent_dim
            self._full_num_users = 69878
            self._full_num_items = 10677
            # these parameters are presented in "On the Difficulty of Baselines" by Rendle et al.
            reg = 0.04
            learn_rate = 0.003
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=128)
        elif name == 'lastfm':
            self.datapath = os.path.expanduser(os.path.join(datapath, 'lastfm-dataset-1K'))
            latent_dim = 128 if latent_dim is None else latent_dim
            self._full_num_users = 992
            self._full_num_items = 177023
            # These parameters are presented in "Recommendations and User Agency" by Dean et al.
            reg = 0.08
            learn_rate = 0.001
            self.train_params = dict(bias_reg=reg, one_way_reg=reg, two_way_reg=reg,
                                     learning_rate=learn_rate, num_iter=128)
        else:
            raise ValueError('dataset name not recognized')
        self._force_retrain = force_retrain

        num_users = min(self._full_num_users, max_num_users)
        num_items = min(self._full_num_items, max_num_items)

        super().__init__(latent_dim, num_users, num_items, **kwargs)

    @property
    def name(self):
        """Name of environment, used for saving."""
        return 'latent-{}'.format(self.dataset_name)

    def _generate_latent_factors(self):
        full_model_params = dict(num_user_features=0, num_item_features=0, num_rating_features=0,
                                 max_num_users=self._full_num_users,
                                 max_num_items=self._full_num_items,
                                 num_two_way_factors=self._latent_dim, **self.train_params)
        if self._num_users < self._full_num_users or self._num_items < self._full_num_items:
            reduced_num_users_items = (min(self._num_users, self._full_num_users),
                                       min(self._num_items, self._full_num_items))
        else:
            reduced_num_users_items = None
        # TODO: This is another source of randomness that isn't accounted for by our seeds.
        # We probably want to make it more obvious that we need to snapshot the output when
        # we want consistency across experiments.
        return generate_latent_factors_from_data(self.dataset_name, self.datapath,
                                                 full_model_params, self._init_random,
                                                 force_retrain=self._force_retrain,
                                                 reduced_num_users_items=reduced_num_users_items)

Ancestors

Inherited members

class LatentFactorBehavior (latent_dim, num_users, num_items, rating_frequency=0.02, num_init_ratings=0, noise=0.0, memory_length=0, affinity_change=0.0, boredom_threshold=0, boredom_penalty=0.0, user_dist_choice='uniform')

An environment where users and items have latent factors and biases.

Ratings are generated as r = clip( + b_u + b_i + b_0 ) where p_u is a user's latent factor, q_i is an item's latent factor, b_u is a user bias, b_i is an item bias, and b_0 is a global bias.

Parameters

latent_dim : int
Size of latent factors p, q.
num_users : int
The number of users in the environment.
num_items : int
The number of items in the environment.
rating_frequency : float
The proportion of users that will need a recommendation at each step. Must be between 0 and 1.
num_init_ratings : int
The number of ratings available from the start. User-item pairs are randomly selected.
noise : float
The standard deviation of the noise added to ratings.
affinity_change : float
How much the user's latent factor is shifted towards that of an item.
memory_length : int
The number of recent items a user remembers which affect the rating.
boredom_threshold : int
The size of the inner product between a new item and an item in the user's history to trigger a boredom response.
boredom_penalty : float
The factor on the penalty on the rating when a user is bored. The penalty is the average of the values which exceed the boredom_threshold, and the decrease in rating is the penalty multiplied by this factor.
user_dist_choice : str
The choice of user distribution for selecting online users. By default, the subset of online users is chosen from a uniform distribution. Currently supports normal and lognormal.

Create a Latent Factor environment.

Expand source code
class LatentFactorBehavior(environment.DictEnvironment):
    """An environment where users and items have latent factors and biases.

    Ratings are generated as
    r = clip( <p_u, q_i> + b_u + b_i + b_0 )
    where p_u is a user's latent factor, q_i is an item's latent factor,
    b_u is a user bias, b_i is an item bias, and b_0 is a global bias.

    Parameters
    ----------
    latent_dim : int
        Size of latent factors p, q.
    num_users : int
        The number of users in the environment.
    num_items : int
        The number of items in the environment.
    rating_frequency : float
        The proportion of users that will need a recommendation at each step.
        Must be between 0 and 1.
    num_init_ratings : int
        The number of ratings available from the start. User-item pairs are randomly selected.
    noise : float
        The standard deviation of the noise added to ratings.
    affinity_change : float
        How much the user's latent factor is shifted towards that of an item.
    memory_length : int
        The number of recent items a user remembers which affect the rating.
    boredom_threshold : int
        The size of the inner product between a new item and an item in the
        user's history to trigger a boredom response.
    boredom_penalty : float
        The factor on the penalty on the rating when a user is bored. The penalty
        is the average of the values which exceed the boredom_threshold, and the decrease
        in rating is the penalty multiplied by this factor.
    user_dist_choice : str
        The choice of user distribution for selecting online users. By default, the subset of
        online users is chosen from a uniform distribution. Currently supports normal and lognormal.

    """

    def __init__(self, latent_dim, num_users, num_items,
                 rating_frequency=0.02, num_init_ratings=0,
                 noise=0.0, memory_length=0, affinity_change=0.0,
                 boredom_threshold=0, boredom_penalty=0.0, user_dist_choice='uniform'):
        """Create a Latent Factor environment."""
        super().__init__(rating_frequency, num_init_ratings, memory_length, user_dist_choice)
        self._latent_dim = latent_dim
        self._num_users = num_users
        self._num_items = num_items
        self._noise = noise
        self._affinity_change = affinity_change
        self._boredom_threshold = boredom_threshold
        self._boredom_penalty = boredom_penalty
        if self._memory_length > 0:
            self._boredom_penalty /= self._memory_length
        self._user_factors = None
        self._user_biases = None
        self._item_factors = None
        self._item_biases = None
        self._offset = None

    @property
    def name(self):
        """Name of environment, used for saving."""
        return 'latent'

    def _get_dense_ratings(self):  # noqa: D102
        ratings = (self._user_factors @ self._item_factors.T + self._user_biases[:, np.newaxis] +
                   self._item_biases[np.newaxis, :] + self._offset)
        # Compute the boredom penalties.
        item_norms = np.linalg.norm(self._item_factors, axis=1)
        normalized_items = self._item_factors / item_norms[:, np.newaxis]
        similarities = normalized_items @ normalized_items.T
        similarities -= self._boredom_threshold
        similarities[similarities < 0] = 0
        penalties = self._boredom_penalty * similarities
        for user_id in range(self._num_users):
            for item_id in self._user_histories[user_id]:
                if item_id is not None:
                    ratings[user_id] -= penalties[item_id]

        return ratings

    def _get_rating(self, user_id, item_id):
        """Compute user's rating of item based on model.

        Parameters
        ----------
        user_id : int
            The id of the user making the rating.
        item_id : int
            The id of the item being rated.

        Returns
        -------
        rating : int
            The rating the item was given by the user.

        """
        raw_rating = (self._user_factors[user_id] @ self._item_factors[item_id]
                      + self._user_biases[user_id] + self._item_biases[item_id] + self._offset)

        # Compute the boredom penalty.
        boredom_penalty = 0
        for item_id_hist in self._user_histories[user_id]:
            item_factor = self._item_factors[item_id_hist]
            if item_factor is not None:
                similarity = ((self._item_factors[item_id] @ item_factor)
                              / np.linalg.norm(item_factor)
                              / np.linalg.norm(self._item_factors[item_id]))
                if similarity > self._boredom_threshold:
                    boredom_penalty += (similarity - self._boredom_threshold)
        boredom_penalty *= self._boredom_penalty
        rating = np.clip(raw_rating - boredom_penalty + self._dynamics_random.randn() *
                         self._noise, 1, 5)

        return rating

    def _rate_item(self, user_id, item_id):
        """Get a user to rate an item and update the internal rating state.

        Parameters
        ----------
        user_id : int
            The id of the user making the rating.
        item_id : int
            The id of the item being rated.

        Returns
        -------
        rating : int
            The rating the item was given by the user.

        """
        rating = self._get_rating(user_id, item_id)

        # Updating underlying affinity
        self._user_factors[user_id] = ((1.0 - self._affinity_change) * self._user_factors[user_id]
                                       + self._affinity_change * self._item_factors[item_id])
        return rating

    def _reset_state(self):
        """Reset the state of the environment."""
        user_factors, user_bias, item_factors, item_bias, offset = self._generate_latent_factors()
        self._user_factors = user_factors
        self._user_biases = user_bias
        self._item_factors = item_factors
        self._item_biases = item_bias
        self._offset = offset

        self._users = collections.OrderedDict((user_id, np.zeros(0))
                                              for user_id in range(self._num_users))
        self._items = collections.OrderedDict((item_id, np.zeros(0))
                                              for item_id in range(self._num_items))

    def _generate_latent_factors(self):
        """Generate random latent factors."""
        # Initialization size determined such that ratings generally fall in 0-5 range
        factor_sd = np.sqrt(np.sqrt(0.5 / self._latent_dim))
        # User latent factors are normally distributed
        user_bias = self._init_random.normal(loc=0., scale=0.5, size=self._num_users)
        user_factors = self._init_random.normal(loc=0., scale=factor_sd,
                                                size=(self._num_users, self._latent_dim))
        # Item latent factors are normally distributed
        item_bias = self._init_random.normal(loc=0., scale=0.5, size=self._num_items)
        item_factors = self._init_random.normal(loc=0., scale=factor_sd,
                                                size=(self._num_items, self._latent_dim))
        # Shift up the mean
        offset = 3.0
        return user_factors, user_bias, item_factors, item_bias, offset

Ancestors

Subclasses

Inherited members