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.
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— howalphabecomes 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, thedirection_orthogonalizedsetting 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3678b537e775ed080bfe6f08a6a0cc572e22829a8b91e6f92383088469a0c62
|
|
| MD5 |
155b8c03ff900d9eda1fae4eb2495c1d
|
|
| BLAKE2b-256 |
067ee3b82029a7c7ba5ba391c6dec7149dae7af29c84bf9a9987c41ff0efb0a4
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff7a031619b3304f36daadb731b68df2f6c5c8293c97582e8fed22ac4d223750
|
|
| MD5 |
d50092949a4c905e2e5a96a8d7e9387d
|
|
| BLAKE2b-256 |
b7f365dbee1a0351a4de1e62ec16fcdceb15a7ce86c979e1e90d22379e51ef8e
|