Skip to main content
Nori’s encoder turns every row into a dense vector — a learned representation that captures how the row relates to the target, conditioned on the rest of your table. You can pull those vectors out and use them for anything you’d use features for: a downstream model (kNN, linear probe, gradient boosting), clustering, nearest-neighbor search, visualization, or meta-learning. Because NoriRegressor is a scikit-learn estimator, embedding extraction plugs straight into the sklearn ecosystem. There are two entry points:
  • NoriEmbedding — an sklearn transformer. With n_fold >= 2 it produces out-of-fold (leak-free) embeddings for the training rows. Recommended.
  • NoriRegressor.get_embeddings — the low-level call, when you want direct control over which rows are embedded against which context.
Nori is regression-only, so embeddings are conditioned on a continuous target.

Install

pip install synthefy-nori
Extracting embeddings and fitting downstream heads needs only the base install (scikit-learn is a core dependency). The t-SNE visualization and the TabArena example below also need matplotlib (and openml for TabArena), which come with the eval extra:
pip install "synthefy-nori[eval]"
Model weights download automatically from Hugging Face on first use — no API key needed. Nori uses a GPU if one is available, otherwise CPU.

The interface

NoriEmbedding is a scikit-learn transformer with two methods:
  • fit_transform(X_train, y_train) — returns embeddings for the training rows. Out-of-fold when n_fold >= 2 (see below).
  • transform(X) — returns embeddings for unseen rows, always from the final model fit on the full training set.
transform never returns cached training embeddings, even if the input happens to equal the training set. For training-row embeddings use fit_transform (or read train_embeddings_).
Both return a 3-D array of shape (n_estimators, n_samples, embed_dim), where n_estimators is the number of preprocessing pipelines in the inference ensemble. Average over the ensemble axis (or pick a single member) for a 2-D matrix a downstream estimator can consume:
Z_train = train_emb.mean(axis=0)   # (n_samples, embed_dim)
# or: Z_train = train_emb[0]
With n_fold >= 2, fit_transform runs K-fold cross-validation: each training row is embedded by a fold model that did not have it in its context, so the embedding never leaks that row’s own label. This matters whenever you fit a downstream model on the training embeddings — leak-free embeddings give an honest estimate of downstream performance (arXiv:2502.17361). Unseen rows are then embedded by a single model fit on the full training set.
from synthefy_nori import NoriEmbedding, NoriRegressor

embedder = NoriEmbedding(n_fold=5, shuffle=True, random_state=0)
train_emb = embedder.fit_transform(X_train, y_train)   # out-of-fold (leak-free)
test_emb  = embedder.transform(X_test)                 # final full-data model

Z_train = train_emb.mean(axis=0)   # (n_train, embed_dim)
Z_test  = test_emb.mean(axis=0)    # (n_test,  embed_dim)
KFold is used for the splits; pass shuffle=True with a random_state to shuffle. To reuse a specific configuration (e.g. skip compilation for many small fold-fits), hand NoriEmbedding a pre-built estimator:
embedder = NoriEmbedding(
    n_fold=5, shuffle=True, random_state=0,
    model=NoriRegressor(compile_model=False),
)

Vanilla embeddings

With n_fold=0, a single model is fit on the entire training set and used for both train and unseen rows. It’s cheaper — one fit instead of n_fold + 1 — but the training embeddings encode their own labels, so don’t fit a downstream model on them and expect an unbiased score.
embedder = NoriEmbedding(n_fold=0)
train_emb = embedder.fit_transform(X_train, y_train)   # not leak-free

The low-level call

NoriRegressor.get_embeddings(X, data_source=...) embeds rows against the context stored by fit. data_source selects which rows come back:
  • "test" (default) — embed the query rows X.
  • "train" — embed the stored context rows. X is ignored here (the representations depend only on what you passed to fit), so you can omit it.
from synthefy_nori import NoriRegressor

model = NoriRegressor().fit(X_train, y_train)

test_emb  = model.get_embeddings(X_test, data_source="test")   # query rows
train_emb = model.get_embeddings(data_source="train")          # context rows
These embeddings come from the full-data model, so the "train" rows encode their own labels — for leak-free training embeddings use NoriEmbedding with n_fold >= 2.

Using embeddings as features

Once you have a 2-D matrix, any sklearn estimator works. A nearest-neighbor regressor on the embeddings:
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import r2_score

knn = KNeighborsRegressor(n_neighbors=10).fit(Z_train, y_train)
print(r2_score(y_test, knn.predict(Z_test)))
Or a linear probe (standardize first for ridge/SVM/MLP heads):
from sklearn.linear_model import RidgeCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

probe = make_pipeline(StandardScaler(), RidgeCV(alphas=(0.1, 1.0, 10.0, 100.0)))
probe.fit(Z_train, y_train)
print(r2_score(y_test, probe.predict(Z_test)))
The same vectors feed clustering, nearest-neighbor search, or a 2-D projection for visualization.

Visualizing the geometry

Because the embeddings are organized by the target, a 2-D t-SNE of the embeddings separates rows by their target value far more cleanly than a t-SNE of the raw features — a quick visual check that the representation captures what you’re predicting.
t-SNE of raw features vs Nori embeddings, colored by target
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler

emb_2d = TSNE(n_components=2, init="pca", random_state=0).fit_transform(
    StandardScaler().fit_transform(Z_test)
)
# scatter emb_2d colored by y_test

Parameters

NoriEmbedding:
ParameterDefaultDescription
n_fold0Number of CV folds. 0 = vanilla (single model); >= 2 enables out-of-fold training embeddings.
modelNoneA pre-configured NoriRegressor. When None, a default one is built at fit time.
shuffleFalseWhether to shuffle the K-fold split.
random_stateNoneSeed for the split when shuffle=True.
After fit, the transformer exposes model_ (the full-data model) and train_embeddings_ (the training embeddings — out-of-fold when n_fold >= 2).

Notes

  • These run on the local Python package (synthefy-nori), not the hosted API — extraction returns per-row vectors rather than predictions.
  • Use out-of-fold embeddings (n_fold >= 2) whenever you train a downstream model on the training rows; vanilla embeddings leak the label.
  • Embeddings are shape (n_estimators, n_samples, embed_dim) — average over axis=0 (or select [0]) before passing them to a 2-D estimator.