{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "code", "execution_count": 3, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "If1R4c4bHAus", "outputId": "08355a41-c6b6-4002-846a-d3db23c9b973" }, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)\n", "Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)\n", "Requirement already satisfied: scikit-learn in /usr/local/lib/python3.12/dist-packages (1.6.1)\n", "Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)\n", "Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)\n", "Requirement already satisfied: tensorflow in /usr/local/lib/python3.12/dist-packages (2.19.0)\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)\n", "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)\n", "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)\n", "Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.16.2)\n", "Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.2)\n", "Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)\n", "Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)\n", "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)\n", "Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.60.1)\n", "Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.4.9)\n", "Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (25.0)\n", "Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)\n", "Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.2.5)\n", "Requirement already satisfied: absl-py>=1.0.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (1.4.0)\n", "Requirement already satisfied: astunparse>=1.6.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (1.6.3)\n", "Requirement already satisfied: flatbuffers>=24.3.25 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (25.9.23)\n", "Requirement already satisfied: gast!=0.5.0,!=0.5.1,!=0.5.2,>=0.2.1 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (0.6.0)\n", "Requirement already satisfied: google-pasta>=0.1.1 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (0.2.0)\n", "Requirement already satisfied: libclang>=13.0.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (18.1.1)\n", "Requirement already satisfied: opt-einsum>=2.3.2 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (3.4.0)\n", "Requirement already satisfied: protobuf!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev,>=3.20.3 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (5.29.5)\n", "Requirement already satisfied: requests<3,>=2.21.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (2.32.4)\n", "Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from tensorflow) (75.2.0)\n", "Requirement already satisfied: six>=1.12.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (1.17.0)\n", "Requirement already satisfied: termcolor>=1.1.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (3.1.0)\n", "Requirement already satisfied: typing-extensions>=3.6.6 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (4.15.0)\n", "Requirement already satisfied: wrapt>=1.11.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (1.17.3)\n", "Requirement already satisfied: grpcio<2.0,>=1.24.3 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (1.75.1)\n", "Requirement already satisfied: tensorboard~=2.19.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (2.19.0)\n", "Requirement already satisfied: keras>=3.5.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (3.10.0)\n", "Requirement already satisfied: h5py>=3.11.0 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (3.14.0)\n", "Requirement already satisfied: ml-dtypes<1.0.0,>=0.5.1 in /usr/local/lib/python3.12/dist-packages (from tensorflow) (0.5.3)\n", "Requirement already satisfied: wheel<1.0,>=0.23.0 in /usr/local/lib/python3.12/dist-packages (from astunparse>=1.6.0->tensorflow) (0.45.1)\n", "Requirement already satisfied: rich in /usr/local/lib/python3.12/dist-packages (from keras>=3.5.0->tensorflow) (13.9.4)\n", "Requirement already satisfied: namex in /usr/local/lib/python3.12/dist-packages (from keras>=3.5.0->tensorflow) (0.1.0)\n", "Requirement already satisfied: optree in /usr/local/lib/python3.12/dist-packages (from keras>=3.5.0->tensorflow) (0.17.0)\n", "Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests<3,>=2.21.0->tensorflow) (3.4.3)\n", "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests<3,>=2.21.0->tensorflow) (3.10)\n", "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests<3,>=2.21.0->tensorflow) (2.5.0)\n", "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests<3,>=2.21.0->tensorflow) (2025.8.3)\n", "Requirement already satisfied: markdown>=2.6.8 in /usr/local/lib/python3.12/dist-packages (from tensorboard~=2.19.0->tensorflow) (3.9)\n", "Requirement already satisfied: tensorboard-data-server<0.8.0,>=0.7.0 in /usr/local/lib/python3.12/dist-packages (from tensorboard~=2.19.0->tensorflow) (0.7.2)\n", "Requirement already satisfied: werkzeug>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from tensorboard~=2.19.0->tensorflow) (3.1.3)\n", "Requirement already satisfied: MarkupSafe>=2.1.1 in /usr/local/lib/python3.12/dist-packages (from werkzeug>=1.0.1->tensorboard~=2.19.0->tensorflow) (3.0.3)\n", "Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.12/dist-packages (from rich->keras>=3.5.0->tensorflow) (4.0.0)\n", "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.12/dist-packages (from rich->keras>=3.5.0->tensorflow) (2.19.2)\n", "Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.12/dist-packages (from markdown-it-py>=2.2.0->rich->keras>=3.5.0->tensorflow) (0.1.2)\n" ] } ], "source": [ "!pip install pandas numpy scikit-learn matplotlib seaborn tensorflow" ] }, { "cell_type": "code", "source": [ "import pandas as pd\n", "import numpy as np\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.preprocessing import LabelEncoder\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from datetime import datetime\n", "import tensorflow as tf" ], "metadata": { "id": "9GzZQK-UHwbd" }, "execution_count": 4, "outputs": [] }, { "cell_type": "code", "source": [ "ratings_df = pd.read_csv(\"ratings.csv\")\n", "links_df = pd.read_csv(\"links.csv\")\n", "movies_df = pd.read_csv(\"movies.csv\")\n", "tags_df = pd.read_csv(\"tags.csv\")" ], "metadata": { "id": "DnkpRTlWIOmh" }, "execution_count": 6, "outputs": [] }, { "cell_type": "code", "source": [ "print(ratings_df.shape)\n", "print(links_df.shape)\n", "print(movies_df.shape)\n", "print(tags_df.shape)" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "AxckluXNJQ7h", "outputId": "c8aae6c5-56e8-4a81-f2d4-786445dee6be" }, "execution_count": 7, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "(2242504, 4)\n", "(87585, 3)\n", "(87585, 3)\n", "(1856522, 4)\n" ] } ] }, { "cell_type": "code", "source": [ "# Ratings distribution\n", "plt.figure(figsize=(10, 6))\n", "sns.countplot(x='rating', data=ratings_df)\n", "plt.title('Distribution of Ratings')\n", "plt.show()" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 564 }, "id": "1EvaK8ATJcB4", "outputId": "61fa3071-a085-416d-f54b-c069fa601142" }, "execution_count": 8, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "
" ], "image/png": "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0KEK61u0aKEHHnhA69atU1ZWlpo2barHHnus1vsCgMuFwAYAuKRNmzbpL3/5i9q2basRI0ZUWvf999+X29azZ09JUlFRkSSpYcOGklRhgKqJl156yS00vf766zp27JgGDhxob7v22mv18ccf21++LUnr168v9/j/6vR21113qaSkRMuWLXPbvmjRIjkcDrfz/xR33XWXsrOztXr1anvbuXPntHTpUjVq1Mi+NbG2zJgxQ999953+/ve/S/rvFbDzr3hZluV2Fa5Mbf+zlf77FwQrVqxw27506VK39yUlJeVuzwwMDFRISIi99gDgSsRj/QEAbjZs2KD9+/fr3LlzysnJ0aZNm5SamqrWrVvr7bffvugXIM+bN09bt25VdHS0WrdurdzcXK1YsUItW7bUTTfdJOnH8BQQEKDExEQ1btxYDRs2VHh4eLnPKFVVkyZNdNNNN2nMmDHKycnR4sWL1a5dO7evHrj//vv1+uuv684779R9992nL7/8Uq+88orbQ0Cq29uvf/1r9e/fX3/605/073//Wz169NB7772nt956S/Hx8eWOXVMTJkzQs88+q9GjRysjI0Nt2rTR66+/rm3btmnx4sUX/UxhTQwcOFBdu3bVU089pdjYWHXs2FHXXnut/vCHP+g///mP/Pz89MYbb1T42buwsDBJ0oMPPqioqCh5e3tr2LBhP6mfsLAwDR48WIsXL9Z3331nP9b/iy++kPTfq3onTpxQy5YtNWTIEPXo0UONGjXS+++/r507d+rJJ5/8ST0AgEd58AmVAACDlD3Wv+zldDqt4OBg64477rCWLFni9vj4Mhc+1j8tLc265557rJCQEMvpdFohISHW8OHDrS+++MJtv7feesvq3Lmz5ePj4/YY/VtvvdXq0qVLhf1V9lj///f//p81a9YsKzAw0Kpfv74VHR3t9qj5Mk8++aR19dVXWy6Xy7rxxhutTz75pNwxL9bbhY/1tyzLOnHihDVlyhQrJCTEqlevntW+fXtr4cKFVmlpqVudKnhUvmVV/nUDF8rJybHGjBljNWvWzHI6nVa3bt0q/OqB6j7Wv7LapKQkt5/9s88+syIjI61GjRpZzZo1s8aPH29/LcH5fZw7d86aPHmy1bx5c8vhcLitDVXyWP8LvwKibB1mZWXZ2woLC63Y2FirSZMmVqNGjaxBgwZZBw4csCRZTzzx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oAEDBqh169bKyMjQwoULNXfuXD333HN2zUcffaThw4dr3Lhx2r17twYNGqRBgwZp79691eoFAAAAAGqTjydPPnDgQA0cOLDCMcuytHjxYj388MO65557JEkvvfSSgoKCtG7dOg0bNkyff/65UlJStHPnTvXu3VuStHTpUt11113629/+ppCQEL366qsqLi7WCy+8IKfTqS5duigzM1NPPfWUHeyWLFmiO++8U9OmTZMk/eUvf1FqaqqWLVumxMTEKvUCAAAAALXN2M+wZWVlKTs7W5GRkfY2f39/hYeHKz09XZKUnp6ugIAAO6xJUmRkpLy8vLR9+3a75pZbbpHT6bRroqKidODAAf3www92zfnnKaspO09VeqlIUVGRCgoK3F4AAAAAUFXGBrbs7GxJUlBQkNv2oKAgeyw7O1uBgYFu4z4+PmrSpIlbTUXHOP8cldWcP36pXioyf/58+fv726/Q0NBL/NQAAAAA8F/GBrZfglmzZik/P99+HTlyxNMtAQAAALiCGBvYgoODJUk5OTlu23Nycuyx4OBg5ebmuo2fO3dO33//vVtNRcc4/xyV1Zw/fqleKuJyueTn5+f2AgAAAICqMjawtW3bVsHBwUpLS7O3FRQUaPv27YqIiJAkRUREKC8vTxkZGXbNpk2bVFpaqvDwcLtm69atOnv2rF2TmpqqDh066KqrrrJrzj9PWU3ZearSCwAAAADUNo8GtpMnTyozM1OZmZmSfny4R2Zmpg4fPiyHw6H4+Hg9+uijevvtt7Vnzx6NGjVKISEhGjRokCSpU6dOuvPOOzV+/Hjt2LFD27ZtU1xcnIYNG6aQkBBJ0v/+7//K6XRq3Lhx2rdvn1avXq0lS5YoISHB7uOhhx5SSkqKnnzySe3fv19z587VJ598ori4OEmqUi8AAAAAUNs8+lj/Tz75RP3797ffl4WomJgYJSUlafr06SosLNSECROUl5enm266SSkpKfL19bX3efXVVxUXF6fbb79dXl5eGjx4sJ5++ml73N/fX++9955iY2MVFhamZs2aafbs2W7f1XbDDTdo1apVevjhh/XHP/5R7du317p169S1a1e7piq9AAAAAEBt8mhg69evnyzLqnTc4XBo3rx5mjdvXqU1TZo00apVqy56nu7du+uf//znRWvuvfde3XvvvT+pFwAAAACoTcZ+hg0AAAAA6joC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Ups2bdL06dMlSTfccIMWL14sPz8/HTt2TMeOHdMf/vCHSo/9yCOP6L777tOnn36qu+66SyNGjND3338vScrKytKQIUM0aNAg/etf/9Lvf/97/elPf7osPzMAwLO4JRIAgCpq3769FixYYL/v0KGD/ec2bdro0Ucf1cSJE7VixQo5nU75+/vL4XAoODj4kscePXq0hg8fLkl6/PHH9fTTT2vHjh2688479eyzz6pDhw5auHChfd69e/fqscceq+WfEABgGgIbAABVFBYW5vb+/fff1/z587V//34VFBTo3LlzOnPmjE6dOqUGDRpU69jdu3e3/9ywYUP5+fkpNzdXknTgwAH16dPHrb5v3741/CkAAFcSbokEAKCKGjZsaP/53//+t+6++251795db7zxhjIyMrR8+XJJPz6QpLrq1avn9t7hcKi0tPSnNQwAuOJxhQ0AgBrIyMhQaWmpnnzySXl5/fj3n2vWrHGrcTqdKikp+cnn6tChg9599123bTt37vzJxwUAmI8rbAAA1EC7du109uxZLV26VF999ZVefvllJSYmutW0adNGJ0+eVFpamr799ludOnWqRuf6/e9/r/3792vGjBn64osvtGbNGiUlJUn68UocAOCXi8AGAEAN9OjRQ0899ZT++te/qmvXrnr11Vc1f/58t5obbrhBEydO1NChQ9W8eXO3B5ZUR9u2bfX666/r//7v/9S9e3c988wz9lMiXS7XT/5ZAADmcliWZXm6CQAAUD2PPfaYEhMTdeTIEU+3AgD4GfEZNgAArgArVqxQnz591LRpU23btk0LFy5UXFycp9sCAPzMCGwAAFwBDh48qEcffVTff/+9WrVqpalTp2rWrFmebgsA8DPjlkgAAAAAMBQPHQEAAAAAQxHYAAAAAMBQBDYAAAAAMBSBDQAAAAAMRWADAAAAAEMR2AAAAADAUAQ2AAAAADAUgQ0AAAAADPX/AfCcS8TenUukAAAAAElFTkSuQmCC\n" }, "metadata": {} } ] }, { "cell_type": "code", "source": [ "ratings_stats = {\n", " 'Number of users': ratings_df['userId'].nunique(),\n", " 'Number of movies': ratings_df['movieId'].nunique(),\n", " 'Sparsity': (1 - len(ratings_df) /\n", " (ratings_df['userId'].nunique() * ratings_df['movieId'].nunique())) * 100\n", "}\n", "\n", "print(\"\\nDataset Statistics:\")\n", "for key, value in ratings_stats.items():\n", " if key == 'Sparsity':\n", " print(f\"{key}: {value:.2f}%\")\n", " else:\n", " print(f\"{key}: {value}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "z2iWAkMOJecq", "outputId": "cb8f2b07-e1b8-4bca-e059-34e71343f5cf" }, "execution_count": 9, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "\n", "Dataset Statistics:\n", "Number of users: 14282\n", "Number of movies: 37666\n", "Sparsity: 99.58%\n" ] } ] }, { "cell_type": "code", "source": [ "# Filter users and movies with minimum ratings\n", "user_counts = ratings_df['userId'].value_counts()\n", "movie_counts = ratings_df['movieId'].value_counts()\n", "min_user_ratings = 60\n", "min_movie_ratings = 5\n", "implicit_threshold = 4.0\n", "active_users = user_counts[user_counts >= min_user_ratings].index\n", "popular_movies = movie_counts[movie_counts >= min_movie_ratings].index" ], "metadata": { "id": "6t04nycCJmm8" }, "execution_count": 10, "outputs": [] }, { "cell_type": "code", "source": [ "filtered_df = ratings_df[ratings_df['userId'].isin(active_users) &\n", " ratings_df['movieId'].isin(popular_movies)]\n", "print(f\"Filtered data: {len(filtered_df)} ratings from {len(active_users)} users on {len(popular_movies)} movies\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "vsdC3kzEJrXH", "outputId": "e171fb57-9502-4f50-dbe4-635492266122" }, "execution_count": 11, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Filtered data: 1987615 ratings from 8097 users on 14981 movies\n" ] } ] }, { "cell_type": "code", "source": [ "# Convert timestamps to datetime and add features\n", "filtered_df.loc[:, 'timestamp'] = pd.to_datetime(filtered_df['timestamp'], unit='s')\n", "filtered_df.loc[:, 'year'] = filtered_df['timestamp'].dt.year\n", "filtered_df.loc[:, 'month'] = filtered_df['timestamp'].dt.month\n", "filtered_df.loc[:, 'dayofweek'] = filtered_df['timestamp'].dt.dayofweek\n", "# Sort by timestamp for temporal splitting\n", "filtered_df = filtered_df.sort_values('timestamp')" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "2jo7GmKxJvPz", "outputId": "983d8dfb-cac3-4e49-8700-305f12ddc8f2" }, "execution_count": 12, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "/tmp/ipython-input-781687869.py:2: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise in a future error of pandas. Value '\n", "['1999-12-03 19:24:37', '1999-12-03 19:43:48', '1999-11-22 00:36:16',\n", " '1999-12-03 19:24:37', '1999-11-22 00:00:58', '1999-11-21 23:54:51',\n", " '1999-12-03 19:23:28', '1999-12-03 19:22:23', '1999-11-22 00:38:39',\n", " '1999-12-03 19:23:28',\n", " ...\n", " '2004-11-19 00:40:47', '2004-11-19 00:44:15', '2004-11-19 00:40:29',\n", " '2004-11-19 01:21:21', '2004-11-19 00:43:45', '2004-11-19 00:39:28',\n", " '2004-11-19 01:05:31', '2004-11-19 00:59:37', '2004-11-19 01:16:52',\n", " 'NaT']\n", "Length: 1987615, dtype: datetime64[ns]' has dtype incompatible with float64, please explicitly cast to a compatible dtype first.\n", " filtered_df.loc[:, 'timestamp'] = pd.to_datetime(filtered_df['timestamp'], unit='s')\n", "/tmp/ipython-input-781687869.py:3: SettingWithCopyWarning: \n", "A value is trying to be set on a copy of a slice from a DataFrame.\n", "Try using .loc[row_indexer,col_indexer] = value instead\n", "\n", "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", " filtered_df.loc[:, 'year'] = filtered_df['timestamp'].dt.year\n", "/tmp/ipython-input-781687869.py:4: SettingWithCopyWarning: \n", "A value is trying to be set on a copy of a slice from a DataFrame.\n", "Try using .loc[row_indexer,col_indexer] = value instead\n", "\n", "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", " filtered_df.loc[:, 'month'] = filtered_df['timestamp'].dt.month\n", "/tmp/ipython-input-781687869.py:5: SettingWithCopyWarning: \n", "A value is trying to be set on a copy of a slice from a DataFrame.\n", "Try using .loc[row_indexer,col_indexer] = value instead\n", "\n", "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", " filtered_df.loc[:, 'dayofweek'] = filtered_df['timestamp'].dt.dayofweek\n" ] } ] }, { "cell_type": "code", "source": [ "user_encoder = LabelEncoder()\n", "movie_encoder = LabelEncoder()\n", "user_encoder.fit(filtered_df['userId'].unique())\n", "movie_encoder.fit(filtered_df['movieId'].unique())" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 80 }, "id": "26P456WaJ12I", "outputId": "b116132c-1555-495f-aa25-6fd8a99a5538" }, "execution_count": 13, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "LabelEncoder()" ], "text/html": [ "
LabelEncoder()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ] }, "metadata": {}, "execution_count": 13 } ] }, { "cell_type": "code", "source": [ "filtered_df['user_idx'] = user_encoder.transform(filtered_df['userId'])\n", "filtered_df['movie_idx'] = movie_encoder.transform(filtered_df['movieId'])" ], "metadata": { "id": "gECPD1GmJ67L" }, "execution_count": 14, "outputs": [] }, { "cell_type": "code", "source": [ "filtered_df['implicit'] = (filtered_df['rating'] >= implicit_threshold).astype(int)\n", "processed_df = filtered_df.copy()" ], "metadata": { "id": "wOMQqqytKPQ2" }, "execution_count": 15, "outputs": [] }, { "cell_type": "code", "source": [ "movies_df = movies_df[movies_df['movieId'].isin(popular_movies)]" ], "metadata": { "id": "ExvLs7b9NJ_Y" }, "execution_count": 16, "outputs": [] }, { "cell_type": "code", "source": [ "# Process genres into a binary matrix\n", "genres = movies_df['genres'].str.split('|').explode().unique()\n", "for genre in genres:\n", " if genre != '(no genres listed)':\n", " movies_df[f'genre_{genre}'] = movies_df['genres'].str.contains(genre).astype(int)" ], "metadata": { "id": "01FxlJqmNMj9" }, "execution_count": 17, "outputs": [] }, { "cell_type": "code", "source": [ "movie_features = movies_df.filter(regex='^genre_').values" ], "metadata": { "id": "FMN8EG6yNQyF" }, "execution_count": 18, "outputs": [] }, { "cell_type": "code", "source": [ "# Map movie_idx to genre features\n", "movie_id_to_genre = dict(zip(movies_df['movieId'], movie_features))\n", "movie_idx_to_genre = {movie_encoder.transform([mid])[0]: vec for mid, vec in movie_id_to_genre.items()}" ], "metadata": { "id": "BVgXll0ONVBJ" }, "execution_count": 19, "outputs": [] }, { "cell_type": "code", "source": [ "# Split the data into training, validation and test sets\n", "# Temporal splitting - last interactions for each user go to test\n", "val_size = 0.1\n", "test_size = 0.1\n", "test_ratings = []\n", "val_ratings = []\n", "train_ratings = []\n", "for user_id, group in processed_df.groupby('user_idx'):\n", " # Sort by timestamp\n", " group = group.sort_values('timestamp')\n", " n_ratings = len(group)\n", "\n", " # Calculate split indices\n", " test_idx = int(n_ratings * (1 - test_size))\n", " val_idx = int(n_ratings * (1 - test_size - val_size))\n", "\n", " # Split the user's ratings\n", " test_ratings.append(group.iloc[test_idx:])\n", " val_ratings.append(group.iloc[val_idx:test_idx])\n", " train_ratings.append(group.iloc[:val_idx])\n", "\n", "# Combine the splits\n", "test_df = pd.concat(test_ratings).reset_index(drop=True)\n", "val_df = pd.concat(val_ratings).reset_index(drop=True)\n", "train_df = pd.concat(train_ratings).reset_index(drop=True)\n", "\n", "print(f\"Train set: {len(train_df)} interactions\")\n", "print(f\"Validation set: {len(val_df)} interactions\")\n", "print(f\"Test set: {len(test_df)} interactions\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "fJNiDFbNNYfJ", "outputId": "96c3aac4-3bca-453f-d727-f94cf08de006" }, "execution_count": 20, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Train set: 1586832 interactions\n", "Validation set: 198327 interactions\n", "Test set: 202456 interactions\n" ] } ] }, { "cell_type": "code", "source": [ "def _sample_negatives(df, neg_ratio):\n", " \"\"\"\n", " Sample negative interactions for each user\n", "\n", " Args:\n", " df: DataFrame with user-item interactions\n", " neg_ratio: Ratio of negative to positive samples\n", " \"\"\"\n", " all_movies = set(range(len(movie_encoder.classes_)))\n", " negatives = []\n", "\n", " for user_id, group in df.groupby('user_idx'):\n", " # Get positive items for this user\n", " pos_items = set(group['movie_idx'])\n", "\n", " # Calculate available negative items\n", " neg_items = list(all_movies - pos_items)\n", "\n", " # Sample negative items\n", " neg_count = min(len(neg_items), int(len(group) * neg_ratio))\n", " if neg_count > 0:\n", " sampled_negs = np.random.choice(neg_items, size=neg_count, replace=False)\n", "\n", " # Create negative samples\n", " user_negs = pd.DataFrame({\n", " 'user_idx': [user_id] * neg_count,\n", " 'movie_idx': sampled_negs,\n", " 'implicit': [0] * neg_count\n", " })\n", "\n", " negatives.append(user_negs)\n", "\n", " if negatives:\n", " return pd.concat(negatives)\n", " else:\n", " return pd.DataFrame(columns=['user_idx', 'movie_idx', 'implicit'])" ], "metadata": { "id": "DLVE4fm_NfFg" }, "execution_count": 21, "outputs": [] }, { "cell_type": "code", "source": [ "# Generate negative samples for training\n", "neg_ratio = 4\n", "train_negatives = _sample_negatives(train_df, neg_ratio)\n", "val_negatives = _sample_negatives(val_df, neg_ratio)\n", "test_negatives = _sample_negatives(test_df, neg_ratio)\n", "\n", "# Combine positive and negative samples\n", "train_data = pd.concat([\n", " train_df[['user_idx', 'movie_idx', 'implicit']],\n", " train_negatives\n", "]).reset_index(drop=True)\n", "\n", "val_data = pd.concat([\n", " val_df[['user_idx', 'movie_idx', 'implicit']],\n", " val_negatives\n", "]).reset_index(drop=True)\n", "\n", "test_data = pd.concat([\n", " test_df[['user_idx', 'movie_idx', 'implicit']],\n", " test_negatives\n", "]).reset_index(drop=True)" ], "metadata": { "id": "iSpPbf3jNsIA" }, "execution_count": 22, "outputs": [] }, { "cell_type": "code", "source": [ "print(f\"Training data: {len(train_data)} samples\")\n", "print(f\"Validation data: {len(val_data)} samples\")\n", "print(f\"Test data: {len(test_data)} samples\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "Pw8uiFEGN0kS", "outputId": "c9cc1a90-abd4-4bc5-cfd5-34bf998a28c9" }, "execution_count": 23, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Training data: 7897928 samples\n", "Validation data: 991635 samples\n", "Test data: 1012280 samples\n" ] } ] }, { "cell_type": "code", "source": [ "# Prepare user and item feature matrices for hybrid models\n", "# User features (could be expanded with demographic data if available)\n", "n_users = len(user_encoder.classes_)\n", "# Create user feature matrix (currently just one-hot encoding)\n", "user_features = np.eye(n_users)\n", "# Item features based on genres\n", "movie_features = movies_df.filter(regex='^genre_').values" ], "metadata": { "id": "dpJTct4pOMWB" }, "execution_count": 24, "outputs": [] }, { "cell_type": "code", "source": [ "print(f\"User features shape: {user_features.shape}\")\n", "print(f\"Movie features shape: {movie_features.shape}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "ldf4zMx1OSpR", "outputId": "f14813a0-9e22-4e8d-b658-f12a95751ded" }, "execution_count": 25, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "User features shape: (8097, 8097)\n", "Movie features shape: (14981, 19)\n" ] } ] }, { "cell_type": "code", "source": [ "from tensorflow.keras.utils import Sequence\n", "\n", "class NCFDataGenerator(Sequence):\n", " def __init__(self, df, batch_size):\n", " self.users = df['user_idx'].values\n", " self.items = df['movie_idx'].values\n", " self.labels = df['implicit'].values\n", " self.batch_size = batch_size\n", "\n", " def __len__(self):\n", " return int(np.ceil(len(self.users) / self.batch_size))\n", "\n", " def __getitem__(self, idx):\n", " start = idx * self.batch_size\n", " end = (idx + 1) * self.batch_size\n", " return (\n", " {\n", " \"user_input\": self.users[start:end],\n", " \"item_input\": self.items[start:end]\n", " },\n", " self.labels[start:end]\n", " )" ], "metadata": { "id": "Zkmcyet8OVd6" }, "execution_count": 26, "outputs": [] }, { "cell_type": "code", "source": [ "# Build the NCF Model\n", "from tensorflow.keras.layers import Input, Embedding, Flatten, Concatenate, Dense, Dropout\n", "from tensorflow.keras.models import Model\n", "\n", "def build_ncf_model(num_users, num_items, embedding_dim=64):\n", " user_input = Input(shape=(1,), name=\"user_input\")\n", " item_input = Input(shape=(1,), name=\"item_input\")\n", "\n", " user_emb = Embedding(num_users, embedding_dim, name=\"user_embedding\")(user_input)\n", " item_emb = Embedding(num_items, embedding_dim, name=\"item_embedding\")(item_input)\n", "\n", " # Flatten and concatenate\n", " user_vec = Flatten()(user_emb)\n", " item_vec = Flatten()(item_emb)\n", " concat = Concatenate()([user_vec, item_vec])\n", "\n", " # MLP layers\n", " x = Dense(128, activation='relu')(concat)\n", " x = Dropout(0.3)(x)\n", " x = Dense(64, activation='relu')(x)\n", " x = Dropout(0.3)(x)\n", " x = Dense(32, activation='relu')(x)\n", " output = Dense(1, activation='sigmoid')(x)\n", "\n", " model = Model(inputs=[user_input, item_input], outputs=output)\n", " model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])\n", "\n", " return model" ], "metadata": { "id": "ZUsDbGWlObh7" }, "execution_count": 28, "outputs": [] }, { "cell_type": "code", "source": [ "batch_size = 1024\n", "train_gen = NCFDataGenerator(train_data, batch_size)\n", "val_gen = NCFDataGenerator(val_data, batch_size)\n", "\n", "n_users = len(user_encoder.classes_)\n", "n_items = len(movie_encoder.classes_)\n", "\n", "ncf_model = build_ncf_model(n_users, n_items)\n", "ncf_model.summary()\n", "\n", "# Train\n", "ncf_model.fit(\n", " train_gen,\n", " validation_data=val_gen,\n", " epochs=5\n", ")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 871 }, "id": "c074f4UxOfCV", "outputId": "6e169b0d-4552-4ee2-fd7d-ab770585447f" }, "execution_count": 29, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1mModel: \"functional\"\u001b[0m\n" ], "text/html": [ "
Model: \"functional\"\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mConnected to \u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n", "│ user_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ user_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m518,208\u001b[0m │ user_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m958,784\u001b[0m │ item_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten (\u001b[38;5;33mFlatten\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ user_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_1 (\u001b[38;5;33mFlatten\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ item_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ concatenate │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ flatten[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ (\u001b[38;5;33mConcatenate\u001b[0m) │ │ │ flatten_1[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m) │ \u001b[38;5;34m16,512\u001b[0m │ concatenate[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ dense[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_1 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m8,256\u001b[0m │ dropout[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout_1 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ dense_1[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_2 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m2,080\u001b[0m │ dropout_1[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_3 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m33\u001b[0m │ dense_2[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n" ], "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n",
              "┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃\n",
              "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n",
              "│ user_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ user_embedding      │ (None, 1, 64)     │    518,208 │ user_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_embedding      │ (None, 1, 64)     │    958,784 │ item_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten (Flatten)   │ (None, 64)        │          0 │ user_embedding[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_1 (Flatten) │ (None, 64)        │          0 │ item_embedding[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ concatenate         │ (None, 128)       │          0 │ flatten[0][0],    │\n",
              "│ (Concatenate)       │                   │            │ flatten_1[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense (Dense)       │ (None, 128)       │     16,512 │ concatenate[0][0] │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout (Dropout)   │ (None, 128)       │          0 │ dense[0][0]       │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_1 (Dense)     │ (None, 64)        │      8,256 │ dropout[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout_1 (Dropout) │ (None, 64)        │          0 │ dense_1[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_2 (Dense)     │ (None, 32)        │      2,080 │ dropout_1[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_3 (Dense)     │ (None, 1)         │         33 │ dense_2[0][0]     │\n",
              "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m1,503,873\u001b[0m (5.74 MB)\n" ], "text/html": [ "
 Total params: 1,503,873 (5.74 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m1,503,873\u001b[0m (5.74 MB)\n" ], "text/html": [ "
 Trainable params: 1,503,873 (5.74 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ], "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "Epoch 1/5\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "/usr/local/lib/python3.12/dist-packages/keras/src/trainers/data_adapters/py_dataset_adapter.py:121: UserWarning: Your `PyDataset` class should call `super().__init__(**kwargs)` in its constructor. `**kwargs` can include `workers`, `use_multiprocessing`, `max_queue_size`. Do not pass these arguments to `fit()`, as they will be ignored.\n", " self._warn_if_super_not_called()\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m183s\u001b[0m 23ms/step - accuracy: 0.9057 - loss: 0.2408 - val_accuracy: 0.9088 - val_loss: 0.2359\n", "Epoch 2/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m174s\u001b[0m 23ms/step - accuracy: 0.9149 - loss: 0.2076 - val_accuracy: 0.9126 - val_loss: 0.2368\n", "Epoch 3/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m177s\u001b[0m 23ms/step - accuracy: 0.9130 - loss: 0.2114 - val_accuracy: 0.9137 - val_loss: 0.2382\n", "Epoch 4/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m176s\u001b[0m 23ms/step - accuracy: 0.9146 - loss: 0.2082 - val_accuracy: 0.9145 - val_loss: 0.2235\n", "Epoch 5/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m173s\u001b[0m 22ms/step - accuracy: 0.9133 - loss: 0.2092 - val_accuracy: 0.9143 - val_loss: 0.2265\n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ "" ] }, "metadata": {}, "execution_count": 29 } ] }, { "cell_type": "code", "source": [ "from tqdm import tqdm\n", "from sklearn.metrics import ndcg_score\n", "\n", "def evaluate_model(model, test_df, k=10):\n", " grouped = test_df.groupby(\"user_idx\")\n", "\n", " precision_list = []\n", " recall_list = []\n", " hitrate_list = []\n", " ndcg_list = []\n", "\n", " for user_id, group in tqdm(grouped):\n", " # True labels\n", " y_true = group.sort_values(\"movie_idx\")[\"implicit\"].values\n", "\n", " # Predict scores\n", " user_input = np.full(len(group), user_id)\n", " item_input = group[\"movie_idx\"].values\n", " y_score = model.predict({\"user_input\": user_input, \"item_input\": item_input}, verbose=0).flatten()\n", "\n", " # Rank predictions\n", " top_k_idx = np.argsort(-y_score)[:k]\n", " top_k_labels = y_true[top_k_idx]\n", "\n", " # Precision@K\n", " precision = np.mean(top_k_labels)\n", " precision_list.append(precision)\n", "\n", " # Recall@K\n", " total_positives = np.sum(y_true)\n", " recall = np.sum(top_k_labels) / total_positives if total_positives > 0 else 0\n", " recall_list.append(recall)\n", "\n", " # HitRate@K\n", " hitrate = 1.0 if np.sum(top_k_labels) > 0 else 0.0\n", " hitrate_list.append(hitrate)\n", "\n", " # NDCG@K\n", " ndcg = ndcg_score([y_true], [y_score], k=k)\n", " ndcg_list.append(ndcg)\n", "\n", " results = {\n", " \"Precision@K\": np.mean(precision_list),\n", " \"Recall@K\": np.mean(recall_list),\n", " \"HitRate@K\": np.mean(hitrate_list),\n", " \"NDCG@K\": np.mean(ndcg_list),\n", " }\n", "\n", " return results" ], "metadata": { "id": "JoY4r00OOngJ" }, "execution_count": 30, "outputs": [] }, { "cell_type": "code", "source": [ "results_at_10 = evaluate_model(ncf_model, test_data, k=10)\n", "print(\"\\n📈 Evaluation Results @10:\")\n", "for metric, score in results_at_10.items():\n", " print(f\"{metric}: {score:.2f}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "_R-xlfjkSTRF", "outputId": "fb9f3532-142e-40bf-821c-581bb83e8320" }, "execution_count": 31, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 8097/8097 [15:07<00:00, 8.93it/s]" ] }, { "output_type": "stream", "name": "stdout", "text": [ "\n", "📈 Evaluation Results @10:\n", "Precision@K: 0.17\n", "Recall@K: 0.23\n", "HitRate@K: 0.74\n", "NDCG@K: 0.22\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "\n" ] } ] }, { "cell_type": "code", "source": [ "import numpy as np\n", "from sklearn.metrics import ndcg_score\n", "from tqdm import tqdm\n", "\n", "def evaluate_model_fast(model, test_df, all_items, k=10, num_negative=100):\n", " precision_list, recall_list, hitrate_list, ndcg_list = [], [], [], []\n", "\n", " grouped = test_df.groupby(\"user_idx\")\n", "\n", " for user_id, group in tqdm(grouped, desc=\"Evaluating\"):\n", " pos_items = group[group[\"implicit\"] == 1][\"movie_idx\"].tolist()\n", "\n", " if not pos_items:\n", " continue # skip users with no positives\n", "\n", " for true_item in pos_items:\n", " # Sample negatives\n", " negatives = list(set(all_items) - set([true_item]))\n", " neg_sample = np.random.choice(negatives, size=num_negative, replace=False)\n", "\n", " item_input = np.array([true_item] + neg_sample)\n", " user_input = np.full_like(item_input, fill_value=user_id)\n", "\n", " # Predict scores for true + negatives\n", " y_score = model.predict({\"user_input\": user_input, \"item_input\": item_input}, verbose=0).flatten()\n", "\n", " # Rank scores\n", " top_k_idx = np.argsort(-y_score)[:k]\n", " top_k_items = item_input[top_k_idx]\n", "\n", " # Binary relevance labels\n", " y_true = np.zeros_like(item_input)\n", " y_true[0] = 1 # first is always true item\n", "\n", " # Top-K labels\n", " top_k_labels = y_true[top_k_idx]\n", "\n", " # Metrics\n", " precision_list.append(np.mean(top_k_labels))\n", " recall_list.append(np.sum(top_k_labels))\n", " hitrate_list.append(1.0 if np.sum(top_k_labels) > 0 else 0.0)\n", " ndcg_list.append(ndcg_score([y_true], [y_score], k=k))\n", "\n", " return {\n", " \"Precision@K\": np.mean(precision_list),\n", " \"Recall@K\": np.mean(recall_list),\n", " \"HitRate@K\": np.mean(hitrate_list),\n", " \"NDCG@K\": np.mean(ndcg_list),\n", " }" ], "metadata": { "id": "eAPjxMXRSW8J" }, "execution_count": 32, "outputs": [] }, { "cell_type": "code", "source": [ "def evaluate_and_store(model, model_name, test_data, k, results_dict):\n", " metrics = evaluate_model(model, test_data, k)\n", " results_dict[model_name] = {\n", " f\"Precision@{k}\": metrics[\"Precision@K\"],\n", " f\"Recall@{k}\": metrics[\"Recall@K\"],\n", " f\"HitRate@{k}\": metrics[\"HitRate@K\"],\n", " f\"NDCG@{k}\": metrics[\"NDCG@K\"]\n", " }\n", " print(f\"{model_name} done.\")" ], "metadata": { "id": "8iNSl9sPV6m_" }, "execution_count": 33, "outputs": [] }, { "cell_type": "code", "source": [ "model_scores = {}\n", "k = 10\n", "# Evaluate NCF model\n", "evaluate_and_store(ncf_model, \"NCF\", test_data, k, model_scores)" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "KYUC_fDaV-4Z", "outputId": "67f0e759-9677-4ed9-e827-864f4c70c55e" }, "execution_count": 34, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 8097/8097 [15:43<00:00, 8.58it/s]" ] }, { "output_type": "stream", "name": "stdout", "text": [ "NCF done.\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "\n" ] } ] }, { "cell_type": "code", "source": [ "def plot_model_comparison(scores_dict, k):\n", " df_scores = pd.DataFrame(scores_dict).T # models as rows\n", "\n", " plt.figure(figsize=(12, 6))\n", " df_scores.plot(kind='bar', figsize=(12, 6), width=0.8)\n", " plt.title(f'Model Performance Comparison @{k}', fontsize=16)\n", " plt.ylabel('Score')\n", " plt.xticks(rotation=45)\n", " plt.grid(axis='y')\n", " plt.ylim(0, 1.0)\n", " plt.legend(loc='upper right')\n", " plt.tight_layout()\n", " plt.show()" ], "metadata": { "id": "22os1pviWERm" }, "execution_count": 35, "outputs": [] }, { "cell_type": "code", "source": [ "model_scores" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "X7rROgNFaTzN", "outputId": "35d3a585-e9ca-4169-aa91-8a3ce9edfd11" }, "execution_count": 36, "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "{'NCF': {'Precision@10': np.float64(0.1700135852784982),\n", " 'Recall@10': np.float64(0.22697651032728935),\n", " 'HitRate@10': np.float64(0.7375571199209584),\n", " 'NDCG@10': np.float64(0.2151775885720339)}}" ] }, "metadata": {}, "execution_count": 36 } ] }, { "cell_type": "code", "source": [ "from tensorflow.keras.layers import Input, Embedding, Flatten, Dot\n", "from tensorflow.keras.models import Model\n", "\n", "# Implement the actual SVD model\n", "def build_svd_model(num_users, num_items, embedding_dim=64):\n", " user_input = Input(shape=(1,), name=\"user_input\")\n", " item_input = Input(shape=(1,), name=\"item_input\")\n", "\n", " user_emb = Embedding(num_users, embedding_dim, name=\"user_embedding\")(user_input)\n", " item_emb = Embedding(num_items, embedding_dim, name=\"item_embedding\")(item_input)\n", "\n", " user_vec = Flatten()(user_emb)\n", " item_vec = Flatten()(item_emb)\n", "\n", " dot_product = Dot(axes=1)([user_vec, item_vec])\n", "\n", " model = Model(inputs=[user_input, item_input], outputs=dot_product)\n", " model.compile(optimizer='adam', loss='mse') # Using Mean Squared Error for regression\n", "\n", " return model\n", "\n", "svd_model = build_svd_model(n_users, n_items)\n", "svd_model.summary()\n", "\n", "# Placeholder for DeepFM model\n", "# You will need to implement the DeepFM model building and training here\n", "# For now, let's assume a dummy model that returns random scores\n", "class DummyDeepFMModel:\n", " def predict(self, inputs, verbose=0):\n", " num_samples = inputs['user_input'].shape[0]\n", " return np.random.rand(num_samples, 1)\n", "\n", "deepfm_model = DummyDeepFMModel()\n", "\n", "# When other models are ready, just call:\n", "evaluate_and_store(deepfm_model, \"DeepFM\", test_data, k, model_scores)\n", "# evaluate_and_store(hybrid_model, \"HybridNCF\", test_data, k, model_scores)\n", "# evaluate_and_store(transformer_model, \"TransformerRec\", test_data, k, model_scores)\n", "evaluate_and_store(svd_model, \"SVD\", test_data, k, model_scores)" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 502 }, "id": "2lVVw3I9c4g_", "outputId": "eb8dd648-a1dc-4133-fdc3-43bf6a7fd0c9" }, "execution_count": 39, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1mModel: \"functional_1\"\u001b[0m\n" ], "text/html": [ "
Model: \"functional_1\"\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mConnected to \u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n", "│ user_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ user_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m518,208\u001b[0m │ user_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m958,784\u001b[0m │ item_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_2 (\u001b[38;5;33mFlatten\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ user_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_3 (\u001b[38;5;33mFlatten\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ item_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dot (\u001b[38;5;33mDot\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ flatten_2[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ │ │ │ flatten_3[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n" ], "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n",
              "┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃\n",
              "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n",
              "│ user_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ user_embedding      │ (None, 1, 64)     │    518,208 │ user_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_embedding      │ (None, 1, 64)     │    958,784 │ item_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_2 (Flatten) │ (None, 64)        │          0 │ user_embedding[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_3 (Flatten) │ (None, 64)        │          0 │ item_embedding[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dot (Dot)           │ (None, 1)         │          0 │ flatten_2[0][0],  │\n",
              "│                     │                   │            │ flatten_3[0][0]   │\n",
              "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m1,476,992\u001b[0m (5.63 MB)\n" ], "text/html": [ "
 Total params: 1,476,992 (5.63 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m1,476,992\u001b[0m (5.63 MB)\n" ], "text/html": [ "
 Trainable params: 1,476,992 (5.63 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ], "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 8097/8097 [00:10<00:00, 766.23it/s]\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "DeepFM done.\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 8097/8097 [15:45<00:00, 8.56it/s]\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "SVD done.\n" ] } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 567 }, "id": "13d7dde1", "outputId": "1adcd53c-bc83-446b-d851-02c858bd4ff2" }, "source": [ "plot_model_comparison(model_scores, k=10)" ], "execution_count": 47, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "
" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "
" ], "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAJOCAYAAABm7rQwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAdqpJREFUeJzs3Xd4FFXfxvF70zYJIQkQQuhduhB6RzrSlQ5CQAURECSilAepIoqIBRBQuoACKoiAFJEiRVAwVKmGohA6hJp63j98sxITIMQwS/l+rovrYc+cM/Ob3c085PbMGZsxxggAAAAAAACwkIuzCwAAAAAAAMDjh1AKAAAAAAAAliOUAgAAAAAAgOUIpQAAAAAAAGA5QikAAAAAAABYjlAKAAAAAAAAliOUAgAAAAAAgOUIpQAAAAAAAGA5QikAAAAAAABYjlAKAOAUefLkkc1mk81mU58+fe7Y97333nP0dXNzs6S+o0ePymazKU+ePGmyv5kzZ8pms6lz5873NC7hvG/94+Xlpfz586tLly7atWtXmtSXUn/99Zc6duyobNmyyc3NLVXnhAfD1q1b9fLLL6tYsWLy9/eXh4eHAgMDVaNGDb311ls6fvy4s0t86CT8jOLuduzYobFjx6pdu3Z64okn5OLiIpvNpjlz5qRo/MKFC/XUU08pQ4YMSpcunUqWLKkxY8YoJibmPlcOAEhL1vzLHgCAO5g7d67ee+89eXh4JLt9+vTpFlf04Klfv76CgoIkSWfOnNEvv/yimTNnau7cuZozZ45at25932swxujZZ5/Vtm3bVLRoUdWsWVPu7u6qWrXqfT820s7169f14osv6osvvpAkBQUFqWrVqvLz89O5c+e0bds2bdiwQSNHjtT8+fPVvHlz5xaMB8rNmzf11Vdf6fvvv9cvv/yi06dPKzY2VtmzZ1eRIkXUsmVLNW/eXOnTp7/jfkaMGKFvv/02VTW8+uqr+uijj+Tm5qZatWrJx8dHP/74o/r376/vvvtOq1atkpeXV6r2DQCwFqEUAMCpypYtq19//VXffvutWrVqlWT75s2btX//fpUrV06//PKLEyp8MAwYMEBPPfWU4/Xly5fVqlUrrV69Wl27dlXdunWVIUOG+1rDsWPHtG3bNuXKlUs7d+60bNYa0k5MTIzq16+vjRs3KmvWrJo8ebKaNm2aqE9sbKwWLVqkQYMG6ejRo84p9CH1+++/O7uE+2rq1KkaOnSoTp48KW9vb5UqVUqlSpWSMUYnT57UDz/8oCVLlihz5swaN26cnnvuudvuq2LFiipWrJhKly6t4OBgPf/881q/fv1da1i8eLE++ugj+fj4aP369SpdurQk6dy5c6pVq5Y2btyoN998U2PHjk2z8wYA3D/cvgcAcKrnn39e0u1nQ02bNi1RP/zNz89Pn376qSQpMjJSK1euvO/HTLidK2/evARSD6mRI0dq48aN8vf316ZNm5IEUpLk5uamVq1a6bffflONGjWcUOXDq3DhwipcuLCzy0hzUVFRat++vbp27So/Pz/NmzdP58+f16ZNm7RgwQItXLhQmzZt0pkzZzR37lz5+PioY8eO6tatm4wxye5zwIABGjVqlFq0aKF8+fKluJa3337bMT4hkJKkgIAAffLJJ5KkCRMm6PLly//hjAEAViGUAgA4VYkSJVS2bFmtWrVKf/31V6JtV69e1YIFC5QjRw7Vq1fvjvu5cOGCBg0apGLFisnb21vp06dXmTJlNGbMGN24ceO245YuXaoaNWooffr08vPzU7Vq1VJ0S8nFixc1dOhQlSpVSunTp5e3t7dKlCiht956S9evX0/Zyf9HefLkUcaMGSUpyYyW7du3q0OHDsqVK5fsdrsyZsyo+vXra/ny5bfdl81m09GjR/Xtt9+qVq1aypgxo2w2m2M9rISAYv369YnWuLr12NevX9c777yj0qVLO96XYsWKafDgwbp48WKS4966dldcXJzGjRun4OBg+fj4ONbmWbdunWw2m5566ilFRUVp+PDheuKJJ+Tp6alcuXKpf//+unnzpqS/Z5D169dP+fLlk6enp/LkyaNhw4YpNjY2ybHPnj2rjz/+WA0bNlTevHnl5eUlX19flS1bVu+++65jn/9267pBX3/9tapWrSpfX1+lS5dOVapUue17LP09C2n69OmqU6eOAgICZLfblSNHDtWpU0fjx49PdsyaNWv07LPPKmvWrI51n5555hlt2bLltsdJzpUrV/TRRx9JkoYMGaK8efPesb+Pj4+Cg4OTtK9cuVKNGzdWYGCgPDw8lC1bNrVp00a//vprsvt56qmnZLPZtG7dOv38889q1KiRMmXKpPTp06tGjRr66aefHH1XrFih2rVrK0OGDPLx8VHdunW1Y8eOJPu89XsT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a9cuBQYGatGiRXJzS/xPokOHDqXp8RJmr+zfvz9F/RM+y0yZMmnmzJlpUkPNmjWVPn16XblyRbNnz1bfvn1TPDZTpkyy2+2KiorSH3/8kewtms74fC9duqRLly4lG3gk/AznyJHD0bZw4UIZY/TKK6/ojTfeSDImrT/3WxUtWlRFixbV66+/LmOMfvzxR7Vv317fffedZs+erS5dukj659r0xx9/JHu7asL77OnpqYwZM963eqW/ZzlduHAhUZuHh4ekv2ckJcxSTJDcrYk//fSTbDZbknArNYKDgyX9HZaFh4cnO5ss4RbC0qVL/+fjAQDuP9aUAgA8MHLlyqVmzZopU6ZMqlixoipUqHDXMQmzh1asWKHTp08n2f7bb78pLCxMLi4uql69uqO9Ro0akqS5c+cmu9/Zs2cn2/70009LkhYsWHDX2qyWLVs2Pfnkk7py5YpjDSOrVK9eXS4uLgoLC9POnTuTbD916pSjppo1a1paW3ISftHOli1bkkBKkubMmZOmx2vQoIGkv9fjudP6SAnKlSungIAA7du37463F94LX19f9e7dW5I0YsQIhYeH37H/1atX9dtvv0mS3NzcHGsD3S4kS1gHzurP9/PPP0/SFh0drfnz50tSohmGCZ97crMYb968qa+//vr+FPkvNptNtWvXVvv27SVJYWFhjm0J9d7tfa5WrVqy3920lDdvXu3atSvRzLGEQDJhfb1bLV++PNHrXbt2ac6cOWrSpEmy673dqxw5cqhcuXKSpHnz5iXZvnHjRp04cUJ2uz3ZNcoAAA8eQikAwAPlm2++0blz57Rly5YU9a9ataoqVKigGzdu6KWXXtL169cd286dO6eXXnpJktS2bVvH7BNJeuWVV+Tq6qoFCxZo0aJFifb55ZdfOtZN+bdu3bopd+7cWrhwofr3768rV64k6RMREaHPPvssRfWntbfeekuS1KVLF8eMs1sZY7R161atWrUqTY+bK1cutWrVSsYYvfTSSzp//rxj27Vr19StWzfdvHlTlStXVuXKldP02KnxxBNPyNXVVbt3706yEPZ3332nDz74IE2PV6pUKTVr1kw3btxQs2bNdPz48UTbY2NjtWTJEsdrd3d3DR06VMYYPfPMM9q4cWOSfcbFxenHH3/Uzz//nOI6hgwZosqVK+vSpUuqWrVqst+RuLg4LVq0SGXKlEn0cIDXXntNkjRp0iStWbMm0ZiZM2dqyZIlcnd3V58+fVJcT1oYOXKk9uzZ43gdHx+v/v37688//1TOnDkT3cqasNj3rFmzEv3s3rx5Uz169LhrUJcas2fP1vbt25O0X7lyxfHduzUk69Onj9zc3LR48eIk4eiqVas0ZcoUSVK/fv3SvNZ/a9CggU6dOuVY5F76e0arh4eHXn/9dX3xxRe6dOmSjh07pp49ezq+p5cuXdKMGTP01FNPKWPGjPr444/TrKZBgwZJkt55551Ei6efP39ePXr0kCT16tVLfn5+aXZMAMB95LTn/gEAHmu5c+c2ksxPP/2Uov4Jj4B3dXVNsu3IkSOO/QUGBpqWLVuaZs2aGV9fXyPJlC5d2ly4cCHJuDFjxjge4V6hQgXTvn17U65cOSPJ9O3b10gyuXPnTjJuz549Jk+ePEaS8ff3N9WrVzft27c3zZs3N0WLFjU2m81kyZIl0ZiER8qHhISk6HwTJNS3du3aFI/56KOPjJubm5FkChQoYBo1amTat29v6tatawIDA40k079//0RjEt6/8PDw2+537dq1RpKpUaNGstvPnTtnSpYsaSQZPz8/07x5c9OyZUuTOXNmI8nkzZs3yf4TPtfk3ueUHvdu7+3QoUONJDN06NBE7X369DGSjIuLi6lRo4Zp166dKV26tJFkBg8e7Hjv/+127QkSHm3/78/swoULpmLFikaS8fDwME899ZRp3769qVWrluM9+rfXX3/dcbxixYqZZs2ambZt25qnnnrK+Pv7G0lm0qRJt60lOVeuXDGtW7d27Ddr1qymcePGpn379qZ+/fomY8aMRpKx2+1m8eLFicYmvC82m81UrVrVtG/f3vGeubq6mmnTpqX4/Uhwt+9ecu93wvcmV65c5plnnjHu7u6mbt26pm3btiZ//vxGkkmXLl2S68vFixcdx8uUKZNp3ry5adGihQkMDDTp06d3fCf+/V1K6c9vcrU2a9bMSDLZsmUzDRs2NB06dDANGzY0fn5+RpIpXry4iYyMTDRmypQpxsXFxXH9at++valSpYqx2WxGkhk2bFiSY4eEhBhJZsaMGcnWlppr0IULF4yfn1+Sn71p06YZV1dXx/kmvN8jRoxI1FatWjVz5MiRZPe9dOlSU6FCBcef9OnTG0kmf/78idqT07t3byPJuLu7mwYNGpgWLVo4fh6qVKlirl+/nuJzBAA4F6EUAMAp0jKUMsaY8+fPm4EDB5oiRYoYT09P4+3tbYKDg80777xzx19Qvv32W1O1alWTLl064+PjYypXrmy++uqru4YlkZGRZsyYMaZSpUrG39/fuLu7m6xZs5py5cqZ119/3WzevDlRfytDKWOM2b17t+nWrZspWLCg4/3Ily+fqV+/vvn444/NX3/9lah/WoRSxhhz7do1M3r0aFOqVCnj7e1tPD09TZEiRcygQYOSDQadGUrFx8ebadOmmTJlyhgfHx/j5+dnqlatar788ktjzO3Dp9SGUsYYExUVZSZNmmSqVatm/P39jYeHh8mRI4epW7eumThxYrL727Rpk+nQoYPJnTu3sdvtJn369OaJJ54wzZs3N1OnTk32fU2JLVu2mG7dupkiRYoYX19f4+bmZgICAkz16tXNqFGjzJ9//pnsuO+//940bNjQZMqUybi5uZmgoCDTqlUrs3Xr1nt+P4z5b6FU7ty5TUxMjBk1apQpXLiwsdvtJmPGjKZFixZm7969ye7v7NmzpkePHiZ//vzGbrebbNmymeeee84cOnTott+l/xJKbdiwwbz66qumfPnyJigoyHh4eJigoCBTqVIlM378eHP16tVk9/Xzzz+bli1bmqCgIOPm5mYyZcpkGjVqZFatWpVs//sRShnzT3j/1ltvJWrfs2ePGTx4sOnWrZt5++23zbFjx0x4eLjp2bOnGTNmjPnll1/uuN+Eeu7253bmz59vqlevbnx9fY2Xl5cpXry4eeedd0xUVNQ9nR8AwLlsxtzm8SIAAADAA+jo0aPKmzevcufOfduHEiBtxMXFqVmzZlq2bJmGDh2qIUOG3PUhCsaYFD3BEgAA1pQCAAAAkCxXV1d9+eWXatiwoYYPH66KFStq6dKliomJSdI3KipKCxYsULly5dS4cWPFx8c7oWIAwMPk/j6yAwAAAMBDzcfHR999953Gjh2r0aNHq0mTJkqfPr3KlCmjLFmyKD4+Xn/++afCwsJ048YNFS5cWO+9995dZ1QBAMDtewAAAHiocPue81y+fFlz587VihUrtGvXLp05c0aurq7Kli2bKleurBYtWqhRo0ay2WzOLhUA8BBwaii1YcMGvffee9q+fbtOnTqlRYsWqXnz5nccs27dOoWGhmrv3r3KmTOnBg8erM6dO1tSLwAAAAAAANKG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krRly5Yks6jq16+vLVu2pHgfDwLWlAIAAAAAAA+lpUuXysfHR7GxsYqKipKLi4smTJigqKgovf322/rhhx8cazbly5dPGzdu1JQpU1SjRg1NnDhRfn5++vLLL+Xu7i5JeuKJJxz7rlWrVqJjffrpp/L399f69evVuHHju9a2aNEi1axZUyVKlNCRI0fUrl07vf/++6pSpYomTJigtWvX6n//+58KFSqkYsWKadOmTWrQoEGKzjsiIkJZsmRJ1JYlSxZFRkbqxo0b8vLyStF+nI1QCgAAAAAAPJRq1qypSZMm6dq1a/rggw/k5uamFi1aaO/evbp+/brq1q2bqH90dLSCg4MlSWFhYapWrZojkPq306dPa/DgwY7FyePi4nT9+nUdP348RbXt3r1blStXliStXLlS1atXV8+ePSVJn3zyib744gtH36xZs+rixYv3fP4PO0IpAAAAAADwUEqXLp0KFCggSZo+fbpKliypadOmqXjx4pKkZcuWKXv27InG2O12SbrrbKKQkBCdP39eH330kXLnzi273a5KlSopOjo6RbXFxsY6jhEdHa106dI5tnl4eDhuM4yPj1dYWJhef/31FO1XkoKCgnT69OlEbadPn5avr+9DM0tKIpQCAAAAAACPABcXFw0aNEihoaE6ePCg7Ha7jh8/rho1aiTb/8knn9SsWbMUExOT7GypTZs26ZNPPlHDhg0lSSdOnNC5c+dSXE+BAgW0e/duSVLVqlX1v//9Tz///LPKlSunSZMm6dKlS4qMjNRrr72m7Nmzq1y5cined6VKlbR8+fJEbatXr3bcqviwYKFzAAAAAADwSGjVqpVcXV01ZcoU9evXT3379tWsWbN05MgR7dixQ+PHj9esWbMkSb169VJkZKTatm2rX3/9VYcOHdLnn3+uAwcOSJIKFiyozz//XL///ru2bt2qDh063NMspKZNm2rhwoW6cOGCypYtqwEDBqhatWqy2+1atWqVypQpo7Zt2+rixYtatGhRorH79u1TWFiYLly4oMuXLyssLExhYWGO7d27d9cff/yhN954Q/v379cnn3yiBQsWqG/fvv/9TbQQM6UAAAAAAMAjwc3NTb169dKYMWMUHh6uzJkza/To0frjjz/k7++v0qVLa9CgQZKkTJky6ccff9Trr7+uGjVqyNXVVaVKlVKVKlUkSdOmTVO3bt1UunRp5cyZU2+//bb69euX4loKFCigVq1aqV27dlq0aJHefPNN9evXT1euXFFgYKDOnDkjf39/x218t2rYsKGOHTvmeJ2wDpYxRpKUN29eLVu2TH379tVHH32kHDlyaOrUqapfv36q3ztnsJmEM3pMREZGys/PT5cvX5avr6+zywEAAAAAwGlu3ryp8PBw5c2bV56ens4u55ETHR2tVq1a6dChQxoyZIiefvpp+fn56dKlS/rmm280btw4rVixQjly5HB2qffsTt+dlGYvzJQCAAAAAAC4Dzw8PLR48WLNmjVL7777rtq1aycPDw/Fx8erWrVq+vjjjx/KQCqtEEoBAAAAAADcJzabTZ07d1bnzp119epVXbhwQZkzZ36onpJ3vxBKAQAAAAAAWMDHx0c+Pj7OLuOBwdP3AAAAAAAAYDlCKQAAAAAAAFiOUAoAAAAAAACWI5QCAAAAAACA5QilAAAAAAAAYDlCKQAAAAAAAFiOUAoAAAAAADySZs6cKX9/f2eXgdtwc3YBAAAAAADgwVJiVglLj7c7ZPc9j+ncubMuXbqkxYsXJ2pft26datasqYsXL6pNmzZq2LChY9uwYcO0ePFihYWFJRqTJ08eHTt2TJLk5eWl/Pnzq0+fPnrxxRfvqSabzaZFixapefPm93w+knT+/HmNHz9ey5Yt09GjR+Xl5aUSJUqoY8eOatOmjWw2W6L+o0aN0rJlyxQWFiYPDw9dunQpyT6PHz+ul19+WWvXrpWPj49CQkI0evRoubk5PxJiphQAAAAAAHgkeXl5KTAwMEV9R4wYoVOnTmnPnj167rnn1LVrV33//ff3ucJ/rFq1Sk888YR++eUX9evXT6tWrdI333yjxo0ba+TIkapfv76uXbuWaEx0dLRatWqll19+Odl9xsXFqVGjRoqOjtbmzZs1a9YszZw5U0OGDLHilO6KUAoAAAAAADySbr19b+bMmRo+fLh27twpm80mm82mmTNnOvqmT59eQUFBypcvn/r376+MGTNq9erVju2//PKL6tatq4CAAPn5+alGjRrasWOHY3uePHkkSc8884xsNpvjtSR9++23Kl26tDw9PZUvXz4NHz5csbGxju2//vqr2rVrp5kzZ2rZsmVq06aNgoODVbZsWb388svauXOncuTIoXbt2iU6v+HDh6tv374qUSL5mW2rVq3Svn37NGfO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8cYb9/w+OhMzpQAAAAAAwEPJ09NT7777ri5evHjHfgcOHNDJkyf1yy+/qH///vrhhx9UvHhx7d6929Hn0qVLqly5smbPnq2BAwdqx44d2rBhg9q0aaM33nhDly9fliRt27ZNpUuX1u+//66JEydqz549WrdunV588UVNmjRJe/fuTXTszz//XMWLF9dff/2lYcOGac2aNfriiy9UsWJFdevWTZ06dVJcXFySmhMCruRERkaqXr16yp07t7Zv36733ntPw4YN06effnqvb6FT2YwxxtlFWCkyMlJ+fn66fPmyfH19nV0OAAAAAABOc/PmTYWHhytv3rzy9PR0tD8sM6XOnz+vw4cPq0mTJhozZoyk5GdKXbx4Uf7+/o6xN27cUHBwsAICArRx40ZJUo8ePTR79mwdPHhQ2bJlS3Ssq1evytPTU66uripWrJi8vb21bds2ubgknetjjHHMqPruu+/UtWtXLV68WBUrVkzS99q1a2rRooUKFiyo8ePHJ9k+bNgwLV68OMlMqUmTJul///ufIiIi5OHhIUkaMGCAFi9erP3796f8TfwPbvfdkVKevTBTCgAAAAAAPJRcXV319ttva/z48frzzz9TPM7Ly0vdu3fXpk2bdObMGcXHx+vLL79Uhw4dkgRSkuTj4yM3NzeFhYXp999/V79+/ZINpCQ5Aqno6Gj16tVLM2fOVMWKFbVx40aVLVtWWbJkUffu3dWpUyctXrxYc+fO1bx583TkyJEU179lyxZVr17dEUhJUv369XXgwIG7zhp7kBBKAQAAAACAh9YzzzyjUqVKaejQofc0rnDhwpKko0eP6ty5c7p48aKj7XYOHjwoSSpUqJCj7cyZM/Lx8XH8+eSTTyRJ69evV+bMmdWgQQNdunRJzZo1U6NGjbRy5UoFBARo3rx5iomJUaZMmdSwYUOtXr06xbVHREQoS5YsidoSXkdERKR4P87GQucAAAAAAOCh9u6776pWrVrq169fisckrGZks9n0X1Y2ypQpk+P2uqeeekrR0dGSpN27d6ty5cqSpM2bNytTpkwaPny4JKlUqVKaP3++Yx9Zs2Z9qGY4pRVmSgEAAAAAgIda9erVVb9+fQ0cODDFY37//e91rPLkyaPMmTPL39//rusxFSxYUNLfC6cncHV1VYECBVSgQIFET92LjY2Vl5eXpL9v5UuXLl2iffn4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WunDDz9U1qxZHf1btmwpDw8PzZ07l1+0AOAW0dHRGjlypKZPn65169apYMGCzi4JwG2wphSAVClYsKDGjx8vm82m0aNHa/PmzYm2E0gBQMrFx8c7gqWbN28qJiZG8fHx6tixoyZPnqyFCxdq/PjxOnHihCTp6tWr+uOPP5QjRw4CKQC4xZw5c/T666/rs88+09KlSwmkgAccM6UA/CeHDh1SaGiozp07pw8++EAVK1Z0dkkA8FCJj4+Xi8vf/53www8/1Pr163X9+nXlzJlTH330kdKlS6dPP/1U3bt3V6lSpVS6dGmdO3dOR48e1bZt2+Th4eHkMwCAB8OBAwfUvXt3ZciQQaNGjVKRIkWcXRKAuyCUAvCf7d+/X2+++abef/995cqVy9nlAMBDacCAAZo+fbrefPNNeXt7a+DAgcqbN682bNggu92u2bNnq3PnzqpcubJ69eqltm3bSvr7Eefu7u5Orh4AHgxnzpyR3W6Xn5+fs0sBkALcXwPgPytcuLDmzp3Lf60HgFTau3evli9frq+++krVq1fXkiVLFBUVpZCQENntdklSp06dFBcXpxdeeEF16tTR9evX5e3tTSAFALcIDAx0dgkA7gGhFIA0QSAFAKl37tw5Xb582RFIdejQQWPHjtVLL72kq1evauHCherUqZO6dOmiuLg49ejRQ9euXdP//vc/+fv7O7t8AACAVGGhcwAAAAvdunJCwt9z5cqlokWL6r333lOHDh30/vvv66WXXpIk7du3T99//7127twpSXrxxRc1btw4TZ06VbGxsdafAAAAQBphTSkAAACL3LqouSTduHFDXl5eioyMVJMmTfTTTz9p6NChGjp0qGN7ixYt5OXlpYULFyYae+nSJWZJAQCAhxqhFAAAgAVuDaTef/99bd++XTt27NCLL76oVq1aydPTU6VLl1bhwoVVvXp1ZcuWTV988YXOnTun7du3y93dXfHx8bLZbLLZbDLGyGazOfmsAAAAUo9QCgAAwEIDBw7UjBkz1L9/f6VLl079+/fXU089pUWLFunQoUMaMWKEwsLCFBQUpNy5c2vy5Mlyc3NTbGys3NxYDhQAADw6+JcNAACARX799Vd98803Wrx4sSpWrKhff/1VV65c0TPPPCNJKliwoGbNmqWoqCi5uLg4nrxHIAUAAB5FLHQOAABgkbi4OPn6+qpixYpasGCBatasqfHjx6tTp066evWqfvjhB8c6UwmBlDGGQAoAADyS/q+9+weJ+o/jOP468XAIosVBykXOXXBx0EVoDpza2iQlhKbAA/+ADiJIGDgkNkXREkTQEA2CKbRcwTkIgttB4CA4iOK/lp/+kN96fn/QPR7r53sfbvzyvOP9FqUAAG7B79+/U6/X8/bt22xvb+fg4CB3795No9HI69evMzo6moWFhYyNjSVJfvz4kZWVlTQajRv3mBsFAPyt/OwGANBkHz9+zNraWmq1Wo6OjnJ6epqHDx+mWq3m8ePHefr0aaanpzM+Pp4kOTk5ycuXL9PR0ZFKpfI/f3sAgGIYdA4A0ESrq6t58eJFqtVq+vr60t/fn1evXuXdu3e5vLzMkydPsr29na2trczOzubg4CBfvnxJo9HIz58/r7fsXW3qAwD4W4lSAABNsrq6mmfPnuX9+/cZGRm5cfbhw4csLi7mzp07GRsby8bGRj5//pxKpZKenh5b9gCAliNKAQA0wfr6eoaHhzMzM5OpqalcvWKdn59fR6bl5eVMTU3lzZs3GRkZyf7+fjo7O6/vEKQAgFbif+EAAE1w//79DA4OplarZWNjI6VSKaVSKe3t7bm4uEiSTExMpLu7O9++fUuS3Lt37/rztuwBAK1GlAIAaILe3t6sra3l5OQk8/Pz+f79+/XZ1Qa9w8PDHB8fp6urK0lSLpf/8wwAQKsQpQAAmqS3tzfLy8splUqZm5vL5ubmjfO9vb08ePAgAwMDSRJTFACAVmamFABAk+3u7mZiYiKXl5epVqsZGhrK2dlZHj16lLa2tnz69Ml2PQCg5YlSAAC34CpMtbW1ZXJyMktLS9nZ2cmvX79SLpdzcXEhTAEALU2UAgC4Jbu7u3n+/Hm+fv2anp6e1Ov1lMtlW/YAACJKAQDcqp2dnaysrGRpaSnt7e2CFADAP0QpAICCCFIAAP8SpQAAAAAonOmaAAAAABROlAIAAACgcKIUAAAAAIUTpQAAAAAonCgFAAAAQOFEKQAAAAAKJ0oBAAAAUDhRCgAAAIDCiVIAAAAAFE6UAgAAAKBwfwDiN3vZiGyDsQAAAABJRU5ErkJggg==\n" }, "metadata": {} } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "fa0b2a24", "outputId": "2c3c0a0a-c471-4877-915b-15dc4f56343e" }, "source": [ "# Evaluate DeepFM model and store results\n", "evaluate_and_store(deepfm_model, \"DeepFM\", test_data, k, model_scores)" ], "execution_count": 45, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "100%|██████████| 8097/8097 [00:10<00:00, 776.46it/s]" ] }, { "output_type": "stream", "name": "stdout", "text": [ "DeepFM done.\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "\n" ] } ] }, { "cell_type": "code", "metadata": { "id": "b347b3d6" }, "source": [ "class DeepFMDataGenerator(Sequence):\n", " def __init__(self, df, movie_features_dict, batch_size):\n", " self.users = df['user_idx'].values\n", " self.items = df['movie_idx'].values\n", " self.labels = df['implicit'].values\n", " self.movie_features_dict = movie_features_dict\n", " self.batch_size = batch_size\n", "\n", " def __len__(self):\n", " return int(np.ceil(len(self.users) / self.batch_size))\n", "\n", " def __getitem__(self, idx):\n", " start = idx * self.batch_size\n", " end = (idx + 1) * self.batch_size\n", "\n", " batch_users = self.users[start:end]\n", " batch_items = self.items[start:end]\n", " batch_labels = self.labels[start:end]\n", "\n", " # Get movie genre features for the batch\n", " batch_movie_features = np.array([self.movie_features_dict[item_idx] for item_idx in batch_items])\n", "\n", " return (\n", " {\n", " \"user_input\": batch_users,\n", " \"item_input\": batch_items,\n", " \"item_genre_input\": batch_movie_features\n", " },\n", " batch_labels\n", " )" ], "execution_count": 41, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "cd78117d" }, "source": [ "from tensorflow.keras.layers import Input, Embedding, Dense, Flatten, concatenate, Add, Dropout, Subtract, Multiply\n", "from tensorflow.keras.models import Model\n", "import tensorflow as tf\n", "import keras\n", "\n", "def build_deepfm_model(num_users, num_items, num_genres, embedding_dim=32, deep_layers=[128, 64, 32]):\n", " # Input layers\n", " user_input = Input(shape=(1,), name=\"user_input\")\n", " item_input = Input(shape=(1,), name=\"item_input\")\n", " item_genre_input = Input(shape=(num_genres,), name=\"item_genre_input\") # For genre features\n", "\n", " # Embedding layers for user and item indices (used in FM and Deep parts)\n", " user_embedding = Embedding(num_users, embedding_dim, name=\"user_embedding\")\n", " item_embedding = Embedding(num_items, embedding_dim, name=\"item_embedding\")\n", "\n", " # Linear Part (First-order interactions)\n", " user_linear = Flatten()(Embedding(num_users, 1, name=\"user_linear\")(user_input))\n", " item_linear = Flatten()(Embedding(num_items, 1, name=\"item_linear\")(item_input))\n", " # Linear part for genre features (assuming direct usage of binary genre features)\n", " genre_linear = Dense(1, use_bias=False, name=\"genre_linear\")(item_genre_input)\n", "\n", " # FM Part (Second-order interactions)\n", " # Embeddings for FM part (shared with Deep part)\n", " user_fm = user_embedding(user_input)\n", " item_fm = item_embedding(item_input)\n", "\n", " # Concatenate user and item embeddings for FM part\n", " concat_fm = concatenate([user_fm, item_fm], axis=1)\n", "\n", " # FM interaction calculation using Keras operations\n", " sum_of_squares = keras.ops.power(keras.ops.sum(concat_fm, axis=2, keepdims=True), 2)\n", " squares_of_sum = keras.ops.sum(keras.ops.power(concat_fm, 2), axis=2, keepdims=True)\n", "\n", " # The Subtract layer expects a list of tensors\n", " diff_of_squares = Subtract()([sum_of_squares, squares_of_sum])\n", "\n", " # Multiply by 0.5 - use keras.ops.convert_to_tensor to ensure it's a KerasTensor\n", " fm_output = Multiply()([keras.ops.convert_to_tensor(0.5, dtype=tf.float32), diff_of_squares])\n", "\n", " fm_output = Flatten()(fm_output) # Flatten the output\n", "\n", "\n", " # Deep Part (Higher-order interactions)\n", " user_deep = Flatten()(user_embedding(user_input))\n", " item_deep = Flatten()(item_embedding(item_input))\n", " # Concatenate user, item and genre features for the deep part\n", " deep_input = concatenate([user_deep, item_deep, item_genre_input])\n", "\n", " x = deep_input\n", " for layer_size in deep_layers:\n", " x = Dense(layer_size, activation='relu')(x)\n", " x = Dropout(0.3)(x) # Added Dropout for regularization\n", "\n", " # Output layer\n", " # Combine Linear, FM, and Deep parts\n", " # Ensure all inputs to Add are tensors\n", " output = Add()([user_linear, item_linear, genre_linear, fm_output, Dense(1)(x)]) # Added Dense layer before adding deep part\n", "\n", "\n", " output = Dense(1, activation='sigmoid')(output) # Sigmoid for binary classification\n", "\n", " model = Model(inputs=[user_input, item_input, item_genre_input], outputs=output)\n", " model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])\n", "\n", " return model\n", "\n", "# Example usage (assuming you have num_users, num_items, num_genres defined)\n", "# num_users = ...\n", "# num_items = ...\n", "# num_genres = ...\n", "# deepfm_model = build_deepfm_model(num_users, num_items, num_genres)\n", "# deepfm_model.summary()" ], "execution_count": 62, "outputs": [] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "f8bc0bb6", "outputId": "9aeb873b-9f20-4176-ae25-d6e44df09a86" }, "source": [ "# Save the SVD model\n", "svd_model.save(\"svd_model.keras\")\n", "\n", "print(\"SVD model saved successfully!\")" ], "execution_count": 64, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "SVD model saved successfully!\n" ] } ] }, { "cell_type": "code", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "id": "6bdcf8eb", "outputId": "00ef8fea-646a-4039-f2df-5bf51a330504" }, "source": [ "# Prepare data generators for DeepFM\n", "batch_size = 1024\n", "deepfm_train_gen = DeepFMDataGenerator(train_data, movie_idx_to_genre, batch_size)\n", "deepfm_val_gen = DeepFMDataGenerator(val_data, movie_idx_to_genre, batch_size)\n", "\n", "# Get the number of genres\n", "num_genres = movie_features.shape[1]\n", "\n", "# Build and train the DeepFM model\n", "deepfm_model = build_deepfm_model(n_users, n_items, num_genres)\n", "deepfm_model.summary()\n", "\n", "deepfm_model.fit(\n", " deepfm_train_gen,\n", " validation_data=deepfm_val_gen,\n", " epochs=5\n", ")\n", "\n", "# Save the DeepFM model\n", "deepfm_model.save(\"deepfm_model.keras\")\n", "\n", "print(\"DeepFM model built, trained, and saved successfully!\")" ], "execution_count": 63, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1mModel: \"functional_2\"\u001b[0m\n" ], "text/html": [ "
Model: \"functional_2\"\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mConnected to \u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n", "│ user_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ user_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m259,104\u001b[0m │ user_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ user_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_embedding │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m479,392\u001b[0m │ item_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ item_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_genre_input │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m19\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_23 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ user_embedding[\u001b[38;5;34m1\u001b[0m… │\n", "│ (\u001b[38;5;33mFlatten\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_24 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ item_embedding[\u001b[38;5;34m1\u001b[0m… │\n", "│ (\u001b[38;5;33mFlatten\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ concatenate_10 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m83\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ flatten_23[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ (\u001b[38;5;33mConcatenate\u001b[0m) │ │ │ flatten_24[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ │ │ │ item_genre_input… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_4 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m) │ \u001b[38;5;34m10,752\u001b[0m │ concatenate_10[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ concatenate_9 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ user_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "│ (\u001b[38;5;33mConcatenate\u001b[0m) │ │ │ item_embedding[\u001b[38;5;34m0\u001b[0m… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout_2 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ dense_4[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ sum_14 (\u001b[38;5;33mSum\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ concatenate_9[\u001b[38;5;34m0\u001b[0m]… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ power_15 (\u001b[38;5;33mPower\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ concatenate_9[\u001b[38;5;34m0\u001b[0m]… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_5 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m8,256\u001b[0m │ dropout_2[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ power_14 (\u001b[38;5;33mPower\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ sum_14[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ sum_15 (\u001b[38;5;33mSum\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ power_15[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout_3 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m64\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ dense_5[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ subtract_7 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ power_14[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ (\u001b[38;5;33mSubtract\u001b[0m) │ │ │ sum_15[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_6 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m2,080\u001b[0m │ dropout_3[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ user_linear │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m8,097\u001b[0m │ user_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ item_linear │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m14,981\u001b[0m │ item_input[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mEmbedding\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ multiply_7 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ subtract_7[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mMultiply\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout_4 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ dense_6[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_20 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ user_linear[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mFlatten\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_21 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ item_linear[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mFlatten\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ genre_linear │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m19\u001b[0m │ item_genre_input… │\n", "│ (\u001b[38;5;33mDense\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ flatten_22 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ multiply_7[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mFlatten\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_7 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m33\u001b[0m │ dropout_4[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ add (\u001b[38;5;33mAdd\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ flatten_20[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ │ │ │ flatten_21[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ │ │ │ genre_linear[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m…\u001b[0m │\n", "│ │ │ │ flatten_22[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m], │\n", "│ │ │ │ dense_7[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_8 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m3\u001b[0m │ add[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n" ], "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n",
              "┃ Layer (type)        ┃ Output Shape      ┃    Param # ┃ Connected to      ┃\n",
              "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n",
              "│ user_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_input          │ (None, 1)         │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ user_embedding      │ (None, 1, 32)     │    259,104 │ user_input[0][0], │\n",
              "│ (Embedding)         │                   │            │ user_input[0][0]  │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_embedding      │ (None, 1, 32)     │    479,392 │ item_input[0][0], │\n",
              "│ (Embedding)         │                   │            │ item_input[0][0]  │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_genre_input    │ (None, 19)        │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_23          │ (None, 32)        │          0 │ user_embedding[1… │\n",
              "│ (Flatten)           │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_24          │ (None, 32)        │          0 │ item_embedding[1… │\n",
              "│ (Flatten)           │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ concatenate_10      │ (None, 83)        │          0 │ flatten_23[0][0], │\n",
              "│ (Concatenate)       │                   │            │ flatten_24[0][0], │\n",
              "│                     │                   │            │ item_genre_input… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_4 (Dense)     │ (None, 128)       │     10,752 │ concatenate_10[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ concatenate_9       │ (None, 2, 32)     │          0 │ user_embedding[0… │\n",
              "│ (Concatenate)       │                   │            │ item_embedding[0… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout_2 (Dropout) │ (None, 128)       │          0 │ dense_4[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ sum_14 (Sum)        │ (None, 2, 1)      │          0 │ concatenate_9[0]… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ power_15 (Power)    │ (None, 2, 32)     │          0 │ concatenate_9[0]… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_5 (Dense)     │ (None, 64)        │      8,256 │ dropout_2[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ power_14 (Power)    │ (None, 2, 1)      │          0 │ sum_14[0][0]      │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ sum_15 (Sum)        │ (None, 2, 1)      │          0 │ power_15[0][0]    │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout_3 (Dropout) │ (None, 64)        │          0 │ dense_5[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ subtract_7          │ (None, 2, 1)      │          0 │ power_14[0][0],   │\n",
              "│ (Subtract)          │                   │            │ sum_15[0][0]      │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_6 (Dense)     │ (None, 32)        │      2,080 │ dropout_3[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ user_linear         │ (None, 1, 1)      │      8,097 │ user_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ item_linear         │ (None, 1, 1)      │     14,981 │ item_input[0][0]  │\n",
              "│ (Embedding)         │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ multiply_7          │ (None, 2, 1)      │          0 │ subtract_7[0][0]  │\n",
              "│ (Multiply)          │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout_4 (Dropout) │ (None, 32)        │          0 │ dense_6[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_20          │ (None, 1)         │          0 │ user_linear[0][0] │\n",
              "│ (Flatten)           │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_21          │ (None, 1)         │          0 │ item_linear[0][0] │\n",
              "│ (Flatten)           │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ genre_linear        │ (None, 1)         │         19 │ item_genre_input… │\n",
              "│ (Dense)             │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ flatten_22          │ (None, 2)         │          0 │ multiply_7[0][0]  │\n",
              "│ (Flatten)           │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_7 (Dense)     │ (None, 1)         │         33 │ dropout_4[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ add (Add)           │ (None, 2)         │          0 │ flatten_20[0][0], │\n",
              "│                     │                   │            │ flatten_21[0][0], │\n",
              "│                     │                   │            │ genre_linear[0][… │\n",
              "│                     │                   │            │ flatten_22[0][0], │\n",
              "│                     │                   │            │ dense_7[0][0]     │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_8 (Dense)     │ (None, 1)         │          3 │ add[0][0]         │\n",
              "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m782,717\u001b[0m (2.99 MB)\n" ], "text/html": [ "
 Total params: 782,717 (2.99 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m782,717\u001b[0m (2.99 MB)\n" ], "text/html": [ "
 Trainable params: 782,717 (2.99 MB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ], "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "Epoch 1/5\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "/usr/local/lib/python3.12/dist-packages/keras/src/trainers/data_adapters/py_dataset_adapter.py:121: UserWarning: Your `PyDataset` class should call `super().__init__(**kwargs)` in its constructor. `**kwargs` can include `workers`, `use_multiprocessing`, `max_queue_size`. Do not pass these arguments to `fit()`, as they will be ignored.\n", " self._warn_if_super_not_called()\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m164s\u001b[0m 21ms/step - accuracy: 0.8969 - loss: 0.3902 - val_accuracy: 0.9115 - val_loss: 0.2499\n", "Epoch 2/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m163s\u001b[0m 21ms/step - accuracy: 0.9074 - loss: 0.2274 - val_accuracy: 0.9126 - val_loss: 0.2360\n", "Epoch 3/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m159s\u001b[0m 21ms/step - accuracy: 0.9131 - loss: 0.2112 - val_accuracy: 0.9120 - val_loss: 0.2279\n", "Epoch 4/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m159s\u001b[0m 21ms/step - accuracy: 0.9113 - loss: 0.2118 - val_accuracy: 0.9136 - val_loss: 0.2218\n", "Epoch 5/5\n", "\u001b[1m7713/7713\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m161s\u001b[0m 21ms/step - accuracy: 0.9138 - loss: 0.2048 - val_accuracy: 0.9131 - val_loss: 0.2195\n", "DeepFM model built, trained, and saved successfully!\n" ] } ] }, { "cell_type": "code", "metadata": { "id": "9514b415" }, "source": [ "def recommend_movies(user_id, N, model, all_movie_indices, user_encoder, movie_encoder, movie_features_dict=None):\n", " \"\"\"\n", " Generates movie recommendations for a given user.\n", "\n", " Args:\n", " user_id: The original user ID.\n", " N: The number of recommendations to generate.\n", " model: The trained recommendation model (NCF, DeepFM, or SVD).\n", " all_movie_indices: A list of all movie indices in the dataset.\n", " user_encoder: The LabelEncoder used for user IDs.\n", " movie_encoder: The LabelEncoder used for movie IDs.\n", " movie_features_dict: A dictionary mapping movie indices to their features (for DeepFM).\n", "\n", " Returns:\n", " A list of top N recommended movie titles.\n", " \"\"\"\n", " try:\n", " # Convert original user ID to encoded user index\n", " encoded_user_id = user_encoder.transform([user_id])[0]\n", " except ValueError:\n", " print(f\"User ID {user_id} not found in the dataset.\")\n", " return []\n", "\n", " # Get movies the user has already interacted with\n", " user_interactions = filtered_df[filtered_df['userId'] == user_id]['movie_idx'].tolist()\n", "\n", " # Identify movies the user has not interacted with\n", " movies_to_predict = [movie_idx for movie_idx in all_movie_indices if movie_idx not in user_interactions]\n", "\n", " if not movies_to_predict:\n", " print(f\"User ID {user_id} has interacted with all movies.\")\n", " return []\n", "\n", " # Prepare data for prediction\n", " user_inputs = np.full(len(movies_to_predict), encoded_user_id)\n", " item_inputs = np.array(movies_to_predict)\n", "\n", " # Predict scores using the specified model\n", " if model.name == \"functional\": # Assuming NCF model is named \"functional\" by default\n", " predictions = model.predict({\"user_input\": user_inputs, \"item_input\": item_inputs}, verbose=0).flatten()\n", " elif model.name == \"functional_1\": # Assuming SVD model is named \"functional_1\" by default\n", " predictions = model.predict({\"user_input\": user_inputs, \"item_input\": item_inputs}, verbose=0).flatten()\n", " elif model.name == \"functional_2\": # Assuming DeepFM model is named \"functional_2\" by default\n", " if movie_features_dict is None:\n", " print(\"Movie features dictionary is required for DeepFM model.\")\n", " return []\n", " item_genre_inputs = np.array([movie_features_dict[item_idx] for item_idx in movies_to_predict])\n", " predictions = model.predict({\"user_input\": user_inputs, \"item_input\": item_inputs, \"item_genre_input\": item_genre_inputs}, verbose=0).flatten()\n", " else:\n", " print(f\"Unknown model type: {model.name}\")\n", " return []\n", "\n", "\n", " # Rank movies based on predicted scores\n", " top_movie_indices = item_inputs[np.argsort(-predictions)[:N]]\n", "\n", " # Convert encoded movie indices back to original movie IDs\n", " top_movie_ids = movie_encoder.inverse_transform(top_movie_indices)\n", "\n", " # Get movie titles\n", " recommended_movies = movies_df[movies_df['movieId'].isin(top_movie_ids)]\n", "\n", " # Sort recommended movies by the order of top_movie_ids\n", " recommended_movies = recommended_movies.set_index('movieId').loc[top_movie_ids].reset_index()\n", "\n", " return recommended_movies[['movieId', 'title', 'genres']]" ], "execution_count": 66, "outputs": [] }, { "cell_type": "code", "source": [ "# Get recommendations for a sample user (e.g., user with original ID 1)\n", "sample_user_id = 1\n", "num_recommendations = 10\n", "\n", "# Get all movie indices for recommendation\n", "all_movie_indices = list(range(len(movie_encoder.classes_)))\n", "\n", "# Use the trained NCF model for recommendations\n", "recommended_movies_ncf = recommend_movies(sample_user_id, num_recommendations, ncf_model, all_movie_indices, user_encoder, movie_encoder)\n", "\n", "if recommended_movies_ncf is not None:\n", " print(f\"Top {num_recommendations} movie recommendations for user {sample_user_id} using NCF:\")\n", " display(recommended_movies_ncf)\n", "\n", "print(\"\\n---\")\n", "\n", "# Use the trained DeepFM model for recommendations\n", "recommended_movies_deepfm = recommend_movies(sample_user_id, num_recommendations, deepfm_model, all_movie_indices, user_encoder, movie_encoder, movie_features_dict=movie_idx_to_genre)\n", "\n", "if recommended_movies_deepfm is not None:\n", " print(f\"Top {num_recommendations} movie recommendations for user {sample_user_id} using DeepFM:\")\n", " display(recommended_movies_deepfm)\n", "\n", "print(\"\\n---\")\n", "\n", "# Use the trained SVD model for recommendations\n", "recommended_movies_svd = recommend_movies(sample_user_id, num_recommendations, svd_model, all_movie_indices, user_encoder, movie_encoder)\n", "\n", "if recommended_movies_svd is not None:\n", " print(f\"Top {num_recommendations} movie recommendations for user {sample_user_id} using SVD:\")\n", " display(recommended_movies_svd)" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "id": "eKxkhpp73s9Y", "outputId": "e2e94333-d310-4fea-8526-7153e96f02e7" }, "execution_count": 70, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Top 10 movie recommendations for user 1 using NCF:\n" ] }, { "output_type": "display_data", "data": { "text/plain": [ " movieId title \\\n", "0 318 Shawshank Redemption, The (1994) \n", "1 858 Godfather, The (1972) \n", "2 296 Pulp Fiction (1994) \n", "3 50 Usual Suspects, The (1995) \n", "4 2959 Fight Club (1999) \n", "5 2571 Matrix, The (1999) \n", "6 5618 Spirited Away (Sen to Chihiro no kamikakushi) ... \n", "7 47 Seven (a.k.a. Se7en) (1995) \n", "8 1198 Raiders of the Lost Ark (Indiana Jones and the... \n", "9 4226 Memento (2000) \n", "\n", " genres \n", "0 Crime|Drama \n", "1 Crime|Drama \n", "2 Comedy|Crime|Drama|Thriller \n", "3 Crime|Mystery|Thriller \n", "4 Action|Crime|Drama|Thriller \n", "5 Action|Sci-Fi|Thriller \n", "6 Adventure|Animation|Fantasy \n", "7 Mystery|Thriller \n", "8 Action|Adventure \n", "9 Mystery|Thriller " ], "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", " \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", "
movieIdtitlegenres
0318Shawshank Redemption, The (1994)Crime|Drama
1858Godfather, The (1972)Crime|Drama
2296Pulp Fiction (1994)Comedy|Crime|Drama|Thriller
350Usual Suspects, The (1995)Crime|Mystery|Thriller
42959Fight Club (1999)Action|Crime|Drama|Thriller
52571Matrix, The (1999)Action|Sci-Fi|Thriller
65618Spirited Away (Sen to Chihiro no kamikakushi) ...Adventure|Animation|Fantasy
747Seven (a.k.a. Se7en) (1995)Mystery|Thriller
81198Raiders of the Lost Ark (Indiana Jones and the...Action|Adventure
94226Memento (2000)Mystery|Thriller
\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" ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "dataframe", "variable_name": "recommended_movies_ncf", "summary": "{\n \"name\": \"recommended_movies_ncf\",\n \"rows\": 10,\n \"fields\": [\n {\n \"column\": \"movieId\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1950,\n \"min\": 47,\n \"max\": 5618,\n \"num_unique_values\": 10,\n \"samples\": [\n 1198,\n 858,\n 2571\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981)\",\n \"Godfather, The (1972)\",\n \"Matrix, The (1999)\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"genres\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 8,\n \"samples\": [\n \"Comedy|Crime|Drama|Thriller\",\n \"Adventure|Animation|Fantasy\",\n \"Crime|Drama\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}" } }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "\n", "---\n", "Top 10 movie recommendations for user 1 using DeepFM:\n" ] }, { "output_type": "display_data", "data": { "text/plain": [ " movieId title \\\n", "0 318 Shawshank Redemption, The (1994) \n", "1 858 Godfather, The (1972) \n", "2 2959 Fight Club (1999) \n", "3 50 Usual Suspects, The (1995) \n", "4 296 Pulp Fiction (1994) \n", "5 2571 Matrix, The (1999) \n", "6 904 Rear Window (1954) \n", "7 1198 Raiders of the Lost Ark (Indiana Jones and the... \n", "8 1193 One Flew Over the Cuckoo's Nest (1975) \n", "9 750 Dr. Strangelove or: How I Learned to Stop Worr... \n", "\n", " genres \n", "0 Crime|Drama \n", "1 Crime|Drama \n", "2 Action|Crime|Drama|Thriller \n", "3 Crime|Mystery|Thriller \n", "4 Comedy|Crime|Drama|Thriller \n", "5 Action|Sci-Fi|Thriller \n", "6 Mystery|Thriller \n", "7 Action|Adventure \n", "8 Drama \n", "9 Comedy|War " ], "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", " \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", "
movieIdtitlegenres
0318Shawshank Redemption, The (1994)Crime|Drama
1858Godfather, The (1972)Crime|Drama
22959Fight Club (1999)Action|Crime|Drama|Thriller
350Usual Suspects, The (1995)Crime|Mystery|Thriller
4296Pulp Fiction (1994)Comedy|Crime|Drama|Thriller
52571Matrix, The (1999)Action|Sci-Fi|Thriller
6904Rear Window (1954)Mystery|Thriller
71198Raiders of the Lost Ark (Indiana Jones and the...Action|Adventure
81193One Flew Over the Cuckoo's Nest (1975)Drama
9750Dr. Strangelove or: How I Learned to Stop Worr...Comedy|War
\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" ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "dataframe", "variable_name": "recommended_movies_deepfm", "summary": "{\n \"name\": \"recommended_movies_deepfm\",\n \"rows\": 10,\n \"fields\": [\n {\n \"column\": \"movieId\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 955,\n \"min\": 50,\n \"max\": 2959,\n \"num_unique_values\": 10,\n \"samples\": [\n 1193,\n 858,\n 2571\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"One Flew Over the Cuckoo's Nest (1975)\",\n \"Godfather, The (1972)\",\n \"Matrix, The (1999)\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"genres\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 9,\n \"samples\": [\n \"Drama\",\n \"Action|Crime|Drama|Thriller\",\n \"Mystery|Thriller\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}" } }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "\n", "---\n", "Top 10 movie recommendations for user 1 using SVD:\n" ] }, { "output_type": "display_data", "data": { "text/plain": [ " movieId title genres\n", "0 4772 Dinner Rush (2000) Drama\n", "1 26231 Performance (1970) Crime|Drama|Thriller\n", "2 278800 Confess, Fletch (2022) Comedy|Crime\n", "3 87298 Everything Must Go (2010) Comedy|Drama\n", "4 286029 The Little Mermaid (2023) Adventure|Children|Fantasy|Romance\n", "5 112727 Deliver Us from Evil (2014) Crime|Horror|Thriller\n", "6 208800 Lady and the Tramp (2019) Comedy|Romance\n", "7 251738 Red Rocket (2021) (no genres listed)\n", "8 7675 Canterville Ghost, The (1944) Comedy|Fantasy\n", "9 151777 The Finest Hours (2016) Drama|Thriller" ], "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", " \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", "
movieIdtitlegenres
04772Dinner Rush (2000)Drama
126231Performance (1970)Crime|Drama|Thriller
2278800Confess, Fletch (2022)Comedy|Crime
387298Everything Must Go (2010)Comedy|Drama
4286029The Little Mermaid (2023)Adventure|Children|Fantasy|Romance
5112727Deliver Us from Evil (2014)Crime|Horror|Thriller
6208800Lady and the Tramp (2019)Comedy|Romance
7251738Red Rocket (2021)(no genres listed)
87675Canterville Ghost, The (1944)Comedy|Fantasy
9151777The Finest Hours (2016)Drama|Thriller
\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" ], "application/vnd.google.colaboratory.intrinsic+json": { "type": "dataframe", "variable_name": "recommended_movies_svd", "summary": "{\n \"name\": \"recommended_movies_svd\",\n \"rows\": 10,\n \"fields\": [\n {\n \"column\": \"movieId\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 110646,\n \"min\": 4772,\n \"max\": 286029,\n \"num_unique_values\": 10,\n \"samples\": [\n 7675,\n 26231,\n 112727\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"Canterville Ghost, The (1944)\",\n \"Performance (1970)\",\n \"Deliver Us from Evil (2014)\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"genres\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"Comedy|Fantasy\",\n \"Crime|Drama|Thriller\",\n \"Crime|Horror|Thriller\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}" } }, "metadata": {} } ] } ] }