{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2022-12-11 20:53:08.610267: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory\n",
      "2022-12-11 20:53:08.610294: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.\n"
     ]
    }
   ],
   "source": [
    "import gzip\n",
    "import random\n",
    "import scipy\n",
    "import tensorflow as tf\n",
    "from collections import defaultdict\n",
    "from implicit import bpr\n",
    "from surprise import SVD, Reader, Dataset\n",
    "from surprise.model_selection import train_test_split"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Data is available at http://cseweb.ucsd.edu/~jmcauley/pml/data/. Download and save to your own directory"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataDir = \"/home/jmcauley/pml_data/\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Latent factor model (Surprise)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Using the library's inbuilt data reader, extract tsv-formatted data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "reader = Reader(line_format='user item rating', sep='\\t')\n",
    "data = Dataset.load_from_file(dataDir + \"goodreads_fantasy.tsv\", reader=reader)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Standard latent-factor model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = SVD()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Inbuilt functions to split into training and test fractions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "trainset, testset = train_test_split(data, test_size=.25)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fit the model and extract predictions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.fit(trainset)\n",
    "predictions = model.test(testset)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Estimate for a single (test) rating"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "4.001383054002914"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "predictions[0].est"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "MSE for model predictions (test set)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.1877783834090485\n"
     ]
    }
   ],
   "source": [
    "sse = 0\n",
    "for p in predictions:\n",
    "    sse += (p.r_ui - p.est)**2\n",
    "\n",
    "print(sse / len(predictions))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bayesian Personalized Ranking (Implicit)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def parseData(fname):\n",
    "    for l in gzip.open(fname):\n",
    "        d = eval(l)\n",
    "        del d['review_text'] # Discard the reviews, to save memory when we don't use them\n",
    "        yield d"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Full dataset of Goodreads fantasy reviews (fairly memory-hungry, could be replaced by something smaller)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = list(parseData(dataDir + \"goodreads_reviews_fantasy_paranormal.json.gz\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "random.shuffle(data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Example from the dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'user_id': '29b4208f9c7fb6d173dfa7393d465d1d',\n",
       " 'book_id': '8087526',\n",
       " 'review_id': 'b2cb7fc6ccba961a3042b090f84a04cf',\n",
       " 'rating': 4,\n",
       " 'date_added': 'Sun Dec 28 10:56:38 -0800 2014',\n",
       " 'date_updated': 'Mon Feb 29 11:33:35 -0800 2016',\n",
       " 'read_at': 'Thu Jan 15 00:00:00 -0800 2015',\n",
       " 'started_at': 'Sun Dec 28 00:00:00 -0800 2014',\n",
       " 'n_votes': 2,\n",
       " 'n_comments': 2}"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Build a few utility data structures. Since we'll be converting the data to a sparse interaction matrix, the main structure here is to assign each user/item to an ID from 0 to nUsers/nItems."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "userIDs,itemIDs = {},{}\n",
    "\n",
    "for d in data:\n",
    "    u,i = d['user_id'],d['book_id']\n",
    "    if not u in userIDs: userIDs[u] = len(userIDs)\n",
    "    if not i in itemIDs: itemIDs[i] = len(itemIDs)\n",
    "\n",
    "nUsers,nItems = len(userIDs),len(itemIDs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(256088, 258212)"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "nUsers,nItems"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Convert dataset to sparse matrix. Only storing positive feedback instances (i.e., rated items)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "Xui = scipy.sparse.lil_matrix((nUsers, nItems))\n",
    "for d in data:\n",
    "    Xui[userIDs[d['user_id']],itemIDs[d['book_id']]] = 1\n",
    "    \n",
    "Xui_csr = scipy.sparse.csr_matrix(Xui)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bayesian Personalized Ranking model with 5 latent factors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = bpr.BayesianPersonalizedRanking(factors = 5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fit the model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "5054da3965a4453e86b932a04d61910d",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/100 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "model.fit(Xui_csr)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Get recommendations for a particular user (the first one) and to get items related to (similar latent factors) to a particular item"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "recommended = model.recommend(0, Xui_csr[0])\n",
    "related = model.similar_items(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array([    0, 50907,  4837, 31854, 13388, 57087, 66904, 21845, 28072,\n",
       "        23460], dtype=int32),\n",
       " array([1.        , 0.9951341 , 0.9949262 , 0.99489623, 0.9937351 ,\n",
       "        0.9932829 , 0.99264044, 0.9920761 , 0.9916638 , 0.99135584],\n",
       "       dtype=float32))"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "related"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Extract user and item factors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "itemFactors = model.item_factors\n",
    "userFactors = model.user_factors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([ 0.0347619 , -0.50331426, -1.475029  ,  1.2996856 , -0.8502873 ,\n",
       "       -0.65859324], dtype=float32)"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "itemFactors[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Latent factor model (Tensorflow)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "def parse(path):\n",
    "    g = gzip.open(path, 'r')\n",
    "    for l in g:\n",
    "        yield eval(l)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Goodreads comic book data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "userIDs = {}\n",
    "itemIDs = {}\n",
    "interactions = []\n",
    "\n",
    "for d in parse(dataDir + \"goodreads_reviews_comics_graphic.json.gz\"):\n",
    "    u = d['user_id']\n",
    "    i = d['book_id']\n",
    "    r = d['rating']\n",
    "    if not u in userIDs: userIDs[u] = len(userIDs)\n",
    "    if not i in itemIDs: itemIDs[i] = len(itemIDs)\n",
    "    interactions.append((u,i,r))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "542338"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "random.shuffle(interactions)\n",
    "len(interactions)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Split into train and test sets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "nTrain = int(len(interactions) * 0.9)\n",
    "nTest = len(interactions) - nTrain\n",
    "interactionsTrain = interactions[:nTrain]\n",
    "interactionsTest = interactions[nTrain:]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "itemsPerUser = defaultdict(list)\n",
    "usersPerItem = defaultdict(list)\n",
    "for u,i,r in interactionsTrain:\n",
    "    itemsPerUser[u].append(i)\n",
    "    usersPerItem[i].append(u)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mean rating, just for initialization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "mu = sum([r for _,_,r in interactionsTrain]) / len(interactionsTrain)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Gradient descent optimizer, could experiment with learning rate"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = tf.keras.optimizers.Adam(0.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Latent factor model tensorflow class"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LatentFactorModel(tf.keras.Model):\n",
    "    def __init__(self, mu, K, lamb):\n",
    "        super(LatentFactorModel, self).__init__()\n",
    "        # Initialize to average\n",
    "        self.alpha = tf.Variable(mu)\n",
    "        # Initialize to small random values\n",
    "        self.betaU = tf.Variable(tf.random.normal([len(userIDs)],stddev=0.001))\n",
    "        self.betaI = tf.Variable(tf.random.normal([len(itemIDs)],stddev=0.001))\n",
    "        self.gammaU = tf.Variable(tf.random.normal([len(userIDs),K],stddev=0.001))\n",
    "        self.gammaI = tf.Variable(tf.random.normal([len(itemIDs),K],stddev=0.001))\n",
    "        self.lamb = lamb\n",
    "\n",
    "    # Prediction for a single instance (useful for evaluation)\n",
    "    def predict(self, u, i):\n",
    "        p = self.alpha + self.betaU[u] + self.betaI[i] +\\\n",
    "            tf.tensordot(self.gammaU[u], self.gammaI[i], 1)\n",
    "        return p\n",
    "\n",
    "    # Regularizer\n",
    "    def reg(self):\n",
    "        return self.lamb * (tf.reduce_sum(self.betaU**2) +\\\n",
    "                            tf.reduce_sum(self.betaI**2) +\\\n",
    "                            tf.reduce_sum(self.gammaU**2) +\\\n",
    "                            tf.reduce_sum(self.gammaI**2))\n",
    "    \n",
    "    # Prediction for a sample of instances\n",
    "    def predictSample(self, sampleU, sampleI):\n",
    "        u = tf.convert_to_tensor(sampleU, dtype=tf.int32)\n",
    "        i = tf.convert_to_tensor(sampleI, dtype=tf.int32)\n",
    "        beta_u = tf.nn.embedding_lookup(self.betaU, u)\n",
    "        beta_i = tf.nn.embedding_lookup(self.betaI, i)\n",
    "        gamma_u = tf.nn.embedding_lookup(self.gammaU, u)\n",
    "        gamma_i = tf.nn.embedding_lookup(self.gammaI, i)\n",
    "        pred = self.alpha + beta_u + beta_i +\\\n",
    "               tf.reduce_sum(tf.multiply(gamma_u, gamma_i), 1)\n",
    "        return pred\n",
    "    \n",
    "    # Loss\n",
    "    def call(self, sampleU, sampleI, sampleR):\n",
    "        pred = self.predictSample(sampleU, sampleI)\n",
    "        r = tf.convert_to_tensor(sampleR, dtype=tf.float32)\n",
    "        return tf.nn.l2_loss(pred - r) / len(sampleR)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Initialize the model. Could experiment with number of factors and regularization rate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2022-12-11 21:00:32.352537: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352607: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcublas.so.11'; dlerror: libcublas.so.11: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352660: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcublasLt.so.11'; dlerror: libcublasLt.so.11: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352712: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcufft.so.10'; dlerror: libcufft.so.10: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352766: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcurand.so.10'; dlerror: libcurand.so.10: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352820: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcusolver.so.11'; dlerror: libcusolver.so.11: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352872: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcusparse.so.11'; dlerror: libcusparse.so.11: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352924: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudnn.so.8'; dlerror: libcudnn.so.8: cannot open shared object file: No such file or directory\n",
      "2022-12-11 21:00:32.352937: W tensorflow/core/common_runtime/gpu/gpu_device.cc:1850] Cannot dlopen some GPU libraries. Please make sure the missing libraries mentioned above are installed properly if you would like to use GPU. Follow the guide at https://www.tensorflow.org/install/gpu for how to download and setup the required libraries for your platform.\n",
      "Skipping registering GPU devices...\n"
     ]
    }
   ],
   "source": [
    "modelLFM = LatentFactorModel(mu, 5, 0.00001)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Training step (for the batch-based model from Chapter 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "def trainingStep(model, interactions):\n",
    "    Nsamples = 50000\n",
    "    with tf.GradientTape() as tape:\n",
    "        sampleU, sampleI, sampleR = [], [], []\n",
    "        for _ in range(Nsamples):\n",
    "            u,i,r = random.choice(interactions)\n",
    "            sampleU.append(userIDs[u])\n",
    "            sampleI.append(itemIDs[i])\n",
    "            sampleR.append(r)\n",
    "\n",
    "        loss = model(sampleU,sampleI,sampleR)\n",
    "        loss += model.reg()\n",
    "    gradients = tape.gradient(loss, model.trainable_variables)\n",
    "    optimizer.apply_gradients((grad, var) for\n",
    "                              (grad, var) in zip(gradients, model.trainable_variables)\n",
    "                              if grad is not None)\n",
    "    return loss.numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run 100 iterations (really 100 batches) of gradient descent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iteration 10, objective = 0.54607564\n",
      "iteration 20, objective = 0.5176252\n",
      "iteration 30, objective = 0.5195416\n",
      "iteration 40, objective = 0.52042997\n",
      "iteration 50, objective = 0.5179602\n",
      "iteration 60, objective = 0.5080225\n",
      "iteration 70, objective = 0.5091005\n",
      "iteration 80, objective = 0.5124716\n",
      "iteration 90, objective = 0.5148142\n",
      "iteration 100, objective = 0.5149693\n"
     ]
    }
   ],
   "source": [
    "for i in range(100):\n",
    "    obj = trainingStep(modelLFM, interactionsTrain)\n",
    "    if (i % 10 == 9): print(\"iteration \" + str(i+1) + \", objective = \" + str(obj))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Prediction for a particular user/item pair"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "u,i,r = interactionsTest[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "4.1900516"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "modelLFM.predict(userIDs[u], itemIDs[i]).numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bayesian personalized ranking (Tensorflow)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "items = list(itemIDs.keys())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Batch-based version from Chapter 5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BPRbatch(tf.keras.Model):\n",
    "    def __init__(self, K, lamb):\n",
    "        super(BPRbatch, self).__init__()\n",
    "        # Initialize variables\n",
    "        self.betaI = tf.Variable(tf.random.normal([len(itemIDs)],stddev=0.001))\n",
    "        self.gammaU = tf.Variable(tf.random.normal([len(userIDs),K],stddev=0.001))\n",
    "        self.gammaI = tf.Variable(tf.random.normal([len(itemIDs),K],stddev=0.001))\n",
    "        # Regularization coefficient\n",
    "        self.lamb = lamb\n",
    "\n",
    "    # Prediction for a single instance\n",
    "    def predict(self, u, i):\n",
    "        p = self.betaI[i] + tf.tensordot(self.gammaU[u], self.gammaI[i], 1)\n",
    "        return p\n",
    "\n",
    "    # Regularizer\n",
    "    def reg(self):\n",
    "        return self.lamb * (tf.nn.l2_loss(self.betaI) +\\\n",
    "                            tf.nn.l2_loss(self.gammaU) +\\\n",
    "                            tf.nn.l2_loss(self.gammaI))\n",
    "    \n",
    "    def score(self, sampleU, sampleI):\n",
    "        u = tf.convert_to_tensor(sampleU, dtype=tf.int32)\n",
    "        i = tf.convert_to_tensor(sampleI, dtype=tf.int32)\n",
    "        beta_i = tf.nn.embedding_lookup(self.betaI, i)\n",
    "        gamma_u = tf.nn.embedding_lookup(self.gammaU, u)\n",
    "        gamma_i = tf.nn.embedding_lookup(self.gammaI, i)\n",
    "        x_ui = beta_i + tf.reduce_sum(tf.multiply(gamma_u, gamma_i), 1)\n",
    "        return x_ui\n",
    "\n",
    "    def call(self, sampleU, sampleI, sampleJ):\n",
    "        x_ui = self.score(sampleU, sampleI)\n",
    "        x_uj = self.score(sampleU, sampleJ)\n",
    "        return -tf.reduce_mean(tf.math.log(tf.math.sigmoid(x_ui - x_uj)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = tf.keras.optimizers.Adam(0.1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "modelBPR = BPRbatch(5, 0.00001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "def trainingStepBPR(model, interactions):\n",
    "    Nsamples = 50000\n",
    "    with tf.GradientTape() as tape:\n",
    "        sampleU, sampleI, sampleJ = [], [], []\n",
    "        for _ in range(Nsamples):\n",
    "            u,i,_ = random.choice(interactions) # positive sample\n",
    "            j = random.choice(items) # negative sample\n",
    "            while j in itemsPerUser[u]:\n",
    "                j = random.choice(items)\n",
    "            sampleU.append(userIDs[u])\n",
    "            sampleI.append(itemIDs[i])\n",
    "            sampleJ.append(itemIDs[j])\n",
    "\n",
    "        loss = model(sampleU,sampleI,sampleJ)\n",
    "        loss += model.reg()\n",
    "    gradients = tape.gradient(loss, model.trainable_variables)\n",
    "    optimizer.apply_gradients((grad, var) for\n",
    "                              (grad, var) in zip(gradients, model.trainable_variables)\n",
    "                              if grad is not None)\n",
    "    return loss.numpy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Run 100 batches of gradient descent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iteration 10, objective = 0.53013414\n",
      "iteration 20, objective = 0.47684863\n",
      "iteration 30, objective = 0.47061586\n",
      "iteration 40, objective = 0.47537118\n",
      "iteration 50, objective = 0.4760192\n",
      "iteration 60, objective = 0.47407478\n",
      "iteration 70, objective = 0.47333395\n",
      "iteration 80, objective = 0.4722594\n",
      "iteration 90, objective = 0.4723976\n",
      "iteration 100, objective = 0.4724716\n"
     ]
    }
   ],
   "source": [
    "for i in range(100):\n",
    "    obj = trainingStepBPR(modelBPR, interactions)\n",
    "    if (i % 10 == 9): print(\"iteration \" + str(i+1) + \", objective = \" + str(obj))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Prediction for a particular user/item pair. Note that this is an unnormalized score (which can be used for ranking)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [],
   "source": [
    "u,i,_ = interactionsTest[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "3.1493201"
      ]
     },
     "execution_count": 42,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# In this case just a score (that can be used for ranking), rather than a prediction of a rating\n",
    "modelBPR.predict(userIDs[u], itemIDs[i]).numpy()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exercises"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Adapt the latent factor model above, simply deleting any terms associated with latent factors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [],
   "source": [
    "class LatentFactorModelBiasOnly(tf.keras.Model):\n",
    "    def __init__(self, mu, lamb):\n",
    "        super(LatentFactorModelBiasOnly, self).__init__()\n",
    "        # Initialize to average\n",
    "        self.alpha = tf.Variable(mu)\n",
    "        # Initialize to small random values\n",
    "        self.betaU = tf.Variable(tf.random.normal([len(userIDs)],stddev=0.001))\n",
    "        self.betaI = tf.Variable(tf.random.normal([len(itemIDs)],stddev=0.001))\n",
    "        self.lamb = lamb\n",
    "\n",
    "    # Prediction for a single instance (useful for evaluation)\n",
    "    def predict(self, u, i):\n",
    "        p = self.alpha + self.betaU[u] + self.betaI[i]\n",
    "        return p\n",
    "\n",
    "    # Regularizer\n",
    "    def reg(self):\n",
    "        return self.lamb * (tf.reduce_sum(self.betaU**2) +\\\n",
    "                            tf.reduce_sum(self.betaI**2))\n",
    "    \n",
    "    # Prediction for a sample of instances\n",
    "    def predictSample(self, sampleU, sampleI):\n",
    "        u = tf.convert_to_tensor(sampleU, dtype=tf.int32)\n",
    "        i = tf.convert_to_tensor(sampleI, dtype=tf.int32)\n",
    "        beta_u = tf.nn.embedding_lookup(self.betaU, u)\n",
    "        beta_i = tf.nn.embedding_lookup(self.betaI, i)\n",
    "        pred = self.alpha + beta_u + beta_i\n",
    "        return pred\n",
    "    \n",
    "    # Loss\n",
    "    def call(self, sampleU, sampleI, sampleR):\n",
    "        pred = self.predictSample(sampleU, sampleI)\n",
    "        r = tf.convert_to_tensor(sampleR, dtype=tf.float32)\n",
    "        return tf.nn.l2_loss(pred - r) / len(sampleR)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [],
   "source": [
    "modelBiasOnly = LatentFactorModelBiasOnly(mu, 0.00001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "def trainingStepBiasOnly(model, interactions):\n",
    "    Nsamples = 50000\n",
    "    with tf.GradientTape() as tape:\n",
    "        sampleU, sampleI, sampleR = [], [], []\n",
    "        for _ in range(Nsamples):\n",
    "            u,i,r = random.choice(interactions)\n",
    "            sampleU.append(userIDs[u])\n",
    "            sampleI.append(itemIDs[i])\n",
    "            sampleR.append(r)\n",
    "\n",
    "        loss = model(sampleU,sampleI,sampleR)\n",
    "        loss += model.reg()\n",
    "    gradients = tape.gradient(loss, model.trainable_variables)\n",
    "    optimizer.apply_gradients((grad, var) for\n",
    "        (grad, var) in zip(gradients, model.trainable_variables)\n",
    "        if grad is not None)\n",
    "    return loss.numpy()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iteration 10, objective = 0.5770031\n",
      "iteration 20, objective = 0.536491\n",
      "iteration 30, objective = 0.5330345\n",
      "iteration 40, objective = 0.52337563\n",
      "iteration 50, objective = 0.5127952\n"
     ]
    }
   ],
   "source": [
    "for i in range(50):\n",
    "    obj = trainingStepBiasOnly(modelBiasOnly, interactionsTrain)\n",
    "    if (i % 10 == 9): print(\"iteration \" + str(i+1) + \", objective = \" + str(obj))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Compute the MSEs for a model which always predicts the mean, versus one which involves bias terms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [],
   "source": [
    "def MSE(predictions, labels):\n",
    "    differences = [(x-y)**2 for x,y in zip(predictions,labels)]\n",
    "    return sum(differences) / len(differences)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "alwaysPredictMean = [mu for _ in interactionsTest]\n",
    "labels = [r for _,_,r in interactionsTest]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.3209473208629037"
      ]
     },
     "execution_count": 49,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MSE(alwaysPredictMean, labels)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "biasOnlyPredictions =\\\n",
    "    [modelBiasOnly.predict(userIDs[u],itemIDs[i]).numpy() for u,i,_ in interactionsTest]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "4.880166"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "biasOnlyPredictions[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.004844762789679"
      ]
     },
     "execution_count": 52,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MSE(biasOnlyPredictions, labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Performance of a complete latent factor model (using the latent factor model implementation in the examples above)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = tf.keras.optimizers.Adam(0.1)\n",
    "modelLFM = LatentFactorModel(mu, 10, 0.00001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iteration 10, objective = 0.53806317\n",
      "iteration 20, objective = 0.526853\n",
      "iteration 30, objective = 0.5331862\n",
      "iteration 40, objective = 0.5367909\n",
      "iteration 50, objective = 0.5340424\n"
     ]
    }
   ],
   "source": [
    "for i in range(50):\n",
    "    obj = trainingStep(modelLFM, interactionsTrain)\n",
    "    if (i % 10 == 9): print(\"iteration \" + str(i+1) + \", objective = \" + str(obj))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictions = [modelLFM.predict(userIDs[u],itemIDs[i]).numpy() for u,i,_ in interactionsTest]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.0092721226433803"
      ]
     },
     "execution_count": 56,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MSE(predictions, labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(probably needs a little more tuning in terms of number of latent factors, learning rate, etc.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Experiment with rounding the predictions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "outputs": [],
   "source": [
    "predictionsRounded = [int(p + 0.5) for p in predictions]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1.0957701810672271"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MSE(predictionsRounded, labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seems to result in worse performance. For a rough explanation, consider a random variable that takes a value of \"1\" half the time and \"2\" half the time; in terms of the MSE, always predicting 1.5 (and always incurring moderate errors) is preferable to always predicting either of 1 or 2 (and incurring a large error half the time)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Following the BPR code from examples above"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = tf.keras.optimizers.Adam(0.1)\n",
    "modelBPR = BPRbatch(10, 0.00001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "iteration 10, objective = 0.5285175\n",
      "iteration 20, objective = 0.48673838\n",
      "iteration 30, objective = 0.48348132\n",
      "iteration 40, objective = 0.4870032\n",
      "iteration 50, objective = 0.49010223\n"
     ]
    }
   ],
   "source": [
    "for i in range(50):\n",
    "    obj = trainingStepBPR(modelBPR, interactionsTrain)\n",
    "    if (i % 10 == 9): print(\"iteration \" + str(i+1) + \", objective = \" + str(obj))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "outputs": [],
   "source": [
    "interactionsTestPerUser = defaultdict(set)\n",
    "itemSet = set()\n",
    "for u,i,_ in interactionsTest:\n",
    "    interactionsTestPerUser[u].add(i)\n",
    "    itemSet.add(i)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AUC implementation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [],
   "source": [
    "def AUCu(u, N): # N samples per user\n",
    "    win = 0\n",
    "    if N > len(interactionsTestPerUser[u]):\n",
    "        N = len(interactionsTestPerUser[u])\n",
    "    positive = random.sample(interactionsTestPerUser[u],N)\n",
    "    negative = random.sample(itemSet.difference(interactionsTestPerUser[u]),N)\n",
    "    for i,j in zip(positive,negative):\n",
    "        si = modelBPR.predict(userIDs[u], itemIDs[i]).numpy()\n",
    "        sj = modelBPR.predict(userIDs[u], itemIDs[j]).numpy()\n",
    "        if si > sj:\n",
    "            win += 1\n",
    "    return win/N"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "outputs": [],
   "source": [
    "def AUC():\n",
    "    av = []\n",
    "    for u in interactionsTestPerUser:\n",
    "        av.append(AUCu(u, 10))\n",
    "    return sum(av) / len(av)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.7953086008585195"
      ]
     },
     "execution_count": 64,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "AUC()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
