Skip to main content

Calibrated recommendations via sparse-autoencoder steering and top-k (D'Hondt) aggregation.

Project description

CALISSTA

Calibrated recommendations, in the retrieval phase. Make a recommender's output match a target mix of concepts (genres, tags, categories) — 60% drama, 30% comedy, 10% action — by steering a sparse autoencoder, not by expensive post-hoc re-ranking.

PyPI CI License: MIT Paper OSF

Most calibration methods re-rank a large candidate pool after the fact — accurate but slow. calissta calibrates during candidate retrieval: it puts a sparse autoencoder (SAE) on your recommendation backbone, steers the user's sparse embedding toward each target concept (e.g. genre), decodes one candidate list per concept, and interleaves them with D'Hondt proportional allocation. In the paper this was ~an order of magnitude faster than re-ranking calibration, at comparable or better calibration and relevance.

Algorithm: Patrik Dokoupil, Ludovico Boratto & Ladislav Peska (UMAP '26, see Citing). This implementation: Patrik Dokoupil. Full paper reproduction (training, all datasets, ablations) lives on OSF; this package is the streamlined, install-able library.

Install

pip install calissta                 # core — NumPy only, bring your own backbone/SAE
pip install "calissta[torch]"        # + reference ELSA backbone & SAE, from_pretrained
pip install "calissta[examples]"     # + everything to run the ML25M example (polars, …)
Extra Adds For
(none) NumPy the algorithm with your own models
[torch] torch, huggingface_hub reference ELSA/SAE, from_pretrained
[examples] + polars, pandas, scipy running the ML25M example & retraining scripts

Zero-setup on MovieLens-25M

With calissta[torch] and a published bundle (checkpoints + concepts.npz), one call gets you a ready calibrator:

from calissta import CalisstaCalibrator

cal = CalisstaCalibrator.from_pretrained("path/to/bundle")   # local dir or a HF repo id
recs = cal.recommend(user_history, target_distribution=[0.6, 0.3, 0.1], alpha=0.5, k=10)

(Build a bundle from your own trained models with examples/build_ml25m_bundle.py.)

Quick start

You bring three things: a backbone (any recommender that encodes a user to a dense embedding and decodes back to item scores), an SAE on top of it, and your dataset's concepts. Then ask for a calibrated list:

from calissta import CalisstaCalibrator, Concepts
from calissta.steering import fit_steering_vectors

# concepts: which items belong to which genre/tag/category, + a steering direction each
s_g = fit_steering_vectors(item_sparse_embeddings, item_concept_matrix)   # (n_concepts, sae_dim)
concepts = Concepts(concept_names, item_concept_matrix, s_g)

cal = CalisstaCalibrator(backbone, sae, concepts)
recs = cal.recommend(
    user_history,                      # item indices the user interacted with
    target_distribution=[0.6, 0.3, 0.1],
    alpha=0.5,                         # calibration strength in [0, 1]
    k=10,
)

python examples/quickstart.py runs the whole pipeline on toy identity models (pure NumPy, <1 s) and shows the recommended list's concept mix tracking the target:

target D_u          ->  achieved concept mix over the top-10
  [0.6, 0.3, 0.1]   ->  [0.6 0.3 0.1]

Bring your own model (it's model-agnostic)

The backbone and SAE are just two tiny protocols (calissta.interfaces) — encode/decode. The paper uses ELSA + an SAE, but anything works (matrix factorization, a neural recommender, …) as long as small embedding perturbations don't wreck its output. There is no hard PyTorch dependency; the reference ELSA/SAE ship in the [torch] extra, everything else is NumPy.

class Backbone:            # yours
    def encode(self, history): ...   # -> dense user embedding
    def decode(self, dense):   ...   # -> scores over all items

class SparseAutoencoder:   # yours
    def encode(self, dense):          ...   # -> (sparse, context)
    def decode(self, sparse, context): ...  # -> dense

Reference models (running on real data)

Don't want to wire up your own? calissta[torch] ships clean implementations so you can run the whole thing:

from calissta.models import ELSA, ELSABackbone, TopKSAE, SAEAdapter, item_sparse_embeddings_ub

backbone = ELSABackbone(trained_elsa)       # ELSA scoring incl. ReLU(recon − interactions)
sae      = SAEAdapter(trained_topk_sae)
s_g      = fit_steering_vectors(item_sparse_embeddings_ub(trained_elsa, trained_sae), M)  # UB source

examples/train_and_calibrate.py trains ELSA + a Top-k SAE on synthetic data and calibrates end-to-end in a few seconds.

Attribution. Neither model is CALISSTA's contribution — please cite them if you use them. ELSA: Vančura et al., Scalable Linear Shallow Autoencoder for Collaborative Filtering, RecSys 2022. SAE-for-CF: Spišák et al., From Knots to Knobs: Towards Steerable Collaborative Filtering Using Sparse Autoencoders (arXiv:2601.11182, 2026), building on the k-sparse autoencoder (Makhzani & Frey 2013; Gao et al. 2024).

Evaluation & the ML25M example

calissta.metrics provides the paper's metrics: genre_exposure_error (GEE — the calibration metric: MAE between target and achieved concept exposure) and ndcg_at_k (relevance). Both are pure NumPy and take a produced recommendation list.

examples/genre_calibration_ml25m.py demonstrates the calibration/relevance trade-off on MovieLens-25M with the paper's checkpoints — Genre Exposure Error falls as calibration strength rises, at a relevance cost (editorial scenario, illustrative 300-user sample; numbers vary with the sample):

alpha GEE ↓ nDCG@10
0.0 0.27 0.34
0.6 0.19 0.28
0.9 0.17 0.26

This mimics the experiment; it is not an exact reproduction (different sample / seed / split). For the paper's exact numbers and full reproduction use OSF. Get the models from OSF or retrain with the paper's config (examples/train_elsa.py, examples/train_sae.py); load_elsa/load_sae read both. See examples/README.md for the runbook.

Any dataset (it's concept-agnostic)

Calibration concepts are described entirely by the Concepts object: an item_matrix M (n_concepts × n_items, which items are in which concept) and a steering vector per concept. Genres for movies, tags for music, categories for products — same code, different M.

fit_steering_vectors(item_sparse_embeddings, M) builds each concept's contrastive direction — normalize(mean(items in concept) − mean(items not in concept)) — and orthogonalizes them. The item sparse embeddings can be user-based (UB) — ELSA item factors projected through the SAE encoder (what the paper used) — or item-based (IB) — one-hot items pushed through ELSA→SAE; you compute whichever and pass it in.

What you can tune

  • alpha — global calibration strength in [0, 1].
  • alpha_mapping — how alpha becomes per-concept: "discounted" (default; the paper's correction that steers rare concepts more and dominant ones less), "fixed", "proportional", "multiplied".
  • steer_mode"direction" (default; e + α_g·‖e‖·s_g, the direction_orthogonalized setting the paper's experiments used) or "convex" ((1-α)e + α·s_g, Algorithm 1's pseudocode form).
  • n_candidates, k, exclude_seen.

How it works

Steering happens in the SAE's sparse, largely monosemantic space, so pushing toward a concept is a clean, local edit. Each concept yields its own candidate list; D'Hondt then fills k seats so the concept composition matches D_u. See the paper for the algorithm, the history-aware alpha correction, complexity analysis, and experiments.

Citing

If you use this software, please cite the paper (GitHub's "Cite this repository" reads CITATION.cff):

@inproceedings{dokoupil2026calissta,
  author    = {Dokoupil, Patrik and Boratto, Ludovico and Peska, Ladislav},
  title     = {CALISSTA: Calibrated Candidate Retrieval Using Sparse Autoencoders and Top-k Aggregation},
  booktitle = {Proceedings of the 34th ACM Conference on User Modeling, Adaptation and Personalization},
  series    = {UMAP '26}, pages = {345--350}, year = {2026},
  doi       = {10.1145/3774935.3806154}
}

License & attribution

The calissta package is MIT-licensed — see LICENSE. That covers our code: the calibrator, steering, D'Hondt aggregation, metrics, and the reference-model code under calissta.models.

The examples/ and reference models build on others' methods and data — our code is an original (MIT) reimplementation, but please credit the sources:

  • ELSA backbone — Vančura et al., Scalable Linear Shallow Autoencoder for Collaborative Filtering, RecSys 2022.
  • SAE-for-CF — Spišák et al., From Knots to Knobs: Towards Steerable Collaborative Filtering Using Sparse Autoencoders, arXiv:2601.11182 (2026); k-sparse SAE: Makhzani & Frey 2013, Gao et al. 2024.
  • MovieLens-25M (genre-calibration example) — GroupLens; not redistributed here (the download script fetches it from GroupLens under their license).

Reproducibility artifacts (trained checkpoints, full ablations) are on OSF.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

calissta-0.1.0.tar.gz (29.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

calissta-0.1.0-py3-none-any.whl (23.3 kB view details)

Uploaded Python 3

File details

Details for the file calissta-0.1.0.tar.gz.

File metadata

  • Download URL: calissta-0.1.0.tar.gz
  • Upload date:
  • Size: 29.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.12

File hashes

Hashes for calissta-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c3678b537e775ed080bfe6f08a6a0cc572e22829a8b91e6f92383088469a0c62
MD5 155b8c03ff900d9eda1fae4eb2495c1d
BLAKE2b-256 067ee3b82029a7c7ba5ba391c6dec7149dae7af29c84bf9a9987c41ff0efb0a4

See more details on using hashes here.

File details

Details for the file calissta-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: calissta-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 23.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.12

File hashes

Hashes for calissta-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ff7a031619b3304f36daadb731b68df2f6c5c8293c97582e8fed22ac4d223750
MD5 d50092949a4c905e2e5a96a8d7e9387d
BLAKE2b-256 b7f365dbee1a0351a4de1e62ec16fcdceb15a7ce86c979e1e90d22379e51ef8e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page