Creating Novel Recommenders

From scratch

DRecPy allows you to easily implement novel recommender models without the need to worry about data handling, id conversion, workflow management and weight updates.

Deep Learning-based Recommenders

To create a DL-based recommender you should create a class that extends the RecommenderABC class, and implements at least the required abstract methods: _pre_fit(), _sample_batch(), _predict_batch(), _compute_batch_loss() and _predict().

The _pre_fit() method should:

  • Create model structure (e.g. neural network layer structure);
  • Initialize model weights;
  • Register the model weights as trainable variables (using the _register_trainable(var) or _register_trainables(vars) method);

The _sample_batch() method should sample N data points from the training data and return them. This step can also be used if your model makes some custom data preprocessing during the training phase. The return value of this function is passed to the _predict_batch() method.

The _predict_batch() method should compute predictions for each of the sampled training data points. The return of this method should be a tuple containing a list of predictions and a list of desired values (in this exact order).

The _compute_batch_loss() method should compute and return the loss value associated with the given predictions and desired values. This loss must be differentiable with respect to all training variables, so that weight updates can be made.

Finally, the _predict() method should compute a single prediction, for the provided user id and item id.

Another method that can be overridden is the _compute_reg_loss(), which should compute and return the regularization loss value associated with the trainable variables. Its default implementation is only useful when the registered trainable variables are of type tf.keras.models.Model or tf.keras.layers.Layer, in which case the registered regularizers (e.g. via kernel_regularizer attribute of a layer) are added to the loss of the recommender for each batch.

If the _rank() method is not implemented, and if you call rank() on the new model, the _predict() method will be used to score each item. Similarly, if the _recommend() method is not implemented, and if you call recommend(), the rank() will also be used to rank all existent items and return the top N.

Usually, the _rank() and _recommend() methods are only implemented when there’s an alternative and more efficient way to compute these values, not relying on the _predict() method.

Here’s a basic example of a deep learning-based recommender, with only one trainable variable:

From file examples/custom_deep_recommender.py
from DRecPy.Recommender import RecommenderABC
from DRecPy.Sampler import PointSampler
import tensorflow as tf
from DRecPy.Dataset import get_train_dataset


class TestRecommender(RecommenderABC):

    def __init__(self, **kwds):
        super(TestRecommender, self).__init__(**kwds)

    def _pre_fit(self, learning_rate, neg_ratio, reg_rate, **kwds):
        # used to declare variables and the neural network structure of the model, as well as register trainable vars
        self._info(f'doing pre-fit with learning_rate={learning_rate}, neg_ratio={neg_ratio}, reg_rate={reg_rate}')
        self._weights = tf.Variable([[0.5], [0.5]])
        self._register_trainable(self._weights)
        self._loss = tf.losses.BinaryCrossentropy()
        self._sampler = PointSampler(self.interaction_dataset, neg_ratio=neg_ratio)

    def _sample_batch(self, batch_size, **kwds):
        self._info(f'doing _sample_batch {batch_size}')
        return self._sampler.sample(batch_size)

    def _predict_batch(self, batch_samples, **kwds):
        # must return predictions from which gradients can be computed in order to update the registered trainable vars
        # and the desired values, so that we're able to compute the batch loss
        self._info(f'doing _predict_batch {batch_samples}')
        predictions = [self._predict(u, i) for u, i, _ in batch_samples]
        desired_values = [y for _, _, y in batch_samples]
        self._info(f'predictions = {predictions}, desired_values = {desired_values}')
        return predictions, desired_values

    def _compute_batch_loss(self, predictions, desired_values, **kwds):
        # receives the predictions and desired values computed during the _predict_batch, and should apply a loss
        # function from which gradients can then be computed
        self._info(f'doing _compute_batch_loss: predictions={predictions}, desired_values={desired_values}')
        return self._loss(desired_values, predictions)

    def _compute_reg_loss(self, reg_rate, batch_size, trainable_models, trainable_layers, trainable_weights, **kwds):
        self._info(f'doing _compute_reg_loss: reg_rate={reg_rate}, batch_size={batch_size}')
        return tf.nn.l2_loss(trainable_weights) * reg_rate / batch_size

    def _predict(self, uid, iid, **kwds):
        # predict for a (user, item) pair
        return tf.sigmoid(tf.matmul(tf.convert_to_tensor([[uid, iid]], dtype=tf.float32), self._weights))


ds_train = get_train_dataset('ml-100k', verbose=False)

print('TestRecommender')
recommender = TestRecommender(verbose=True)
recommender.fit(ds_train, epochs=2, batch_size=10)
print(recommender.predict(1, 1))

Some other important notes:

  • Do not forget to call the __init__() method of the super class, on the new model __init__();
  • The _pre_fit() method should register all trainable variables via calls to the _register_trainable(var) or _register_trainables(vars) methods;
  • The _compute_batch_loss() return value must be differentiable with respect to all trainable variables;
  • If your model depends on custom random processes, such as weight initialization or id sampling, always use random generators that are created using the self.seed attribute. A seeded random generator object is already provided via the self._rng attribute. If a seed argument is provided, both self._rng and all tensorflow.random methods are seeded with the given value.

Non-Deep Learning-based Recommenders

To create a non-DL-based recommender you should create a class that extends the RecommenderABC class, and implements at least the required abstract methods: _pre_fit(), _sample_batch(), _predict_batch(), _compute_batch_loss() and _predict().

Note that in this case, the implementation of the _sample_batch(), _predict_batch(), _compute_batch_loss() and _compute_reg_loss() is irrelevant, since they will never be called.

The _pre_fit() method should create the required data structures and do the computations to completely fit the model, because no batch training will be applied. In this case, trainable variables must not be registered, otherwise batch-training will proceed.

All other methods such as the _predict(), _rank() and _recommend(), follow the same guidelines.

An example of an implemented non-deep learning-based recommender, follows bellow:

From file examples/custom_non_deep_recommender.py
from DRecPy.Recommender import RecommenderABC
from DRecPy.Dataset import get_train_dataset


class TestRecommenderNonDeepLearning(RecommenderABC):

    def __init__(self, **kwds):
        super(TestRecommenderNonDeepLearning, self).__init__(**kwds)

    def _pre_fit(self, learning_rate, neg_ratio, reg_rate, **kwds):
        # used to declare variables and do the non-deep learning fit process, such as computing similarities and
        # neighbours for knn-based models
        self._info(f'doing pre-fit with learning_rate={learning_rate}, neg_ratio={neg_ratio}, reg_rate={reg_rate}')
        pass

    def _sample_batch(self, batch_size, **kwds):
        raise NotImplemented  # since it's non-deep learning based, no need for batch training

    def _predict_batch(self, batch_samples, **kwds):
        raise NotImplemented  # since it's non-deep learning based, no need for batch training

    def _compute_batch_loss(self, predictions, desired_values, **kwds):
        raise NotImplemented  # since it's non-deep learning based, no need for batch training

    def _predict(self, uid, iid, **kwds):
        return 5  # predict for a (user, item) pair


ds_train = get_train_dataset('ml-100k', verbose=False)

print('TestRecommenderNonDeepLearning')
recommender = TestRecommenderNonDeepLearning(verbose=True)
recommender.fit(ds_train, epochs=2, batch_size=10)
print(recommender.predict(1, 1))

Extending existing models

To extend an existent recommender, one should:

  • Create a subclass of the original recommender;
  • Override the __init__() method, by first calling the original recommender __init__(), followed by instructions with specific logic for the extended recommender;
  • If new weights are introduced by this extension, override the _pre_fit() method and call its original, followed by the initialization of the new weights and registering them as trainable variables (via _register_trainable(var) or _register_trainables(vars) method calls).
  • If there are changes to the batch sampling workflow, override the _sample_batch() method and call its original (if some of its logic can be reused), and then apply the custom logic;
  • When there are changes on the way predictions are made, override the _predict_batch() and call its original (if some of its logic can be reused), followed by adding the custom prediction logic to the existent predictions. Sometimes, if the original model has independent logic on the _predict() that does not use the _predict_batch(), you might need to override it too.
  • If there are changes to the way the predictive loss function is computed, override the _compute_batch_loss() and adapt it to return the new predictive loss value from the provided predictions and expected values.
  • Overriding the _compute_reg_loss() is only necessary in some situations. Make sure that:
    1. If the new model uses trainable variables of type tf.keras.models.Model or tf.keras.layers.Layer, and you’ve added their regularization using the regularization parameters of each layer (e.g. kerner_regularizer attribute), the RecommenderABC._compute_reg_loss() should always be called.
    2. If the new model uses trainable variables of type tf.Variable, override the _compute_reg_loss() to compute their regularization values. Note that if the original model uses trainable variables of type tf.keras.models.Model or tf.keras.layers.Layer, the RecommenderABC._compute_reg_loss() should always be called.

A very simple example of extending an existing model is shown bellow, which is a modification of the DMF (Deep Matrix Factorization) recommender:

From file examples/extending_recommender_dmf.py
from DRecPy.Recommender import DMF
import tensorflow as tf


class ModifiedDMF(DMF):
    def __init__(self, **kwds):
        super(ModifiedDMF, self).__init__(**kwds)

    def _pre_fit(self, learning_rate, neg_ratio, reg_rate, **kwds):
        super(ModifiedDMF, self)._pre_fit(learning_rate, neg_ratio, reg_rate, **kwds)
        self._extra_weight = tf.Variable([1.])
        self._register_trainable(self._extra_weight)

    def _predict_batch(self, batch_samples, **kwds):
        predictions, desired_values = super(ModifiedDMF, self)._predict_batch(batch_samples, **kwds)
        predictions = [(self._extra_weight * pred) for pred in predictions]
        return predictions, desired_values