Ary2Tower: Two-Tower Neural Recommendation

Aryanto
August 22, 2026
4 min read

Purpose

Ary2Tower learns separate user and item representations and compares the resulting tower outputs in a shared vector space. It is useful when a learned continuous representation can capture interaction structure better than a pure sparse-CF model.

The Python-facing package is:

src/cooprecsys/models/ary2tower/
├── CLtowers/       # compiled Cython/OpenMP kernels
├── inout/          # lower-level architect, predictor, fallback logic
├── narative/       # training/inference report rendering
├── viztower/       # embedding/metric visualizations
├── a2tcysetup.py   # native extension build entry point
├── config.py
├── towers.py
├── inference.py
├── report.py
└── trainer.py

Architecture

The tower shape is intentionally simple:

entity id

embedding lookup

dense layer + ReLU

final dense layer

user/item representation

dot-product score

TwoTowerConfig controls embedding_dim, hidden_dim, output_dim, learning rate, momentum, epoch count, native thread count, verbosity, and random seed.

Training

TwoTowerTrainer.fit() accepts a sparse interaction matrix. The DataFrame adapter fit_dataframe() is useful when the input starts as pandas-style transaction rows with configurable user/item columns.

from cooprecsys.models.ary2tower import TwoTowerTrainer, TwoTowerConfig

trainer = TwoTowerTrainer(
    n_users=n_users,
    n_items=n_items,
    config=TwoTowerConfig(
        embedding_dim=32,
        hidden_dim=64,
        output_dim=16,
        learning_rate=0.01,
        momentum=0.9,
        n_epochs=10,
        num_threads=4,
        random_state=42,
    ),
)
trainer.fit(interactions)
trainer.save_model('artifacts/models/ary2tower.npz')

When the compiled extensions are present, the training path calls the native fit_two_tower kernel. The NumPy path remains available as a portability fallback.

Native kernels

CLtowers/ contains the compiled components used by the Python layer:

_cy_types
_cy_forward
_cy_predict
_cy_similarity
_cy_train

The development build entry point is:

python ./src/cooprecsys/models/ary2tower/a2tcysetup.py build_ext --inplace

On Windows PowerShell or Unix, the helper scripts in the same directory wrap this operation. The important detail is that the build occurs inside models/ary2tower’s Cython package, so Python later imports the extension modules from CLtowers rather than from an unrelated build directory.

For a published wheel, consumers should not run the Cython setup script; install the wheel instead.

Inference and the top-N contract

TwoTowerInference.predict() scores explicit (user_id, item_id) pairs. recommend() is the serving-oriented top-N API.

from cooprecsys.models.ary2tower import TwoTowerInference

inference = TwoTowerInference(
    'artifacts/models/ary2tower.npz',
    num_threads=4,
    purchase_data=purchases,
)

recommendations = inference.recommend(
    user_id=7,
    n_items=10,
    exclude_purchased=True,
)

The current recommendation path is designed to avoid the old shortfall pattern:

old: score a tiny top-k → exclude purchased → return too few items

The current path is:

score the eligible catalogue → rank → exclude/validate → fill residual slots

As a result, the API returns exactly the requested number of unique, unseen items whenever that many eligible catalogue items exist.

Modern residual fallback

When the model path still leaves a shortfall, TwoTowerFallBack fills the missing slots with a global prior rather than item-item similarity.

The current prior is based on:

  • weighted interaction counts;
  • Bayesian-style shrinkage toward the mean positive item interaction level;
  • log1p score compression;
  • optional recency decay using a detected timestamp, event_time, created_at, datetime, or date column.

The effective idea is:

raw interaction evidence

optional time decay

weighted item counts

prior shrinkage

log-compressed fallback score

fill only the remaining recommendation slots

No item-to-item cosine filtering is used in this residual path.

The important contract is that the fallback never duplicates model candidates or purchased items and only returns fewer than n_items when fewer than n_items eligible unique catalogue items actually exist.

Cython vs NumPy backend

The same Python API is intentionally available on both paths. The compiled path uses OpenMP-aware Cython kernels; the NumPy path keeps the package functional in environments where native compilation is unavailable.

Use backend diagnostics when troubleshooting a deployment:

from cooprecsys.models.ary2tower.towers import backend_info
print(backend_info())

For production latency-sensitive inference, validate that the expected compiled backend is actually loaded rather than assuming that a successful .pyx build automatically means the runtime is using it.

Reporting and visualization

report.py generates a self-contained HTML-oriented model report, while viztower/ provides embedding, metrics, and performance visualizations. The narative/ tree contains the HTML templates/static assets used by training and inference reports.

Choosing Ary2Tower

Choose Ary2Tower when:

  • you have stable user/item IDs and enough interactions to learn embeddings;
  • dense representations are valuable;
  • you want compiled scoring/training hot paths;
  • you need a serving API that can enforce a strict top-N contract with a residual fallback.

Use AryColBring when the problem is better expressed as sparse collaborative filtering, and use LTR-LightGBM when tabular feature richness and query-group ranking dominate.

Last updated on August 22, 2026

Was this article helpful?

Your response is saved on this device.