Models API

See Citing Compresso Recsys for method references and attribution guidance, including the distinction between published models and Compresso’s simple baselines.

Recommender Contract

Models evaluated by compresso_recsys.evaluation.evaluate_recommender() implement the small compresso_recsys.models.Recommender protocol. The required predict_on_batch(source, *, k, exclude_seen=True) method returns ranked top-k predictions as an compresso.SRPTensor. EASE and ELSA exclude items already present in the source interactions by default. Pass exclude_seen=False to inspect rankings that may contain previously interacted items.

class compresso_recsys.models.Recommender(*args, **kwargs)[source]

A fitted recommender that produces ranked predictions for one batch.

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return top-k predictions, optionally excluding source items.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

Feature-based cold-start models additionally implement compresso_recsys.models.ColdStartRecommender. Its source vocabulary is fixed by training, while its identified candidate catalog can be rebuilt or updated independently.

class compresso_recsys.models.ColdStartRecommender(*args, **kwargs)[source]

Recommender with distinct identified source and candidate spaces.

The source vocabulary is no longer a member here: it lives on the catalog the model owns, reachable as model.candidates.source_vocabulary.

class compresso_recsys.models.ItemVocabulary(item_ids, id_to_row)[source]

Immutable mapping between stable item IDs and catalog rows.

Parameters:
  • item_ids (ndarray)

  • id_to_row (Mapping[Hashable, int])

classmethod positional(n_items)[source]

Build the default integer identity for an unnamed catalog.

Return type:

ItemVocabulary

Parameters:

n_items (int)

property n_items: int

Number of item IDs in the vocabulary.

rows_for(item_ids, *, name='item_ids')[source]

Resolve IDs to rows, preserving duplicates and request order.

Return type:

ndarray

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • name (str)

align_csr(matrix, *, item_ids, name='source')[source]

Select and reorder sparse columns to match this vocabulary.

Return type:

csr_matrix

Parameters:
  • matrix (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray)

  • name (str)

Models that read chronological histories implement compresso_recsys.models.SequentialRecommender instead. It is the same one-method contract, differing only in what a source is: an compresso_recsys.ItemSequences rather than a csr_matrix. compresso_recsys.evaluation.evaluate_recommender() accepts either, asking a source only for its row count and a row slice, so a sequential model and a matrix model can appear in one compresso_recsys.stats.compare_models() call with no statistics-side changes.

class compresso_recsys.models.SequentialRecommender(*args, **kwargs)[source]

A fitted recommender that ranks from chronological histories.

The same contract as Recommender with a different source type. Kept structural, like its sibling, so a model satisfies it by having the method rather than by inheriting anything.

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return top-k predictions, optionally excluding source items.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

Production Recommendations

The low-level predict methods consume catalog-shaped matrices or compresso_recsys.ItemSequences and return catalog column indices. For serving, every built-in model also inherits one identified interface that works with stable item IDs:

model = EASE().fit(interactions, item_ids=item_ids)

ranked = model.recommend(
    [["item-14", "item-87"], ["item-32"]],
    k=20,
    exclude_seen=True,
    allowlist=eligible_item_ids,
    blocklist=unavailable_item_ids,
    on_insufficient="truncate",
)

ranked.item_ids   # shape: (2, 20)
ranked.scores     # shape: (2, 20)
payload = ranked.to_dicts()

histories is always a batch. To recommend for one user, pass one nested history such as [["item-14", "item-87"]]. Collaborative models interpret a history as binary interactions, while sequential models preserve its order and repeated IDs. allowlist and blocklist are optional batch-wide candidate filters; both apply before top-k and the blocklist wins when an ID is present in both. Unknown history or filter IDs raise.

Unlike the evaluation-oriented predict methods, recommend defaults to exclude_seen=False. Recommending a previously seen item is legal in serving, and a caller such as a worker can put whichever seen IDs are inappropriate into blocklist. Pass exclude_seen=True to apply the conventional offline evaluation policy to the complete history.

k is a requested maximum. By default, a row with fewer than k eligible candidates is truncated independently; when exclude_seen=True, eligibility is calculated after removing seen items. Other users in the same batch still receive all available results up to k. The arrays remain (users, k): unused item positions contain None, their scores are -inf, and valid_mask plus valid_counts distinguish them from recommendations. to_dicts() omits those positions. Pass on_insufficient="raise" when a short row should instead fail the complete request. Neither policy reintroduces blocked items, or seen items when their exclusion was requested.

compresso_recsys.models.Recommendations holds immutable (users, k) arrays. to_dicts() returns one insertion-ordered item_id: score dictionary per user, so dictionary order is rank order and short rows become shorter dictionaries. Fixed-catalog models use positional integer IDs when item_ids is omitted from fitting. The mapping is stored in fitted-model checkpoints.

class compresso_recsys.models.IdentifiedRecommender(*args, **kwargs)[source]

A recommender accepting histories and filters as stable item IDs.

class compresso_recsys.models.Recommendations(item_ids, scores, valid_mask=None)[source]

Batch of ranked stable item IDs and their scores.

Parameters:
  • item_ids (ndarray)

  • scores (ndarray)

  • valid_mask (ndarray | None)

property valid_counts: ndarray

Number of real recommendations in each batch row.

to_dicts()[source]

Return one rank-ordered mapping per row, omitting padded positions.

Return type:

list[dict[Hashable, float]]

Training and Prediction Progress

Every iterative trainer accepts a duck-typed logger in its constructor; the object only needs an info(message: str) method. Pass the logger on an individual fit, predict, or recommend call to override that job-level default. A logger always replaces tqdm, even if show_progress=True, so service logs never receive carriage-return progress bars and callers do not have to disable the bar separately:

trainer = ELSATrainer(
    ELSAConfig(log_every_n_steps=1_000),
    logger=job_logger,
)
model = trainer.fit(interactions)

The logger receives start and finish lines, one line per completed epoch, and an intra-epoch line every log_every_n_steps batches. Set the interval to zero for epoch boundaries only. Reporting is resolved separately for each call: pass another logger to redirect one operation, or logger=None to make one call quiet. If logger.info raises, the first failure emits a warning, logging is disabled for that call, and the training or prediction continues.

Without a logger, notebook behavior remains controlled by show_progress. Fixed-epoch training uses two bars: an outer epoch bar and one batch bar that is reset and reused at each epoch. Reusing the batch bar is the recommended pattern for new trainers because creating one bar per epoch leaves a growing stack of completed bars in notebooks and terminals. Compressed ELSA’s unbounded mask-search phase has no fixed epoch total, so it uses only one reusable batch bar. ELSA, Mult-DAE, Mult-VAE, SimpleGPT, SimpleRNN, SimpleBidirectionalTransformer, and TEASER-GD all follow the logger reporting contract above.

Fitted Model Persistence

Built-in fitted recommenders inherit compresso_recsys.models.BasePersistableRecommender and expose model.save(path) plus ModelClass.load(path, device="cpu"). The archive is self-contained and loading produces a prediction-ready model. Torch-backed recommenders can subsequently be moved with model.to(device). Optimizer state is optional and exact training resumption is outside this contract. See Fitted Model Persistence for the format, device behavior, extension helpers, and the reason a compresso_recsys.models.WarmCatalogAdapter is rebuilt rather than persisted.

class compresso_recsys.models.PersistableRecommender(*args, **kwargs)[source]

A fitted recommender with the package model-checkpoint API.

class compresso_recsys.models.BasePersistableRecommender[source]

Common fitted-model persistence workflow.

The base owns the versioned archive, configuration, Torch state, device routing and optional optimizer state. Subclasses describe construction and any state that does not naturally live in a Torch state_dict.

abstract property is_fitted: bool

Whether the recommender is ready to save and predict.

classmethod _from_checkpoint_config(config, reader, *, device)[source]

Construct the model shape before learned state is installed.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
_checkpoint_module()[source]

Torch module whose state is learned, if this recommender has one.

Return type:

Module | None

_save_checkpoint_state(writer)[source]

Write non-module fitted state.

Return type:

None

Parameters:

writer (ModelCheckpointWriter)

_load_checkpoint_state(reader)[source]

Restore non-module fitted state.

Return type:

None

Parameters:

reader (ModelCheckpointReader)

_build_checkpoint_optimizer()[source]

Construct the optimizer before optional optimizer state is loaded.

Return type:

None

_finish_checkpoint_load()[source]

Restore derived inference state after the checkpoint is installed.

Return type:

None

to(device)[source]

Move this recommender’s Torch state to device and return self.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • self (_PersistableT)

  • device (str | device)

save(path, *, include_optimizer=False)[source]

Persist this fitted recommender as a safe, versioned ZIP checkpoint.

Return type:

None

Parameters:
  • path (str | Path)

  • include_optimizer (bool)

save_to_checkpoint(checkpoint_path, name, *, include_optimizer=False)[source]

Save this model under models/<name>.zip in a data checkpoint.

Return type:

None

Parameters:
  • checkpoint_path (str | Path)

  • name (str)

  • include_optimizer (bool)

classmethod load(path, *, device='cpu', load_optimizer=False)[source]

Load a fitted, prediction-ready recommender on device.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • path (str | Path)

  • device (str | device)

  • load_optimizer (bool)

classmethod load_from_checkpoint(checkpoint_path, name, *, device='cpu', load_optimizer=False)[source]

Load models/<name>.zip from a data checkpoint.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • checkpoint_path (str | Path)

  • name (str)

  • device (str | device)

  • load_optimizer (bool)

Implementing New Models

The protocols above are sufficient when a model only needs to work with the evaluation API. Model authors who want Compresso RecSys validation, batched prediction, and catalog management can instead inherit one of the abstract bases.

All three bases derive from compresso_recsys.models.BaseIdentifiedRecommender, which supplies the complete recommend workflow. A fixed-catalog model records identities with self._set_item_ids(item_ids, n_items=...) during a simple fitting path and supports the optional candidate_ids selection in predict_on_batch. When fitting also computes learned state, prepare the vocabulary first with self._prepare_item_vocabulary(...) and publish it together with the learned fields using self._publish_item_vocabulary(...) only after computation succeeds. This keeps failed fits and refits from exposing mixed model and catalog state. The source base then handles ID validation, history conversion, candidate filters, seen-item capacity checks, and result decoding automatically.

The original _predict_identified hook remains the compatibility path for subclasses implementing custom prediction. Reporting-aware dispatch uses a separate hook supplied by the standard bases, so adding logger support does not require existing subclasses to change their predict or _predict_identified signatures.

class compresso_recsys.models.BaseIdentifiedRecommender[source]

Shared production-facing recommendation workflow.

Histories and candidate filters enter as stable IDs. Source-specific bases turn the mapped rows into a CSR matrix or ItemSequences; concrete models only need to apply the selected candidates before their top-k.

_prepare_item_vocabulary(item_ids, *, n_items)[source]

Validate a fitted catalog without publishing it on the model.

Return type:

ItemVocabulary

Parameters:
  • item_ids (Sequence[Hashable] | ndarray | None)

  • n_items (int)

_set_item_ids(item_ids, *, n_items)[source]

Validate and publish the fitted catalog on the model.

Return type:

None

Parameters:
  • item_ids (Sequence[Hashable] | ndarray | None)

  • n_items (int)

_publish_item_vocabulary(vocabulary)[source]

Publish a vocabulary previously prepared for a successful fit.

Return type:

None

Parameters:

vocabulary (ItemVocabulary)

property source_item_ids: ndarray

Stable IDs accepted in recommendation histories.

property candidate_item_ids: ndarray

Stable IDs that can be returned by recommend().

abstractmethod _recommendation_source(rows, *, vocabulary)[source]

Build the low-level batched source for mapped history rows.

Return type:

csr_matrix | ItemSequences

Parameters:
abstractmethod _predict_identified(source, *, k, exclude_seen, candidate_ids)[source]

Predict after candidate IDs have been resolved and filtered.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix | ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (ndarray)

_predict_identified_with_reporting(source, *, k, exclude_seen, candidate_ids, reporter)[source]

Reporting-aware prediction hook with a legacy-compatible fallback.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix | ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (ndarray)

  • reporter (_Reporter)

recommend(histories, *, k=100, exclude_seen=False, allowlist=None, blocklist=None, on_insufficient='truncate', logger=<inherit>, show_progress=<inherit>)[source]

Recommend up to k item IDs for each item-ID history.

logger and show_progress override prediction reporting for this call. Passing logger=None makes the request quiet even when the recommender has a constructor logger.

Return type:

Recommendations

Parameters:
  • histories (Sequence[Sequence[Hashable]])

  • k (int)

  • exclude_seen (bool)

  • allowlist (Sequence[Hashable] | ndarray | None)

  • blocklist (Sequence[Hashable] | ndarray | None)

  • on_insufficient (Literal['truncate', 'raise'])

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

Use compresso_recsys.models.BaseCollaborativeRecommender for a model whose fitted source and candidate catalog has one fixed positional item space. Implement fit, is_fitted, n_items, and candidate-aware predict_on_batch. Call the inherited _prepare_source at the start of predict_on_batch; the base then supplies predict with bounded batching and optional progress, plus recommend for stable IDs.

class compresso_recsys.models.BaseCollaborativeRecommender[source]

Reusable base for fixed-catalog collaborative recommenders.

Implementors provide fit(), is_fitted, n_items, and predict_on_batch(). The base validates source matrices and supplies a memory-bounded predict() implementation that concatenates ranked batches without materializing a complete score matrix.

abstract property is_fitted: bool

Whether the model is ready for prediction.

abstract property n_items: int | None

Number of fitted item columns, or None before fitting.

abstractmethod fit(interactions, *, item_ids=None)[source]

Fit the model from a user-item CSR interaction matrix.

Return type:

BaseCollaborativeRecommender

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

abstractmethod predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

_prepare_source(source)[source]

Validate a source matrix against the fitted item catalog.

Return type:

csr_matrix

Parameters:

source (csr_matrix)

predict(source, *, k=100, batch_size=1024, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Predict all source rows by repeatedly calling predict_on_batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • batch_size (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

Use compresso_recsys.models.BaseColdStartRecommender when source items are fixed by fitting but identified candidates can be rebuilt or updated from features, and the source is a csr_matrix. Subclass constructors must call super().__init__(). After fitting the source encoder, call self.candidates.install(...) once with the fitted source IDs and initial candidate features. Later changes then validate features against that feature space and preserve stable IDs. predict_on_batch can call self.candidates.resolve_selection(...) to resolve an optional candidate allowlist while keeping returned compresso.SRPTensor columns in the complete catalog space.

The catalog lifecycle is owned rather than inherited – see The Owned Candidate Catalog below – which is why this base is only about reading a matrix. The methods on it are a facade over self.candidates, kept because they are the documented model surface.

class compresso_recsys.models.BaseColdStartRecommender[source]

Reusable base for feature-driven cold-start recommenders that read a matrix.

Subclasses implement fit(), is_fitted, and predict_on_batch(). The catalog lifecycle is owned rather than inherited: candidates is a MutableCandidateCatalog holding the fitted source vocabulary, the current snapshot and the operations over them. The methods below are a facade over it, kept because they are the documented model surface.

That composition is why this class is only about reading a csr_matrix source. A cold-capable model that reads ordered histories owns the same catalog from BaseSequentialRecommender instead, rather than needing a fourth base class or multiple inheritance.

Subclass constructors must call super().__init__(). During fitting, call self.candidates.install(...) after learning the source encoder to publish the initial catalog.

property source_item_ids: ndarray

Stable IDs accepted in recommendation histories.

property candidate_item_ids: ndarray

Stable IDs in the current candidate snapshot.

abstract property is_fitted: bool

Whether the model is ready for prediction.

abstractmethod fit(interactions, item_features, **kwargs)[source]

Fit a source encoder and publish the initial candidate catalog.

Return type:

BaseColdStartRecommender

Parameters:
  • interactions (csr_matrix)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

abstractmethod predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions against the current candidate catalog.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

_prepare_source(source)[source]

Validate source columns against the fitted source vocabulary.

Return type:

csr_matrix

Parameters:

source (csr_matrix)

predict(source, *, k=100, batch_size=1024, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Predict all source rows by repeatedly calling predict_on_batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • batch_size (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

build_candidates(*, item_ids, item_features, metadata=None, feature_space_id=None)[source]

Atomically replace the complete candidate catalog.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

update_candidates(*, item_ids, item_features, metadata=None, on_conflict='error', feature_space_id=None)[source]

Add or update candidates and atomically publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • on_conflict (Literal['error', 'replace', 'ignore'])

  • feature_space_id (str | None)

remove_candidates(item_ids, *, missing='error')[source]

Remove registered candidates and publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • missing (Literal['error', 'ignore'])

align_source(source, *, item_ids)[source]

Align external sparse columns to the fitted source vocabulary.

Return type:

csr_matrix

Parameters:
  • source (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray)

Use compresso_recsys.models.BaseSequentialRecommender for a model that reads ordered histories. It is parallel to compresso_recsys.models.BaseCollaborativeRecommender rather than derived from it, because crossing the two source representations with cold-start capability in the type hierarchy would give four classes for two ideas. Candidate capability is composed instead: a model that scores unseen items owns a catalog rather than inheriting one. Implement is_fitted, n_items and candidate-aware predict_on_batch; the base supplies predict with bounded batching over row slices and recommend with order-preserving ID histories.

fit is deliberately outside the contract. Trainers keep the package’s existing SomeTrainer(config).fit(data) shape and a fitted model owes only prediction.

The base is careful not to assume two things. n_items describes what can be scored, which need not be the vocabulary a history is expressed over — a truncated or hashed context, or a cold-capable model scoring items that appear in no history at all. And truncation is not exclusion: exclude_seen=True must mask every item in the full history handed to it, even where the encoder reads only a suffix. A model attending to the last 200 interactions must still refuse to recommend the 201st.

class compresso_recsys.models.BaseSequentialRecommender[source]

Reusable base for recommenders that read chronological histories.

Parallel to BaseCollaborativeRecommender rather than derived from it. The two differ only in how a user’s history arrives — a CSR row of items interacted with, or an ordered history that keeps repeats — and crossing that with cold-start capability in the type hierarchy would give four classes for two ideas. Candidate capability is composed instead: a model that scores unseen items owns a catalog rather than inheriting one.

Implementors provide is_fitted, n_items, and predict_on_batch(). fit is deliberately absent from the contract: trainers follow the package’s existing shape, where SomeTrainer(config).fit(data) returns a fitted model and the model owes only the prediction contract.

Two properties this base is careful not to assume.

The source vocabulary need not equal the candidate catalog. n_items describes what can be scored. A history may be expressed over a different, usually smaller, vocabulary — a truncated context, a hashed one — and a cold-capable model scores candidates that never appear in any history at all. Nothing here compares the two.

Truncation is not exclusion. exclude_seen=True must mask every item in the full history handed to it, even where the encoder reads only a suffix. A model that attends to the last 200 interactions must still refuse to recommend the 201st.

abstract property is_fitted: bool

Whether the model is ready for prediction.

abstract property n_items: int | None

Number of scoreable candidates, or None before fitting.

abstractmethod predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one batch of histories.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

_prepare_source(source)[source]

Check a batch of histories against the fitted model.

Return type:

ItemSequences

Parameters:

source (ItemSequences)

predict(source, *, k=100, batch_size=1024, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Predict all histories by repeatedly calling predict_on_batch.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • batch_size (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

The Owned Candidate Catalog

compresso_recsys.models.CandidateCatalog is an immutable snapshot. compresso_recsys.models.MutableCandidateCatalog is the lifecycle around it: the lock, the current snapshot, the fitted source vocabulary, and the operations that publish, extend, shrink and align against them.

While that lifecycle lived on a base class, “cold-capable” and “inherits compresso_recsys.models.BaseColdStartRecommender” were the same statement. Adding a second axis – a model that reads ordered histories rather than a matrix – would then have forced a choice between multiple inheritance and a fourth base class, for two independent ideas. An owned object removes the choice: a model holds one, whichever base it derives from.

Composition rather than a mixin, because the state is what decides it. A mixin would not encapsulate these attributes; it would install them on whatever class it is mixed into, six of them public. Two stateful mixins both initialising through super().__init__() is where MRO ordering and private-name collisions live. An owned object has its own __init__, its own lock and its own tests, and a model could hold two if that ever made sense.

class SequentialContentRNN(BaseSequentialRecommender):
    def __init__(self) -> None:
        self.candidates = MutableCandidateCatalog()

    def fit(self, sequences, item_features, *, item_ids):
        ...
        self.candidates.install(...)
        return self

    def predict_on_batch(self, source, *, k, exclude_seen=True):
        catalog = self.candidates.snapshot()

Reads go through snapshot() rather than through forwarded properties, deliberately. A snapshot is a consistent view: several reads off one snapshot cannot straddle a concurrent republish, which forwarding item_ids, rows_for and ids_for separately would silently allow. n_items is the one convenience, because “how many candidates are there” needs an answer before installation, which a snapshot cannot give.

on_publish is called with each new snapshot while the lock is held, and is how an owner drops caches derived from the previous one. The six fitted source_* attributes live on the catalog, so a model’s fitted source vocabulary is model.candidates.source_item_ids rather than model.source_item_ids_.

class compresso_recsys.models.CandidateCatalog(*, item_ids, item_features, metadata, feature_space_id, version, id_to_row)[source]

Immutable snapshot of a feature-based candidate catalog.

Parameters:
  • item_ids (ndarray)

  • item_features (csr_matrix | ndarray)

  • metadata (pd.DataFrame | None)

  • feature_space_id (str | None)

  • version (int)

  • id_to_row (Mapping[Hashable, int])

property n_items: int

Number of candidates in this snapshot.

property metadata: DataFrame | None

Return a defensive copy of metadata aligned with candidate rows.

rows_for(item_ids)[source]

Resolve stable item IDs to candidate rows in request order.

Return type:

ndarray

Parameters:

item_ids (Sequence[Hashable])

ids_for(rows)[source]

Resolve candidate row indices to stable item IDs.

Return type:

ndarray

Parameters:

rows (ndarray | Tensor)

class compresso_recsys.models.MutableCandidateCatalog(*, on_publish=None)[source]

The lifecycle around a CandidateCatalog, as an owned object.

CandidateCatalog is an immutable snapshot and needs nothing. What used to be stuck inside BaseColdStartRecommender was the lifecycle around it: the lock, the current snapshot, the fitted source vocabulary, and the dozen methods that publish, extend, shrink and align against them.

While that lived on a base class, “cold-capable” meant “inherits BaseColdStartRecommender”. Adding a second axis – a model that reads ordered histories rather than a matrix – then forced a choice between multiple inheritance and a fourth base class for two independent ideas. An owned object removes the choice: any model can hold one.

Composition rather than a mixin, because the state is what decides it. A mixin would not encapsulate these attributes, it would install them on whatever class it is mixed into – and two stateful mixins initialising through super().__init__() is where MRO pain lives. This has its own __init__, its own lock and its own tests, and a model could own two if that ever made sense:

class SequentialContentRNN(BaseSequentialRecommender):
    def __init__(self) -> None:
        self.candidates = MutableCandidateCatalog()

    def predict_on_batch(self, source, *, k, exclude_seen=True):
        catalog = self.candidates.snapshot()

Reads go through snapshot(), deliberately, rather than through forwarded properties. A snapshot is a consistent view: several reads off one snapshot cannot straddle a concurrent republish, which forwarding n_items, item_ids and rows_for separately would silently allow.

on_publish is called with each new snapshot while the lock is held, which is how an owner drops caches derived from the previous one.

Parameters:

on_publish (Callable[[CandidateCatalog], None] | None)

property is_installed: bool

Whether a catalog has been published yet.

snapshot()[source]

The current immutable snapshot.

Take one and read every field off it, rather than reading fields off this object one at a time: only the snapshot is guaranteed internally consistent against a concurrent build(), update() or remove().

Return type:

CandidateCatalog

property n_items: int | None

Number of current candidates, or None before installation.

property source_vocabulary: ItemVocabulary | None

Item space a source matrix must be expressed over.

property source_item_ids: ndarray | None

Stable IDs of the fitted source items, in column order.

property source_id_to_row: Mapping[Hashable, int] | None

Source item ID to source column.

property source_popularity: ndarray | None

Per-source-item popularity recorded at installation.

property feature_space_id: str | None

Identifier of the feature space, when one was declared.

property n_input_features: int | None

Feature columns every candidate must supply.

install(*, source_item_ids, source_popularity, n_input_features, candidate_features, metadata, feature_space_id, dtype, include_popularity)[source]

Atomically replace the complete candidate catalog.

Return type:

CandidateCatalog

Parameters:
  • source_item_ids (ndarray)

  • source_popularity (ndarray)

  • n_input_features (int)

  • candidate_features (csr_matrix | ndarray)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

  • dtype (dtype)

  • include_popularity (bool)

build(*, item_ids, item_features, metadata=None, feature_space_id=None)[source]

Atomically replace the complete catalog and publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

update(*, item_ids, item_features, metadata=None, on_conflict='error', feature_space_id=None)[source]

Add or update candidates and atomically publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • on_conflict (Literal['error', 'replace', 'ignore'])

  • feature_space_id (str | None)

remove(item_ids, *, missing='error')[source]

Remove registered candidates and publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • missing (Literal['error', 'ignore'])

align_source(source, *, item_ids)[source]

Align external sparse columns to the fitted source vocabulary.

Return type:

csr_matrix

Parameters:
  • source (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray)

Training Interaction Batches

compresso_recsys.models.InteractionBatchSampler provides the compact source-prefix batching used by ELSA and TEASERGD. It keeps the interaction matrix sparse until the training step chooses to densify the selected active columns. batch.x uses local compact columns; batch.sources maps those columns to global fitted item rows.

When max_output is an integer, batch.candidates begins with exactly batch.sources and appends items absent from the whole batch. It is a soft limit because active sources are never dropped. With max_output=None, batch.candidates is None and the model should score its complete output catalog. Call on_epoch_end() after each epoch to advance shuffling and negative sampling reproducibly.

from compresso_recsys.models import (
    InteractionBatchSampler,
    dense_training_target,
)

sampler = InteractionBatchSampler(
    interactions,
    device="cuda",
    batch_size=1024,
    shuffle=True,
    max_output=5000,
    seed=0,
)

for batch_index in range(len(sampler)):
    batch = sampler[batch_index]
    x = batch.x.to_dense()
    targets = dense_training_target(
        x,
        sources=batch.sources,
        candidates=batch.candidates,
        input_dim=interactions.shape[1],
    )
    predictions = model(
        x,
        sources=batch.sources,
        candidates=batch.candidates,
    )

sampler.on_epoch_end()
class compresso_recsys.models.InteractionBatch(x, sources, candidates)[source]

Compact interaction batch with source-prefix output candidates.

x is a sparse COO tensor whose columns correspond positionally to the global item rows in sources. candidates is either None for the full output catalog or a global-row tensor beginning with sources.

Parameters:
  • x (Tensor)

  • sources (Tensor)

  • candidates (Tensor | None)

class compresso_recsys.models.InteractionBatchSampler(interactions, *, device, batch_size, shuffle, max_output, seed)[source]

Batched sparse interactions with optional output candidate sampling.

Every batch packs the union of its active source items into batch.x. batch.sources maps those local columns back to global item rows. When max_output is set, batch.candidates starts with that exact source prefix and appends sampled items absent from the complete batch. The limit is therefore soft when a batch already contains more active source items. None leaves output selection to the model and denotes the full catalog.

Parameters:
  • interactions (csr_matrix)

  • device (torch.device | str)

  • batch_size (int)

  • shuffle (bool)

  • max_output (int | None)

  • seed (int)

compresso_recsys.models.dense_training_target(x, *, sources, candidates, input_dim)[source]

Expand compact source interactions into the selected output space.

Return type:

Tensor

Parameters:
  • x (Tensor)

  • sources (Tensor)

  • candidates (Tensor | None)

  • input_dim (int)

Transductive Models in Expanding Catalogs

compresso_recsys.models.WarmCatalogAdapter evaluates a fixed-catalog model such as EASE, ELSA or SimpleRNN against a larger validation or test catalog. It projects the stage source into the fitted item space and remaps warm prediction indices into the expanded target space. Cold targets remain part of the metric calculation, but the wrapped transductive model cannot recommend them. This makes the result directly comparable with a cold-start model on the same users and targets while preserving the transductive model’s limitation.

Both source representations work because checkpoint stage catalogs grow by appending: train_item_ids is an exact ordered prefix of every later catalog. A csr_matrix is projected to that fitted prefix. An compresso_recsys.ItemSequences is passed through whole: its warm indices keep their meaning, while the sequential model’s tokenizer turns appended cold indices into unk without deleting their positions or inventing adjacency. Rows survive either way, which keeps the source aligned with the targets. The adapter validates the prefix invariant when it is constructed rather than silently interpreting a reordered catalog.

from compresso_recsys.evaluation import evaluate_recommender
from compresso_recsys.models import WarmCatalogAdapter

adapted_elsa = WarmCatalogAdapter(
    elsa,
    train_item_ids=split["train_item_ids"],
    catalog_item_ids=split["test_item_ids"],
)

result = evaluate_recommender(
    adapted_elsa,
    source=adapted_elsa.align_source(split["test_source_matrix"]),
    targets=split["test_target_matrix"],
    metrics=metrics,
    batch_size=1024,
)

A sequential model is wrapped the same way, but its history is passed directly because dropping cold positions would change the sequence:

adapted_rnn = WarmCatalogAdapter(
    rnn,
    train_item_ids=split["train_item_ids"],
    catalog_item_ids=split["test_item_ids"],
)

result = evaluate_recommender(
    adapted_rnn,
    source=split["test_source_sequences"],
    targets=split["test_target_matrix"],
    metrics=metrics,
)

The input to align_source() must be a matrix using the exact column order declared by catalog_item_ids; sequences go straight to predict_on_batch or evaluate_recommender. Construct a separate adapter for validation when its catalog differs from the test catalog.

When to Reach for It Outside temporal

Under temporal the adapter is mandatory: stage catalogs expand, so a model fitted on the training window cannot even accept a test source. Under leave_last_out the catalogs match and nothing forces the issue – but items whose every occurrence falls in a held-out tail are still absent from training, and the model families do not treat such columns alike.

A softmax next-item objective pushes down every non-target logit at every step. An item that never appears in training is never a target, so it collects only downward pressure. A reconstruction objective has no such term and leaves the item near its initialization. Measured on MovieLens-1M under leave_last_out, ranking the full catalog for 300 test users:

Rank percentile of never-trained items

Model

Percentile

ELSA

60th

SimpleRNN

95th

A random item would sit at the 50th. Neither figure says anything about recommendation quality, so a comparison spanning both families is sounder with the cold items made unreachable for each – which is what wrapping both models does.

Whether it matters is a question about the data rather than the protocol. On MovieLens-1M only 3 of 6,033 test users have a never-trained target, so the bias cannot move a metric. On a sparse catalog the share grows. Count first:

never_trained = np.flatnonzero(
    np.asarray(split["x_train"].sum(axis=0)).ravel() == 0
)
cold = set(never_trained.tolist())
targets = split["test_target_matrix"]
affected = sum(
    1
    for row in range(targets.shape[0])
    if cold & set(targets[row].indices.tolist())
)
print(f"{affected} of {targets.shape[0]} rows have a never-trained target")

Note that leave_last_out sets train_item_ids to the whole catalog, since that mode does not partition items. The warm subset is split["item_ids"][split["warm_item_indices"]], and passing it is what makes the adapter do anything at all in that mode.

class compresso_recsys.models.WarmCatalogAdapter(model, train_item_ids, catalog_item_ids)[source]

Expose a fixed-catalog recommender in a larger identified catalog.

The wrapped model continues to consume and rank only its training items. align_source() expresses a source over the expanded catalog in the fitted item space, while predict_on_batch() remaps the resulting ranked columns back into that catalog. Cold candidates remain valid target items but can never be emitted by the wrapped model.

Stage catalogs must follow the checkpoint invariant: the training IDs are an exact ordered prefix and cold items are appended. A csr_matrix is projected to that fitted prefix. An ItemSequences is passed through whole, because its warm indices already mean the same thing and the wrapped model’s tokenizer turns appended cold indices into unk without deleting positions. Rows survive either way, so alignment with the targets is preserved.

This is mandatory whenever the model’s item space is narrower than the evaluation catalog, which the temporal split mode guarantees by construction. It is also worth reaching for under leave_last_out, where the catalogs do match but items whose every occurrence falls in a held-out tail are still absent from training – and the model families do not treat such columns alike. A softmax next-item objective pushes every non-target logit down on every step, and a never-trained item is never a target, so it is buried: on MovieLens-1M such items land at the 95th rank percentile for SimpleRNNTrainer against the 60th for ELSATrainer, which leaves them near their initialization. Neither number is about recommendation quality, so a comparison spanning both families is sounder with the cold items made unreachable for each. Whether it matters is a question about the data rather than the protocol: count the evaluation rows whose target is absent from training before deciding.

Parameters:
  • model (Recommender | SequentialRecommender) – Fitted recommender whose prediction columns follow train_item_ids.

  • train_item_ids (Union[Sequence[Hashable], ndarray]) – Item IDs in the exact column order used to fit model.

  • catalog_item_ids (Union[Sequence[Hashable], ndarray]) – Expanded source and target catalog. train_item_ids must be its exact ordered prefix; additional cold items are appended after it.

property source_item_ids: ndarray

Stable IDs accepted from the expanded stage catalog.

property candidate_item_ids: ndarray

Stable IDs in the expanded output catalog.

align_source(source)[source]

Select the fitted training-item columns from an expanded-catalog matrix.

Matrices only. A history needs no alignment: a sequential model’s tokenizer maps an out-of-catalog index to its own unk token, keeping the position, and projecting one instead would delete interior events and thereby assert transitions that never happened. Pass sequences straight to predict_on_batch().

Return type:

csr_matrix

Parameters:

source (csr_matrix)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Predict warm items and express their columns in the full catalog.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix | ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

Collaborative Filtering Models

Baselines

Two deliberately simple models provide checks that every experiment should include. RandomBaseline produces a stable pseudorandom ranking for each source history, independent of evaluation batch size. PopularityBaseline ranks the catalog by the number of interacting training users, or optionally by summed interaction values. Both support stable item IDs, candidate selection, seen-item exclusion, and fitted-model checkpoints through the same API as learned models.

class compresso_recsys.models.RandomBaselineConfig(seed=0)[source]

Configuration for RandomBaseline.

seed determines a stable pseudorandom ranking for each distinct source history. Predictions are invariant to evaluation batch size and checkpoint round trips.

Parameters:

seed (int)

class compresso_recsys.models.RandomBaseline(config=None)[source]

Deterministic random-ranking baseline for a fixed item catalog.

Parameters:

config (RandomBaselineConfig | None)

fit(interactions, *, item_ids=None)[source]

Record the fitted catalog used by the random baseline.

Return type:

RandomBaseline

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

class compresso_recsys.models.PopularityBaselineConfig(use_values=False)[source]

Configuration for PopularityBaseline.

When use_values is false, popularity counts users with a nonzero interaction. When true, it sums the interaction values instead.

Parameters:

use_values (bool)

class compresso_recsys.models.PopularityBaseline(config=None)[source]

Non-personalized baseline ranking items by training popularity.

Parameters:

config (PopularityBaselineConfig | None)

property is_fitted: bool

Whether the model is ready for prediction.

fit(interactions, *, item_ids=None)[source]

Count item popularity in the fitted interaction matrix.

Return type:

PopularityBaseline

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

Neighborhood Models

UserKNN finds the k fitted users with greatest cosine similarity to each source history. It scores candidates by their similarity-weighted interactions, normalized by the absolute sum of neighbor similarities. ItemKNN builds a sparse cosine-neighbor graph between item columns and applies the corresponding normalized weighted sum over the source user’s interacted items.

Both accept nonnegative implicit or weighted interactions. Install their neighbor-search dependency with pip install "compresso-recsys[knn]". Checkpoints store only the fitted sparse matrices; transient scikit-learn indexes are rebuilt when needed. See Citing Compresso Recsys for the foundational neighborhood-method references.

class compresso_recsys.models.UserKNNConfig(n_neighbors=100, dtype='float32', n_jobs=None)[source]

Configuration for cosine user-neighborhood collaborative filtering.

Parameters:
  • n_neighbors (int)

  • dtype (Literal['float32', 'float64'])

  • n_jobs (int | None)

class compresso_recsys.models.UserKNNRecommender(config=None)[source]

User-user cosine KNN using fitted users as the neighbor population.

Parameters:

config (UserKNNConfig | None)

property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of fitted item columns, or None before fitting.

fit(interactions, *, item_ids=None)[source]

Store fitted users and build the transient cosine-neighbor index.

Return type:

UserKNNRecommender

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

class compresso_recsys.models.ItemKNNConfig(n_neighbors=100, dtype='float32', n_jobs=None)[source]

Configuration for cosine item-neighborhood collaborative filtering.

Parameters:
  • n_neighbors (int)

  • dtype (Literal['float32', 'float64'])

  • n_jobs (int | None)

class compresso_recsys.models.ItemKNNRecommender(config=None)[source]

Item-item cosine KNN fitted over item interaction vectors.

Parameters:

config (ItemKNNConfig | None)

property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of fitted item columns, or None before fitting.

fit(interactions, *, item_ids=None)[source]

Build a sparse cosine-neighbor graph over item columns.

Return type:

ItemKNNRecommender

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

Multinomial Autoencoders

Mult-DAE is a deterministic multinomial denoising autoencoder for implicit feedback. It L2-normalizes a dense user vector, corrupts it with dropout during training, passes it through a tanh bottleneck, and reconstructs logits over the catalog. MultDAETrainer optimizes multinomial log likelihood plus l2_reg * (||W_encoder||^2 + ||W_decoder||^2) and serves the fitted network through the standard collaborative recommender API. The L2 term applies only to weight matrices, not biases, matching the original implementation. Mult-DAE history and progress output name the data term reconstruction_loss; the L2 term is applied by the optimizer and is not included in that reported metric. Both Mult-DAE and Mult-VAE preload a dense training matrix on the configured device by default. Set preload_training_data=False to retain bounded-memory CSR minibatch streaming when the matrix does not fit. A failed preload raises a clear memory error rather than silently selecting the slower path. Training statistics are accumulated on the device and transferred to the host only at reporting points: each epoch boundary and, when a logger is present, each configured log_every_n_steps interval. After fitting, training_data_preloaded_ reports which path was selected.

Mult-DAE and Mult-VAE use the standard two-bar training display described in Training and Prediction Progress: one outer epoch bar and one batch bar reused across epochs. Their logger output reports the same epoch and batch progress without creating either bar.

Mult-VAE replaces the deterministic bottleneck with a diagonal Gaussian posterior. Its symmetric encoder produces a mean and log variance, training samples with the reparameterization trick, and inference decodes the posterior mean for deterministic rankings. The trainer computes the KL coefficient as min(kl_cap, updates / kl_anneal_steps), matching the original implementation; it reaches the cap after kl_cap * kl_anneal_steps optimizer updates. Set the step count to zero to use the cap immediately.

The network is intentionally exposed separately from its configuration and trainer. See Citing Compresso Recsys for the Mult-VAE/Mult-DAE paper and AutoRec, its autoencoder predecessor. Implementing a Recommender uses a simpler Top Popular algorithm to show the complete integration contract without hiding serving or persistence details behind a large training loop.

class compresso_recsys.models.MultDAEConfig(latent_dim=200, dropout=0.5, epochs=20, batch_size=256, lr=0.001, l2_reg=2e-05, preload_training_data=True, device='cpu', show_progress=True, seed=0, log_prefix='MultDAE', log_every_n_steps=1000)[source]

Configuration for MultDAETrainer.

latent_dim is the deterministic bottleneck width. dropout corrupts normalized interaction vectors during training only, as in Mult-DAE. l2_reg is the coefficient on the squared L2 norm of the encoder and decoder weight matrices; biases are not regularized. The default matches the original implementation’s 0.01 / 500 setting. preload_training_data=True caches the dense interaction matrix on the training device by default. Set it to False to stream CSR minibatches when the complete dense matrix does not fit.

Parameters:
  • latent_dim (int)

  • dropout (float)

  • epochs (int)

  • batch_size (int)

  • lr (float)

  • l2_reg (float)

  • preload_training_data (bool)

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.MultDAE(n_items, latent_dim, dropout)[source]

The deterministic n_items -> latent -> n_items Mult-DAE network.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • n_items (int)

  • latent_dim (int)

  • dropout (float)

forward(interactions)[source]

Return one unnormalized multinomial score per catalog item.

Return type:

Tensor

Parameters:

interactions (Tensor)

class compresso_recsys.models.MultDAETrainer(config=None, logger=None)[source]

Train and serve Mult-DAE on complete implicit-feedback user rows.

Parameters:
property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of fitted item columns, or None before fitting.

fit(interactions, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Fit Mult-DAE using multinomial reconstruction likelihood.

Return type:

MultDAETrainer

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

class compresso_recsys.models.MultVAEConfig(latent_dim=200, hidden_dim=600, dropout=0.5, epochs=20, batch_size=256, lr=0.001, weight_decay=0.0, kl_cap=0.2, kl_anneal_steps=200000, preload_training_data=True, device='cpu', show_progress=True, seed=0, log_prefix='MultVAE', log_every_n_steps=1000)[source]

Configuration for MultVAETrainer.

kl_cap is the maximum coefficient on KL divergence. kl_anneal_steps is the denominator in updates / kl_anneal_steps; the coefficient is clipped at kl_cap. It therefore reaches the cap after kl_cap * kl_anneal_steps updates. Set the step count to zero to use kl_cap from the first update. preload_training_data=True caches the dense interaction matrix on the training device by default. Set it to False to stream CSR minibatches when the complete dense matrix does not fit.

Parameters:
  • latent_dim (int)

  • hidden_dim (int)

  • dropout (float)

  • epochs (int)

  • batch_size (int)

  • lr (float)

  • weight_decay (float)

  • kl_cap (float)

  • kl_anneal_steps (int)

  • preload_training_data (bool)

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.MultVAE(n_items, latent_dim, hidden_dim, dropout)[source]

Symmetric multinomial VAE with a Gaussian latent representation.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • n_items (int)

  • latent_dim (int)

  • hidden_dim (int)

  • dropout (float)

encode(interactions)[source]

Return posterior mean and log variance for interaction rows.

Return type:

tuple[Tensor, Tensor]

Parameters:

interactions (Tensor)

decode(latent)[source]

Decode latent rows to unnormalized multinomial item scores.

Return type:

Tensor

Parameters:

latent (Tensor)

forward(interactions, *, sample=None)[source]

Return item logits, posterior mean, and posterior log variance.

Sampling defaults to the module’s training mode. Evaluation therefore uses the posterior mean and produces deterministic rankings.

Return type:

tuple[Tensor, Tensor, Tensor]

Parameters:
  • interactions (Tensor)

  • sample (bool | None)

class compresso_recsys.models.MultVAETrainer(config=None, logger=None)[source]

Train and serve Mult-VAE on implicit-feedback user rows.

Parameters:
property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of fitted item columns, or None before fitting.

fit(interactions, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Fit Mult-VAE with multinomial likelihood and annealed KL loss.

Return type:

MultVAETrainer

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Return ranked predictions for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

EASE

EASE is a closed-form collaborative-filtering model. Fitting creates a dense item-by-item coefficient matrix, so its memory use grows quadratically with the number of items. float32 is the memory-efficient default. Select float64 explicitly when additional numerical precision is more important than fit and prediction speed. See Citing Compresso Recsys for the original EASE paper and copy-ready BibTeX.

class compresso_recsys.models.EASEConfig(l2=500.0, dtype='float32')[source]

Configuration for EASE.

Parameters:
  • l2 (float) – Positive L2 regularization added to the item Gram matrix diagonal.

  • dtype (Literal['float32', 'float64']) – Floating-point precision used to fit and score the model. float32 is the memory-efficient default; float64 is available for experiments that need additional numerical precision.

class compresso_recsys.models.EASE(config=None)[source]

Embarrassingly Shallow Autoencoder recommender.

EASE learns a closed-form item-to-item coefficient matrix from a sparse user-item interaction matrix. Predictions are returned as ranked compresso.SRPTensor objects, with seen source items excluded by default.

Parameters:

config (EASEConfig | None)

property is_fitted: bool

Whether the item coefficient matrix has been fitted.

property n_items: int | None

Number of fitted item columns, or None before fitting.

property dtype: dtype

NumPy dtype used by the model.

fit(interactions, *, item_ids=None)[source]

Fit EASE from a CSR user-item interaction matrix.

Return type:

EASE

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Predict ranked top-k items for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

ELSA

ELSA learns a low-rank matrix of normalized item embeddings with a shallow linear autoencoder objective. Unlike EASE, its model size grows linearly with the number of items, making it suitable for larger catalogs and GPU training. See Citing Compresso Recsys for citations covering standard ELSA, large-scale candidate sampling, and compressed ELSA.

During training, max_output can limit each batch’s output candidates. All items with positive interactions in the batch are always retained, and the remaining candidate budget is sampled without replacement from items absent from the whole batch. This makes max_output a soft upper bound when a batch contains more distinct positive items than the configured limit. Use None to score the complete catalog during training.

Compressed ELSA uses Compresso’s lottery-ticket schedule to retain a fixed number of latent values per item. During mask search, each stable stage rewinds the item factors to their original initialization under the new mask and restarts the optimizer. After the final mask stabilizes, the factors are converted to a row-packed sparse parameter and only its values are trained for ELSAConfig.epochs. Learning-rate decay, when enabled, applies only to this final sparse fine-tuning phase.

By default, a mask-search stage advances only after its mask change stays within change_threshold for stability_window updates. Set max_epochs_per_stage to force a stage to accept its latest proposed mask after a fixed number of epochs. This bounds training time but may select a less stable ticket. Training checkpoints during this phase are not currently resumable, although a fitted ELSA model can be saved and loaded after training; torch.compile is not supported. When max_output limits the candidate set, mask search projects only those MaskedParam rows and sparse fine-tuning selects only those gradient-connected SRPParam rows. The default dense fine-tuning backend densifies that selection, while the COO backend keeps it sparse through differentiable sparse matrix multiplications. COO can reduce both memory and runtime for highly sparse tickets, while dense matrix multiplication can win as the retained k grows. The crossover is hardware- and workload-dependent. max_output=None scores the complete catalog during training.

Sparse inference defaults to cached CSR full-catalog scoring and densifies only the selected source rows. The dense inference backend instead caches one full normalized factor matrix and can be faster for less sparse tickets. Configure the normal backend in ELSACompressionConfig or override it per predict or predict_on_batch call without retraining.

Configuration

class compresso_recsys.models.ELSAConfig(latent_dim=1024, batch_size=1024, max_output=None, epochs=1, lr=0.001, weight_decay=0.0, decay=False, compile=False, device='cpu', show_progress=True, seed=0, use_relu=True, optimizer='NAdam', compression=None, log_prefix='ELSA', log_every_n_steps=1000)[source]

Configuration for ELSATrainer.

max_output limits the number of output candidates used by a training batch. Every item with a positive interaction in the batch is retained, and the remaining budget is sampled without replacement from items absent from the entire batch. Consequently, a batch with more positive columns than max_output exceeds the requested limit rather than dropping positive targets. None evaluates the full item output during training.

Parameters:
  • latent_dim (int)

  • batch_size (int)

  • max_output (int | None)

  • epochs (int)

  • lr (float)

  • weight_decay (float)

  • decay (bool)

  • compile (bool)

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • use_relu (bool)

  • optimizer (Literal['NAdam', 'AdamW'])

  • compression (ELSACompressionConfig | None)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.ELSACompressionConfig(k_target, k_schedule=None, num_stages=10, stability_window=5, change_threshold=0.01, mask_update_interval=10, max_epochs_per_stage=None, score_mode='abs', ste_alpha=1.0, sparse_finetune_backend='dense', sparse_inference_backend='csr')[source]

Lottery-ticket compression settings for ELSATrainer.

Mask-search stages advance only when the proposed mask remains below change_threshold for stability_window mask updates. Once the final ticket is found, it is converted to an compresso.SRPParam and its values are trained for ELSAConfig.epochs. max_epochs_per_stage can force an unstable stage to accept its latest proposed mask; None leaves stability search unlimited. sparse_finetune_backend="dense" densifies only the selected SRP rows and uses dense matrix multiplication, while "coo" preserves sparse factors for lower-memory fine-tuning. sparse_inference_backend selects cached CSR or dense full-catalog scoring and can be overridden by each prediction call.

Parameters:
  • k_target (int)

  • k_schedule (tuple[int, ...] | None)

  • num_stages (int)

  • stability_window (int)

  • change_threshold (float)

  • mask_update_interval (int)

  • max_epochs_per_stage (int | None)

  • score_mode (Literal['abs', 'raw', 'relu'])

  • ste_alpha (float)

  • sparse_finetune_backend (Literal['dense', 'coo'])

  • sparse_inference_backend (Literal['csr', 'dense'])

Models and Trainer

class compresso_recsys.models.ELSA(input_dim, latent_dim, *, use_relu=True)[source]

Scalable linear shallow autoencoder with normalized item embeddings.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • input_dim (int)

  • latent_dim (int)

  • use_relu (bool)

normalized_item_embeddings()[source]

Return row-normalized item embeddings.

Return type:

Tensor

forward(x, *, sources=None, candidates=None, x_out=None)[source]

Score items, using candidate rows as the source prefix when given.

Return type:

Tensor

Parameters:
  • x (Tensor)

  • sources (Tensor | None)

  • candidates (Tensor | None)

  • x_out (Tensor | None)

class compresso_recsys.models.CompressedELSA(input_dim, latent_dim, compression, *, use_relu=True)[source]

ELSA item factors compressed to fixed row-wise sparsity.

The model starts with a dense compresso.MaskedParam. After its mask schedule is complete, convert_to_srp() replaces that parameter with an compresso.SRPParam whose structure is fixed and whose values remain trainable.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
train(mode=True)[source]

Set the module in training mode.

This has an effect only on certain modules. See the documentation of particular modules for details of their behaviors in training/evaluation mode, i.e., whether they are affected, e.g. Dropout, BatchNorm, etc.

Parameters:

mode (bool) – whether to set training mode (True) or evaluation mode (False). Default: True.

Returns:

self

Return type:

CompressedELSA

property is_sparse: bool

Whether the final fixed SRP structure has been installed.

normalized_item_embeddings(rows=None)[source]

Return normalized dense factors, optionally for selected rows.

Return type:

Tensor

Parameters:

rows (Tensor | None)

normalized_item_srp(rows=None)[source]

Return normalized sparse factors, optionally for selected rows.

Return type:

SRPTensor

Parameters:

rows (Tensor | None)

convert_to_srp()[source]

Install Compresso’s final fixed SRP parameter.

Return type:

None

prepare_inference(backend=None)[source]

Cache normalized factors for the selected inference backend.

Return type:

None

Parameters:

backend (Literal['csr', 'dense'] | None)

export_item_embeddings()[source]

Return a detached copy of the normalized final item factors.

Return type:

SRPTensor

forward(x, *, sources=None, candidates=None, x_out=None)[source]

Score candidates during mask search or sparse-value training.

Return type:

Tensor

Parameters:
  • x (Tensor)

  • sources (Tensor | None)

  • candidates (Tensor | None)

  • x_out (Tensor | None)

score_all_items(x, *, sources, backend=None)[source]

Score the full catalog with cached sparse or dense factors.

Return type:

Tensor

Parameters:
  • x (Tensor)

  • sources (Tensor)

  • backend (Literal['csr', 'dense'] | None)

class compresso_recsys.models.ELSATrainer(config=None, logger=None)[source]

Fit and run ELSA with sparse interaction matrices.

Parameters:
property is_built: bool

Whether the underlying ELSA model has been initialized.

property is_fitted: bool

Whether fit() has completed at least once.

property n_items: int | None

Number of fitted item columns, or None before building.

build(input_dim)[source]

Initialize the ELSA model and optimizer.

Return type:

ELSATrainer

Parameters:

input_dim (int)

train_step(x, sources, candidates)[source]

Run one optimization step.

Return type:

dict[str, Tensor]

Parameters:
  • x (Tensor)

  • sources (Tensor)

  • candidates (Tensor | None)

fit(interactions, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Fit dense ELSA or search and fine-tune a compressed ELSA ticket.

Return type:

ELSATrainer

Parameters:
  • interactions (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None, sparse_inference_backend=None)[source]

Predict ranked items for one source batch.

Seen source items are excluded unless exclude_seen is false. For compressed ELSA, sparse_inference_backend overrides the configured inference backend for this call.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • sparse_inference_backend (Literal['csr', 'dense'] | None)

predict(source, *, k=100, batch_size=None, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>, sparse_inference_backend=None)[source]

Predict ranked items for all source rows in batches.

Each batch delegates to predict_on_batch(). Seen source items are excluded unless exclude_seen is false. For compressed ELSA, sparse_inference_backend overrides the configured inference backend for every batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • batch_size (int | None)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

  • sparse_inference_backend (Literal['csr', 'dense'] | None)

Backend Performance

Sparse backends are not necessarily slower. In one representative benchmark with latent_dim=4096, their speed advantage disappeared between k_target=16 and k_target=32:

Relative throughput compared with the corresponding dense backend

k_target

COO fine-tuning

CSR inference

8

32% faster

9% faster

16

13% faster

4% faster

32

4% slower

2.6% slower

This table is illustrative rather than a universal selection rule. Sparse kernel overhead, device characteristics, batch size, candidate count, catalog size, and latent_dim all affect the crossover. Even above it, COO or CSR may be preferable because they avoid the much larger dense factor representation. For a new workload, benchmark both backends with the same trained ticket; sparse_inference_backend can be overridden per prediction call without retraining. Tiny metric differences between backends can occur because floating-point reductions use a different order.

Compressed ELSA uses Compresso’s exact MaskedParam.to_srp_param() conversion, which preserves the final selected mask even for tied or zero-valued entries. Compresso currently moves its initialization copy with the model, so mask search temporarily retains an additional dense factor buffer on the training device.

Cold-Start Models

ContentRecommender

ContentRecommender is a cold-start baseline that learns nothing. A user profile is the sum of the feature vectors of the items they interacted with, and candidates are ranked by similarity to that profile, so items are recommendable as soon as they are registered on the catalog. fit takes item features alone; unlike TEASER there is no encoder for an interaction matrix to train.

With its default configuration it reproduces the scoring in compresso_recsys.retrieval.evaluate_item_embeddings_with_holdout() exactly, which makes it the reference point for judging whether a learned cold-start model beats plain feature similarity on the same embeddings. That function is an ELSA-forward recommender fused with an evaluator rather than a neutral evaluator, so matching it needs L2-normalized item vectors, the self-subtraction and ReLU of ELSA-forward, and seen-item masking. normalize and elsa_forward expose the first and the middle two; masking always follows exclude_seen.

elsa_forward does not change the ranking while exclude_seen is set, because the self-subtraction only touches entries that masking then sets to -inf. It matters only when predicting with exclude_seen=False.

Every matrix product runs through torch, so device moves scoring onto a GPU; only the score matrix returns to the host.

class compresso_recsys.models.ContentRecommenderConfig(normalize=True, elsa_forward=True, device='cpu', dtype='float32')[source]

Configuration for ContentRecommender.

Parameters:
  • normalize (bool) – L2-normalize item feature vectors before scoring, making the profile/candidate product a cosine similarity instead of a raw dot product. Leaving this off lets high-norm items dominate the ranking.

  • elsa_forward (bool) – Subtract the user’s own interaction vector from the scores and apply ReLU, reproducing the ELSA-forward scoring used by evaluate_item_embeddings_with_holdout. This has no effect on the ranking when predicting with exclude_seen=True, since it only touches entries that seen-item masking then sets to -inf.

  • device (str) – Torch device for every matrix product, for example "cuda" or "mps". Only the final score matrix is copied back to the host.

  • dtype (Literal['float32', 'float64']) – Floating-point precision used for the stored features and all products.

class compresso_recsys.models.ContentRecommender(config=None)[source]

Cold-start baseline scoring items by content-feature similarity.

The model learns nothing. A user profile is the sum of the feature vectors of the items they interacted with, and candidates are ranked by their similarity to that profile. Because items are scored from features alone, unseen items are recommendable as soon as they are registered on the catalog.

>>> model = ContentRecommender(ContentRecommenderConfig(device="cuda"))
>>> model.fit(item_features, item_ids=item_ids)
>>> top = model.predict(source, k=20)

With the default configuration this reproduces the scoring in compresso_recsys.retrieval.evaluate_item_embeddings_with_holdout() exactly, so the same item embeddings yield the same metrics through either path. That function is an ELSA-forward recommender fused with an evaluator rather than a neutral evaluator, which is why normalize and elsa_forward exist at all.

Parameters:

config (ContentRecommenderConfig | None)

property is_fitted: bool

Whether the model is ready for prediction.

fit(item_features, *, item_ids=None)[source]

Publish item_features as both the source and candidate space.

Unlike TEASER, this model takes no interaction matrix. It holds no parameters and scores directly in feature space, so there is nothing to learn from user histories.

Return type:

ContentRecommender

Parameters:
  • item_features (csr_matrix | ndarray)

  • item_ids (Sequence[Hashable] | ndarray | None)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Predict ranked top-k items for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

TEASER

TEASER learns item-to-feature encoder weights from binary implicit interactions while keeping the supplied item-feature matrix fixed as its decoder. The reference implementation uses the original ADMM updates. It accepts item features as a SciPy CSR matrix, compresso.SRPTensor, NumPy array, or PyTorch tensor. Binary tags reproduce the original model, while real-valued dense or sparse embeddings provide the same fixed-decoder abstraction without necessarily retaining human-readable feature explanations.

TEASER separates two item spaces for production cold start. The source vocabulary is fixed at fit time: only items with fitted encoder rows may appear in user history. The candidate catalog is an immutable, versioned snapshot that can be rebuilt or updated without retraining. New candidates receive a decoder row from their features and can be recommended immediately, but cannot appear in source history until the model is retrained.

When an external interaction matrix uses a larger or differently ordered item space, align_source() projects it into the fitted source vocabulary by stable ID. The operation uses sparse CSR column indexing, never densifies user histories, and returns an already aligned matrix unchanged.

For an item cold-start split, pass the checkpoint’s warm_item_indices to fit. Only those interaction columns and feature rows participate in ADMM, but the decoder keeps feature rows for every item. Validation and test items can therefore be ranked from metadata without being treated as zero-valued training targets. Source histories must contain fitted training items.

Pass stable item_ids and optional aligned metadata to fit to build the initial catalog. update_candidates() appends new IDs and can replace existing rows; build_candidates() atomically replaces the entire catalog; and remove_candidates() removes IDs. When an embedding model supplies the features, set feature_space_id during fit so later updates can reject an explicitly different model or revision.

predict and predict_on_batch accept candidate_ids as a shared allowlist for the batch. They gather and score only those registered rows. The result remains an compresso.SRPTensor over the complete current catalog, so its columns can be resolved with ids_for(). Unknown and duplicate allowlist IDs are rejected.

The original solver forms a dense warm-item Gram matrix and eigendecomposes it, so fit memory grows quadratically and fit time cubically with the number of warm items. It also forms a dense feature Gram matrix. Sparse item features reduce input storage and can accelerate prediction, but do not remove those training costs. float64 is the parity-oriented default.

See Citing Compresso Recsys for the original TEASER paper.

class compresso_recsys.models.TEASERConfig(l2_coefficients=0.05, l2_encoder=0.05, rho=0.05, max_iterations=10, include_popularity=False, dtype='float64')[source]

Configuration for the reference ADMM implementation of TEASER.

Parameters:
  • l2_coefficients (float) – L2 regularization on the diagonal-free item coefficient matrix.

  • l2_encoder (float) – L2 regularization on the learned item-to-feature encoder.

  • rho (float) – Positive ADMM penalty parameter.

  • max_iterations (int) – Number of fixed ADMM iterations. The reference implementation uses 10.

  • include_popularity (bool) – Append normalized training-item popularity as an additional feature.

  • dtype (Literal['float32', 'float64']) – Numerical precision used by fitting and prediction. float64 matches the reference implementation.

class compresso_recsys.models.TEASER(config=None)[source]

Transparent and explainable aspect-space recommender.

TEASER learns an item-to-feature encoder from binary implicit interactions while keeping the supplied item-feature matrix fixed as its decoder. The ADMM implementation follows the original algorithm and supports warm-item training with metadata-only cold candidate items.

Parameters:

config (TEASERConfig | None)

property is_fitted: bool

Whether the encoder and fixed decoder have been fitted.

property dtype: dtype

NumPy dtype used by the model.

fit(interactions, item_features, *, train_item_indices=None, item_ids=None, metadata=None, feature_space_id=None, feature_names=None, show_progress=False)[source]

Fit the original TEASER objective with ADMM.

item_features must have one row per source item. When train_item_indices is supplied, only those item columns and feature rows participate in fitting. All supplied feature rows initialize the candidate catalog, allowing the remaining items to be scored cold. item_ids defaults to positional integer IDs for compatibility.

Return type:

TEASER

Parameters:
  • interactions (csr_matrix)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • train_item_indices (ndarray | Sequence[int] | None)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

  • feature_names (Sequence[str] | None)

  • show_progress (bool)

align_source(source, *, item_ids)

Align external sparse columns to the fitted source vocabulary.

Return type:

csr_matrix

Parameters:
  • source (csr_matrix)

  • item_ids (Sequence[Hashable] | ndarray)

build_candidates(*, item_ids, item_features, metadata=None, feature_space_id=None)

Atomically replace the complete candidate catalog.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

property candidate_item_ids: ndarray

Stable IDs in the current candidate snapshot.

classmethod load(path, *, device='cpu', load_optimizer=False)

Load a fitted, prediction-ready recommender on device.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • path (str | Path)

  • device (str | device)

  • load_optimizer (bool)

classmethod load_from_checkpoint(checkpoint_path, name, *, device='cpu', load_optimizer=False)

Load models/<name>.zip from a data checkpoint.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • checkpoint_path (str | Path)

  • name (str)

  • device (str | device)

  • load_optimizer (bool)

predict(source, *, k=100, batch_size=1024, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>)

Predict ranked top-k items for all source rows in batches.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • batch_size (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)

Predict ranked top-k items for one source batch.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

recommend(histories, *, k=100, exclude_seen=False, allowlist=None, blocklist=None, on_insufficient='truncate', logger=<inherit>, show_progress=<inherit>)

Recommend up to k item IDs for each item-ID history.

logger and show_progress override prediction reporting for this call. Passing logger=None makes the request quiet even when the recommender has a constructor logger.

Return type:

Recommendations

Parameters:
  • histories (Sequence[Sequence[Hashable]])

  • k (int)

  • exclude_seen (bool)

  • allowlist (Sequence[Hashable] | ndarray | None)

  • blocklist (Sequence[Hashable] | ndarray | None)

  • on_insufficient (Literal['truncate', 'raise'])

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

remove_candidates(item_ids, *, missing='error')

Remove registered candidates and publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • missing (Literal['error', 'ignore'])

save(path, *, include_optimizer=False)

Persist this fitted recommender as a safe, versioned ZIP checkpoint.

Return type:

None

Parameters:
  • path (str | Path)

  • include_optimizer (bool)

save_to_checkpoint(checkpoint_path, name, *, include_optimizer=False)

Save this model under models/<name>.zip in a data checkpoint.

Return type:

None

Parameters:
  • checkpoint_path (str | Path)

  • name (str)

  • include_optimizer (bool)

property source_item_ids: ndarray

Stable IDs accepted in recommendation histories.

to(device)

Move this recommender’s Torch state to device and return self.

Return type:

TypeVar(_PersistableT, bound= BasePersistableRecommender)

Parameters:
  • self (_PersistableT)

  • device (str | device)

update_candidates(*, item_ids, item_features, metadata=None, on_conflict='error', feature_space_id=None)

Add or update candidates and atomically publish a new snapshot.

Return type:

CandidateCatalog

Parameters:
  • item_ids (Sequence[Hashable] | ndarray)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • metadata (DataFrame | None)

  • on_conflict (Literal['error', 'replace', 'ignore'])

  • feature_space_id (str | None)

user_profiles(source)

Transform binary source histories into item-feature profiles.

Return type:

ndarray

Parameters:

source (csr_matrix)

TEASERGD

TEASERGD keeps TEASER’s fixed item-feature decoder but learns the encoder with PyTorch instead of the reference ADMM solver. It never materializes the dense item-by-item coefficient matrix. A batch first forms user feature profiles and then scores candidate feature rows, so model storage grows with warm_items * feature_dim rather than warm_items ** 2.

Training shares ELSA’s scalable controls: max_output retains every source item appearing in a batch as the candidate prefix and fills the remaining budget with sampled negatives, cosine-normalized reconstruction is optimized with NAdam or AdamW, and optional cosine learning-rate decay and torch.compile are available. diagonal_scale multiplies the self coefficient removed using (encoder * item_features).sum(-1) for each source item. The default 1.0 removes it completely, 0.0 leaves it untouched, and intermediate values subtract that fraction. No dense coefficient matrix is needed for the correction. Values below one intentionally relax TEASER’s anti-identity constraint and apply consistently during training and inference.

loss="normalized_mse" is the default and preserves the ELSA-style row-normalized reconstruction objective. loss="teaser" instead optimizes the original TEASER objective divided by the number of training users: per-user Frobenius reconstruction plus the complete off-diagonal coefficient norm and encoder norm. Dividing every term by the same constant does not change the minimizer. With sampled output candidates, negative reconstruction errors are importance-weighted to estimate the complete output error. Set use_relu=False with this mode for parity with the paper; enabling ReLU is an optional modification of the original model. Paper parity also requires diagonal_scale=1.0.

The encoder defaults to Xavier initialization. Set encoder_init="features" to initialize each warm encoder row from its fixed decoder feature row, making the initial coefficient matrix a scaled-diagonal variant of the metadata-similarity model S @ S.T. At the default diagonal_scale=1.0 its diagonal is removed. This is particularly useful for sparse SAE codes because active feature dimensions receive a meaningful signal before the first gradient update. Dense and CSR features are both supported; the CSR path fills the allocated encoder directly without constructing another dense feature matrix.

normalize_encoder=True applies row-wise L2 normalization to the effective encoder in every training and inference path, as ELSA does for its item factors. The underlying trainable parameter remains unnormalized. Explicit l2_encoder and optimizer weight_decay still regularize that underlying parameter, so start with both set to zero when evaluating encoder normalization.

The TEASER coefficient penalty is estimated from random off-diagonal item pairs and, when diagonal_scale < 1, matching sampled residual-diagonal entries. Its cost is coefficient_regularization_samples * feature_dim per batch; set the sample count to zero to disable it. TEASER loss mode scales the estimate to the full coefficient-matrix norm, while normalized-MSE mode preserves the previous mean penalty. exact_coefficient_squared_norm() is provided for diagnostics on small problems, but deliberately materializes the coefficient matrix and should not be used in large training loops.

Dense feature matrices are cached on the training device and indexed there. CSR feature matrices remain sparse for candidate scoring and only selected source rows are densified. Full candidate tensors are cached for prediction and invalidated when the catalog changes. TEASERGD uses the same stable-ID catalog, align_source, cold-candidate updates, metadata handling, and candidate allowlists as the ADMM implementation above.

See Citing Compresso Recsys for the original TEASER paper.

class compresso_recsys.models.TEASERGDConfig(batch_size=1024, max_output=None, epochs=1, lr=0.001, weight_decay=0.0, l2_coefficients=0.05, l2_encoder=0.05, coefficient_regularization_samples=4096, decay=False, compile=False, device='cpu', show_progress=True, seed=0, use_relu=True, include_popularity=True, optimizer='NAdam', loss='normalized_mse', encoder_init='xavier', normalize_encoder=False, diagonal_scale=1.0, log_prefix='TEASERGD', log_every_n_steps=1000)[source]

Configuration for gradient-trained TEASER.

loss="normalized_mse" preserves the ELSA-style objective, while loss="teaser" uses the original TEASER Frobenius reconstruction and regularization scale. max_output uses the same source-prefix candidate sampling as ELSA. In TEASER mode, sampled negatives are importance-weighted to estimate full-output reconstruction. coefficient_regularization_samples controls a Monte Carlo estimate of the effective coefficient norm; zero disables that term.

Parameters:
  • batch_size (int)

  • max_output (int | None)

  • epochs (int)

  • lr (float)

  • weight_decay (float)

  • l2_coefficients (float)

  • l2_encoder (float)

  • coefficient_regularization_samples (int)

  • decay (bool)

  • compile (bool)

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • use_relu (bool)

  • include_popularity (bool)

  • optimizer (Literal['NAdam', 'AdamW'])

  • loss (Literal['normalized_mse', 'teaser'])

  • encoder_init (Literal['xavier', 'features'])

  • normalize_encoder (bool)

  • diagonal_scale (float)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.TEASERGD(input_dim, feature_dim, *, use_relu=True, normalize_encoder=False, diagonal_scale=1.0)[source]

Trainable TEASER encoder with a fixed feature decoder.

The model represents the coefficient matrix as E @ S.T without constructing it. source_candidate_positions identifies each source item’s position in the candidate output so diagonal_scale can remove all or part of the diagonal contribution (E * S).sum(-1) exactly.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • input_dim (int)

  • feature_dim (int)

  • use_relu (bool)

  • normalize_encoder (bool)

  • diagonal_scale (float)

encoder_weights(rows=None)[source]

Return effective encoder rows, optionally normalized as in ELSA.

Return type:

Tensor

Parameters:

rows (Tensor | None)

forward(x, *, sources, source_features, candidate_features, source_candidate_positions)[source]

Score candidates and scale represented self-coefficient removal.

Return type:

Tensor

Parameters:
  • x (Tensor)

  • sources (Tensor)

  • source_features (Tensor)

  • candidate_features (Tensor)

  • source_candidate_positions (Tensor)

exact_coefficient_squared_norm(item_features)[source]

Return the exact effective coefficient squared norm.

Return type:

Tensor

Parameters:

item_features (Tensor)

class compresso_recsys.models.TEASERGDTrainer(config=None, logger=None)[source]

Fit TEASER with PyTorch, sampled outputs, and cold candidate catalogs.

Parameters:
property is_fitted: bool

Whether the model is ready for prediction.

fit(interactions, item_features, *, train_item_indices=None, item_ids=None, metadata=None, feature_space_id=None, feature_names=None, logger=<inherit>, show_progress=<inherit>)[source]

Fit the encoder while keeping item features fixed.

Return type:

TEASERGDTrainer

Parameters:
  • interactions (csr_matrix)

  • item_features (csr_matrix | SRPTensor | ndarray | Tensor)

  • train_item_indices (ndarray | Sequence[int] | None)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • metadata (DataFrame | None)

  • feature_space_id (str | None)

  • feature_names (Sequence[str] | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Predict one batch against the current or restricted catalog.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

predict(source, *, k=100, batch_size=1024, exclude_seen=True, candidate_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Predict all source rows while reusing one candidate tensor.

Return type:

SRPTensor

Parameters:
  • source (csr_matrix)

  • k (int)

  • batch_size (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

Sequential Models

Tokenizing Histories

compresso_recsys.ItemSequences holds catalog indices and nothing else — no padding, no special tokens, no length limit — because those are modelling decisions. Two components add them back, and they are separate on purpose.

compresso_recsys.models.ItemTokenizer owns the vocabulary: which token an item is, and what an item the model has never seen becomes. compresso_recsys.models.SequenceBatcher owns ragged-to-dense: how far back to read, how right padding forms a dense tensor, and which positions are real.

They are split because they have different lifetimes. A vocabulary is a property of the dataset and outlives any model. max_length is a property of the model — it is block_size under another name, and it sizes a transformer’s positional embedding — so one tokenizer can serve two models that read different amounts of history. Fusing them gives one object two owners, which shows up as the same number written into two configs and a runtime check to keep them honest.

Vocabulary layout puts the specials first:

0 .. n_reserved-1        specials -- named, or reserved for later
n_reserved .. vocab-1    catalog item i  ->  token  i + n_reserved

That ordering is chosen because catalog growth appends. Stage catalogs nest by prefix, a cold-start catalog grows by appending, and an incremental fit extends the embedding table at the end — where a cat splices the optimizer state correctly. Reserving ids at the back instead would place each new item exactly where the specials sit, turning every extension into a permutation of the parameter and its momentum; a wrong permutation attaches one item’s history to a special token without raising.

Front-loading has one cost: introducing a special later would shift every item. n_reserved removes it. Name the specials you use, reserve a few more, and a token added later lands in the reserve while every trained id stays put — for the price of a few embedding rows that never receive a gradient.

The offset stops at the tokenizer. A model’s head is n_items wide and indexed by catalog position, so predictions leave in the same space as the target matrix, the metrics and the item IDs, and nothing downstream of a model sees a token id. The one other place it appears is a next-item objective, which decodes its targets back with tokens - n_reserved.

An item outside the catalog becomes unk, keeping its position. That matters more than it sounds: a later split stage genuinely contains items the model was not fitted on, and dropping them instead would join their neighbours as though they had been consecutive. On a temporal MovieLens-1M split that fabricates 21% of all adjacencies across every row and costs about 9% of ndcg@20. A vocabulary built without unk cannot express such an item and raises rather than guesses.

SequenceBatcher defaults to right padding and also exposes the left padding SASRec requires. SimpleRNN and SimpleGPT explicitly require the default: for an RNN, the final-state helpers read each row before its trailing pads; for a causal transformer, real tokens cannot attend to padding that follows them, so training needs no padding mask — only a loss mask. Both trainers reject left padding before building a model because a raw GRU or LSTM would process the leading pad steps, while the transformer would need an additional key-padding mask and different target alignment.

max_length truncates to the most recent interactions, the only sensible direction, since a context window is a claim about recency rather than about where a history happened to start.

Those two reading helpers exist for the single easiest thing to get wrong. Under right padding the last column is padding for every row shorter than the batch maximum, so reading states[:, -1] silently scores most users from a pad embedding — and agrees with itself at batch_size=1, where every row fills its own batch, which is what lets the bug survive casual testing. Empty rows report position 0, which is padding; pair the position with has_history().

Neither component owns a training objective. A next-item shift, a masked-position target and sampled negatives differ between architectures, and a component with three mutually exclusive modes is not an abstraction. Nor does either apply corruption: injecting unk or selecting mask positions is stochastic, and keeping encode a pure function of its input is what lets a test assert that batching cannot change a prediction. Both live in trainers.

from compresso_recsys.models import ItemTokenizer, SequenceBatcher

tokenizer = ItemTokenizer(
    n_items=1085,
    special_tokens={"pad": 0, "unk": 1},
    n_reserved=4,                       # room for mask, cls, later
    item_ids=split["train_item_ids"],   # optional; enables the ID path
)
batcher = SequenceBatcher(tokenizer, max_length=200)

# A later stage may be wider than the tokenizer; unknown items become unk.
tokens, mask = batcher.encode(split["test_source_sequences"], device="cuda")
states = model(tokens)                        # (rows, length, hidden)
final = batcher.gather_final(states, mask)    # (rows, hidden)

Bring Your Own Vocabulary

compresso_recsys.models.ItemTokenizer is a convenience. What compresso_recsys.models.SequenceBatcher actually depends on is compresso_recsys.models.Tokenizer, a two-member structural protocol — pad_id and encode_indices — so a custom vocabulary qualifies by having them rather than by inheriting anything, exactly as compresso_recsys.models.Recommender works.

encode_indices must return one token per value, in order. The batcher computes each destination before it maps anything, so a vocabulary that expands one item into several tokens — semantic IDs from a residual-quantised autoencoder, say — cannot be used with it and needs its own batcher.

class compresso_recsys.models.Tokenizer(*args, **kwargs)[source]

What SequenceBatcher requires of a vocabulary.

Two members, because that is all the batcher reads: a per-value mapping, and something to fill the gaps between rows with. Structural, like Recommender, so a custom vocabulary satisfies it by having the members rather than by inheriting anything.

A model asks for more — vocab_size for its embedding rows, n_items for its head width, and unk_id or token_id if it injects corruption — but that is a per-model requirement rather than a library-wide one, so it is documented rather than declared.

encode_indices must be one token per value, in order. SequenceBatcher.encode() computes each destination from indptr before it maps anything, so a vocabulary that expands one item into several tokens — semantic IDs from a residual-quantised autoencoder, say — cannot be used with it and needs its own batcher.

class compresso_recsys.models.ItemTokenizer(n_items, *, special_tokens=None, n_reserved=None, item_ids=None)[source]

Maps catalog indices to token ids, and back.

Immutable. extended() returns a new tokenizer rather than growing this one, which is what makes “every existing token id is unchanged” checkable rather than promised.

special_tokens names the ids explicitly and they must be exactly 0 .. len - 1: no gaps, so n_reserved is unambiguous. "pad" is required because every batching path needs something to fill with. "unk" is optional, and its absence is a real choice — without it, an index the tokenizer cannot embed is an error rather than a token.

Nothing here interprets a name. "cls" may be named and receives an id like any other; whether a model emits it, or keeps its CLS purely as a prepended parameter, is the model’s business.

Parameters:
  • n_items (int)

  • special_tokens (Mapping[str, int] | None)

  • n_reserved (int | None)

  • item_ids (Sequence[Hashable] | np.ndarray | None)

property n_items: int

how many items can be embedded and scored.

Type:

Catalog size

property n_reserved: int

Token ids held for specials, and so the offset applied to items.

property vocab_size: int

the reserve plus the catalog.

Type:

Embedding rows needed

property special_tokens: Mapping[str, int]

Named special tokens and their ids, read-only.

token_id(name)[source]

Token id of a named special.

Return type:

int

Parameters:

name (str)

property pad_id: int

Token used to fill unused positions. Always present.

property unk_id: int | None

Token for an item outside the catalog, or None if not named.

None rather than an exception so a caller can branch on capability: a model without an unk slot genuinely cannot represent an unknown item, and should say so rather than guess.

property item_ids: ndarray | None

Stable IDs in catalog order, when the ID path is available.

property has_ids: bool

Whether encode_ids() and decode_ids() can be used.

encode_indices(values)[source]

Catalog indices to token ids, one for one, in order.

An index at or above n_items is an item this vocabulary cannot embed. That is the normal case rather than an error: checkpoint stages nest by prefix, so a model fitted on the training catalog reads a later stage’s indices directly and anything past its own count is simply an item that did not exist when it was fitted. Such values become unk_id, which keeps the history’s length and adjacency intact — dropping them instead would assert transitions that never happened.

Return type:

ndarray

Parameters:

values (ndarray | Sequence[int])

encode_ids(ids)[source]

Stable item IDs to token ids, one for one, in order.

Return type:

ndarray

Parameters:

ids (Sequence[Hashable] | ndarray)

encode(values)[source]

Encode indices or IDs, choosing by dtype.

Integer input is catalog indices, anything else is stable IDs. The two are distinguishable because ItemSequences.values is always integral and stored item IDs are always strings, and construction refuses integer item_ids so the ambiguous case cannot arise.

Return type:

ndarray

Parameters:

values (ndarray | Sequence[Hashable])

decode_indices(tokens)[source]

Token ids to catalog indices; every special becomes -1.

Return type:

ndarray

Parameters:

tokens (ndarray | Sequence[int])

decode_ids(tokens)[source]

Token ids to stable item IDs; a special becomes its own name.

Return type:

ndarray

Parameters:

tokens (ndarray | Sequence[int])

decode(tokens)[source]

Alias for decode_ids().

Unlike encode() this cannot choose: both decoders take token ids, so the input carries no signal and only the return type differs. Picking by whether item_ids happens to be present would make the return type depend on construction, silently, so this picks IDs and fails loudly when it has none.

Return type:

ndarray

Parameters:

tokens (ndarray | Sequence[int])

extended(*, n_items=None, item_ids=None)[source]

A tokenizer over a larger catalog, with every token id unchanged.

Give item_ids to grow by appending IDs, or n_items to grow by count alone. The catalog may only grow: shrinking would move nothing but would leave a trained model scoring items the vocabulary denies.

Return type:

ItemTokenizer

Parameters:
  • n_items (int | None)

  • item_ids (Sequence[Hashable] | ndarray | None)

to_dict(*, include_item_ids=True)[source]

A JSON-serialisable description, sufficient to rebuild this exactly.

item_ids are included by default because serving is the whole reason they exist, and a saved tokenizer that cannot resolve an ID defeats it. Pass include_item_ids=False when only the index path matters and the catalog is large enough for the size to.

Return type:

dict

Parameters:

include_item_ids (bool)

classmethod from_dict(state)[source]

Rebuild from to_dict().

Return type:

ItemTokenizer

Parameters:

state (Mapping[str, object])

class compresso_recsys.models.SequenceBatcher(tokenizer, max_length=None, padding='right')[source]

Encodes ragged histories into dense token tensors.

The tokenizer is positional because it is a collaborator rather than an option: without a vocabulary there is nothing to encode.

Padding defaults to the right, where a recurrent model can gather each row’s last real state and a causal model never lets a real token attend to the padding that follows it. padding="left" is the opt-in for models whose absolute positions have to be anchored to the newest interaction; it obliges the model to mask the leading pad steps out of attention itself, because causal masking no longer does it for free.

max_length truncates to the most recent interactions, the only sensible direction: a context window is a claim about recency, not about where a history happened to start.

padding chooses the side, and the two sides are not interchangeable. Right padding, the default, packs each batch to its own longest row and leaves a model to find each row’s last real state. Left padding instead fills every row to max_length, so the newest interaction always lands in the final column and an absolute position means “this far from the end” for every user alike – which is what a model with a learned positional table needs, and what SASRec’s published results assume. It costs the ragged packing: every batch is max_length wide however short its histories are, so max_length must be set for it.

Parameters:
  • tokenizer (Tokenizer)

  • max_length (int | None)

  • padding (Literal['left', 'right'])

property pad_id: int

Convenience passthrough, since every caller of encode() wants it.

encode(sequences, *, device=None)[source]

Return (tokens, mask) for a batch of histories.

tokens is (rows, length) of int64 holding token ids, padded with the tokenizer’s pad_id. mask is (rows, length) of bool, true where a token came from a history rather than from padding.

length is the longest history in the batch after truncation, floored at one so the shape stays usable when every row is empty.

Nothing here inspects the catalog. A history may span a wider item space than the tokenizer covers — which is the normal case for a later split stage — and the tokenizer decides what those values become.

Return type:

tuple[Tensor, Tensor]

Parameters:
truncated_lengths(sequences)[source]

History lengths after max_length is applied.

Return type:

ndarray

Parameters:

sequences (ItemSequences)

final_positions(mask)[source]

Index of each row’s last real token.

The state a model reads for prediction, and the single easiest thing to get wrong: with right padding the last column is padding for every row shorter than the batch maximum, so reading [:, -1] silently scores from a pad embedding — and agrees with itself at batch_size=1, where every row fills its own batch, which is what lets the bug survive.

Under left padding it is the last column for every row, since the real tokens are the suffix – the arithmetic below would instead point at the padding that precedes them.

Empty rows report a padding position either way. Pair this with has_history() rather than trusting the position alone.

Return type:

Tensor

Parameters:

mask (Tensor)

static has_history(mask)[source]

Whether each row carries any real token at all.

Return type:

Tensor

Parameters:

mask (Tensor)

gather_final(states, mask)[source]

Select each row’s last real state from (rows, length, dim).

Return type:

Tensor

Parameters:
  • states (Tensor)

  • mask (Tensor)

SimpleRNN

SimpleRNN is a GRU or LSTM trained on next-item cross entropy at every position, one training example per user. It is the smallest model that actually uses order, which makes it the baseline a transformer has to beat before its extra machinery has earned anything.

Training reads each history left to right and predicts the following item:

tokens   [a, b, c, PAD, PAD]      mask   [T, T, T, F, F]
inputs   [a, b, c, PAD]
targets  [b, c, PAD, PAD]         valid  [T, T, F, F]

Under right padding, mask[:, 1:] is exactly the set of positions whose target is a real item, so no arithmetic over lengths is needed and padding can never become a target. The head scores n_items rather than the full vocabulary: a special token is never a target, so an output column for it could only learn to be wrong, and a shift bug raises an index error instead of scoring plausibly.

A history retaining one interaction after truncation yields no training example, since a next-item target needs a preceding item. fit raises if truncation leaves every history this short. Such rows remain predictable, and an entirely empty history yields the state after a single pad — identical for every empty row, so effectively a learned prior.

history records the mean loss per epoch alongside the number of positions it was averaged over. That count is worth reading rather than assuming: it is sum(max(min(length, max_length) - 1, 0)), so it reports what truncation costs.

The trainer currently runs a fixed epoch budget and rebuilds the model on every fit call. It does not provide validation-based early stopping or incremental training. Tied embeddings and sampled softmax are also not implemented.

from compresso_recsys.evaluation import evaluate_recommender
from compresso_recsys.metrics import CalibratedRecall, NDCG
from compresso_recsys.models import (
    ItemTokenizer,
    SequenceBatcher,
    SimpleRNNConfig,
    SimpleRNNTrainer,
)

model = SimpleRNNTrainer(
    SimpleRNNConfig(
        rnn_type="gru",
        embedding_dim=64,
        hidden_dim=128,
        epochs=8,
        batch_size=256,
        lr=3e-3,
    ),
    # The window belongs to the encoder, not to the network.
    SequenceBatcher(ItemTokenizer(split["x_train_sequences"].n_items),
                    max_length=200),
).fit(split["x_train_sequences"])

result = evaluate_recommender(
    model,
    source=split["test_source_sequences"],
    targets=split["test_target_matrix"],
    metrics=[CalibratedRecall(20), NDCG(20)],
    batch_size=512,
)
class compresso_recsys.models.SimpleRNNConfig(rnn_type='gru', embedding_dim=128, hidden_dim=256, num_layers=1, dropout=0.0, unk_dropout=0.05, lr_schedule='constant', warmup_fraction=0.05, min_lr_ratio=0.1, batch_size=256, epochs=10, lr=0.001, weight_decay=0.0, optimizer='NAdam', device='cpu', show_progress=True, seed=0, log_prefix='SimpleRNN', log_every_n_steps=1000)[source]

Configuration for SimpleRNNTrainer.

dropout is applied to the states before scoring, and additionally between recurrent layers when num_layers > 1. A single-layer RNN has no between-layer position to apply it, which is PyTorch’s own behaviour rather than a choice made here.

unk_dropout replaces that fraction of input positions with the tokenizer’s unk token, teaching the model to read a history containing an item it cannot identify. It defaults to a non-zero rate because otherwise unk is never trained at all: the training vocabulary is the training window, so an out-of-catalog item cannot occur until evaluation, and its embedding would still sit at its initialisation when a quarter of a temporal test history turns out to need it.

The right rate tracks the out-of-catalog share the model will actually face, which is a property of the split rather than of the model: near zero under leave_last_out, and far higher on a late temporal stage. It is ignored when the tokenizer has no unk to substitute.

Parameters:
  • rnn_type (Literal['gru', 'lstm'])

  • embedding_dim (int)

  • hidden_dim (int)

  • num_layers (int)

  • dropout (float)

  • unk_dropout (float)

  • lr_schedule (Literal['constant', 'cosine'])

  • warmup_fraction (float)

  • min_lr_ratio (float)

  • batch_size (int)

  • epochs (int)

  • lr (float)

  • weight_decay (float)

  • optimizer (Literal['NAdam', 'AdamW'])

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.SimpleRNN(*, vocab_size, n_items, embedding_dim, hidden_dim, num_layers, dropout, rnn_type, pad_id)[source]

Embedding, recurrence, and a linear head over the catalog.

The head outputs n_items scores rather than vocab_size: special tokens are never prediction targets, so giving them output columns would train weights that can only ever be wrong.

forward() returns states and score() turns states into logits, kept separate because prediction needs logits at one position per row. Scoring first and gathering after would materialise rows x length x n_items, which on a real catalog is where the memory goes.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • vocab_size (int)

  • n_items (int)

  • embedding_dim (int)

  • hidden_dim (int)

  • num_layers (int)

  • dropout (float)

  • rnn_type (RNNType)

  • pad_id (int)

forward(tokens)[source]

Hidden states for every position, shape (rows, length, hidden).

Return type:

Tensor

Parameters:

tokens (Tensor)

score(states)[source]

Catalog logits for the given states, one score per item.

Return type:

Tensor

Parameters:

states (Tensor)

class compresso_recsys.models.SimpleRNNTrainer(config=None, batcher=None, logger=None)[source]

Trains and serves SimpleRNN.

Follows the package’s existing shape, where fit returns the trainer and the trainer answers the prediction contract:

model = SimpleRNNTrainer(SimpleRNNConfig(rnn_type="gru")).fit(
    split["x_train_sequences"]
)
result = evaluate_recommender(
    model, source=split["test_source_sequences"],
    targets=split["test_target_matrix"], metrics=[NDCG(20)],
)

The encoder is a parameter, not something fit invents. Passing one is how you change the context window or the vocabulary – including giving it an unk slot so a later split stage’s unseen items become a token rather than an error:

batcher = SequenceBatcher(
    ItemTokenizer(n_items, item_ids=split["train_item_ids"]),
    max_length=50,
)
model = SimpleRNNTrainer(SimpleRNNConfig(), batcher).fit(sequences)

Without one, fit builds a default over the training catalog with DEFAULT_MAX_LENGTH and right padding. A supplied batcher must also use right padding: leading padding would advance the recurrent state and turn the first real item into a target of padding.

Users retaining fewer than two interactions after truncation contribute no training example, since a next-item target needs a preceding item. fit refuses a dataset where that leaves no usable history. Short histories are still predictable: a history the model can read yields its state, and an empty history yields the state after a single pad, which is the same for every empty row and therefore a learned popularity-like prior.

history records one entry per epoch, numbered from one as ELSA’s is, carrying the mean loss and the number of positions it was averaged over. That count is worth reading rather than assuming: it is sum(max(min(length, batcher.max_length) - 1, 0)), so it shows what truncation costs. On MovieLens-1M at the default window of 200, 697 of 6,033 users exceed it and 80k of 543k training positions are dropped.

Parameters:
DEFAULT_MAX_LENGTH = 200

Context window used when fit has to build its own batcher.

property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of scoreable candidates, or None before fitting.

fit(sequences, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Train on chronological histories, one example per row.

Return type:

SimpleRNNTrainer

Parameters:
  • sequences (ItemSequences)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Rank the catalog for each history from its final recurrent state.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

SimpleGPT

SimpleGPT is a causal transformer over the same histories SimpleRNN reads. The architecture is nanoGPT — pre-norm blocks, fused QKV attention, a learned absolute position per slot — with the recommendation-shaped adjustments below, and it is the model compresso_recsys.models.ItemTokenizer and compresso_recsys.models.SequenceBatcher were split apart for.

A `CLS` prefix replaces the shift. Position 0 holds a learned vector, so states[:, i] has read CLS plus tokens[:, :i] and therefore predicts tokens[:, i]. The next-item alignment becomes a property of the input rather than arithmetic in the trainer:

tokens   [a, b, c, PAD]      mask   [T, T, T, F]
input    [CLS, a, b, c, PAD]
targets  [a,   b, c, PAD]    valid  [T, T, T, F]

Two things follow. Every real position is a target, where a left shift makes every position but the first one — so a history of a single interaction is a usable training example here, and CLS buys back one example per user. And an empty history has a defined input: it reads CLS alone and scores from the learned prefix, rather than from the state after reading one pad.

CLS is an nn.Parameter rather than a vocabulary entry, which is the more complicated of the two options and the deliberate one. A parameter can be conditioned — a user embedding or a global feature added into position 0 per row, as rstar does — and a vocabulary lookup cannot express that. Nothing in this library has user features yet, so today it does the job BOS would.

There is no attention mask. SimpleGPT requires its batcher to pad on the right, so a causal mask already excludes padding: a real token at position i attends only to <= i, all of which are real. Pad positions do compute garbage and nothing reads it — the loss is masked and prediction reads each row’s last real position. This invariant is why the attention module needs no padding-mask argument.

The head is tied to the input embedding, and scores the catalog rather than the vocabulary. Items occupy the last n_items rows of the vocabulary, so the output weight is a slice of the embedding and pad and unk fall below it — which is what we want, since neither is ever a prediction target. Tying halves the parameters and is the default; set tie_embeddings=False to use a separate output head. A tied head starts with a flatter softmax because nn.Linear initialises near ±1/sqrt(d_model) while an embedding starts at std=0.02, so tying can also change the training curve rather than only the parameter count.

Initialisation follows GPT-2, including the depth-scaled residual init. Every weight starts at std=0.02 — PyTorch’s nn.Linear default is roughly 2.5× wider at d_model=128 — and the two projections in each block that write into the residual stream start at 0.02 / sqrt(2 * n_layers) instead. Each block adds to the stream twice, so without that scaling its variance grows with depth and a deeper model starts further from anything usable.

A cosine learning-rate schedule is on by default. It gives linear warmup over warmup_fraction of the run, then cosine decay to min_lr_ratio × lr, with the final optimizer update using that floor. The curve is measured in optimizer steps so its shape does not move with batch size. Set lr_schedule="constant" to disable both. Warmup exists because the earliest steps of a transformer are the ones most able to wreck it — attention has learned nothing, so gradients are large and badly aimed. Neither half is expressible through the optimizer alone, which is why they arrive as one option rather than two.

The context window is derived, not configured. max_length on the batcher sizes the positional table, so SimpleGPTConfig carries no block_size and the two cannot disagree. The consequence is that max_length=None is an error for this model — learned absolute positions need a bound — which is a real difference from SimpleRNN, where the window only decides how much history is read.

from compresso_recsys.models import (
    ItemTokenizer,
    SequenceBatcher,
    SimpleGPTConfig,
    SimpleGPTTrainer,
    TransformerConfig,
)

tokenizer = ItemTokenizer(split["x_train_sequences"].n_items)
batcher = SequenceBatcher(tokenizer, max_length=200)   # required, and it
                                                       # sizes the positions

model = SimpleGPTTrainer(
    SimpleGPTConfig(
        transformer=TransformerConfig(
            d_model=128, n_heads=4, n_layers=2, dropout=0.1
        ),
        # Select the fixed budget on validation data.
        epochs=10,
        batch_size=128,
        lr=1e-3,
    ),
    batcher,
).fit(split["x_train_sequences"])

result = evaluate_recommender(
    model,
    source=split["test_source_sequences"],
    targets=split["test_target_matrix"],
    metrics=[CalibratedRecall(20), NDCG(20)],
)

The trainer currently runs a fixed epoch budget and rebuilds the model on every fit call. It does not provide validation-based early stopping or incremental training, so select epochs using validation data. Sampled softmax, a logit temperature, and pooling strategies other than reading the final real position are also not implemented.

Saving carries the vocabulary with the weights, because a served model that cannot say what column 41 means is not much use. It uses the same persistence API as every other fitted recommender:

from compresso_recsys.models import SimpleGPTTrainer

model.save("artifacts/simple_gpt.ckpt")
restored = SimpleGPTTrainer.load("artifacts/simple_gpt.ckpt")
class compresso_recsys.models.TransformerConfig(d_model=128, n_heads=4, n_layers=2, dropout=0.1, bias=False)[source]

The backbone, separated from the recommendation concerns around it.

A transformer has one width. Unlike SimpleRNNConfig, which lets embedding_dim and hidden_dim differ, the residual stream forces the embedding, the attention and the output of every block to share d_model — and n_heads must divide it, since each head takes an equal slice.

bias turns off the additive terms in the linear projections and the layer norms together. Off by default, following nanoGPT: it is slightly faster and marginally better, and having one flag rather than three keeps the combinations that were never tested from being expressible.

Parameters:
  • d_model (int)

  • n_heads (int)

  • n_layers (int)

  • dropout (float)

  • bias (bool)

property head_dim: int

Width of each attention head.

class compresso_recsys.models.SimpleGPTConfig(transformer=<factory>, tie_embeddings=True, lr_schedule='cosine', warmup_fraction=0.05, min_lr_ratio=0.1, unk_dropout=0.05, batch_size=256, epochs=10, lr=0.001, weight_decay=0.0, optimizer='NAdam', device='cpu', show_progress=True, seed=0, log_prefix='SimpleGPT', log_every_n_steps=1000)[source]

Configuration for SimpleGPTTrainer.

transformer carries the backbone; everything else is about training it. The context window is deliberately not a field — it belongs to the batcher, because it describes what the encoder reads rather than the shape of the network, and duplicating it is how the two drift apart. rstar carries it in both places and needs a runtime check to keep them equal.

tie_embeddings scores with the input embedding’s item rows instead of a separate head, halving the parameters. It is on by default; set it False to use an independent output projection.

Tying can change convergence as well as parameter count. nn.Linear initialises around +/-1/sqrt(d_model) while the embedding starts at std=0.02, so a tied head begins with a flatter softmax. Compare variants at independently validated budgets rather than assuming their training curves match.

unk_dropout replaces that fraction of input positions with the tokenizer’s unk token. Non-zero by default because otherwise unk is never trained at all: the training vocabulary is the training window, so an out-of-catalog item cannot occur until evaluation, and its embedding would still sit at initialisation when a quarter of a temporal test history needs it. Match the rate to the out-of-catalog share you expect — near zero under leave_last_out, far higher on a late temporal stage.

Parameters:
  • transformer (TransformerConfig)

  • tie_embeddings (bool)

  • lr_schedule (Literal['constant', 'cosine'])

  • warmup_fraction (float)

  • min_lr_ratio (float)

  • unk_dropout (float)

  • batch_size (int)

  • epochs (int)

  • lr (float)

  • weight_decay (float)

  • optimizer (Literal['NAdam', 'AdamW'])

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.SimpleGPT(*, vocab_size, n_items, max_positions, pad_id, config, tie_embeddings=True)[source]

Embeddings, a CLS prefix, causal blocks, and a linear head.

The head scores n_items rather than vocab_size: a special token is never a prediction target, so an output column for one could only ever learn to be wrong — and it would let a misaligned objective score plausibly instead of raising.

forward() returns states and score() turns states into logits, kept separate because prediction needs logits at one position per row. Scoring first would materialise rows x length x n_items, which on a real catalog is where the memory goes.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • vocab_size (int)

  • n_items (int)

  • max_positions (int)

  • pad_id (int)

  • config (TransformerConfig)

  • tie_embeddings (bool)

forward(tokens)[source]

States for CLS and every token, shape (rows, length + 1, d_model).

states[:, i] has read CLS and tokens[:, :i], so it is the state from which tokens[:, i] should be predicted.

Return type:

Tensor

Parameters:

tokens (Tensor)

score(states)[source]

Catalog logits for the given states, one score per item.

When tied, the weight is a slice of the embedding rather than its own parameter: pad and unk sit below item_offset and so stay out of the head, which is what we want anyway — neither is ever a target. Autograd carries the output-side gradient back into the item rows, so a tied embedding is trained from both directions.

Return type:

Tensor

Parameters:

states (Tensor)

class compresso_recsys.models.SimpleGPTTrainer(config=None, batcher=None, logger=None)[source]

Trains and serves SimpleGPT.

Follows the package’s shape, where fit returns the trainer and the trainer answers the prediction contract:

model = SimpleGPTTrainer(
    SimpleGPTConfig(transformer=TransformerConfig(d_model=128, n_heads=4)),
    SequenceBatcher(ItemTokenizer(n_items), max_length=200),
).fit(split["x_train_sequences"])

The encoder is a parameter, not something fit invents, which is how the context window and vocabulary are replaceable. Without one, fit builds a default over the training catalog with DEFAULT_MAX_LENGTH and right padding.

One property of that batcher is load-bearing rather than advisory, so fit refuses a batcher without it. max_length must be set because it sizes the positional table and learned absolute positions need a bound. This trainer requires right padding, which lets the causal mask stand in for a padding mask. A left-padded batcher is rejected before the model is built.

A history of a single interaction is a usable training example here, unlike for SimpleRNNTrainer — the CLS prefix supplies the context, so every position is a target rather than every position but the first.

history records one entry per epoch, numbered from one as ELSA’s is, carrying the mean loss and the number of positions it was averaged over.

Parameters:
DEFAULT_MAX_LENGTH = 200

Context window used when fit has to build its own batcher.

property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of scoreable candidates, or None before fitting.

fit(sequences, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Train on chronological histories, one example per position.

Return type:

SimpleGPTTrainer

Parameters:
  • sequences (ItemSequences)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Rank the catalog for each history from its last real state.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

SASRec

SASRec is a causal transformer over chronological histories, trained against sampled negatives under a binary objective rather than a softmax over the catalog. The network itself is deliberately plain – a couple of self-attention blocks over one shared residual width – and the choices worth knowing about are in the objective, in how a history is laid out for it, and in what the config does and does not let you move. See Citing Compresso Recsys for the paper this follows; SASRecConfig’s defaults are its MovieLens-1M settings. The Reproducing SASRec Results on ML1M notebook walks through the full MovieLens-1M experiment and both the paper’s sampled protocol and the package’s full-catalog protocol.

This is a modernized SASRec variant, not a line-for-line port. Its attention block uses the conventional PyTorch pre-norm form: one normalized stream supplies queries, keys, and values; the residual bypasses the complete attention module; and torch.nn.MultiheadAttention includes an output projection. The original TensorFlow model instead normalizes only the queries and supplies unnormalized keys and values, while its attention implementation adds the residual to those normalized queries and has no output projection. This package keeps SASRec’s sequential objective, tied item scoring, and experimental hyperparameters, but published paper results are contextual reference values, not an exact implementation-parity claim.

The objective is binary, not cross entropy. Each position scores its true next item and n_negatives sampled items, and each score is pushed toward one or zero independently. Nothing is normalised over the catalog, so the cost of a training step stops depending on catalog size – which is the property the model exists for. Where a softmax over the catalog builds a rows x length x n_items logit tensor per step, SASRec builds rows x length x (1 + n_negatives). One negative is enough at MovieLens scale. Raising it sharpens the gradient on a large catalog at a proportional cost.

Scoring is tied to the input embedding, and the tie is structural. A candidate is scored by the dot product of a state with that candidate’s input embedding, and there is no output head. SASRecConfig carries no tie_embeddings switch, because an untied model is a different model rather than this one configured differently. The scored weight is the catalog slice of the embedding table, so pad and unk sit below it and are never predictions.

A negative is drawn from outside the whole history, not merely past the positive. The draw is uniform over the catalog minus that row’s item set: an item the user interacted with earlier – or later, which the next-item shift makes just as reachable – is one they did engage with, so training the model to rank it below the target teaches the opposite of what the data says. The set is read from the row’s full sequence rather than from the window that happens to be retained, and excluding it subsumes excluding the position’s own positive, so no separate collision test is needed. The mapping avoids a rejection loop whose length would depend on the data: each draw is uniform over the allowed n_items - |S_u| slots and is then stepped onto the complement in one comparison. Two consequences follow. fit refuses a catalog of fewer than two items, and a history that covers the entire catalog leaves nothing to draw and raises rather than looping.

Inputs and targets are aligned by a left shift:

tokens    [PAD, a, b, c]     mask    [F, T, T, T]
input     [PAD, a, b]
positive  [a,   b, c]        valid   [F, T, T]

A position counts only when both ends are real. The step before the first real one has a real target sitting on a padded input, and “given padding, predict this” is not a lesson, so valid requires both masks rather than the target’s alone. It also drops any position whose positive is unk: “predict the item you cannot identify” is not a question with an answer.

The consequence is that a history needs two retained interactions to yield even one example. It is counted after truncation, since a long history whose retained tail is one item is no more trainable than a one-item history. Training encodes one interaction wider than the model’s window to pay for the shift: n + 1 interactions become n inputs and n targets, so a history that fills the window puts an input on every position the model owns. Prediction does not shift and reads at the model’s own window.

Padding is on the left, and every row is filled to the window. The newest interaction therefore always lands in the final column, so position n means “n from the end” for a user with twenty interactions and a user with two hundred alike. Under right padding, position 1 would instead mean “oldest item still retained” – a different anchor for every history length, leaving the highest positions trained only by the longest histories. It costs two things: batches are max_history_length wide however short their histories, and causal attention no longer excludes padding on its own, since the pad steps now precede the real ones and sit inside every causal window, so the model masks them out of attention explicitly. Positions are numbered from one and row 0 of the positional table is pinned to zero for padding steps. Prediction reads each row’s own last real state through gather_final().

Most of the architecture is fixed rather than configurable. d_model is one width for the whole residual stream – item embedding, positional embedding, attention output and feed-forward all share it – and n_heads must divide it. The feed-forward block is d_model -> d_model with a ReLU; the projections and norms carry their biases; layer norms use eps=1e-8; every matrix starts at Xavier normal and the embedding is rescaled by sqrt(d_model) to compensate, with the pinned padding rows re-zeroed afterwards. dropout is one rate applied to the embedding sum, inside attention, and between the feed-forward layers, because three independently tuned rates would be three numbers nobody has evidence for.

``unk_dropout`` is what trains the unknown-item embedding. It replaces that fraction of input positions with unk, never a positive, so a corrupted position teaches “an item was here that you cannot identify, predict the next one anyway” rather than costing a training example. It defaults to zero and is worth raising whenever evaluation will contain items training never saw: the training vocabulary is the training window, so an out-of-catalog item cannot occur until evaluation, and its embedding would still sit at its initialisation when a quarter of a temporal test history turns out to need it. The right rate tracks the out-of-catalog share the split will actually produce – near zero under leave_last_out, far higher on a late temporal stage. It is ignored when the tokenizer names no unk.

The context window is a config field, and it has a single owner. SASRecConfig.max_history_length sizes the positional table, which a checkpoint cannot grow after the fact, so the number lives there rather than on the batcher. It sizes the batcher fit builds when none was passed, and a batcher passed with max_length=None inherits it – the usual case, because the reason to hand fit a batcher is the vocabulary it carries rather than the window. A batcher that states a different window is refused instead of silently overruling the config or being overruled by it. Left padding is set by fit in the same way rather than asked of the caller.

The learning rate is constant. There is no schedule field: lr is flat for the whole run. betas defaults to (0.9, 0.98), shortening the window Adam’s variance estimate averages over – one sampled negative per position makes the gradient noisy between steps but not biased, and a longer window spends that noise on a stale scale instead of adapting through it. It reaches the optimizer through optimizer_kwargs(), since it is Adam’s hyperparameter and not a universal one.

from compresso_recsys.models import (
    ItemTokenizer,
    SASRecConfig,
    SASRecTrainer,
    SequenceBatcher,
)

tokenizer = ItemTokenizer(
    split["x_train_sequences"].n_items,
    item_ids=split["train_item_ids"],   # optional; enables the ID path
)
# No max_length: the window and the padding side come from the config.
batcher = SequenceBatcher(tokenizer)

model = SASRecTrainer(
    SASRecConfig(
        d_model=50,
        n_blocks=2,
        n_heads=1,
        dropout=0.2,
        # The context window lives here, not on the batcher.
        max_history_length=200,
        # Raise it on a large catalog.
        n_negatives=1,
        # Track the out-of-catalog share this split actually produces.
        unk_dropout=0.05,
        # Select the fixed budget on validation data.
        epochs=201,
        batch_size=128,
        lr=1e-3,
    ),
    batcher,
).fit(split["x_train_sequences"])

result = evaluate_recommender(
    model,
    source=split["test_source_sequences"],
    targets=split["test_target_matrix"],
    metrics=[CalibratedRecall(20), NDCG(20)],
)

Omitting the batcher is supported and builds a default one over the training catalog at the config’s window. Pass your own to supply a vocabulary – including one whose unk slot lets a later split stage’s unseen items become a reserved id rather than an error.

The trainer runs a fixed epoch budget and rebuilds the model on every fit call, so select epochs using validation data. There is no validation-based early stopping and no incremental training. Prediction forbids every item in the full history, truncated part included; items beyond the fitted catalog are dropped from that mask rather than clipped, since they were never scoreable.

Saving carries the vocabulary with the weights, through the same persistence API as every other fitted recommender:

from compresso_recsys.models import SASRecTrainer

model.save("artifacts/sasrec.ckpt")
restored = SASRecTrainer.load("artifacts/sasrec.ckpt")
class compresso_recsys.models.SASRecConfig(d_model=50, n_blocks=2, n_heads=1, dropout=0.2, max_history_length=200, n_negatives=1, unk_dropout=0.0, batch_size=128, epochs=201, lr=0.001, optimizer='Adam', betas=(0.9, 0.98), device='cpu', show_progress=True, seed=0, log_prefix='SASRec', log_every_n_steps=1000)[source]

Configuration for SASRec.

max_history_length is the context window, and this field owns it. It sizes the batcher fit builds when none was passed, and a batcher that was passed inherits it whenever that batcher’s own max_length is None – the usual case, because the reason to hand fit a batcher is the vocabulary it carries rather than the window. Stating the window in both places and disagreeing is an error rather than a silent win for either: it sizes the positional table, and a table that outlives the run cannot be built from a number the config does not know about.

It belongs here rather than on the trainer because the paper tunes it per dataset alongside dropout – 200 and 0.2 on MovieLens-1M, 50 and 0.5 on the sparse ones – so a dataset’s settings stay one object that a checkpoint records whole.

d_model is one width for the whole residual stream: the item embedding, the positional embedding, attention and the feed-forward output all share it, and n_heads must divide it. Unlike TransformerConfig, there is no bias switch – SASRec’s projections and norms carry their biases, and the feed-forward is d_model -> d_model with a ReLU rather than the 4x GELU block SimpleGPT uses. Those are the architecture, not options.

There is likewise no tie_embeddings. SASRec scores a candidate by the dot product of the final state with that item’s input embedding, so the tie is structural: an untied SASRec is a different model.

dropout is the paper’s single rate, applied to the embedding sum, inside attention, and between the feed-forward layers – one knob because the reference implementation exposes one, and three independently tuned rates would be three numbers nobody has evidence for.

n_negatives is how many sampled items each position scores against its true next item under the binary objective. One is the paper’s setting and is enough on MovieLens-scale catalogs; raising it sharpens the gradient on a large catalog at a proportional cost per step.

unk_dropout replaces that fraction of input positions with the tokenizer’s unk token, teaching the model to read a history containing an item it cannot identify. It defaults to zero for paper parity. Set it above zero when otherwise unk would never be trained: the training vocabulary is the training window, so an out-of-catalog item cannot occur until evaluation, and its embedding would still sit at its initialisation when a quarter of a temporal test history turns out to need it. The right rate tracks the out-of-catalog share the split will actually produce – near zero under leave_last_out, far higher on a late temporal stage. It is ignored when the tokenizer has no unk to substitute.

betas belongs to Adam and to no other optimizer, which is why it is applied through optimizer_kwargs() rather than passed unconditionally. The reference sets the second moment to 0.98 against PyTorch’s 0.999, shortening the window the variance estimate averages over – one sampled negative per position makes the gradient noisy between steps but not biased, and a longer window spends that noise on a stale scale instead of adapting through it.

The learning rate is deliberately constant: there is no schedule field, because the published results are a flat 0.001 for the whole run.

Parameters:
  • d_model (int)

  • n_blocks (int)

  • n_heads (int)

  • dropout (float)

  • max_history_length (int)

  • n_negatives (int)

  • unk_dropout (float)

  • batch_size (int)

  • epochs (int)

  • lr (float)

  • optimizer (Literal['Adam'])

  • betas (tuple[float, float])

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

optimizer_kwargs()[source]

Optimizer arguments beyond the parameters and lr.

betas is Adam’s own hyperparameter rather than a universal one, so it is selected by optimizer here instead of being handed to whatever torch.optim class the name resolves to. Today that name can only be Adam; the indirection is what keeps adding a second one from silently passing it an argument it does not take.

Return type:

dict[str, object]

class compresso_recsys.models.SASRec(*, n_items, n_reserved, max_history_length, pad_id, d_model, n_blocks, n_heads, dropout)[source]

Item and position embeddings, causal blocks, and a tied dot-product score.

This is a modernized SASRec variant, not a line-for-line port of the original TensorFlow attention block. Each block uses the conventional PyTorch pre-norm form: the normalized residual stream supplies queries, keys, and values; MultiheadAttention applies its output projection; and the result is added to the unnormalized residual stream. The reference code normalizes only the queries, uses the unnormalized stream for keys and values, adds its residual to the normalized queries, and has no attention output projection. The sequential objective, tied item scoring, and published MovieLens hyperparameters remain SASRec-derived, but published results are a point of comparison rather than exact implementation parity.

There is no output head. A candidate is scored by the dot product of a state with that candidate’s input embedding, which is what makes the tie structural rather than an option – see SASRecConfig.

forward() returns states; score() and score_items() turn states into scores, kept separate because the two callers want different widths. Training scores a handful of sampled items per position, while prediction scores the whole catalog at one position per row. Fusing them would materialise rows x length x n_items, which on a real catalog is where the memory goes.

The embedding table holds n_reserved + n_items rows: the reserved ids first, the catalog after them, so catalog item i lives at row i + n_reserved. That is ItemTokenizer’s layout, and taking n_reserved rather than a total keeps this module from having to work the split out for itself.

Padding is on the left, as the reference implementation has it, and fit configures the batcher for it. The reason is the positional table: every row is filled to max_length, so the newest interaction always lands in the final column and position n means “n from the end” for a user with twenty interactions and a user with two hundred alike. Under right padding position 1 would instead mean “oldest item still retained”, which is a different anchor for every history length and leaves the highest rows trained only by the longest histories.

It costs two things. Batches are max_length wide however short their histories, and causal masking no longer excludes padding on its own – the pad steps now precede the real ones and sit inside every causal window, so forward() masks them out of attention explicitly.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • n_items (int)

  • n_reserved (int)

  • max_history_length (int)

  • pad_id (int)

  • d_model (int)

  • n_blocks (int)

  • n_heads (int)

  • dropout (float)

forward(item_history)[source]

States for every step, shape (rows, length, d_model).

item_history is (rows, length) of embedding-row ids, left padded – what the batcher’s encode returns. states[:, i] has read item_history[:, :i + 1], so it is the state from which item_history[:, i + 1] should be predicted.

Return type:

Tensor

Parameters:

item_history (Tensor)

score(states)[source]

Catalog scores for the given states, one per item.

The weight is a slice of the embedding rather than its own parameter. The reserved rows – padding, and an unknown item if the tokenizer names one – sit below n_reserved and so stay out of the scores, which is what we want anyway: neither is ever a recommendation.

Return type:

Tensor

Parameters:

states (Tensor)

score_items(states, items)[source]

Score each state against specific items, for sampled negatives.

items holds embedding-row ids: (rows, length) to score one item per step, or (rows, length, n) for n of them, and the result carries the shape of items. Scoring a handful this way is the point of the binary objective – the full-catalog pass score() would give is the cost SASRec is avoiding.

Return type:

Tensor

Parameters:
  • states (Tensor)

  • items (Tensor)

class compresso_recsys.models.SASRecTrainer(config=None, batcher=None, logger=None)[source]

Trains and serves SASRec.

Follows the package’s existing shape, where fit returns the trainer and the trainer answers the prediction contract:

model = SASRecTrainer(SASRecConfig()).fit(split["x_train_sequences"])
result = evaluate_recommender(
    model, source=split["test_source_sequences"],
    targets=split["test_target_matrix"], metrics=[NDCG(20)],
)

The encoder is a parameter, not something fit invents. Passing one is how you change the vocabulary – including giving it an unk slot so a later split stage’s unseen items become a reserved id rather than an error. Leave its max_length unset and it inherits the config’s window, so the number stays in one place:

batcher = SequenceBatcher(
    ItemTokenizer(n_items, item_ids=split["train_item_ids"]),
)
model = SASRecTrainer(SASRecConfig(), batcher).fit(sequences)

The context window is SASRecConfig.max_history_length, so a shorter one is SASRecConfig(max_history_length=50) rather than a number written on the batcher. A batcher that does state its own max_length must agree with the config, and fit refuses the pair when they differ: the window sizes a positional embedding that cannot be extended at prediction time.

Hold the config and encoder; build nothing until fit.

Sets self.cfg, self.device, self.history, self.model, self.optimizer, self.batcher, self._owns_batcher and self._n_items, matching the two sibling trainers so the inherited persistence and to() paths find what they expect.

self._rng is one addition. Negative sampling draws from NumPy and _train_step’s signature is fixed by the loop that calls it, so the generator fit seeds reaches it as state rather than as an argument. It is deliberately not checkpointed: a reloaded model predicts, and a further fit reseeds from cfg.seed.

self._train_batcher is the other, and it exists because training reads one interaction more than the model has positions for – see _train_step(). fit derives it from self.batcher, so it is not checkpointed either: the window that a checkpoint records is the model’s, and a further fit derives this from it again.

Parameters:
checkpoint_type: ClassVar[str] = 'sasrec_trainer'

Context window used when fit has to build its own batcher.

property is_fitted: bool

Whether the model has been built and trained.

property n_items: int | None

Number of scoreable candidates, or None before fitting.

fit(sequences, *, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Train on chronological histories, one example per position.

Validates the input, builds a default batcher when none was supplied, checks it against the training catalog, records the item IDs, seeds Torch and NumPy from cfg.seed, builds the model, optimizer and scheduler, then runs cfg.epochs passes over shuffled rows and appends one entry per epoch to history.

Two of those validations are SASRec’s own. A history needs two retained interactions to yield even one shifted example, as SimpleRNNTrainer does and unlike SimpleGPTTrainer, whose CLS prefix makes a one-item history trainable. And the catalog needs two items, because a negative is drawn from the catalog minus the position’s own positive.

Rebuilds the model on every call: early stopping and incremental training are not part of this contract.

Return type:

SASRecTrainer

Parameters:
  • sequences (ItemSequences)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Rank the catalog for each history from its final state.

Resolves candidates, validates k, and returns an empty SRPTensor for an empty batch. Otherwise encodes the source, takes each row’s own last real state through the batcher’s gather_final – never states[:, -1], which is padding for every row shorter than the batch maximum – scores the catalog, masks seen items when asked, and takes the top k over the candidate columns.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)

SimpleBidirectionalTransformer

SimpleBidirectionalTransformer reads an ordered history with bidirectional, padding-aware self-attention and scores an unordered item set from its final CLS representation. It is the target-capable counterpart to SimpleGPT: the causal model learns next-token prediction inside a history, while this model can optimize the same target matrix used by evaluation.

The temporal split already produces the aligned pair expected by fit:

from compresso_recsys.evaluation import evaluate_recommender
from compresso_recsys.metrics import CalibratedRecall, NDCG
from compresso_recsys.models import (
    ItemTokenizer,
    SequenceBatcher,
    SimpleBidirectionalTransformerConfig,
    SimpleBidirectionalTransformerTrainer,
    TransformerConfig,
    WarmCatalogAdapter,
)

trainer = SimpleBidirectionalTransformerTrainer(
    SimpleBidirectionalTransformerConfig(
        transformer=TransformerConfig(
            d_model=128, n_heads=4, n_layers=2, dropout=0.1
        ),
        epochs=10,
        batch_size=128,
        lr=1e-3,
    ),
    SequenceBatcher(
        ItemTokenizer(
            split["train_source_sequences"].n_items,
            item_ids=split["train_item_ids"],
        ),
        max_length=200,
    ),
).fit(
    split["train_source_sequences"],
    targets=split["train_target_matrix"],
)

val_model = WarmCatalogAdapter(
    trainer,
    train_item_ids=split["train_item_ids"],
    catalog_item_ids=split["val_item_ids"],
)
val_result = evaluate_recommender(
    val_model,
    source=split["val_source_sequences"],
    targets=split["val_target_matrix"],
    metrics=[CalibratedRecall(20), NDCG(20)],
    sample_ids=split["val_eval_user_ids"],
)

test_model = WarmCatalogAdapter(
    trainer,
    train_item_ids=split["train_item_ids"],
    catalog_item_ids=split["test_item_ids"],
)
test_result = evaluate_recommender(
    test_model,
    source=split["test_source_sequences"],
    targets=split["test_target_matrix"],
    metrics=[CalibratedRecall(20), NDCG(20)],
    sample_ids=split["test_eval_user_ids"],
)

Temporal catalogs grow by appending newly observed items, whereas the fitted transformer can score only the training catalog. A separate compresso_recsys.models.WarmCatalogAdapter widens predictions to the validation or test catalog so their shape matches the corresponding target matrix. Histories remain unmodified: cold item indices reach the tokenizer as unk, while cold candidates remain valid metric targets but cannot be recommended by the training-only model.

targets must be a CSR matrix with exactly one row per sequence and the same item width. Its nonzero locations are binary membership; stored values are not weights. Duplicate entries and stored zeros are canonicalized. All-zero user rows stay in place to preserve alignment and are skipped by the loss. Training fails only when the complete matrix contains no positive target at all.

When targets=None, the trainer reconstructs the set of items in the source histories. Only this new trainer declares the targets keyword; sequential models that cannot use target sets, including SimpleGPTTrainer and SimpleRNNTrainer, continue to reject it instead of silently ignoring it.

Target-trained prediction deliberately keeps source items eligible even when exclude_seen=True. A post-cutoff target may repeat a pre-cutoff interaction, and a rated target may also occur in the viewed source stream, so masking the source would suppress a correct answer. The checkpoint records whether explicit targets were used and restores that behavior. Self-supervised fits retain the usual exclude_seen masking.

class compresso_recsys.models.SimpleBidirectionalTransformerConfig(transformer=<factory>, tie_embeddings=True, lr_schedule='cosine', warmup_fraction=0.05, min_lr_ratio=0.1, unk_dropout=0.05, batch_size=256, epochs=10, lr=0.001, weight_decay=0.0, optimizer='NAdam', device='cpu', show_progress=True, seed=0, log_prefix='SimpleBidirectionalTransformer', log_every_n_steps=1000)[source]

Architecture and training settings for the bidirectional trainer.

Parameters:
  • transformer (TransformerConfig)

  • tie_embeddings (bool)

  • lr_schedule (Literal['constant', 'cosine'])

  • warmup_fraction (float)

  • min_lr_ratio (float)

  • unk_dropout (float)

  • batch_size (int)

  • epochs (int)

  • lr (float)

  • weight_decay (float)

  • optimizer (Literal['NAdam', 'AdamW'])

  • device (str | device)

  • show_progress (bool)

  • seed (int)

  • log_prefix (str)

  • log_every_n_steps (int)

class compresso_recsys.models.SimpleBidirectionalTransformer(*, vocab_size, n_items, max_positions, pad_id, config, tie_embeddings=True)[source]

Item embeddings, bidirectional blocks, and a catalog-scoring head.

Initialize internal Module state, shared by both nn.Module and ScriptModule.

Parameters:
  • vocab_size (int)

  • n_items (int)

  • max_positions (int)

  • pad_id (int)

  • config (TransformerConfig)

  • tie_embeddings (bool)

forward(tokens, mask)[source]

Return states for CLS and each token.

Return type:

Tensor

Parameters:
  • tokens (Tensor)

  • mask (Tensor)

score(states)[source]

Turn one or more hidden states into catalog logits.

Return type:

Tensor

Parameters:

states (Tensor)

class compresso_recsys.models.SimpleBidirectionalTransformerTrainer(config=None, batcher=None, logger=None)[source]

Train a bidirectional sequence encoder against unordered item sets.

Parameters:
property is_fitted: bool

Whether the model is ready for prediction.

property n_items: int | None

Number of scoreable candidates, or None before fitting.

property trained_with_explicit_targets: bool

Whether the most recent fit used a separate target matrix.

fit(sequences, *, targets=None, item_ids=None, logger=<inherit>, show_progress=<inherit>)[source]

Train on source histories and optional explicit target sets.

Return type:

SimpleBidirectionalTransformerTrainer

Parameters:
  • sequences (ItemSequences)

  • targets (csr_matrix | None)

  • item_ids (Sequence[Hashable] | ndarray | None)

  • logger (Any | None)

  • show_progress (bool | None | _Inherit)

predict_on_batch(source, *, k, exclude_seen=True, candidate_ids=None)[source]

Rank catalog items from the bidirectional CLS representation.

Return type:

SRPTensor

Parameters:
  • source (ItemSequences)

  • k (int)

  • exclude_seen (bool)

  • candidate_ids (Sequence[Hashable] | ndarray | None)