Skip to content

Instantly share code, notes, and snippets.

@myui
Created December 3, 2024 04:27
Show Gist options
  • Save myui/83ec5077d0e582de5943b98941b59f71 to your computer and use it in GitHub Desktop.
Save myui/83ec5077d0e582de5943b98941b59f71 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 195,
"id": "f26a223d-db9c-463d-b52b-2ea532accd13",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"import numpy as np\n",
"from numpy.typing import ArrayLike\n",
"import scipy.sparse as sp\n",
"from sklearn.linear_model import ElasticNet, SGDRegressor\n",
"import warnings\n",
"from sklearn.exceptions import ConvergenceWarning\n",
"from tqdm import tqdm\n",
"from scipy.sparse.linalg import cg\n",
"\n",
"class ColumnarView:\n",
" def __init__(self, csr_matrix: sp.csr_matrix):\n",
" self.csr_matrix = csr_matrix\n",
" # create the columnar view of the matrix\n",
" ind = self.csr_matrix.copy()\n",
" ind.data = np.arange(len(ind.data)) # store the original data indices\n",
" self.col_view = ind.tocsc()\n",
"\n",
" @property\n",
" def shape(self) -> tuple:\n",
" return self.csr_matrix.shape\n",
" \n",
" def get_col(self, j: int) -> sp.csc_matrix:\n",
" \"\"\"\n",
" Return the j-th column of the matrix.\n",
"\n",
" Parameters\n",
" ----------\n",
" j : int\n",
" The column index.\n",
"\n",
" Returns\n",
" -------\n",
" scipy.sparse.csc_matrix\n",
" The j-th column of the matrix.\n",
" \"\"\"\n",
"\n",
" col = self.col_view[:, j].copy()\n",
" col.data = self.csr_matrix.data[col.data]\n",
" return col\n",
" \n",
" def set_col(self, j: int, values: ArrayLike) -> None:\n",
" \"\"\"\n",
" Set the j-th column of the matrix to the given values.\n",
"\n",
" Parameters\n",
" ----------\n",
" j : int\n",
" The column index.\n",
" values : ArrayLike\n",
" The new values for the column.\n",
" \"\"\"\n",
" self.csr_matrix.data[self.col_view[:, j].data] = values\n",
"\n",
"class FeatureSelectionWrapper:\n",
"\n",
" def __init__(self, model: ElasticNet, n_neighbors: int = 30):\n",
" assert n_neighbors > 0, f\"n_neighbors must be a positive integer: {n_neighbors}\"\n",
" self.model = model\n",
" self.n_neighbors = n_neighbors\n",
" self.coef_ = None\n",
"\n",
" def fit(self, X: sp.csr_matrix, y: np.ndarray):\n",
" # Compute dot products between items and the target\n",
" feature_scores = X.T.dot(y).flatten()\n",
" # Select the top-k similar items to the target item by sorting the dot products\n",
" selected_features = np.argsort(feature_scores)[-1:-1-self.n_neighbors:-1]\n",
"\n",
" # Only fit the model with the selected features\n",
" self.model.fit(X[:, selected_features], y)\n",
" \n",
" # Store the coefficients of the fitted model\n",
" coef = np.zeros(X.shape[1])\n",
" coef[selected_features] = self.model.coef_\n",
" self.coef_ = coef\n",
" return self\n",
"\n",
"class SLIMElastic:\n",
" \"\"\"\n",
" SLIMElastic is a sparse linear method for top-K recommendation, which learns\n",
" a sparse aggregation coefficient matrix by solving an L1-norm and L2-norm\n",
" regularized optimization problem.\n",
" \"\"\"\n",
"\n",
" def __init__(self, config: dict={}):\n",
" \"\"\"\n",
" Initialize the SLIMElastic model.\n",
" \n",
" Args:\n",
" alpha (float): Regularization strength.\n",
" l1_ratio (float): The ratio between L1 and L2 regularization.\n",
" positive_only (bool): Whether to enforce positive coefficients.\n",
" \"\"\"\n",
" self.model_name = config.get(\"model_name\", \"cd\")\n",
" self.eta0 = config.get(\"eta0\", 0.001)\n",
" self.alpha = config.get(\"alpha\", 0.1)\n",
" self.l1_ratio = config.get(\"l1_ratio\", 0.1)\n",
" self.positive_only = config.get(\"positive_only\", True)\n",
" self.max_iter = config.get(\"max_iter\", 30)\n",
" self.tol = config.get(\"tol\", 1e-4)\n",
" self.random_state = config.get(\"random_state\", 43)\n",
" self.nn_feature_selection = config.get(\"nn_feature_selection\", None)\n",
"\n",
" # Initialize an empty item similarity matrix (will be computed during fit)\n",
" self.item_similarity = None\n",
"\n",
" def get_model(self):\n",
" if self.model_name == \"cd\" or self.model_name == \"coordinate_descent\":\n",
" return ElasticNet(\n",
" alpha=self.alpha, # Regularization strength\n",
" l1_ratio=self.l1_ratio,\n",
" positive=self.positive_only, # Enforce positive coefficients\n",
" fit_intercept=False,\n",
" copy_X=False, # Avoid copying the input matrix\n",
" precompute=True, # Precompute Gram matrix for faster computation\n",
" max_iter=self.max_iter,\n",
" tol=self.tol,\n",
" selection='random', # Randomize the order of features\n",
" random_state=self.random_state,\n",
" )\n",
" elif self.model_name == \"sgd\":\n",
" return SGDRegressor(\n",
" penalty='elasticnet',\n",
" eta0 = self.eta0,\n",
" alpha=self.alpha,\n",
" l1_ratio=self.l1_ratio,\n",
" fit_intercept=False,\n",
" max_iter=self.max_iter,\n",
" tol=self.tol,\n",
" random_state=self.random_state,\n",
" )\n",
" else:\n",
" raise ValueError(f\"Invalid model name: {self.model_name}\")\n",
"\n",
" def fit(self, interaction_matrix: sp.csr_matrix):\n",
" \"\"\"\n",
" Fit the SLIMElastic model to the interaction matrix.\n",
"\n",
" Args:\n",
" interaction_matrix (csr_matrix): User-item interaction matrix (sparse).\n",
" \"\"\"\n",
" if not isinstance(interaction_matrix, sp.csr_matrix):\n",
" raise ValueError(\"Interaction matrix must be a scipy.sparse.csr_matrix of user-item interactions.\")\n",
"\n",
" X = ColumnarView(interaction_matrix)\n",
" num_items = X.shape[1]\n",
"\n",
" self.item_similarity = np.zeros((num_items, num_items)) # Initialize similarity matrix\n",
" \n",
" model = self.get_model()\n",
"\n",
" if self.nn_feature_selection is not None:\n",
" model = FeatureSelectionWrapper(model, n_neighbors=int(self.nn_feature_selection))\n",
"\n",
" # Ignore convergence warnings for ElasticNet\n",
" with warnings.catch_warnings():\n",
" warnings.simplefilter(\"ignore\", category=ConvergenceWarning)\n",
"\n",
" # Iterate through each item (column) and fit the model\n",
" for j in tqdm(range(num_items), desc=\"Fitting SLIMElastic\"):\n",
" # Target column (current item)\n",
" y = X.get_col(j)\n",
"\n",
" # Set the target item column to 0\n",
" X.set_col(j, np.zeros_like(y.data))\n",
"\n",
" # Fit the model\n",
" model.fit(X.csr_matrix, y.toarray().ravel())\n",
"\n",
" # Update the item similarity matrix with new coefficients (weights for each user-item interaction)\n",
" self.item_similarity[:, j] = model.coef_\n",
"\n",
" # Reattach the item column after training\n",
" X.set_col(j, y.data)\n",
"\n",
" return self\n",
"\n",
" def partial_fit(self, interaction_matrix: sp.csr_matrix, user_ids: List[int]):\n",
" \"\"\"\n",
" Incrementally fit the SLIMElastic model with new or updated users.\n",
"\n",
" Args:\n",
" interaction_matrix (coo_matrix): user-item interaction matrix (sparse).\n",
" user_ids (list): List of user indices that were updated.\n",
" \"\"\" \n",
" user_items = set()\n",
" for user_id in user_ids:\n",
" user_items.update(interaction_matrix[user_id].indices.tolist())\n",
" return self.partial_fit_items(interaction_matrix, list(user_items))\n",
"\n",
" def partial_fit_items(self, interaction_matrix: sp.csr_matrix, updated_items: List[int]):\n",
" \"\"\"\n",
" Incrementally fit the SLIMElastic model with new or updated items.\n",
"\n",
" Args:\n",
" interaction_matrix (coo_matrix): user-item interaction matrix (sparse).\n",
" updated_items (list): List of item indices that were updated.\n",
" \"\"\"\n",
" if not isinstance(interaction_matrix, sp.csr_matrix):\n",
" raise ValueError(\"Interaction matrix must be a scipy.sparse.csr_matrix of user-item interactions.\")\n",
"\n",
" X = ColumnarView(interaction_matrix)\n",
"\n",
" model = self.get_model()\n",
"\n",
" if self.nn_feature_selection is not None:\n",
" model = FeatureSelectionWrapper(model, n_neighbors=int(self.nn_feature_selection))\n",
"\n",
" old_size = self.item_similarity.shape[1]\n",
" new_size = max(updated_items) + 1\n",
" if new_size > old_size:\n",
" # Expand both rows and columns symmetrically\n",
" expanded_similarity = np.zeros((new_size, new_size))\n",
" expanded_similarity[:old_size, :old_size] = self.item_similarity\n",
" self.item_similarity = expanded_similarity\n",
"\n",
" # Iterate through the updated items and fit the model incrementally\n",
" with warnings.catch_warnings():\n",
" warnings.simplefilter(\"ignore\", category=ConvergenceWarning)\n",
"\n",
" for j in tqdm(updated_items):\n",
" # Target column (current item)\n",
" y = X.get_col(j)\n",
"\n",
" # Set the target item column to 0\n",
" X.set_col(j, np.zeros_like(y.data))\n",
"\n",
" # Fit the model for the updated item\n",
" model.fit(X.csr_matrix, y.toarray().ravel())\n",
"\n",
" # Update the item similarity matrix with new coefficients (weights for each user-item interaction)\n",
" self.item_similarity[:, j] = model.coef_\n",
"\n",
" # Reattach the item column after training\n",
" X.set_col(j, y.data)\n",
"\n",
" return self\n",
"\n",
" def predict(self, user_id: int, interaction_matrix: sp.csr_matrix):\n",
" \"\"\"\n",
" Compute the predicted scores for a specific user across all items.\n",
"\n",
" Args:\n",
" user_id (int): The user ID (row index in interaction_matrix).\n",
" interaction_matrix (csr_matrix): User-item interaction matrix.\n",
"\n",
" Returns:\n",
" numpy.ndarray: Predicted scores for the user across all items.\n",
" \"\"\"\n",
" if self.item_similarity is None:\n",
" raise RuntimeError(\"Model must be fitted before calling predict.\")\n",
"\n",
" # Compute the predicted scores by performing dot product between the user interaction vector\n",
" # and the item similarity matrix\n",
" return interaction_matrix[user_id].dot(self.item_similarity)\n",
"\n",
" def predict_all(self, interaction_matrix: sp.csr_matrix):\n",
" \"\"\"\n",
" Compute the predicted scores for all users and items.\n",
"\n",
" Args:\n",
" interaction_matrix (csr_matrix): User-item interaction matrix.\n",
"\n",
" Returns:\n",
" numpy.ndarray: Predicted scores for all users and items.\n",
" \"\"\"\n",
" if self.item_similarity is None:\n",
" raise RuntimeError(\"Model must be fitted before calling predict_all.\")\n",
"\n",
" # Compute the predicted scores for all users by performing dot product between the interaction matrix\n",
" # and the item similarity matrix\n",
" return interaction_matrix.dot(self.item_similarity)\n",
"\n",
" def recommend(self, user_id: int, interaction_matrix: sp.csr_matrix, top_k: int=10, exclude_seen: bool=True) -> List[int]:\n",
" \"\"\"\n",
" Recommend top-K items for a given user.\n",
"\n",
" Args:\n",
" user_id (int): ID of the user (row index in interaction_matrix).\n",
" interaction_matrix (csr_matrix): User-item interaction matrix (sparse).\n",
" top_k (int): Number of recommendations to return.\n",
" exclude_seen (bool): Whether to exclude items the user has already interacted with.\n",
"\n",
" Returns:\n",
" List of recommended item indices.\n",
" \"\"\"\n",
" # Get predicted scores for all items for the given user\n",
" user_scores = self.predict(user_id, interaction_matrix).ravel()\n",
"\n",
" # Exclude items that the user has already interacted with\n",
" if exclude_seen:\n",
" seen_items = interaction_matrix[user_id].indices\n",
" user_scores[seen_items] = -np.inf # Exclude seen items by setting scores to -inf\n",
"\n",
" # Get the top-K items by sorting the predicted scores in descending order\n",
" # [::-1] reverses the order to get the items with the highest scores first\n",
" top_items = np.argsort(user_scores)[-top_k:][::-1]\n",
"\n",
" return top_items"
]
},
{
"cell_type": "markdown",
"id": "e4d16d42-e30e-4a8b-85ff-9b18f8ceb798",
"metadata": {},
"source": [
"## Movielens 1M Temporal User split"
]
},
{
"cell_type": "code",
"execution_count": 196,
"id": "740b40a9-29fd-4873-bc6a-cf9aaef52fac",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"\n",
"sys.path.append(\"/home/td-user/rtrec\")"
]
},
{
"cell_type": "code",
"execution_count": 197,
"id": "1aacde5c-d39d-4ba3-8a29-315fbf67771c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Using existing ratings.dat file.\n"
]
}
],
"source": [
"from rtrec.experiments.datasets import load_movielens_1m\n",
"df = load_movielens_1m()\n",
"\n",
"#from rtrec.experiments.datasets import load_movielens_20m\n",
"#df = load_movielens_20m()\n",
"\n",
"#from rtrec.experiments.datasets import load_movielens_100k\n",
"#df = load_movielens_100k()"
]
},
{
"cell_type": "code",
"execution_count": 198,
"id": "a2eecf7c-851f-4156-8385-c8cc2d0305c5",
"metadata": {},
"outputs": [],
"source": [
"df['user'] = df['user'].astype(\"category\")\n",
"df['item'] = df['item'].astype(\"category\")\n",
"df['user_id'] = df['user'].cat.codes\n",
"df['item_id'] = df['item'].cat.codes"
]
},
{
"cell_type": "code",
"execution_count": 199,
"id": "2e24aef4-3825-4153-9fe1-5936bfef13a7",
"metadata": {},
"outputs": [],
"source": [
"iid2id = dict(enumerate(df['item'].cat.categories))"
]
},
{
"cell_type": "code",
"execution_count": 200,
"id": "c262cade-c806-4786-b621-e96ecd2383e8",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/td-user/rtrec/rtrec/experiments/split.py:104: FutureWarning: The default of observed=False is deprecated and will be changed to True in a future version of pandas. Pass observed=False to retain current behavior or observed=True to adopt the future default and silence this warning.\n",
" for user, user_df in df.groupby('user'):\n"
]
}
],
"source": [
"from rtrec.experiments.split import temporal_user_split, temporal_split, random_split\n",
"train_df, test_df = temporal_user_split(df, test_frac=0.2)\n",
"#train_df, test_df = random_split(df, test_frac=0.2)"
]
},
{
"cell_type": "code",
"execution_count": 201,
"id": "08de34fe-a833-49eb-bad8-e71e915fcd3a",
"metadata": {},
"outputs": [],
"source": [
"import scipy.sparse as sp\n",
"train_interactions = sp.csr_matrix((train_df['rating'].astype(float), (train_df['user_id'], train_df['item_id'])))"
]
},
{
"cell_type": "code",
"execution_count": 202,
"id": "f26aa7ac-2286-4014-8995-8c2402dac850",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Fitting SLIMElastic: 100%|██████████████████| 3706/3706 [00:51<00:00, 72.16it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 51.1 s, sys: 299 ms, total: 51.4 s\n",
"Wall time: 51.4 s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/plain": [
"<__main__.SLIMElastic at 0xffff51cff4a0>"
]
},
"execution_count": 202,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%%time\n",
"\n",
"slim = SLIMElastic()\n",
"#slim = SLIMElastic({\"max_iter\": 100})\n",
"#slim = SLIMElastic({\"model_name\": \"cd\"})\n",
"#slim = SLIMElastic({\"model_name\": \"sgd\"})\n",
"slim.fit(train_interactions)"
]
},
{
"cell_type": "code",
"execution_count": 203,
"id": "c2d51880-a0ae-41d7-8942-841c867279b2",
"metadata": {},
"outputs": [],
"source": [
"recos = []\n",
"ground_truths = []\n",
"for row in test_df.groupby('user_id')['item'].apply(list).reset_index(name='ground_truth').itertuples():\n",
" recommended = slim.recommend(row.user_id, train_interactions, top_k=10, exclude_seen=True)\n",
" recos.append([iid2id[iid] for iid in recommended])\n",
" ground_truths.append(row.ground_truth)"
]
},
{
"cell_type": "code",
"execution_count": 204,
"id": "1d4ba2cf-55c4-472e-9fe7-34cce1f6b373",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'precision': 0.13653973509934245,\n",
" 'recall': 0.07254307328372998,\n",
" 'f1': 0.07699715413884953,\n",
" 'ndcg': 0.1502815555406185,\n",
" 'hit_rate': 0.6360927152317881,\n",
" 'mrr': 0.2907404341427504,\n",
" 'map': 0.07394531840514003,\n",
" 'tp': 8247,\n",
" 'auc': 0.33803536607799917}"
]
},
"execution_count": 204,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from rtrec.utils.metrics import compute_scores\n",
"\n",
"compute_scores(zip(recos, ground_truths), recommend_size=10)"
]
},
{
"cell_type": "markdown",
"id": "536b20bb-23ee-486a-ac97-fd37f6f4331e",
"metadata": {},
"source": [
"### With NN feature selection to speed up training"
]
},
{
"cell_type": "code",
"execution_count": 208,
"id": "126e7f7a-4e3d-44f3-8159-fc9685827ac5",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Fitting SLIMElastic: 100%|█████████████████| 3706/3706 [00:13<00:00, 275.48it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 13.2 s, sys: 311 ms, total: 13.5 s\n",
"Wall time: 13.5 s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/plain": [
"<__main__.SLIMElastic at 0xffff38ebef90>"
]
},
"execution_count": 208,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%%time\n",
"\n",
"slim = SLIMElastic({\"nn_feature_selection\": 50})\n",
"#slim = SLIMElastic({\"nn_feature_selection\": 30, \"max_iter\": 100})\n",
"slim.fit(train_interactions)"
]
},
{
"cell_type": "code",
"execution_count": 209,
"id": "518d8eba-abf2-452f-bd07-da3ca85463de",
"metadata": {},
"outputs": [],
"source": [
"recos = []\n",
"ground_truths = []\n",
"for row in test_df.groupby('user_id')['item'].apply(list).reset_index(name='ground_truth').itertuples():\n",
" recommended = slim.recommend(row.user_id, train_interactions, top_k=10, exclude_seen=True)\n",
" recos.append([iid2id[iid] for iid in recommended])\n",
" ground_truths.append(row.ground_truth)"
]
},
{
"cell_type": "code",
"execution_count": 210,
"id": "1c17525d-42f9-42af-8344-de864e21ae89",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'precision': 0.13162251655629614,\n",
" 'recall': 0.0673229648143244,\n",
" 'f1': 0.07279097819160797,\n",
" 'ndcg': 0.14422455075026505,\n",
" 'hit_rate': 0.6352649006622516,\n",
" 'mrr': 0.28689963733837737,\n",
" 'map': 0.06884724159316662,\n",
" 'tp': 7950,\n",
" 'auc': 0.33836494796594224}"
]
},
"execution_count": 210,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from rtrec.utils.metrics import compute_scores\n",
"\n",
"compute_scores(zip(recos, ground_truths), recommend_size=10)"
]
},
{
"cell_type": "markdown",
"id": "bf390e02-8fda-446e-be56-0cd7597bbe7b",
"metadata": {},
"source": [
"### Test for partial fit"
]
},
{
"cell_type": "code",
"execution_count": 128,
"id": "11db8175-4cef-4f49-84f8-0b807b4a448f",
"metadata": {},
"outputs": [],
"source": [
"test_df1, test_df2 = temporal_split(test_df, test_frac=0.5)"
]
},
{
"cell_type": "code",
"execution_count": 129,
"id": "13f3ad47-180f-43ea-82d0-c2d213d131f0",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Fitting SLIMElastic: 100%|█████████████████| 3706/3706 [00:11<00:00, 317.69it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 11.4 s, sys: 251 ms, total: 11.7 s\n",
"Wall time: 11.7 s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
},
{
"data": {
"text/plain": [
"<__main__.SLIMElastic at 0xffff7c322d20>"
]
},
"execution_count": 129,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%%time\n",
"\n",
"slim = SLIMElastic({\"nn_feature_selection\": 30})\n",
"slim.fit(train_interactions)"
]
},
{
"cell_type": "code",
"execution_count": 130,
"id": "53b21898",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████████████████████████████████| 3563/3563 [00:12<00:00, 292.59it/s]\n"
]
},
{
"data": {
"text/plain": [
"<__main__.SLIMElastic at 0xffff7c322d20>"
]
},
"execution_count": 130,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import scipy.sparse as sp\n",
"import pandas as pd\n",
"\n",
"past_df = pd.concat([train_df, test_df1])\n",
"past_interactions = sp.csr_matrix((past_df['rating'].astype(float), (past_df['user_id'], past_df['item_id'])))\n",
"\n",
"slim.partial_fit(past_interactions, test_df1.user_id.unique())"
]
},
{
"cell_type": "code",
"execution_count": 131,
"id": "235fc09a-9790-41e1-ab56-4a886c97850d",
"metadata": {},
"outputs": [],
"source": [
"recos = []\n",
"ground_truths = []\n",
"for row in test_df2.groupby('user_id')['item'].apply(list).reset_index(name='ground_truth').itertuples():\n",
" recommended = slim.recommend(row.user_id, past_interactions, top_k=10, exclude_seen=True)\n",
" recos.append([iid2id[iid] for iid in recommended])\n",
" ground_truths.append(row.ground_truth)"
]
},
{
"cell_type": "code",
"execution_count": 132,
"id": "698230df-35f9-434b-83dd-465f628a52dd",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'precision': 0.145448054266334,\n",
" 'recall': 0.06708294204397466,\n",
" 'f1': 0.07410286587963384,\n",
" 'ndcg': 0.15939018809141217,\n",
" 'hit_rate': 0.6583363084612638,\n",
" 'mrr': 0.31001824745130796,\n",
" 'map': 0.07853834303499396,\n",
" 'tp': 4074,\n",
" 'auc': 0.3599746547434874}"
]
},
"execution_count": 132,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from rtrec.utils.metrics import compute_scores\n",
"\n",
"compute_scores(zip(recos, ground_truths), recommend_size=10)"
]
},
{
"cell_type": "markdown",
"id": "d3c1b64a-439a-4b20-817b-e7fab99ed5d6",
"metadata": {},
"source": [
"# ImplicitFM for Implicit Feedbacks (WARP/BPR)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "8cd296e2-9490-4167-8100-af071ec68513",
"metadata": {},
"outputs": [],
"source": [
"import bisect\n",
"import numpy as np\n",
"from typing import List, Optional\n",
"from scipy.sparse import csr_matrix\n",
"from tqdm import tqdm\n",
"\n",
"class ImplicitFactorizationMachines:\n",
" def __init__(self,\n",
" num_factors: int = 10,\n",
" learning_rate: float = 0.05,\n",
" reg: float = 0.0001,\n",
" loss: str = 'warp',\n",
" random_state: Optional[int] = None,\n",
" epsilon: float = 1.0,\n",
" max_sampled: int = 10,\n",
" max_loss: float = 10.0):\n",
" \"\"\"\n",
" Factorization Machines for implicit feedback.\n",
"\n",
" Args:\n",
" num_factors (int): Number of latent factors.\n",
" learning_rate (float): Learning rate for updates.\n",
" reg (float): Regularization parameter.\n",
" loss (str): Loss function to use ('bpr' or 'warp').\n",
" random_state (Optional[int]): Random seed for reproducibility.\n",
" epsilon (float): Smoothing term for AdaGrad.\n",
" \"\"\"\n",
" self.num_factors = num_factors\n",
" self.learning_rate = learning_rate\n",
" self.reg = reg\n",
" self.loss = loss.lower()\n",
" assert self.loss in {'bpr', 'warp'}, \"Loss must be 'bpr' or 'warp'\"\n",
" self.random_state = random_state\n",
" self.rng = np.random.default_rng(random_state)\n",
" self.max_sampled = max_sampled\n",
" self.max_loss = max_loss\n",
"\n",
" self.user_factors = None\n",
" self.item_factors = None\n",
" # User and item biases\n",
" self.user_biases = None\n",
" self.item_biases = None\n",
"\n",
" # AdaGrad caches\n",
" self.adagrad_cache_user = None\n",
" self.adagrad_cache_item = None\n",
" self.adagrad_cache_user_bias = None\n",
" self.adagrad_cache_item_bias = None\n",
"\n",
" self.epsilon = epsilon\n",
"\n",
" def fit(self, interactions: csr_matrix, epochs: int = 10) -> None:\n",
" \"\"\"\n",
" Fits the model to the provided interaction data.\n",
"\n",
" Args:\n",
" interactions (csr_matrix): User-item interaction matrix (implicit feedback).\n",
" epochs (int): Number of training epochs.\n",
" \"\"\"\n",
" num_users, num_items = interactions.shape\n",
" if self.user_factors is None:\n",
" self.user_factors = self.rng.normal(0, 0.01, (num_users, self.num_factors))\n",
" if self.item_factors is None:\n",
" self.item_factors = self.rng.normal(0, 0.01, (num_items, self.num_factors))\n",
" if self.user_biases is None:\n",
" self.user_biases = np.zeros(num_users)\n",
" if self.item_biases is None:\n",
" self.item_biases = np.zeros(num_items)\n",
"\n",
" # Initialize AdaGrad caches\n",
" self.adagrad_cache_user = np.zeros_like(self.user_factors)\n",
" self.adagrad_cache_item = np.zeros_like(self.item_factors)\n",
" self.adagrad_cache_user_bias = np.zeros(num_users)\n",
" self.adagrad_cache_item_bias = np.zeros(num_items)\n",
"\n",
" with tqdm(total = num_users * epochs) as pbar:\n",
" for epoch in range(epochs):\n",
" for user in range(num_users):\n",
" user_interactions = interactions[user].indices\n",
" if len(user_interactions) == 0:\n",
" pbar.update(1)\n",
" continue\n",
"\n",
" for pos_item in user_interactions:\n",
" if self.loss == 'bpr':\n",
" self._update_bpr(interactions, user, pos_item)\n",
" elif self.loss == 'warp':\n",
" self._update_warp(interactions, user, pos_item)\n",
" pbar.update(1)\n",
"\n",
" def partial_fit(self, interactions: csr_matrix, users: np.ndarray, items: np.ndarray) -> None:\n",
" \"\"\"\n",
" Incrementally fits the model to new interaction data, considering new users and items.\n",
"\n",
" Args:\n",
" interactions (csr_matrix): User-item interaction matrix (implicit feedback).\n",
" users (np.ndarray): Array of user indices to update.\n",
" items (np.ndarray): Array of item indices to update.\n",
" \"\"\"\n",
" num_users, num_items = interactions.shape\n",
"\n",
" for user, pos_item in zip(users, items):\n",
" # Ensure user and item factors are initialized for new users/items\n",
" while user >= self.user_factors.shape[0]:\n",
" new_user_factors = self.rng.normal(0, 0.01, (1, self.num_factors))\n",
" self.user_factors = np.vstack((self.user_factors, new_user_factors))\n",
" self.adagrad_cache_user = np.vstack((self.adagrad_cache_user, np.zeros((1, self.num_factors))))\n",
"\n",
" while pos_item >= self.item_factors.shape[0]:\n",
" new_item_factors = self.rng.normal(0, 0.01, (1, self.num_factors))\n",
" self.item_factors = np.vstack((self.item_factors, new_item_factors))\n",
" self.adagrad_cache_item = np.vstack((self.adagrad_cache_item, np.zeros((1, self.num_factors))))\n",
"\n",
" if self.loss == 'bpr': \n",
" self._update_bpr(interactions, user, pos_item)\n",
" elif self.loss == 'warp':\n",
" self._update_warp(interactions, user, pos_item)\n",
"\n",
" def _update_bpr(self, interactions: csr_matrix, user: int, pos_item: int) -> None:\n",
" def _sample_negative(interactions: csr_matrix, user: int) -> int:\n",
" \"\"\"Samples a random negative item for a given user using binary search and random number generation.\"\"\"\n",
" num_items = interactions.shape[1]\n",
" positives = interactions[user].indices # Already sorted as per the csr_matrix format\n",
" while True:\n",
" # Generate a random item index\n",
" item = self.rng.integers(0, num_items)\n",
" # Check if the item is not in the positives list using binary search\n",
" idx = bisect.bisect_left(positives, item)\n",
" if idx == len(positives) or positives[idx] != item:\n",
" return item\n",
"\n",
" \"\"\"Performs an update using the BPR loss with AdaGrad.\"\"\"\n",
" neg_item = _sample_negative(interactions, user)\n",
"\n",
" user_factors = self.user_factors[user]\n",
" pos_factors = self.item_factors[pos_item]\n",
" neg_factors = self.item_factors[neg_item]\n",
" user_bias = self.user_biases[user]\n",
" pos_bias = self.item_biases[pos_item]\n",
" neg_bias = self.item_biases[neg_item]\n",
"\n",
" x_uij = (user_factors @ (pos_factors - neg_factors)) + (pos_bias - neg_bias) + user_bias\n",
" dloss = 1 / (1 + np.exp(-x_uij)) # Sigmoid\n",
"\n",
" # Compute gradients\n",
" grad_user = (pos_factors - neg_factors) * dloss + self.reg * user_factors\n",
" grad_pos = user_factors * dloss + self.reg * pos_factors\n",
" grad_neg = -user_factors * dloss + self.reg * neg_factors\n",
" grad_user_bias = dloss + self.reg * user_bias\n",
" grad_pos_bias = dloss + self.reg * pos_bias\n",
" grad_neg_bias = -dloss + self.reg * neg_bias\n",
" \n",
" # Update AdaGrad cache for user and items\n",
" self.adagrad_cache_user[user] += grad_user**2\n",
" self.adagrad_cache_item[pos_item] += grad_pos**2\n",
" self.adagrad_cache_item[neg_item] += grad_neg**2\n",
" self.adagrad_cache_user_bias[user] += grad_user_bias**2\n",
" self.adagrad_cache_item_bias[pos_item] += grad_pos_bias**2\n",
" self.adagrad_cache_item_bias[neg_item] += grad_neg_bias**2\n",
"\n",
" # Calculate learning rates\n",
" user_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_user[user] + self.epsilon))\n",
" pos_item_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item[pos_item] + self.epsilon))\n",
" neg_item_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item[neg_item] + self.epsilon))\n",
" user_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_user_bias[user]) + self.epsilon)\n",
" pos_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item_bias[pos_item]) + self.epsilon)\n",
" neg_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item_bias[neg_item]) + self.epsilon)\n",
"\n",
" # Apply updates\n",
" self.user_factors[user] += user_lr * grad_user\n",
" self.item_factors[pos_item] += pos_item_lr * grad_pos\n",
" self.item_factors[neg_item] += neg_item_lr * grad_neg\n",
" self.user_biases[user] += user_bias_lr * grad_user_bias\n",
" self.item_biases[pos_item] += pos_bias_lr * grad_pos_bias\n",
" self.item_biases[neg_item] += neg_bias_lr * grad_neg_bias\n",
"\n",
" def _update_warp(self, interactions: csr_matrix, user: int, pos_item: int) -> None:\n",
" \"\"\"Performs an update using the WARP (Weighted Approximate-Rank Pairwise) loss with AdaGrad.\"\"\"\n",
" num_items = interactions.shape[1] # Total number of items\n",
" pos_item_vector = self.item_factors[pos_item] # Vector for the positive item\n",
" user_vector = self.user_factors[user] # Vector for the user\n",
"\n",
" # Compute the prediction for the positive item\n",
" positive_prediction = user_vector @ pos_item_vector + self.user_biases[user] + self.item_biases[pos_item]\n",
"\n",
" negative_items = np.setdiff1d(np.arange(num_items), interactions[user].indices)\n",
" self.rng.shuffle(negative_items)\n",
" \n",
" # Initialize rank and loss weight\n",
" sampled = 0\n",
" loss = 0.0\n",
"\n",
" for neg_item in negative_items[:self.max_sampled]:\n",
" sampled += 1\n",
"\n",
" # Compute the negative item vector\n",
" neg_item_vector = self.item_factors[neg_item]\n",
"\n",
" # Compute the prediction for the negative item\n",
" negative_prediction = user_vector @ neg_item_vector + self.user_biases[user] + self.item_biases[neg_item]\n",
" \n",
" # Negative items are sampled until a \"violator\" is found.\n",
" # A violator is a negative item where the positive item's score is not sufficiently higher\n",
" if positive_prediction - negative_prediction < 1:\n",
" # Approx warp loss used in LightFM\n",
" # see https://building-babylon.net/2016/03/18/warp-loss-for-implicit-feedback-recommendation/ for WARP loss derivation\n",
" # loss = np.log(num_items - 1 // sampled) # LightFM's WARP loss\n",
" # Non approximated WARP loss is slightly better in my experiments \n",
" loss = sum(1.0 / k for k in range(1, sampled + 1)) # WARP loss\n",
" self._warp_update(loss, user, pos_item, neg_item, user_vector, pos_item_vector, neg_item_vector)\n",
" break\n",
"\n",
" def _warp_update(self, loss: float, user: int, pos_item: int, neg_item: int, user_vector: np.ndarray, pos_item_vector: np.ndarray, neg_item_vector: np.ndarray) -> None:\n",
" \"\"\"Performs a WARP update with AdaGrad.\"\"\"\n",
" # Compute the gradient\n",
" grad_user = loss * (pos_item_vector - neg_item_vector) + self.reg * user_vector\n",
" grad_pos = loss * user_vector + self.reg * pos_item_vector\n",
" grad_neg = -loss * user_vector + self.reg * neg_item_vector\n",
" grad_user_bias = loss + self.reg * self.user_biases[user]\n",
" grad_pos_item_bias = loss + self.reg * self.item_biases[pos_item]\n",
" grad_neg_item_bias = -loss + self.reg * self.item_biases[neg_item]\n",
"\n",
" # Update the AdaGrad cache\n",
" self.adagrad_cache_user[user] += grad_user**2\n",
" self.adagrad_cache_item[pos_item] += grad_pos**2\n",
" self.adagrad_cache_item[neg_item] += grad_neg**2\n",
" self.adagrad_cache_user_bias[user] += grad_user_bias**2\n",
" self.adagrad_cache_item_bias[pos_item] += grad_pos_item_bias**2\n",
" self.adagrad_cache_item_bias[neg_item] += grad_neg_item_bias**2\n",
"\n",
" # Compute the learning rates\n",
" user_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_user[user]) + self.epsilon)\n",
" pos_item_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item[pos_item]) + self.epsilon)\n",
" neg_item_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item[neg_item]) + self.epsilon)\n",
" user_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_user_bias[user]) + self.epsilon)\n",
" pos_item_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item_bias[pos_item]) + self.epsilon)\n",
" neg_item_bias_lr = self.learning_rate / (np.sqrt(self.adagrad_cache_item_bias[neg_item]) + self.epsilon)\n",
"\n",
" # Update the user and item factors\n",
" self.user_factors[user] += user_lr * (grad_user + self.reg * user_vector)\n",
" self.item_factors[pos_item] += pos_item_lr * (grad_pos + self.reg * pos_item_vector)\n",
" self.item_factors[neg_item] += neg_item_lr * (grad_neg + self.reg * neg_item_vector)\n",
" self.user_biases[user] += user_bias_lr * grad_user_bias\n",
" self.item_biases[pos_item] += pos_item_bias_lr * grad_pos_item_bias\n",
" self.item_biases[neg_item] += neg_item_bias_lr * grad_neg_item_bias\n",
"\n",
" def predict(self, user: int, items: np.ndarray) -> np.ndarray:\n",
" \"\"\"\n",
" Predicts the scores for a user and a set of items.\n",
"\n",
" Args:\n",
" user (int): User index.\n",
" items (np.ndarray): Array of item indices.\n",
"\n",
" Returns:\n",
" np.ndarray: Predicted scores for the items.\n",
" \"\"\"\n",
" user_factors = self.user_factors[user]\n",
" item_factors = self.item_factors[items]\n",
" user_bias = self.user_biases[user]\n",
" item_bias = self.item_biases[items]\n",
" return user_factors @ item_factors.T + user_bias + item_bias\n",
"\n",
" def predict_all(self, user: int) -> np.ndarray:\n",
" \"\"\"\n",
" Predicts the scores for all items for a given user.\n",
"\n",
" Args:\n",
" user (int): User index.\n",
"\n",
" Returns:\n",
" np.ndarray: Predicted scores for all items.\n",
" \"\"\"\n",
" return self.user_factors[user] @ self.item_factors.T + self.user_biases[user] + self.item_biases\n",
"\n",
" def recommend(self, user_id: int, interactions: csr_matrix, top_k: int = 10, exclude_seen: bool = True) -> List[int]:\n",
" \"\"\"\n",
" Recommend top-K items for a given user.\n",
"\n",
" Args:\n",
" user_id (int): ID of the user (row index in interactions).\n",
" interactions (csr_matrix): User-item interaction matrix.\n",
" top_k (int): Number of recommendations to return.\n",
" exclude_seen (bool): Whether to exclude items the user has already interacted with.\n",
"\n",
" Returns:\n",
" List of recommended item indices.\n",
" \"\"\"\n",
" if exclude_seen:\n",
" num_items = interactions.shape[1]\n",
" seen_items = interactions[user_id].indices\n",
" candidate_items = np.setdiff1d(np.arange(num_items), seen_items)\n",
" scores = self.predict(user_id, candidate_items)\n",
" # Get the top-K items by sorting the predicted scores in descending order\n",
" top_items = candidate_items[np.argsort(-scores)][:top_k]\n",
" else:\n",
" scores = self.predict_all(user_id)\n",
" top_items = np.argsort(-scores)[:top_k]\n",
"\n",
" return top_items\n"
]
},
{
"cell_type": "markdown",
"id": "04506f01-b18c-45a1-99b3-b211dfc07ab8",
"metadata": {},
"source": [
"### WARP loss"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "9d4055f8-645f-47d3-bf83-521da2934db1",
"metadata": {},
"outputs": [],
"source": [
"implicit_fm = ImplicitFactorizationMachines(num_factors=10, loss='warp')"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "16099b8e-4380-4e6a-84fd-51530e4cc2ee",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|█████████████████████████████████████| 60400/60400 [18:21<00:00, 54.83it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 18min 21s, sys: 1.05 s, total: 18min 22s\n",
"Wall time: 18min 21s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
}
],
"source": [
"%%time\n",
"\n",
"implicit_fm.fit(train_interactions, epochs=10)"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "c1762da8-723b-4d0c-879c-5fe67e04438f",
"metadata": {},
"outputs": [],
"source": [
"recos = []\n",
"ground_truths = []\n",
"for row in test_df.groupby('user_id')['item'].apply(list).reset_index(name='ground_truth').itertuples():\n",
" recommended = implicit_fm.recommend(row.user_id, train_interactions, top_k=10, exclude_seen=True)\n",
" recos.append([iid2id[iid] for iid in recommended])\n",
" ground_truths.append(row.ground_truth)"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "85387b91-7e47-4d3e-8e71-aa14a8ba9d24",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'precision': 0.12519867549669247,\n",
" 'recall': 0.05452121327320763,\n",
" 'f1': 0.0627046655670288,\n",
" 'ndcg': 0.13758538152201555,\n",
" 'hit_rate': 0.5528145695364238,\n",
" 'mrr': 0.2730837406706596,\n",
" 'map': 0.06979350033642343,\n",
" 'tp': 7562,\n",
" 'auc': 0.30544857694733635}"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from rtrec.utils.metrics import compute_scores\n",
"\n",
"compute_scores(zip(recos, ground_truths), recommend_size=10)"
]
},
{
"cell_type": "markdown",
"id": "ee987370-b1f2-47cd-acc0-5d2a6306a03d",
"metadata": {},
"source": [
"### BPR loss"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "6141cf36-33b4-4344-905a-b57f9a9134a1",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
" 57%|████████████████████▍ | 34360/60400 [03:30<02:31, 172.39it/s]/tmp/ipykernel_2780/1650988818.py:143: RuntimeWarning: overflow encountered in exp\n",
" dloss = 1 / (1 + np.exp(-x_uij)) # Sigmoid\n",
"100%|████████████████████████████████████| 60400/60400 [06:08<00:00, 163.95it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 6min 15s, sys: 24 s, total: 6min 39s\n",
"Wall time: 6min 8s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
}
],
"source": [
"%%time\n",
"\n",
"implicit_fm = ImplicitFactorizationMachines(num_factors=10, loss='bpr')\n",
"implicit_fm.fit(train_interactions, epochs=10)"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "c0fe0133-a2c7-4ede-9b15-40b4ab21e6f3",
"metadata": {},
"outputs": [],
"source": [
"recos = []\n",
"ground_truths = []\n",
"for row in test_df.groupby('user_id')['item'].apply(list).reset_index(name='ground_truth').itertuples():\n",
" recommended = implicit_fm.recommend(row.user_id, train_interactions, top_k=10, exclude_seen=True)\n",
" recos.append([iid2id[iid] for iid in recommended])\n",
" ground_truths.append(row.ground_truth)"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "1aa2af87-33e9-4437-8c17-8c326de0fdd4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'precision': 0.09071192052980298,\n",
" 'recall': 0.03468395778253375,\n",
" 'f1': 0.041657320292548816,\n",
" 'ndcg': 0.09786905305986224,\n",
" 'hit_rate': 0.43211920529801323,\n",
" 'mrr': 0.19890472248502067,\n",
" 'map': 0.0473134919852784,\n",
" 'tp': 5479,\n",
" 'auc': 0.2315746741301384}"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from rtrec.utils.metrics import compute_scores\n",
"\n",
"compute_scores(zip(recos, ground_truths), recommend_size=10)"
]
},
{
"cell_type": "markdown",
"id": "4a98912a-9f82-4965-8ccc-fe7d036bb3a8",
"metadata": {},
"source": [
"# Factorization Machines with Explicit Feedbacks"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "9ea5a857-8490-4ef3-aad2-8a7c1d93455e",
"metadata": {},
"outputs": [],
"source": [
"from rtrec.recommender import Recommender\n",
"from rtrec.models import FactorizationMachines\n",
"\n",
"#model = FactorizationMachines(n_factors=10, alpha=0.001, decay_in_days=None)\n",
"explicit_fm = FactorizationMachines(n_factors=10, alpha=0.001, decay_in_days=None)\n",
"recommender = Recommender(explicit_fm)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "73500c70-ac57-47fe-b919-5adfcf51007a",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
" 0%| | 0/10 [00:00<?, ?it/s]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Starting epoch 1/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 10%|████▍ | 1/10 [00:14<02:07, 14.13s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 1 completed in 14.04 seconds\n",
"Throughput: 56822.53 samples/sec\n",
"Empirical loss after epoch 1: 0.9040485061051541\n",
"Starting epoch 2/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 20%|████████▊ | 2/10 [00:29<01:57, 14.64s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 2 completed in 14.93 seconds\n",
"Throughput: 53422.34 samples/sec\n",
"Empirical loss after epoch 2: 0.8848850820186098\n",
"Starting epoch 3/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 30%|█████████████▏ | 3/10 [00:43<01:43, 14.74s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 3 completed in 14.84 seconds\n",
"Throughput: 53743.98 samples/sec\n",
"Empirical loss after epoch 3: 0.8709187531438037\n",
"Starting epoch 4/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 40%|█████████████████▌ | 4/10 [00:58<01:28, 14.83s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 4 completed in 14.93 seconds\n",
"Throughput: 53426.64 samples/sec\n",
"Empirical loss after epoch 4: 0.8597163788716761\n",
"Starting epoch 5/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 50%|██████████████████████ | 5/10 [01:14<01:14, 14.95s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 5 completed in 15.16 seconds\n",
"Throughput: 52638.86 samples/sec\n",
"Empirical loss after epoch 5: 0.850401642913619\n",
"Starting epoch 6/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 60%|██████████████████████████▍ | 6/10 [01:29<00:59, 14.99s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 6 completed in 15.06 seconds\n",
"Throughput: 52971.76 samples/sec\n",
"Empirical loss after epoch 6: 0.8424714351463535\n",
"Starting epoch 7/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 70%|██████████████████████████████▊ | 7/10 [01:44<00:45, 15.00s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 7 completed in 15.02 seconds\n",
"Throughput: 53127.80 samples/sec\n",
"Empirical loss after epoch 7: 0.8356077018215934\n",
"Starting epoch 8/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 80%|███████████████████████████████████▏ | 8/10 [01:59<00:30, 15.04s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 8 completed in 15.11 seconds\n",
"Throughput: 52795.26 samples/sec\n",
"Empirical loss after epoch 8: 0.8295762436695558\n",
"Starting epoch 9/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" 90%|███████████████████████████████████████▌ | 9/10 [02:14<00:15, 15.04s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 9 completed in 15.02 seconds\n",
"Throughput: 53124.21 samples/sec\n",
"Empirical loss after epoch 9: 0.8242226909950443\n",
"Starting epoch 10/10\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|███████████████████████████████████████████| 10/10 [02:29<00:00, 14.92s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 10 completed in 14.76 seconds\n",
"Throughput: 54039.27 samples/sec\n",
"Empirical loss after epoch 10: 0.8194283203577859\n",
"CPU times: user 2min 29s, sys: 162 ms, total: 2min 29s\n",
"Wall time: 2min 29s\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n"
]
}
],
"source": [
"%%time\n",
"#%pdb on\n",
"\n",
"recommender.fit(train_df, epochs=10, bulk_identify=False)"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "3ce4c580-c38c-4ca8-b8c2-8e230a8e5bd9",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|███████████████████████████████████████████| 61/61 [01:55<00:00, 1.90s/it]\n"
]
},
{
"data": {
"text/plain": [
"{'precision': 0.07622516556291505,\n",
" 'recall': 0.028854421300375105,\n",
" 'f1': 0.03458337370708451,\n",
" 'ndcg': 0.08065297737220792,\n",
" 'hit_rate': 0.37649006622516556,\n",
" 'mrr': 0.1610233364869128,\n",
" 'map': 0.038115181616551544,\n",
" 'tp': 4604,\n",
" 'auc': 0.19739979501734475}"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"recommender.evaluate(test_df, recommend_size=10, filter_interacted=True)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment