{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp models.tf.ncf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# NCF\n",
    "> Neural Collaborative Filtering (NCF)\n",
    "\n",
    "NCF is a deep learning-based framework for making recommendations. The key idea is to learn the user-item interaction using neural networks. Despite the effectiveness of MF for collaborative filtering, it is well-known that its performance can be hindered by the simple choice of the interaction function — the inner product. The hypothesis is that the inner product, which simply combines the multiplication of latent features linearly, may not be sufficient to capture the complex structure of user interaction data. NCF tries to learn this interaction function from data. \n",
    "\n",
    "![[Source](https://d2l.ai/chapter_recommender-systems/neumf.html)](https://github.com/RecoHut-Stanzas/S021355/raw/main/images/img1.png)\n",
    "\n",
    "[Source](https://d2l.ai/chapter_recommender-systems/neumf.html)\n",
    "\n",
    "Two instantiations of NCF are Generalized Matrix Factorization (GMF) and Multi-Layer Perceptron (MLP). GMF applies a linear kernel to model the latent feature interactions, and MLP uses a nonlinear kernel to learn the interaction function from data. NeuMF is a fused model of GMF and MLP to better model the complex user-item interactions and unifies the strengths of linearity of MF and non-linearity of MLP for modeling the user-item latent structures. NeuMF allows GMF and MLP to learn separate embeddings and combines the two models by concatenating their last hidden layer.\n",
    "\n",
    "The interaction matrix is based on implicit data and contains only 1 or 0. Here a value of 1 indicates that there is an interaction between user u and item i; however, it does not mean u actually likes i. Similarly, a value of 0 does not necessarily mean u does not like i, it can be that the user is not aware of the item. This poses challenges in learning from implicit data since it provides only noisy signals about users’ preferences. While observed entries at least reflect users’ interest in items, the unobserved entries can be just missing data and there is a natural scarcity of negative feedback.\n",
    "\n",
    "The loss function can either be pointwise or pairwise. Due to the non-convexity of the objective function of NeuMF, gradient-based optimization methods only find locally optimal solutions. It is reported that initialization plays an important role in the convergence and performance of deep learning models. Since NeuMF is an ensemble of GMF and MLP, we usually initialize NeuMF using the pre-trained models of GMF and MLP."
   ]
  },
  {
   "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.regularizers import l2\n",
    "from tensorflow.keras.layers import Layer, Dense, Dropout, Embedding, Input"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class DNN(Layer):\n",
    "\t\"\"\"\n",
    "\tDeep part\n",
    "\t\"\"\"\n",
    "\tdef __init__(self, hidden_units, activation='relu', dnn_dropout=0., **kwargs):\n",
    "\t\t\"\"\"\n",
    "\t\tDNN part\n",
    "\t\t:param hidden_units: A list. List of hidden layer units's numbers\n",
    "\t\t:param activation: A string. Activation function\n",
    "\t\t:param dnn_dropout: A scalar. dropout number\n",
    "\t\t\"\"\"\n",
    "\t\tsuper(DNN, self).__init__(**kwargs)\n",
    "\t\tself.dnn_network = [Dense(units=unit, activation=activation) for unit in hidden_units]\n",
    "\t\tself.dropout = Dropout(dnn_dropout)\n",
    "\n",
    "\tdef call(self, inputs, **kwargs):\n",
    "\t\tx = inputs\n",
    "\t\tfor dnn in self.dnn_network:\n",
    "\t\t\tx = dnn(x)\n",
    "\t\tx = self.dropout(x)\n",
    "\t\treturn x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class NCF(Model):\n",
    "    def __init__(self, feature_columns, hidden_units=None, dropout=0.2, activation='relu', embed_reg=1e-6, **kwargs):\n",
    "        \"\"\"\n",
    "        NCF model\n",
    "        :param feature_columns: A list. user feature columns + item feature columns\n",
    "        :param hidden_units: A list.\n",
    "        :param dropout: A scalar.\n",
    "        :param activation: A string.\n",
    "        :param embed_reg: A scalar. The regularizer of embedding.\n",
    "        \"\"\"\n",
    "        super(NCF, self).__init__(**kwargs)\n",
    "        if hidden_units is None:\n",
    "            hidden_units = [64, 32, 16, 8]\n",
    "        # feature columns\n",
    "        self.user_fea_col, self.item_fea_col = feature_columns\n",
    "        # MF user embedding\n",
    "        self.mf_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",
    "                                           embeddings_initializer='random_normal',\n",
    "                                           embeddings_regularizer=l2(embed_reg))\n",
    "        # MF item embedding\n",
    "        self.mf_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",
    "                                           embeddings_initializer='random_normal',\n",
    "                                           embeddings_regularizer=l2(embed_reg))\n",
    "        # MLP user embedding\n",
    "        self.mlp_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",
    "                                            embeddings_initializer='random_normal',\n",
    "                                            embeddings_regularizer=l2(embed_reg))\n",
    "        # MLP item embedding\n",
    "        self.mlp_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",
    "                                            embeddings_initializer='random_normal',\n",
    "                                            embeddings_regularizer=l2(embed_reg))\n",
    "        # dnn\n",
    "        self.dnn = DNN(hidden_units, activation=activation, dnn_dropout=dropout)\n",
    "        self.dense = Dense(1, activation=None)\n",
    "\n",
    "    def call(self, inputs):\n",
    "        user_inputs, pos_inputs, neg_inputs = inputs  # (None, 1), (None, 1), (None, 1/101)\n",
    "        # user info\n",
    "        mf_user_embed = self.mf_user_embedding(user_inputs)  # (None, 1, dim)\n",
    "        mlp_user_embed = self.mlp_user_embedding(user_inputs)  # (None, 1, dim)\n",
    "        # item\n",
    "        mf_pos_embed = self.mf_item_embedding(pos_inputs)  # (None, 1, dim)\n",
    "        mf_neg_embed = self.mf_item_embedding(neg_inputs)  # (None, 1/101, dim)\n",
    "        mlp_pos_embed = self.mlp_item_embedding(pos_inputs)  # (None, 1, dim)\n",
    "        mlp_neg_embed = self.mlp_item_embedding(neg_inputs)  # (None, 1/101, dim)\n",
    "        # MF\n",
    "        mf_pos_vector = tf.nn.sigmoid(tf.multiply(mf_user_embed, mf_pos_embed))  # (None, 1, dim)\n",
    "        mf_neg_vector = tf.nn.sigmoid(tf.multiply(mf_user_embed, mf_neg_embed))  # (None, 1, dim)\n",
    "        # MLP\n",
    "        mlp_pos_vector = tf.concat([mlp_user_embed, mlp_pos_embed], axis=-1)  # (None, 1, 2 * dim)\n",
    "        mlp_neg_vector = tf.concat([tf.tile(mlp_user_embed, multiples=[1, mlp_neg_embed.shape[1], 1]),\n",
    "                                    mlp_neg_embed], axis=-1)  # (None, 1/101, 2 * dim)\n",
    "        mlp_pos_vector = self.dnn(mlp_pos_vector)  # (None, 1, dim)\n",
    "        mlp_neg_vector = self.dnn(mlp_neg_vector)  # (None, 1/101, dim)\n",
    "        # concat\n",
    "        pos_vector = tf.concat([mf_pos_vector, mlp_pos_vector], axis=-1)  # (None, 1, 2 * dim)\n",
    "        neg_vector = tf.concat([mf_neg_vector, mlp_neg_vector], axis=-1)  # (None, 1/101, 2 * dim)\n",
    "        # pos_vector = mlp_pos_vector\n",
    "        # neg_vector = mlp_neg_vector\n",
    "        # result\n",
    "        pos_logits = tf.squeeze(self.dense(pos_vector), axis=-1)  # (None, 1)\n",
    "        neg_logits = tf.squeeze(self.dense(neg_vector), axis=-1)  # (None, 1/101)\n",
    "        # loss\n",
    "        losses = tf.reduce_mean(- tf.math.log(tf.nn.sigmoid(pos_logits)) -\n",
    "                                tf.math.log(1 - tf.nn.sigmoid(neg_logits))) / 2\n",
    "        self.add_loss(losses)\n",
    "        logits = tf.concat([pos_logits, neg_logits], 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": [
    "def test_model():\n",
    "    user_features = {'feat': 'user_id', 'feat_num': 100, 'embed_dim': 8}\n",
    "    item_features = {'feat': 'item_id', 'feat_num': 100, 'embed_dim': 8}\n",
    "    features = [user_features, item_features]\n",
    "    model = NCF(features)\n",
    "    model.summary()"
   ]
  },
  {
   "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",
      " embedding_2 (Embedding)        (None, 1, 8)         800         ['input_1[0][0]']                \n",
      "                                                                                                  \n",
      " input_3 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " embedding (Embedding)          (None, 1, 8)         800         ['input_1[0][0]']                \n",
      "                                                                                                  \n",
      " embedding_1 (Embedding)        (None, 1, 8)         800         ['input_2[0][0]',                \n",
      "                                                                  'input_3[0][0]']                \n",
      "                                                                                                  \n",
      " embedding_3 (Embedding)        (None, 1, 8)         800         ['input_2[0][0]',                \n",
      "                                                                  'input_3[0][0]']                \n",
      "                                                                                                  \n",
      " tf.tile (TFOpLambda)           (None, 1, 8)         0           ['embedding_2[0][0]']            \n",
      "                                                                                                  \n",
      " tf.math.multiply (TFOpLambda)  (None, 1, 8)         0           ['embedding[0][0]',              \n",
      "                                                                  'embedding_1[0][0]']            \n",
      "                                                                                                  \n",
      " tf.concat (TFOpLambda)         (None, 1, 16)        0           ['embedding_2[0][0]',            \n",
      "                                                                  'embedding_3[0][0]']            \n",
      "                                                                                                  \n",
      " tf.math.multiply_1 (TFOpLambda  (None, 1, 8)        0           ['embedding[0][0]',              \n",
      " )                                                                'embedding_1[1][0]']            \n",
      "                                                                                                  \n",
      " tf.concat_1 (TFOpLambda)       (None, 1, 16)        0           ['tf.tile[0][0]',                \n",
      "                                                                  'embedding_3[1][0]']            \n",
      "                                                                                                  \n",
      " tf.math.sigmoid (TFOpLambda)   (None, 1, 8)         0           ['tf.math.multiply[0][0]']       \n",
      "                                                                                                  \n",
      " dnn (DNN)                      (None, 1, 8)         3832        ['tf.concat[0][0]',              \n",
      "                                                                  'tf.concat_1[0][0]']            \n",
      "                                                                                                  \n",
      " tf.math.sigmoid_1 (TFOpLambda)  (None, 1, 8)        0           ['tf.math.multiply_1[0][0]']     \n",
      "                                                                                                  \n",
      " tf.concat_2 (TFOpLambda)       (None, 1, 16)        0           ['tf.math.sigmoid[0][0]',        \n",
      "                                                                  'dnn[0][0]']                    \n",
      "                                                                                                  \n",
      " tf.concat_3 (TFOpLambda)       (None, 1, 16)        0           ['tf.math.sigmoid_1[0][0]',      \n",
      "                                                                  'dnn[1][0]']                    \n",
      "                                                                                                  \n",
      " dense_4 (Dense)                (None, 1, 1)         17          ['tf.concat_2[0][0]',            \n",
      "                                                                  'tf.concat_3[0][0]']            \n",
      "                                                                                                  \n",
      " tf.compat.v1.squeeze (TFOpLamb  (None, 1)           0           ['dense_4[0][0]']                \n",
      " da)                                                                                              \n",
      "                                                                                                  \n",
      " tf.compat.v1.squeeze_1 (TFOpLa  (None, 1)           0           ['dense_4[1][0]']                \n",
      " mbda)                                                                                            \n",
      "                                                                                                  \n",
      " tf.concat_4 (TFOpLambda)       (None, 2)            0           ['tf.compat.v1.squeeze[0][0]',   \n",
      "                                                                  'tf.compat.v1.squeeze_1[0][0]'] \n",
      "                                                                                                  \n",
      "==================================================================================================\n",
      "Total params: 7,049\n",
      "Trainable params: 7,049\n",
      "Non-trainable params: 0\n",
      "__________________________________________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "test_model()"
   ]
  },
  {
   "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:51:18\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",
      "IPython   : 5.5.0\n",
      "tensorflow: 2.7.0\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
}
