Skip to main content

Vyntri

Rapid analytic adaptation of pretrained vision representations, CPU-first.

Vyntri adapts pretrained image backbones to your classification task in seconds without gradient training loops. It extracts frozen features, applies FK discriminative whitening with diagonal covariance shrinkage, and solves an analytic ridge classifier in closed form.

from vyntri import Vyntri

model = Vyntri()
model.fit("./dataset")          # folder-per-class, ~1-2 s on a laptop CPU
print(model.evaluate("./test").accuracy)
model.predict("./image.jpg")

Why Vyntri

  • No training loop. Adaptation is linear algebra — milliseconds to a few seconds on CPU, no GPU required.
  • Simple by default, configurable by design.
model = Vyntri()                              # validated defaults

model = Vyntri(                               # controlled experiment
    backbone="mobilenet_v3_small",
    whitening="fk",
    shrinkage="diagonal",
    shrinkage_alpha=0.25,
    regularization=1e-3,
    projection_dim=32,
    seed=42,
)
  • Honest about trade-offs. Vyntri seeks a better accuracy/computation trade-off, not guaranteed maximum accuracy. In the reduced-sample regimes it targets, analytic adaptation beats last-layer fine-tuning in both speed and accuracy (research evidence, below); in regimes where fine-tuning wins, that result is reported, not hidden.

Installation

pip install vyntri

Requires Python ≥ 3.9. Dependencies: numpy, torch, torchvision, pillow. Vyntri is designed for CPU-first use; CUDA can be selected when available, but GPU acceleration is not required. The pretrained backbone weights download on first use.

Quick start

Dataset layout — one folder per class:

dataset/
  cat/
    img1.jpg
    img2.jpg
  dog/
    img1.jpg

Or explicit splits:

dataset/
  train/cat/...
  val/cat/...
  test/cat/...

Fit, evaluate, predict:

from vyntri import Vyntri

model = Vyntri()
model.fit("./dataset")          # single folder -> deterministic 80/20 split
model.fit("./dataset", target_accuracy=0.90)  # escalate until validation hits 90%
Vyntri(backbone="auto").fit("./dataset")     # auto-select the backbone (v0.7)
result = model.evaluate("./test")
print(result.accuracy, result.macro_f1, result.confusion_matrix)

model.predict("./cat.jpg")      # -> PredictionResult(label, confidence)
model.predict_batch("./images") # -> BatchPredictionResult (CSV/JSON export)
model.analyze("./dataset")      # cheap dataset inspection, no extraction

model.save("model.vyntri")
model = Vyntri.load("model.vyntri")

fit() prints a concise summary; model.summary() returns the same information with full timing breakdown. Feature extraction is cached on disk (keyed by dataset fingerprint + backbone + preprocessing + dtype) — the second fit of the same data is much faster, and model.clear_cache() resets it. A cache hit/miss is always reported.

Continual learning (v0.2):

model.fit("./initial")            # classes: cat, dog
result = model.update("./new_data")  # adds dog examples + a new class: bird
print(result.new_classes)         # -> ["bird"]

update() maintains sufficient statistics (X^T X, per-class sums/counts) in the raw feature space and re-solves the FK projection and ridge classifier from them — old raw features are never retained. The result matches a joint refit over all data (measured, V3 Stage 7); updates are order-invariant and handle mixed old/new class batches. It does not claim "zero forgetting": per-task accuracy can shift because the joint solution legitimately re-allocates boundaries as classes arrive. Memory note: the statistics are O(d²), so they beat raw features when n >> d and are larger at n < d.

Features

Area What
Data folder-per-class and explicit train/val/test layouts, deterministic hash-based stratified split, tiny-class handling
Backbone MobileNetV3-Small + ResNet18 + ResNet50 (frozen, ImageNet pretrained); registry validated at construction
Whitening FK/discriminative whitening (default) or SLCE (v0.8) — eigenvalue flooring / positivity floors, float64-stable
Shrinkage diagonal (default) or Ledoit-Wolf (v0.8, data-estimated) covariance shrinkage
Classifier analytic ridge, float64 solve, condition diagnostics
Evaluation accuracy, macro/weighted F1, balanced accuracy, confusion matrix
Persistence .vyntri zip (JSON + npy) — schema-versioned, no pickle on load
Continual update() via sufficient statistics; stable class registry; sequential == joint (measured)
Config (v0.3) validated advanced controls (whitening/shrinkage/alpha/regularization/projection_dim/dtype/seed/device/cache), describe(), JSON export/import, interaction notes, non-default repr
Fine-tuning (v0.5) optional fine_tune() — linear head / last block / full backbone, validation-based checkpoint selection, full training metadata; kept separate from the analytic path
Scheduling (v0.6) fit(target_accuracy=..., max_time_seconds=...) — deterministic escalation ladder (ridge → FK → limited refinement), validation-only selection, refit on train+val, full per-decision log
Selection (v0.7) backbone="auto" — LogME scoring + top-k analytic validation (research-validated), validation-only selection, select_backbone() returns a SelectionResult
Advanced (v0.8) whitening="slce" (supervised centroid-encoder projection) and shrinkage="ledoit_wolf" (data-estimated intensity) — faithful ports with attribution, resource estimates, clear statistics-only-update errors
Notebooks (v0.9) notebooks/ classroom tutorials — first model, backbone comparison, whitening comparison, continual update — ready for a teaching setting, structurally tested
Benchmark (v1.0) benchmarks/ pipeline comparison: Vyntri default vs frozen-feature ridge vs fine-tuning — same split/backbone/hardware, per-phase timing, cache accounting
Reproducibility seed, resolved config stored with the fitted model

Roadmap

Future releases may add user-requested functionality, performance improvements, additional backbones, and additional training workflows. Research-only techniques and experimental algorithms remain in the separate vyntri-research repository unless they prove useful and appropriate for the public API.

Backbones

Backbone Features Params Pretrained weights CPU
mobilenet_v3_small (default) 576 2.5M MobileNet_V3_Small_Weights.IMAGENET1K_V1 ✅ fast
resnet18 512 11.7M ResNet18_Weights.IMAGENET1K_V1 ✅ ~4× slower than MobileNet
resnet50 2048 25.6M ResNet50_Weights.IMAGENET1K_V1 ✅ ~2× slower than ResNet18

All three share one ImageNet preprocessing system (resize-256 → center-crop → normalize) and expose a frozen feature endpoint (classifier/fc replaced with an identity). Switch with Vyntri(backbone="resnet18") or Vyntri(backbone="resnet50"). The registry validates names at construction and embeds the exact pretrained weights in the feature-cache key, so switching backbones never reuses another backbone's cached features.

Backbone auto-selection (v0.7)

model = Vyntri(backbone="auto")
model.fit("./dataset")   # scores candidates, validates the top-2, fits the winner

backbone="auto" resolves to a concrete backbone at fit time (Master spec §12, §54):

  1. Score every registered backbone cheaply — LogME transferability (You et al., ICML 2021) on frozen features, using the deterministic train split.
  2. Rank candidates by score.
  3. Validate the top-2 with the configured analytic pipeline on the validation split.
  4. Select the best validation performer (ties break toward the cheaper backbone — the CPU classroom priority).
  5. Freeze + refit: the winner is fit as a normal model and refit on train + validation (V3 41.2). The final test set is never inspected.

The choice is logged — every candidate's score, the ranked order, the validated top-k with accuracies, and the winner are recorded in model.metadata_["selection"] (and model.selection_result_), and printed at fit:

Backbone: auto -> resnet18 (selected)
Selection (LogME): resnet50=0.73, resnet18=0.68, mobilenet_v3_small=0.52
Validated (top-k): resnet50=88.4%, resnet18=88.4%

Why LogME: the V3 Stage-6 selection benchmark measured ranking quality (Spearman, top-1 selection accuracy, regret vs the oracle backbone) for LogME / H-score / LEEP / NLEEP / PACTran across five datasets. LogME picked the oracle backbone in 4/5 datasets with zero regret; LEEP was unreliable (1/5) and is not exposed. Advanced users can run selection standalone: result = model.select_backbone("./dataset") returns a SelectionResult without fitting.

Cost note: auto extracts features for every candidate once (cached), so the first run costs more than a fixed-backbone fit; repeat fits reuse the cache and pay only the (cheap) scoring and top-k validation.

Fine-tuning (v0.5)

Optional gradient fine-tuning, deliberately separate from the analytic path:

model.fit("./dataset")
result = model.fine_tune("./dataset", scope="last_layer", epochs=3)
print(result.best_validation_accuracy)  # best-on-validation checkpoint
model.evaluate("./test")                # now runs through the fine-tuned model

Scopes are architecture-aware (no module paths hardcoded):

Scope Trains
last_layer new linear head only, backbone frozen (linear probe)
last_block head + last feature block (ResNet layer4 / MobileNet's final block)
full head + the entire backbone

Defaults follow the V3 Stage 9 protocol (Adam, lr=1e-3, weight_decay=1e-4, 3 epochs, batch from config). Validation is used every epoch and the best-on-validation checkpoint is restored — the test set is never touched during fitting. Every run records epochs, gradient steps, trainable parameters, optimizer, learning rate, weight decay, and training / validation / total time. A fine-tuned model persists through save()/load() (weights stored as plain numpy in the no-pickle archive).

Honest trade-off: fine-tuning replaces the analytic classifier, needs enough gradient steps to converge (more than 3 epochs on small datasets), and on reduced-sample data the analytic path is typically both faster and more accurate (V3 head-to-head). Analytic update() is unavailable after fine_tune() until you re-fit().

Target accuracy & time budget (v0.6)

model.fit("./dataset", target_accuracy=0.90)  # stop when validation hits 90%
model.fit("./dataset", max_time_seconds=120)  # spend at most 2 minutes adapting
model.fit("./dataset", target_accuracy=0.90, max_time_seconds=120)

When either is given, a deterministic escalation scheduler runs a cheap- methods-first ladder — ridge on raw frozen features → the configured analytic pipeline (FK + shrinkage + ridge) → limited head-only gradient refinement — validating each stage on the validation split and stopping as soon as the target is met or the budget expires:

Target accuracy: 0.900
Ladder: ridge -> fk -> refine_last_layer
Best stage: fk (validation 88.4%) | target not reached
  - ridge: validation 82.1% in 0.20s
  - fk: validation 88.4% in 0.42s
  - refine_last_layer: validation 91.0% in 18.3s

Every decision is recorded in model.metadata_["schedule"] (per-stage accuracy, cost, elapsed time, and the reason for each choice), so the run is explainable. Semantics, following the V3 Phase 7 evidence:

  • Selection is validation-only — the final test set is never used during fitting. The selected analytic configuration is then refit on train + validation (V3 41.2) so the model uses all data that did not participate in selection.
  • max_time_seconds budgets the adaptation ladder only (V3 41.1): it starts after feature extraction, which is cached and reported separately. The cheapest analytic stage always runs, so a fitted model is always produced; remaining stages are deterministically skipped.
  • The refinement rung uses the same V3 Stage 9 protocol as fine_tune() (head-only, Adam lr=1e-3, 3 epochs) and stops early once the remaining budget is spent, keeping the best-on-validation checkpoint.

Advanced analytic methods (v0.8)

Two research-validated, advanced-only options (Master spec §31-32, §7, §55):

model = Vyntri(whitening="slce")        # SLCE projection instead of FK
model = Vyntri(shrinkage="ledoit_wolf") # data-estimated shrinkage intensity

SLCE (Supervised Linear Centroid-Encoder, Ghosh & Kirby, Pattern Recognition 2024, arXiv:2306.04622 — not a Vyntri invention) maps each sample toward its class centroid with an orthonormal linear projection. Its closed-form objective (paper Eq. 17) is a symmetric eigenproblem; only the positive-eigenvalue directions are kept, at most C-1 of them (paper Properties 3/6). Diagnostics: model.projection_.spectrum_, .n_positive_, .effective_rank_; resource note: SLCEProjection.estimate_memory(n, d) reports the O(n d + d²) fit memory.

Ledoit-Wolf shrinkage (Ledoit & Wolf, "A Well-Conditioned Estimator for Large-Dimensional Covariance Matrices", J. Multivariate Analysis 88(2), 2004) shrinks the FK within-class covariance toward a scaled identity with a data-estimated intensity — shrinkage_alpha is ignored. The estimator is a faithful NumPy port of the scikit-learn reference implementation (cross-checked to 1e-12 in the test suite).

Both are advanced controls with honest limits:

  • SLCE and Ledoit-Wolf are not supported by the statistics-only continual path — model.update() raises a clear error telling you to re-fit or use whitening="fk" / shrinkage="diagonal" (Master spec §26, §86: statistics-only updates apply where the method supports them).
  • Ledoit-Wolf's intensity is data-estimated and can be aggressive when the feature dimension is small relative to the sample count (its documented finite-sample behavior; the same as the reference implementation).
  • SLCE is a supervised projection, not a whitening: shrinkage does not apply to it.

Both are exposed through the low-level components namespace too (FKProjection, SLCEProjection, AnalyticRidge).

Benchmark (v1.0)

benchmarks/benchmark_pipeline.py compares the default analytic pipeline against two baselines — a frozen-feature ridge and conventional fine-tuning — on the same data, split, backbone, pretrained weights, and hardware (spec §81, §47, §68):

Method What Cache
Vyntri default (cold) full fit() with a cleared feature cache MISS
Vyntri default (warm) same fit, warm cache — adaptation-only HIT
Frozen-feature ridge NumPy closed-form ridge on raw frozen features HIT (shared)
Fine-tune (last_layer) Adam head, validation checkpoint, same split n/a (gradient loop)

Reported numbers on Oxford-IIIT Pets (37 classes × 100 images, MobileNetV3-Small, CPU):

method                     acc    macroF1   extract  adapt/train   total(E2E)
Vyntri default (cold)    0.8274   0.8281     97.0s       1.5s      125.0s  [cache MISS]
Vyntri default (warm)    0.8274   0.8281      0.1s       1.4s        3.8s  [cache HIT]
Frozen-feature ridge     0.8383   0.8375      0.0s       0.3s        0.3s  [cache HIT, shared]
Fine-tune (last_layer)   0.7948   0.7934      0.0s      81.2s      115.8s  [gradient loop]

The key insight: analytic adaptation is ~30x faster than fine-tuning on the same data (warm run vs fine-tune), while matching or exceeding its accuracy on this dataset. The accuracy/speed trade-off is honest — Vyntri seeks a better computation trade-off, not guaranteed maximum accuracy (§82).

Run the benchmark:

python benchmarks/benchmark_pipeline.py --dataset folder --dataset-dir /path/to/data
python benchmarks/benchmark_pipeline.py --dataset synthetic            # offline smoke
python benchmarks/benchmark_pipeline.py --dataset cifar10 --limit 200  # needs internet

Configuration

All defaults are documented with provenance:

  • backbone="mobilenet_v3_small" — chosen for the ordinary-laptop classroom use case (V3 research environment: CPU-only).
  • whitening="fk" + shrinkage="diagonal" — the configuration that won the V3 research matrix (fk+diag best in 14/20 dataset × backbone cells).
  • regularization=1e-4 — the V3 baseline lambda; a λ sweep is not yet part of the research archive, so this is a documented baseline, not a claimed optimum.
  • val_fraction=0.2 — deterministic 80/20 train/validation split; V3 used 60/20/20 including a research test fold.

Advanced controls (all validated at construction): whitening, shrinkage, shrinkage_alpha, regularization, projection_dim, dtype, eigenvalue_floor, floor_ratio, device, cache_dir, cache_enabled, batch_size, num_workers, input_size, seed. Every parameter is documented in code with type, valid values, example, interaction notes, and cache implications:

from vyntri import Config

model.config.describe_param("shrinkage_alpha")  # one parameter
Config.describe()                                # all parameters

# JSON export / import round-trips through validation
cfg_json = model.config.to_json()
restored = Config.from_json(cfg_json)

print(model.config)          # shows only non-default values
model.config.interaction_notes()  # e.g. shrinkage is ignored when whitening="none"

The resolved configuration (e.g. the auto-picked projection_dim) is stored with the fitted model and is authoritative for all post-fit operations: mutating model.config after fit() affects only the next fit(), never the interpretation of the current model (Master spec §18). model.config.classify_changes(other) reports which pipeline stages a config change would invalidate (extraction / fit / safe).

Documentation

  • API Reference — every public method, parameter, return type, example, and warning (spec §63).
  • Benchmark — protocol, fairness rules, and how to run the §81 pipeline comparison.
  • Notebooks — classroom tutorials (first model, backbone comparison, whitening comparison, continual update).

Research foundation

Vyntri's defaults and design are backed by the experiments in the separate research repository — github.com/AreebShahid07/vyntri-research (V3 protocol, 1,571 result rows): the FK + diagonal-shrinkage + analytic-ridge pipeline, the float32 numerical-stability fixes, the n≈d ridge-collapse regime, and the fine-tuning comparison are all measured there. The public library is a clean rebuild around stable interfaces; research-only algorithms (DS-AL, pooled shrinkage, SLCE, intrinsic-dimension diagnostics, zero-cost proxies) intentionally stay in the research repository.

Limitations

  • Image classification on folder-per-class data only (no detection / segmentation).
  • Three backbones (MobileNetV3-Small, ResNet18, ResNet50); MobileNetV3-Large is not currently planned.
  • CPU timings are single-machine; relative cost ratios are robust, absolute times vary.
  • In regimes the research did not cover (very large per-class sample counts, distribution shift), fine-tuning may be competitive or better — measured, not assumed.

Release notes

  • v1.0 — stable public library: the spec §88 release gate passes (clean install, full user-flow smoke against the installed wheel, 170 tests, twine check, attribution verified, corruption handled with clear errors, benchmarks/ pipeline comparison on real data). See benchmarks/README.md for the accounting protocol.
  • v0.9 — classroom notebooks: four executable tutorials under notebooks/ covering the spec §60 teaching arc — first model, backbone comparison, whitening comparison, continual update — plus a folder README and a structural test that keeps them valid and on-topic.
  • v0.8 — advanced analytic methods: whitening="slce" (Supervised Linear Centroid-Encoder projection, Ghosh & Kirby 2024) and shrinkage="ledoit_wolf" (Ledoit & Wolf 2004, faithful port of the scikit-learn reference). Both carry attribution, resource estimates, and clear statistics-only-update errors; SLCE saves/loads correctly.
  • v0.7 — backbone auto-selection: Vyntri(backbone="auto") scores every registered backbone with LogME (research-validated, V3 Stage 6), validates the top-k with the configured analytic pipeline on the validation split, selects the winner validation-only (ties toward the cheaper backbone), and refits it on train+val. select_backbone() returns a SelectionResult in advanced mode; the full choice log is stored in model.metadata_["selection"].
  • v0.6 — target accuracy & time budget: fit(target_accuracy=..., max_time_seconds=...) runs a deterministic escalation ladder (ridge → configured analytic pipeline → limited head-only refinement) with validation-only selection, refit of the analytic winner on train+val, a budget that gates escalation (never the first stage), and a full per-decision log in model.metadata_["schedule"].
  • v0.5 — optional gradient fine-tuning (fine_tune): architecture-aware scopes (linear head / last block / full backbone), Adam protocol from V3 Stage 9, validation-based checkpoint selection, full training metadata, fine-tuned models save/load without pickle.
  • v0.4 — three backbones (MobileNetV3-Small, ResNet18, ResNet50) behind one registry and preprocessing system; backbone names validated at construction; cache keys proven distinct per backbone.
  • v0.3 — advanced configuration layer: validated controls for whitening, shrinkage, alpha, regularization, projection dimension, dtype, seed, device, cache; Config.describe(), JSON export/import, interaction notes, non-default repr; resolved config stored with the model.
  • v0.2 — continual learning: update() via sufficient statistics, stable class registry, mixed old/new classes, sequential == joint (measured).
  • v0.1 — clean core: folder datasets, deterministic split, frozen MobileNetV3-Small features, FK whitening, diagonal shrinkage, analytic ridge, fit/evaluate/predict/predict_batch, save/load, CPU-first.

License

MIT

Download files

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

Source Distribution

vyntri-1.0.0.tar.gz (98.4 kB view details)

Uploaded Source

Built Distribution

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

vyntri-1.0.0-py3-none-any.whl (80.3 kB view details)

Uploaded Python 3

File details

Details for the file vyntri-1.0.0.tar.gz.

File metadata

  • Download URL: vyntri-1.0.0.tar.gz
  • Upload date:
  • Size: 98.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vyntri-1.0.0.tar.gz
Algorithm Hash digest
SHA256 827e91c816a1c0f4a79971702fe6a32028d320f0fb9e48a6490b7214a336e232
MD5 3d23f0028c2e00aba5b19ab5c6aae1f9
BLAKE2b-256 9ac56ac954361ec77d9ee36e6b4c89862ae08dd1f5b93042ef00d06f4e909d36

See more details on using hashes here.

Provenance

The following attestation bundles were made for vyntri-1.0.0.tar.gz:

Publisher: workflow.yml on AreebShahid07/vyntri

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vyntri-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: vyntri-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 80.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vyntri-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f1f37467a59510137c3bb7a7bf650b23caeacc126cc1408b8afd83309f74e49
MD5 09dce2f6b279d67dea1207d139a7e15a
BLAKE2b-256 58f8248f226ed26150a0bfcba461d2f3c83c6d0510b886968e92001a53a050a7

See more details on using hashes here.

Provenance

The following attestation bundles were made for vyntri-1.0.0-py3-none-any.whl:

Publisher: workflow.yml on AreebShahid07/vyntri

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.5.0

2 files

1.4.0

2 files

1.3.2

2 files

1.3.1

2 files

1.2.0

2 files

1.1.5

2 files

This release

1.0.0 This release

2 files

0.5.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page