Download src/data/dataset.py from yashb18/deepreads: direct link, hf CLI and curl.
- Browser
- Download file 5.88 kB
-
https://huggingface.co/spaces/yashb18/deepreads/resolve/main/src/data/dataset.py
- Command line
-
hf download hf://spaces/yashb18/deepreads/src/data/dataset.py
-
curl -L -o dataset.py https://huggingface.co/spaces/yashb18/deepreads/resolve/main/src/data/dataset.py
5.88 kB
| """ | |
| PyTorch Dataset classes for the Two-Tower neural recommender. | |
| Provides: | |
| - InteractionDataset — for training (user_idx, item_idx, label) | |
| - InferenceDataset — for building item tower embeddings at inference time | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from torch.utils.data import Dataset | |
| class InteractionDataset(Dataset): | |
| """ | |
| Dataset of (user_idx, item_idx, label) triples for training the Two-Tower model. | |
| Optionally attaches item feature vectors when `item_features` is provided. | |
| Args: | |
| df: DataFrame with columns [user_idx, item_idx, label]. | |
| item_features: Optional numpy array of shape (n_items, n_item_features). | |
| """ | |
| def __init__( | |
| self, | |
| df: pd.DataFrame, | |
| item_features: np.ndarray | None = None, | |
| ) -> None: | |
| self.user_idxs = torch.LongTensor(df["user_idx"].values) | |
| self.item_idxs = torch.LongTensor(df["item_idx"].values) | |
| self.labels = torch.FloatTensor(df["label"].values) | |
| self.item_features = ( | |
| torch.FloatTensor(item_features) if item_features is not None else None | |
| ) | |
| def __len__(self) -> int: | |
| return len(self.labels) | |
| def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: | |
| sample: dict[str, torch.Tensor] = { | |
| "user_idx": self.user_idxs[idx], | |
| "item_idx": self.item_idxs[idx], | |
| "label": self.labels[idx], | |
| } | |
| if self.item_features is not None: | |
| sample["item_features"] = self.item_features[self.item_idxs[idx]] | |
| return sample | |
| class BPRDataset(Dataset): | |
| """ | |
| Bayesian Personalized Ranking dataset: (user, pos_item, neg_item) triples. | |
| For each positive interaction, a random negative item is paired. | |
| If the DataFrame has pre-sampled negatives (label=0 rows from train split), | |
| those are used. Otherwise (e.g., val split with only positives), random | |
| item indices are sampled globally. | |
| Args: | |
| df: DataFrame with columns [user_idx, item_idx, label]. | |
| item_features: Optional numpy array of shape (n_items, n_item_features). | |
| n_items: Total number of items (required when df has no negatives). | |
| """ | |
| def __init__( | |
| self, | |
| df: pd.DataFrame, | |
| item_features: np.ndarray | None = None, | |
| n_items: int | None = None, | |
| ) -> None: | |
| label_col = "label" if "label" in df.columns else None | |
| pos_df = ( | |
| df[df["label"] == 1].reset_index(drop=True) | |
| if label_col | |
| else df.reset_index(drop=True) | |
| ) | |
| neg_df = ( | |
| df[df["label"] == 0].reset_index(drop=True) | |
| if label_col | |
| else pd.DataFrame() | |
| ) | |
| n_pos = len(pos_df) | |
| rng = np.random.default_rng(42) | |
| sampled_negs = np.empty(n_pos, dtype=np.int64) | |
| if len(neg_df) > 0: | |
| # Per-user pairing: for each user's positive, sample from THAT user's negatives. | |
| # This ensures user-specific gradients (critical for non-collapsed user tower). | |
| user_neg_map: dict[int, np.ndarray] = { | |
| uid: grp["item_idx"].values | |
| for uid, grp in neg_df.groupby("user_idx") | |
| } | |
| pos_users = pos_df["user_idx"].values | |
| for i, uid in enumerate(pos_users): | |
| neg_pool = user_neg_map.get(uid) | |
| if neg_pool is not None and len(neg_pool) > 0: | |
| sampled_negs[i] = neg_pool[rng.integers(0, len(neg_pool))] | |
| else: | |
| # Fallback: global random item | |
| assert n_items is not None | |
| sampled_negs[i] = rng.integers(0, n_items) | |
| else: | |
| # No pre-sampled negatives (val/test split) — sample globally | |
| assert n_items is not None, "n_items required when df has no label=0 rows" | |
| sampled_negs = rng.integers(0, n_items, size=n_pos).astype(np.int64) | |
| self.user_idxs = torch.LongTensor(pos_df["user_idx"].values) | |
| self.pos_item_idxs = torch.LongTensor(pos_df["item_idx"].values) | |
| self.neg_item_idxs = torch.LongTensor(sampled_negs) | |
| # Store full item feature matrix for indexed lookup | |
| self.item_features = ( | |
| torch.FloatTensor(item_features) if item_features is not None else None | |
| ) | |
| def __len__(self) -> int: | |
| return len(self.user_idxs) | |
| def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: | |
| pos_iid = self.pos_item_idxs[idx] | |
| neg_iid = self.neg_item_idxs[idx] | |
| sample: dict[str, torch.Tensor] = { | |
| "user_idx": self.user_idxs[idx], | |
| "pos_item_idx": pos_iid, | |
| "neg_item_idx": neg_iid, | |
| } | |
| if self.item_features is not None: | |
| sample["pos_item_features"] = self.item_features[pos_iid] | |
| sample["neg_item_features"] = self.item_features[neg_iid] | |
| return sample | |
| class InferenceDataset(Dataset): | |
| """ | |
| Dataset over all items for computing item tower embeddings in batch. | |
| Args: | |
| n_items: Total number of items. | |
| item_features: Optional numpy array of shape (n_items, n_item_features). | |
| """ | |
| def __init__( | |
| self, | |
| n_items: int, | |
| item_features: np.ndarray | None = None, | |
| ) -> None: | |
| self.item_idxs = torch.arange(n_items, dtype=torch.long) | |
| self.item_features = ( | |
| torch.FloatTensor(item_features) if item_features is not None else None | |
| ) | |
| def __len__(self) -> int: | |
| return len(self.item_idxs) | |
| def __getitem__(self, idx: int) -> dict[str, torch.Tensor]: | |
| sample: dict[str, torch.Tensor] = {"item_idx": self.item_idxs[idx]} | |
| if self.item_features is not None: | |
| sample["item_features"] = self.item_features[idx] | |
| return sample | |