{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Neural Networks for Collaborative Filtering\n", "> Neural networks to learn the embeddings! and how to combine them\n", "\n", "- toc: true \n", "- badges: true\n", "- comments: true\n", "- author: Nipun Batra\n", "- categories: [ML]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recently, I had a chance to read an interesting WWW 2017 paper entitled: [Neural Collaborative Filtering](https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf). The first paragraph of the abstract reads as follows:\n", "\n", ">In recent years, deep neural networks have yielded immense success on speech recognition, computer vision and natural language processing. However, the exploration of deep neural networks on recommender systems has received relatively less scrutiny. In this work, we strive to develop techniques based on neural networks to tackle the key problem in recommendation — collaborative filtering — on the basis of implicit feedback.\n", "\n", "I'd recently written a [blog post](../recommend-keras.html) on using Keras (deep learning library) for implementing traditional matrix factorization based collaborative filtering. So, I thought to get my hands dirty with building a prototype for the paper mentioned above. The authors have already provided their [code on Github](https://github.com/hexiangnan/neural_collaborative_filtering), which should serve as a reference for the paper and not my post, whose purpose is merely educational!\n", "\n", "\n", "Here's how the proposed network architecture looks in the paper:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![](https://nipunbatra.github.io/blog/images/neumf.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are a few terms that we need to understand:\n", " \n", "1. User (u) and Item (i) are used to create embeddings (low-dimensional) for user and item\n", "2. Generalized Matrix Factorisation (GMF) combines the two embeddings using the dot product. This is our regular matrix factorisation.\n", "3. Multi-layer perceptron can also create embeddings for user and items. However, instead of taking a dot product of these to obtain the rating, we can concatenate them to create a feature vector which can be passed on to the further layers.\n", "4. Neural MF can then combine the predictions from MLP and GMF to obtain the following prediction." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As done in my previous post, I'll use the MovieLens-100k dataset for illustration. Please refer to my [previous post](../recommend-keras.html) for more details." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Peak into the dataset" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": true }, "outputs": [], "source": [ "dataset = pd.read_csv(\"/Users/nipun/Downloads/ml-100k/u.data\",sep='\\t',names=\"user_id,item_id,rating,timestamp\".split(\",\"))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
user_iditem_idratingtimestamp
01962423881250949
11863023891717742
2223771878887116
3244512880606923
41663461886397596
\n", "
" ], "text/plain": [ " user_id item_id rating timestamp\n", "0 196 242 3 881250949\n", "1 186 302 3 891717742\n", "2 22 377 1 878887116\n", "3 244 51 2 880606923\n", "4 166 346 1 886397596" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dataset.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, each record (row) shows the rating for a user, item (movie) pair. It should be noted that I use item and movie interchangeably in this post." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(943, 1682)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(dataset.user_id.unique()), len(dataset.item_id.unique())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We assign a unique number between (0, #users) to each user and do the same for movies." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": true }, "outputs": [], "source": [ "dataset.user_id = dataset.user_id.astype('category').cat.codes.values\n", "dataset.item_id = dataset.item_id.astype('category').cat.codes.values" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
user_iditem_idratingtimestamp
01952413881250949
11853013891717742
2213761878887116
3243502880606923
41653451886397596
\n", "
" ], "text/plain": [ " user_id item_id rating timestamp\n", "0 195 241 3 881250949\n", "1 185 301 3 891717742\n", "2 21 376 1 878887116\n", "3 243 50 2 880606923\n", "4 165 345 1 886397596" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dataset.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Train test split\n", "\n", "We'll now split our dataset of 100k ratings into train (containing 80k ratings) and test (containing 20k ratings). Given the train set, we'd like to accurately estimate the ratings in the test set." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from sklearn.model_selection import train_test_split\n", "train, test = train_test_split(dataset, test_size=0.2)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
user_iditem_idratingtimestamp
1318571955880037203
233911445094882181859
90744895502887159951
30432552795882151167
893255944892683274
\n", "
" ], "text/plain": [ " user_id item_id rating timestamp\n", "13185 71 95 5 880037203\n", "23391 144 509 4 882181859\n", "90744 895 50 2 887159951\n", "3043 255 279 5 882151167\n", "8932 55 94 4 892683274" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train.head()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": true }, "outputs": [], "source": [ "test.head()\n", "y_true = test.rating" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating the model" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Using TensorFlow backend.\n" ] } ], "source": [ "import keras\n", "n_latent_factors_user = 8\n", "n_latent_factors_movie = 10\n", "n_latent_factors_mf = 3\n", "n_users, n_movies = len(dataset.user_id.unique()), len(dataset.item_id.unique())\n", "\n", "movie_input = keras.layers.Input(shape=[1],name='Item')\n", "movie_embedding_mlp = keras.layers.Embedding(n_movies + 1, n_latent_factors_movie, name='Movie-Embedding-MLP')(movie_input)\n", "movie_vec_mlp = keras.layers.Flatten(name='FlattenMovies-MLP')(movie_embedding_mlp)\n", "movie_vec_mlp = keras.layers.Dropout(0.2)(movie_vec_mlp)\n", "\n", "movie_embedding_mf = keras.layers.Embedding(n_movies + 1, n_latent_factors_mf, name='Movie-Embedding-MF')(movie_input)\n", "movie_vec_mf = keras.layers.Flatten(name='FlattenMovies-MF')(movie_embedding_mf)\n", "movie_vec_mf = keras.layers.Dropout(0.2)(movie_vec_mf)\n", "\n", "\n", "user_input = keras.layers.Input(shape=[1],name='User')\n", "user_vec_mlp = keras.layers.Flatten(name='FlattenUsers-MLP')(keras.layers.Embedding(n_users + 1, n_latent_factors_user,name='User-Embedding-MLP')(user_input))\n", "user_vec_mlp = keras.layers.Dropout(0.2)(user_vec_mlp)\n", "\n", "user_vec_mf = keras.layers.Flatten(name='FlattenUsers-MF')(keras.layers.Embedding(n_users + 1, n_latent_factors_mf,name='User-Embedding-MF')(user_input))\n", "user_vec_mf = keras.layers.Dropout(0.2)(user_vec_mf)\n", "\n", "\n", "concat = keras.layers.merge([movie_vec_mlp, user_vec_mlp], mode='concat',name='Concat')\n", "concat_dropout = keras.layers.Dropout(0.2)(concat)\n", "dense = keras.layers.Dense(200,name='FullyConnected')(concat_dropout)\n", "dense_batch = keras.layers.BatchNormalization(name='Batch')(dense)\n", "dropout_1 = keras.layers.Dropout(0.2,name='Dropout-1')(dense_batch)\n", "dense_2 = keras.layers.Dense(100,name='FullyConnected-1')(dropout_1)\n", "dense_batch_2 = keras.layers.BatchNormalization(name='Batch-2')(dense_2)\n", "\n", "\n", "dropout_2 = keras.layers.Dropout(0.2,name='Dropout-2')(dense_batch_2)\n", "dense_3 = keras.layers.Dense(50,name='FullyConnected-2')(dropout_2)\n", "dense_4 = keras.layers.Dense(20,name='FullyConnected-3', activation='relu')(dense_3)\n", "\n", "pred_mf = keras.layers.merge([movie_vec_mf, user_vec_mf], mode='dot',name='Dot')\n", "\n", "\n", "pred_mlp = keras.layers.Dense(1, activation='relu',name='Activation')(dense_4)\n", "\n", "combine_mlp_mf = keras.layers.merge([pred_mf, pred_mlp], mode='concat',name='Concat-MF-MLP')\n", "result_combine = keras.layers.Dense(100,name='Combine-MF-MLP')(combine_mlp_mf)\n", "deep_combine = keras.layers.Dense(100,name='FullyConnected-4')(result_combine)\n", "\n", "\n", "result = keras.layers.Dense(1,name='Prediction')(deep_combine)\n", "\n", "\n", "model = keras.Model([user_input, movie_input], result)\n", "opt = keras.optimizers.Adam(lr =0.01)\n", "model.compile(optimizer='adam',loss= 'mean_absolute_error')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's now see how our model looks like:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "G\n", "\n", "\n", "4515113056\n", "\n", "Item: InputLayer\n", "\n", "\n", "4515113392\n", "\n", "Movie-Embedding-MLP: Embedding\n", "\n", "\n", "4515113056->4515113392\n", "\n", "\n", "\n", "\n", "112084624776\n", "\n", "Movie-Embedding-MF: Embedding\n", "\n", "\n", "4515113056->112084624776\n", "\n", "\n", "\n", "\n", "4408641744\n", "\n", "User: InputLayer\n", "\n", "\n", "112085071184\n", "\n", "User-Embedding-MLP: Embedding\n", "\n", "\n", "4408641744->112085071184\n", "\n", "\n", "\n", "\n", "112085982960\n", "\n", "User-Embedding-MF: Embedding\n", "\n", "\n", "4408641744->112085982960\n", "\n", "\n", "\n", "\n", "112084623992\n", "\n", "FlattenMovies-MLP: Flatten\n", "\n", "\n", "4515113392->112084623992\n", "\n", "\n", "\n", "\n", "4378375728\n", "\n", "FlattenUsers-MLP: Flatten\n", "\n", "\n", "112085071184->4378375728\n", "\n", "\n", "\n", "\n", "4515113224\n", "\n", "dropout_1: Dropout\n", "\n", "\n", "112084623992->4515113224\n", "\n", "\n", "\n", "\n", "112085499184\n", "\n", "dropout_3: Dropout\n", "\n", "\n", "4378375728->112085499184\n", "\n", "\n", "\n", "\n", "112085917424\n", "\n", "Concat: Merge\n", "\n", "\n", "4515113224->112085917424\n", "\n", "\n", "\n", "\n", "112085499184->112085917424\n", "\n", "\n", "\n", "\n", "112085764920\n", "\n", "dropout_5: Dropout\n", "\n", "\n", "112085917424->112085764920\n", "\n", "\n", "\n", "\n", "112086436832\n", "\n", "FullyConnected: Dense\n", "\n", "\n", "112085764920->112086436832\n", "\n", "\n", "\n", "\n", "112086434816\n", "\n", "Batch: BatchNormalization\n", "\n", "\n", "112086436832->112086434816\n", "\n", "\n", "\n", "\n", "112086597360\n", "\n", "Dropout-1: Dropout\n", "\n", "\n", "112086434816->112086597360\n", "\n", "\n", "\n", "\n", "112086994000\n", "\n", "FullyConnected-1: Dense\n", "\n", "\n", "112086597360->112086994000\n", "\n", "\n", "\n", "\n", "112086761144\n", "\n", "Batch-2: BatchNormalization\n", "\n", "\n", "112086994000->112086761144\n", "\n", "\n", "\n", "\n", "112087744464\n", "\n", "Dropout-2: Dropout\n", "\n", "\n", "112086761144->112087744464\n", "\n", "\n", "\n", "\n", "4399310888\n", "\n", "FlattenMovies-MF: Flatten\n", "\n", "\n", "112084624776->4399310888\n", "\n", "\n", "\n", "\n", "4407942728\n", "\n", "FlattenUsers-MF: Flatten\n", "\n", "\n", "112085982960->4407942728\n", "\n", "\n", "\n", "\n", "112087744128\n", "\n", "FullyConnected-2: Dense\n", "\n", "\n", "112087744464->112087744128\n", "\n", "\n", "\n", "\n", "112084624160\n", "\n", "dropout_2: Dropout\n", "\n", "\n", "4399310888->112084624160\n", "\n", "\n", "\n", "\n", "112085423664\n", "\n", "dropout_4: Dropout\n", "\n", "\n", "4407942728->112085423664\n", "\n", "\n", "\n", "\n", "112087225176\n", "\n", "FullyConnected-3: Dense\n", "\n", "\n", "112087744128->112087225176\n", "\n", "\n", "\n", "\n", "112088890336\n", "\n", "Dot: Merge\n", "\n", "\n", "112084624160->112088890336\n", "\n", "\n", "\n", "\n", "112085423664->112088890336\n", "\n", "\n", "\n", "\n", "112089386176\n", "\n", "Activation: Dense\n", "\n", "\n", "112087225176->112089386176\n", "\n", "\n", "\n", "\n", "112089474160\n", "\n", "Concat-MF-MLP: Merge\n", "\n", "\n", "112088890336->112089474160\n", "\n", "\n", "\n", "\n", "112089386176->112089474160\n", "\n", "\n", "\n", "\n", "112089888696\n", "\n", "Combine-MF-MLP: Dense\n", "\n", "\n", "112089474160->112089888696\n", "\n", "\n", "\n", "\n", "112089888752\n", "\n", "FullyConnected-4: Dense\n", "\n", "\n", "112089888696->112089888752\n", "\n", "\n", "\n", "\n", "112089753920\n", "\n", "Prediction: Dense\n", "\n", "\n", "112089888752->112089753920\n", "\n", "\n", "\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.display import SVG\n", "from keras.utils.vis_utils import model_to_dot\n", "SVG(model_to_dot(model, show_shapes=False, show_layer_names=True, rankdir='HB').create(prog='dot', format='svg'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, it wasn't very complicated to set up. Courtesy Keras, we can do even more complex stuff!" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__________________________________________________________________________________________________\n", "Layer (type) Output Shape Param # Connected to \n", "==================================================================================================\n", "Item (InputLayer) (None, 1) 0 \n", "__________________________________________________________________________________________________\n", "User (InputLayer) (None, 1) 0 \n", "__________________________________________________________________________________________________\n", "Movie-Embedding-MLP (Embedding) (None, 1, 10) 16830 Item[0][0] \n", "__________________________________________________________________________________________________\n", "User-Embedding-MLP (Embedding) (None, 1, 8) 7552 User[0][0] \n", "__________________________________________________________________________________________________\n", "FlattenMovies-MLP (Flatten) (None, 10) 0 Movie-Embedding-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "FlattenUsers-MLP (Flatten) (None, 8) 0 User-Embedding-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "dropout_1 (Dropout) (None, 10) 0 FlattenMovies-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "dropout_3 (Dropout) (None, 8) 0 FlattenUsers-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "Concat (Merge) (None, 18) 0 dropout_1[0][0] \n", " dropout_3[0][0] \n", "__________________________________________________________________________________________________\n", "dropout_5 (Dropout) (None, 18) 0 Concat[0][0] \n", "__________________________________________________________________________________________________\n", "FullyConnected (Dense) (None, 200) 3800 dropout_5[0][0] \n", "__________________________________________________________________________________________________\n", "Batch (BatchNormalization) (None, 200) 800 FullyConnected[0][0] \n", "__________________________________________________________________________________________________\n", "Dropout-1 (Dropout) (None, 200) 0 Batch[0][0] \n", "__________________________________________________________________________________________________\n", "FullyConnected-1 (Dense) (None, 100) 20100 Dropout-1[0][0] \n", "__________________________________________________________________________________________________\n", "Batch-2 (BatchNormalization) (None, 100) 400 FullyConnected-1[0][0] \n", "__________________________________________________________________________________________________\n", "Movie-Embedding-MF (Embedding) (None, 1, 3) 5049 Item[0][0] \n", "__________________________________________________________________________________________________\n", "User-Embedding-MF (Embedding) (None, 1, 3) 2832 User[0][0] \n", "__________________________________________________________________________________________________\n", "Dropout-2 (Dropout) (None, 100) 0 Batch-2[0][0] \n", "__________________________________________________________________________________________________\n", "FlattenMovies-MF (Flatten) (None, 3) 0 Movie-Embedding-MF[0][0] \n", "__________________________________________________________________________________________________\n", "FlattenUsers-MF (Flatten) (None, 3) 0 User-Embedding-MF[0][0] \n", "__________________________________________________________________________________________________\n", "FullyConnected-2 (Dense) (None, 50) 5050 Dropout-2[0][0] \n", "__________________________________________________________________________________________________\n", "dropout_2 (Dropout) (None, 3) 0 FlattenMovies-MF[0][0] \n", "__________________________________________________________________________________________________\n", "dropout_4 (Dropout) (None, 3) 0 FlattenUsers-MF[0][0] \n", "__________________________________________________________________________________________________\n", "FullyConnected-3 (Dense) (None, 20) 1020 FullyConnected-2[0][0] \n", "__________________________________________________________________________________________________\n", "Dot (Merge) (None, 1) 0 dropout_2[0][0] \n", " dropout_4[0][0] \n", "__________________________________________________________________________________________________\n", "Activation (Dense) (None, 1) 21 FullyConnected-3[0][0] \n", "__________________________________________________________________________________________________\n", "Concat-MF-MLP (Merge) (None, 2) 0 Dot[0][0] \n", " Activation[0][0] \n", "__________________________________________________________________________________________________\n", "Combine-MF-MLP (Dense) (None, 100) 300 Concat-MF-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "FullyConnected-4 (Dense) (None, 100) 10100 Combine-MF-MLP[0][0] \n", "__________________________________________________________________________________________________\n", "Prediction (Dense) (None, 1) 101 FullyConnected-4[0][0] \n", "==================================================================================================\n", "Total params: 73,955\n", "Trainable params: 73,355\n", "Non-trainable params: 600\n", "__________________________________________________________________________________________________\n" ] } ], "source": [ "model.summary()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that the number of parameters is more than what we had in the Matrix Factorisation case. Let's see how this model works. I'll run it for more epochs given that we have more parameters." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "history = model.fit([train.user_id, train.item_id], train.rating, epochs=25, verbose=0, validation_split=0.1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Prediction performance of Neural Network based recommender system" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.716\n", "0.737380115688\n" ] } ], "source": [ "from sklearn.metrics import mean_absolute_error\n", "y_hat_2 = np.round(model.predict([test.user_id, test.item_id]),0)\n", "print(mean_absolute_error(y_true, y_hat_2))\n", "\n", "print(mean_absolute_error(y_true, model.predict([test.user_id, test.item_id])))\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pretty similar to the result we got using matrix factorisation. This isn't very optimised, and I am sure doing so, we can make this approach perform much better than GMF!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Thanks for reading. This post has been a good learning experience for me. Hope you enjoyed too! " ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.4" } }, "nbformat": 4, "nbformat_minor": 1 }