rc-docker / src /recsys /models /base.py
moecr7
Dockerize rc-ranked: FastAPI service for HF Spaces
188f0cf
"""Common interface for every recommender model.
Trainer and evaluator depend only on this class — concrete models (MF,
TwoTower, future GNNs) plug in without any changes upstream.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
import torch
from torch import Tensor, nn
class BaseRecommender(nn.Module, ABC):
"""Abstract base. Subclasses implement `score` and `score_all_items`."""
num_users: int
num_items: int
def __init__(self, num_users: int, num_items: int) -> None:
super().__init__()
if num_users < 1 or num_items < 1:
raise ValueError("num_users and num_items must be >= 1")
self.num_users = int(num_users)
self.num_items = int(num_items)
@abstractmethod
def score(self, users: Tensor, items: Tensor) -> Tensor:
"""Score (user, item) pairs.
Args:
users: int64 tensor of shape [B] or broadcastable to items.
items: int64 tensor of shape [B] or [B, K].
Returns:
float tensor with shape matching `items`.
"""
@abstractmethod
def score_all_items(self, users: Tensor) -> Tensor:
"""Score a batch of users against every item in the catalog.
Args:
users: int64 tensor of shape [B].
Returns:
float tensor of shape [B, num_items].
"""
def forward(
self, users: Tensor, pos_items: Tensor, neg_items: Tensor
) -> tuple[Tensor, Tensor]:
"""Shared forward used by BPR training.
Args:
users: [B] int64.
pos_items: [B] int64.
neg_items: [B, K] int64.
Returns:
(pos_scores [B], neg_scores [B, K]).
"""
pos_scores = self.score(users, pos_items)
# Broadcast users along the K dim before scoring negatives.
users_expanded = users.unsqueeze(-1).expand_as(neg_items)
neg_scores = self.score(users_expanded, neg_items)
return pos_scores, neg_scores