{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp models.tf.bpr"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# BPR\n",
    "> Bayesian Personalized Ranking (BPR)\n",
    "\n",
    "In matrix factorization (MF), to compute the prediction we have to multiply the user factors to the item factors:\n",
    "\n",
    "$$\\hat{x}_{ui} = \\langle w_u,h_i \\rangle = \\sum_{f=1}^k w_{uf} \\cdot h_{if}$$\n",
    "\n",
    "The usual approach for item recommenders is to predict a personalized score $\\hat{x}_{ui}$ for an item that reflects the preference of the user for the item. Then the items are ranked by sorting them according to that score. Machine learning approaches are typically fit by using observed items as a positive sample and missing ones for the negative class. A perfect model would thus be useless, as it would classify as negative (non-interesting) all the items that were non-observed at training time. The only reason why such methods work is regularization.\n",
    "\n",
    "BPR use a different approach. The training dataset is composed by triplets $(u,i,j)$ representing that user $u$ is assumed to prefer $i$ over $j$. For an implicit dataset this means that $u$ observed $i$ but not $j$:\n",
    "\n",
    "$$D_S := \\{(u,i,j) \\mid i \\in I_u^+ \\wedge j \\in I \\setminus I_u^+\\}$$\n",
    "\n",
    "A machine learning model can be represented by a parameter vector $Θ$ which is found at fitting time. BPR wants to find the parameter vector that is most probable given the desired, but latent, preference structure $>_u$:\n",
    "\n",
    "$$\\begin{align} p(\\Theta \\mid >_u) \\propto p(>_u \\mid \\Theta)p(\\Theta) \\\\ \\prod_{u\\in U} p(>_u \\mid \\Theta) = \\dots = \\prod_{(u,i,j) \\in D_S} p(i >_u j \\mid \\Theta) \\end{align}$$\n",
    "\n",
    "The probability that a user really prefers item $i$ to item $j$ is defined as:\n",
    "\n",
    "$$\\begin{align} p(i >_u j \\mid \\Theta) := \\sigma(\\hat{x}_{uij}(\\Theta)) \\end{align}$$\n",
    "\n",
    "Where $σ$ represent the logistic sigmoid and $\\hat{x}_{uij}(Θ)$ is an arbitrary real-valued function of $Θ$ (the output of your arbitrary model).\n",
    "\n",
    "User $u_1$ has interacted with item $i_2$ but not item $i_1$, so we assume that this user prefers item $i_2$ over $i_1$. For items that the user have both interacted with, we cannot infer any preference. The same is true for two items that a user has not interacted yet (e.g. item $i_1$ and $i_4$ for user $u_1$).\n",
    "\n",
    "![https://github.com/RecoHut-Stanzas/S021355/raw/main/images/img0.png](https://github.com/RecoHut-Stanzas/S021355/raw/main/images/img0.png)\n",
    "\n",
    "To complete the Bayesian setting, we define a prior density for the parameters:\n",
    "\n",
    "$$p(\\Theta) \\sim N(0, \\Sigma_\\Theta)$$\n",
    "\n",
    "And we can now formulate the maximum posterior estimator:\n",
    "\n",
    "$$\\begin{equation}\n",
    "\\begin{split}   BPR-OPT &=\\log p(\\Theta \\mid >_u)\\\\\n",
    "      &=\\log p(>_u \\mid \\Theta) p(\\Theta) \\\\ &= \\log \\prod_{(u,i,j) \\in D_S} \\sigma(\\hat{x}_{uij})p(\\Theta) \\\\ &= \\sum_{(u,i,j) \\in D_S} \\log \\sigma(\\hat{x}_{uij}) + \\log p(\\Theta) \\\\ &= \\sum_{(u,i,j) \\in D_S} \\log \\sigma(\\hat{x}_{uij}) - \\lambda_\\Theta ||\\Theta||^2\n",
    "\\end{split}\n",
    "\\end{equation} $$\n",
    "\n",
    "Where $λ_Θ$ are model specific regularization parameters.\n",
    "\n",
    "Once obtained the log-likelihood, we need to maximize it in order to find our optimal $Θ$. As the criterion is differentiable, gradient descent algorithms are an obvious choiche for maximization.\n",
    "\n",
    "The basic version of gradient descent consists in evaluating the gradient using all the available samples and then perform a single update. The problem with this is, in our case, that our training dataset is very skewed. Suppose an item $i$ is very popular. Then we have many terms of the form $\\hat{x}_{uij}$ in the loss because for many users $u$ the item $i$ is compared against all negative items $j$. The other popular approach is stochastic gradient descent, where for each training sample an update is performed. This is a better approach, but the order in which the samples are traversed is crucial. To solve this issue BPR uses a stochastic gradient descent algorithm that chooses the triples randomly.\n",
    "\n",
    "The gradient of BPR-OPT with respect to the model parameters is:\n",
    "\n",
    "$$\\begin{equation}\n",
    "\\begin{split}   \\frac{\\partial BPR-OPT}{\\partial \\Theta} &= \\sum_{(u,i,j) \\in D_S} \\frac{\\partial}{\\partial \\Theta} \\log \\sigma (\\hat{x}_{uij}) - \\lambda_\\Theta \\frac{\\partial}{\\partial\\Theta} || \\Theta ||^2\\\\\n",
    "      &=  \\sum_{(u,i,j) \\in D_S} \\frac{-e^{-\\hat{x}_{uij}}}{1+e^{-\\hat{x}_{uij}}} \\frac{\\partial}{\\partial \\Theta}\\hat{x}_{uij} - \\lambda_\\Theta \\Theta\n",
    "\\end{split}\n",
    "\\end{equation} $$\n",
    "\n",
    "In order to practically apply this learning schema to an existing algorithm, we first split the real valued preference term: $\\hat{x}_{uij} := \\hat{x}_{ui} − \\hat{x}_{uj}$. And now we can apply any standard collaborative filtering model that predicts $\\hat{x}_{ui}$.\n",
    "\n",
    "The problem of predicting $\\hat{x}_{ui}$ can be seen as the task of estimating a matrix $X:U×I$. With matrix factorization the target matrix $X$ is approximated by the matrix product of two low-rank matrices $W:|U|×k$ and $H:|I|×k$:\n",
    "\n",
    "$$X := WH^t$$\n",
    "\n",
    "The prediction formula can also be written as:\n",
    "\n",
    "$$\\hat{x}_{ui} = \\langle w_u,h_i \\rangle = \\sum_{f=1}^k w_{uf} \\cdot h_{if}$$\n",
    "\n",
    "Besides the dot product ⟨⋅,⋅⟩, in general any kernel can be used.\n",
    "\n",
    "We can now specify the derivatives:\n",
    "\n",
    "$$\\frac{\\partial}{\\partial \\theta} \\hat{x}_{uij} = \\begin{cases}\n",
    "(h_{if} - h_{jf}) \\text{ if } \\theta=w_{uf}, \\\\\n",
    "w_{uf} \\text{ if } \\theta = h_{if}, \\\\\n",
    "-w_{uf} \\text{ if } \\theta = h_{jf}, \\\\\n",
    "0 \\text{ else }\n",
    "\\end{cases}$$\n",
    "\n",
    "Which basically means: user $u$ prefer $i$ over $j$, let's do the following:\n",
    "\n",
    "- Increase the relevance (according to $u$) of features belonging to $i$ but not to $j$ and vice-versa\n",
    "- Increase the relevance of features assigned to $i$\n",
    "- Decrease the relevance of features assigned to $j$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "from nbdev.showdoc import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "import tensorflow as tf\n",
    "from tensorflow.keras import Model\n",
    "from tensorflow.keras.layers import Embedding, Input\n",
    "from tensorflow.keras.regularizers import l2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class BPR(Model):\n",
    "    def __init__(self, feature_columns, mode='inner', embed_reg=1e-6):\n",
    "        \"\"\"\n",
    "        BPR\n",
    "        :param feature_columns: A list. user feature columns + item feature columns\n",
    "        :mode: A string. 'inner' or 'dist'.\n",
    "        :param embed_reg: A scalar.  The regularizer of embedding.\n",
    "        \"\"\"\n",
    "        super(BPR, self).__init__()\n",
    "        # feature columns\n",
    "        self.user_fea_col, self.item_fea_col = feature_columns\n",
    "        # mode\n",
    "        self.mode = mode\n",
    "        # user embedding\n",
    "        self.user_embedding = Embedding(input_dim=self.user_fea_col['feat_num'],\n",
    "                                        input_length=1,\n",
    "                                        output_dim=self.user_fea_col['embed_dim'],\n",
    "                                        mask_zero=False,\n",
    "                                        embeddings_initializer='random_normal',\n",
    "                                        embeddings_regularizer=l2(embed_reg))\n",
    "        # item embedding\n",
    "        self.item_embedding = Embedding(input_dim=self.item_fea_col['feat_num'],\n",
    "                                        input_length=1,\n",
    "                                        output_dim=self.item_fea_col['embed_dim'],\n",
    "                                        mask_zero=True,\n",
    "                                        embeddings_initializer='random_normal',\n",
    "                                        embeddings_regularizer=l2(embed_reg))\n",
    "\n",
    "    def call(self, inputs):\n",
    "        user_inputs, pos_inputs, neg_inputs = inputs  # (None, 1), (None, 1)\n",
    "        # user info\n",
    "        user_embed = self.user_embedding(user_inputs)  # (None, 1, dim)\n",
    "        # item\n",
    "        pos_embed = self.item_embedding(pos_inputs)  # (None, 1, dim)\n",
    "        neg_embed = self.item_embedding(neg_inputs)  # (None, 1, dim)\n",
    "        if self.mode == 'inner':\n",
    "            # calculate positive item scores and negative item scores\n",
    "            pos_scores = tf.reduce_sum(tf.multiply(user_embed, pos_embed), axis=-1)  # (None, 1)\n",
    "            neg_scores = tf.reduce_sum(tf.multiply(user_embed, neg_embed), axis=-1)  # (None, 1)\n",
    "            # add loss. Computes softplus: log(exp(features) + 1)\n",
    "            # self.add_loss(tf.reduce_mean(tf.math.softplus(neg_scores - pos_scores)))\n",
    "            self.add_loss(tf.reduce_mean(-tf.math.log(tf.nn.sigmoid(pos_scores - neg_scores))))\n",
    "        else:\n",
    "            # clip by norm\n",
    "            # user_embed = tf.clip_by_norm(user_embed, 1, -1)\n",
    "            # pos_embed = tf.clip_by_norm(pos_embed, 1, -1)\n",
    "            # neg_embed = tf.clip_by_norm(neg_embed, 1, -1)\n",
    "            pos_scores = tf.reduce_sum(tf.square(user_embed - pos_embed), axis=-1)\n",
    "            neg_scores = tf.reduce_sum(tf.square(user_embed - neg_embed), axis=-1)\n",
    "            self.add_loss(tf.reduce_sum(tf.nn.relu(pos_scores - neg_scores + 0.5)))\n",
    "        logits = tf.concat([pos_scores, neg_scores], axis=-1)\n",
    "        return logits\n",
    "\n",
    "    def summary(self):\n",
    "        user_inputs = Input(shape=(1, ), dtype=tf.int32)\n",
    "        pos_inputs = Input(shape=(1, ), dtype=tf.int32)\n",
    "        neg_inputs = Input(shape=(1, ), dtype=tf.int32)\n",
    "        Model(inputs=[user_inputs, pos_inputs, neg_inputs],\n",
    "            outputs=self.call([user_inputs, pos_inputs, neg_inputs])).summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "feature_columns = [{'embed_dim': 4, 'feat': 'user_id', 'feat_num': 5},\n",
    "                   {'embed_dim': 4, 'feat': 'item_id', 'feat_num': 5}]\n",
    "\n",
    "model = BPR(feature_columns)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"model\"\n",
      "__________________________________________________________________________________________________\n",
      " Layer (type)                   Output Shape         Param #     Connected to                     \n",
      "==================================================================================================\n",
      " input_1 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " input_2 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " input_3 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " embedding (Embedding)          (None, 1, 4)         20          ['input_1[0][0]']                \n",
      "                                                                                                  \n",
      " embedding_1 (Embedding)        (None, 1, 4)         20          ['input_2[0][0]',                \n",
      "                                                                  'input_3[0][0]']                \n",
      "                                                                                                  \n",
      " tf.math.multiply (TFOpLambda)  (None, 1, 4)         0           ['embedding[0][0]',              \n",
      "                                                                  'embedding_1[0][0]']            \n",
      "                                                                                                  \n",
      " tf.math.multiply_1 (TFOpLambda  (None, 1, 4)        0           ['embedding[0][0]',              \n",
      " )                                                                'embedding_1[1][0]']            \n",
      "                                                                                                  \n",
      " tf.math.reduce_sum (TFOpLambda  (None, 1)           0           ['tf.math.multiply[0][0]']       \n",
      " )                                                                                                \n",
      "                                                                                                  \n",
      " tf.math.reduce_sum_1 (TFOpLamb  (None, 1)           0           ['tf.math.multiply_1[0][0]']     \n",
      " da)                                                                                              \n",
      "                                                                                                  \n",
      " tf.concat (TFOpLambda)         (None, 2)            0           ['tf.math.reduce_sum[0][0]',     \n",
      "                                                                  'tf.math.reduce_sum_1[0][0]']   \n",
      "                                                                                                  \n",
      "==================================================================================================\n",
      "Total params: 40\n",
      "Trainable params: 40\n",
      "Non-trainable params: 0\n",
      "__________________________________________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Author: Sparsh A.\n",
      "\n",
      "Last updated: 2021-12-20 08:44:01\n",
      "\n",
      "Compiler    : GCC 7.5.0\n",
      "OS          : Linux\n",
      "Release     : 5.4.104+\n",
      "Machine     : x86_64\n",
      "Processor   : x86_64\n",
      "CPU cores   : 2\n",
      "Architecture: 64bit\n",
      "\n",
      "tensorflow: 2.7.0\n",
      "numpy     : 1.19.5\n",
      "IPython   : 5.5.0\n",
      "pandas    : 1.1.5\n",
      "\n"
     ]
    }
   ],
   "source": [
    "#hide\n",
    "!pip install -q watermark\n",
    "%reload_ext watermark\n",
    "%watermark -a \"Sparsh A.\" -m -iv -u -t -d"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
