{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp models.tf.caser"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Caser\n",
    "> Convolutional Sequence Embedding Recommendation (Caser)\n",
    "\n",
    "Top-N sequential recommendation models each user as a sequence of items interacted in the past and aims to predict top-N ranked items that a user will likely interact in a 'near future'. The order of interaction implies that sequential patterns play an important role where more recent items in a sequence have a larger impact on the next item. Convolutional Sequence Embedding Recommendation Model (Caser) address this requirement by embedding a sequence of recent items into an image' in the time and latent spaces and learn sequential patterns as local features of the image using convolutional filters. This approach provides a unified and flexible network structure for capturing both general preferences and sequential patterns.\n",
    "\n",
    "![https://github.com/RecoHut-Stanzas/S021355/raw/main/images/img2.png](https://github.com/RecoHut-Stanzas/S021355/raw/main/images/img2.png)\n",
    "\n",
    "Caser adopts convolutional neural networks capture the dynamic pattern influences of users’ recent activities. The main component of Caser consists of a horizontal convolutional network and a vertical convolutional network, aiming to uncover the union-level and point-level sequence patterns, respectively. Point-level pattern indicates the impact of single item in the historical sequence on the target item, while union level pattern implies the influences of several previous actions on the subsequent target. For example, buying both milk and butter together leads to higher probability of buying flour than just buying one of them. Moreover, users’ general interests, or long term preferences are also modeled in the last fully-connected layers, resulting in a more comprehensive modeling of user interests."
   ]
  },
  {
   "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.preprocessing.sequence import pad_sequences\n",
    "from tensorflow.keras.layers import Embedding, Input, Conv1D, GlobalMaxPooling1D, Dense, Dropout"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "class Caser(Model):\n",
    "    def __init__(self, feature_columns, maxlen=40, hor_n=2, hor_h=8, ver_n=8, dropout=0.5, activation='relu', embed_reg=1e-6):\n",
    "        \"\"\"\n",
    "        AttRec\n",
    "        :param feature_columns: A feature columns list. user + seq\n",
    "        :param maxlen: A scalar. In the paper, maxlen is L, the number of latest items.\n",
    "        :param hor_n: A scalar. The number of horizontal filters.\n",
    "        :param hor_h: A scalar. Height of horizontal filters.\n",
    "        :param ver_n: A scalar. The number of vertical filters.\n",
    "        :param dropout: A scalar. The number of dropout.\n",
    "        :param activation: A string. 'relu', 'sigmoid' or 'tanh'.\n",
    "        :param embed_reg: A scalar. The regularizer of embedding.\n",
    "        \"\"\"\n",
    "        super(Caser, self).__init__()\n",
    "        # maxlen\n",
    "        self.maxlen = maxlen\n",
    "        # feature columns\n",
    "        self.user_fea_col, self.item_fea_col = feature_columns\n",
    "        # embed_dim\n",
    "        self.embed_dim = self.item_fea_col['embed_dim']\n",
    "        # total number of item set\n",
    "        self.total_item = self.item_fea_col['feat_num']\n",
    "        # horizontal filters\n",
    "        self.hor_n = hor_n\n",
    "        self.hor_h = hor_h if hor_h <= self.maxlen else self.maxlen\n",
    "        # vertical filters\n",
    "        self.ver_n = ver_n\n",
    "        self.ver_w = 1\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",
    "        # item2 embedding\n",
    "        self.item2_embedding = Embedding(input_dim=self.item_fea_col['feat_num'],\n",
    "                                        input_length=1,\n",
    "                                        output_dim=self.item_fea_col['embed_dim'] * 2,\n",
    "                                        mask_zero=True,\n",
    "                                        embeddings_initializer='random_normal',\n",
    "                                        embeddings_regularizer=l2(embed_reg))\n",
    "        # horizontal conv\n",
    "        self.hor_conv = Conv1D(filters=self.hor_n, kernel_size=self.hor_h)\n",
    "        # vertical conv, should transpose\n",
    "        self.ver_conv = Conv1D(filters=self.ver_n, kernel_size=self.ver_w)\n",
    "        # max_pooling\n",
    "        self.pooling = GlobalMaxPooling1D()\n",
    "        # dense\n",
    "        self.dense = Dense(self.embed_dim, activation=activation)\n",
    "        self.dropout = Dropout(dropout)\n",
    "\n",
    "    def call(self, inputs):\n",
    "        # input\n",
    "        user_inputs, seq_inputs, item_inputs = inputs\n",
    "        # user info\n",
    "        user_embed = self.user_embedding(tf.squeeze(user_inputs, axis=-1))  # (None, dim)\n",
    "        # seq info\n",
    "        seq_embed = self.item_embedding(seq_inputs)  # (None, maxlen, dim)\n",
    "        # horizontal conv (None, (maxlen - kernel_size + 2 * pad) / stride +1, hor_n)\n",
    "        hor_info = self.hor_conv(seq_embed)\n",
    "        hor_info = self.pooling(hor_info)  # (None, hor_n)\n",
    "        # vertical conv  (None, (dim - 1 + 2 * pad) / stride + 1, ver_n)\n",
    "        ver_info = self.ver_conv(tf.transpose(seq_embed, perm=(0, 2, 1)))\n",
    "        ver_info = tf.reshape(ver_info, shape=(-1, ver_info.shape[1] * ver_info.shape[2]))  # (None, ?)\n",
    "        # info\n",
    "        seq_info = self.dense(tf.concat([hor_info, ver_info], axis=-1))  # (None, d)\n",
    "        seq_info = self.dropout(seq_info)\n",
    "        # concat\n",
    "        info = tf.concat([seq_info, user_embed], axis=-1)  # (None, 2 * d)\n",
    "        # item info\n",
    "        item_embed = self.item2_embedding(tf.squeeze(item_inputs, axis=-1))  # (None, dim)\n",
    "        # predict\n",
    "        outputs = tf.nn.sigmoid(tf.reduce_sum(tf.multiply(info, item_embed), axis=1, keepdims=True))\n",
    "        return outputs\n",
    "\n",
    "    def summary(self):\n",
    "        seq_inputs = Input(shape=(self.maxlen,), dtype=tf.int32)\n",
    "        user_inputs = Input(shape=(1, ), dtype=tf.int32)\n",
    "        item_inputs = Input(shape=(1,), dtype=tf.int32)\n",
    "        Model(inputs=[user_inputs, seq_inputs, item_inputs],\n",
    "              outputs=self.call([user_inputs, seq_inputs, item_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",
    "    seq_features = {'feat': 'item_id', 'feat_num': 100, 'embed_dim': 8}\n",
    "\n",
    "    features = [user_features, seq_features]\n",
    "    model = Caser(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, 40)]         0           []                               \n",
      "                                                                                                  \n",
      " embedding_1 (Embedding)        (None, 40, 8)        800         ['input_1[0][0]']                \n",
      "                                                                                                  \n",
      " tf.compat.v1.transpose (TFOpLa  (None, 8, 40)       0           ['embedding_1[0][0]']            \n",
      " mbda)                                                                                            \n",
      "                                                                                                  \n",
      " conv1d (Conv1D)                (None, 33, 2)        130         ['embedding_1[0][0]']            \n",
      "                                                                                                  \n",
      " conv1d_1 (Conv1D)              (None, 8, 8)         328         ['tf.compat.v1.transpose[0][0]'] \n",
      "                                                                                                  \n",
      " global_max_pooling1d (GlobalMa  (None, 2)           0           ['conv1d[0][0]']                 \n",
      " xPooling1D)                                                                                      \n",
      "                                                                                                  \n",
      " tf.reshape (TFOpLambda)        (None, 64)           0           ['conv1d_1[0][0]']               \n",
      "                                                                                                  \n",
      " tf.concat (TFOpLambda)         (None, 66)           0           ['global_max_pooling1d[0][0]',   \n",
      "                                                                  'tf.reshape[0][0]']             \n",
      "                                                                                                  \n",
      " input_2 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " dense (Dense)                  (None, 8)            536         ['tf.concat[0][0]']              \n",
      "                                                                                                  \n",
      " tf.compat.v1.squeeze (TFOpLamb  (None,)             0           ['input_2[0][0]']                \n",
      " da)                                                                                              \n",
      "                                                                                                  \n",
      " input_3 (InputLayer)           [(None, 1)]          0           []                               \n",
      "                                                                                                  \n",
      " dropout (Dropout)              (None, 8)            0           ['dense[0][0]']                  \n",
      "                                                                                                  \n",
      " embedding (Embedding)          (None, 8)            800         ['tf.compat.v1.squeeze[0][0]']   \n",
      "                                                                                                  \n",
      " tf.compat.v1.squeeze_1 (TFOpLa  (None,)             0           ['input_3[0][0]']                \n",
      " mbda)                                                                                            \n",
      "                                                                                                  \n",
      " tf.concat_1 (TFOpLambda)       (None, 16)           0           ['dropout[0][0]',                \n",
      "                                                                  'embedding[0][0]']              \n",
      "                                                                                                  \n",
      " embedding_2 (Embedding)        (None, 16)           1600        ['tf.compat.v1.squeeze_1[0][0]'] \n",
      "                                                                                                  \n",
      " tf.math.multiply (TFOpLambda)  (None, 16)           0           ['tf.concat_1[0][0]',            \n",
      "                                                                  'embedding_2[0][0]']            \n",
      "                                                                                                  \n",
      " tf.math.reduce_sum (TFOpLambda  (None, 1)           0           ['tf.math.multiply[0][0]']       \n",
      " )                                                                                                \n",
      "                                                                                                  \n",
      " tf.math.sigmoid (TFOpLambda)   (None, 1)            0           ['tf.math.reduce_sum[0][0]']     \n",
      "                                                                                                  \n",
      "==================================================================================================\n",
      "Total params: 4,194\n",
      "Trainable params: 4,194\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 09:00:43\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",
      "IPython   : 5.5.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
}
