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):
- Score every registered backbone cheaply — LogME transferability (You et al., ICML 2021) on frozen features, using the deterministic train split.
- Rank candidates by score.
- Validate the top-2 with the configured analytic pipeline on the validation split.
- Select the best validation performer (ties break toward the cheaper backbone — the CPU classroom priority).
- 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_secondsbudgets 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 usewhitening="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). Seebenchmarks/README.mdfor 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) andshrinkage="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 aSelectionResultin advanced mode; the full choice log is stored inmodel.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 inmodel.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-defaultrepr; 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
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 vyntri-1.1.5.tar.gz.
File metadata
- Download URL: vyntri-1.1.5.tar.gz
- Upload date:
- Size: 109.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2c95fbcb617e9bb638872b8eb6d6506d8038d59f9a610058d9b3c27351210384
|
|
| MD5 |
bbe95f6756914a82979f08a9929acd9e
|
|
| BLAKE2b-256 |
b5c15d572d6083171e4fb38cfdec8ff16c7f35e2481795e2fbf82be57a6d4eb9
|
Provenance
The following attestation bundles were made for vyntri-1.1.5.tar.gz:
Publisher:
workflow.yml on AreebShahid07/vyntri
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vyntri-1.1.5.tar.gz -
Subject digest:
2c95fbcb617e9bb638872b8eb6d6506d8038d59f9a610058d9b3c27351210384 - Sigstore transparency entry: 2518240238
- Sigstore integration time:
-
Permalink:
AreebShahid07/vyntri@df6135d763319f9871e71abafdd3d3745ac0b38b -
Branch / Tag:
refs/tags/v1.1.5 - Owner: https://github.com/AreebShahid07
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@df6135d763319f9871e71abafdd3d3745ac0b38b -
Trigger Event:
push
-
Statement type:
File details
Details for the file vyntri-1.1.5-py3-none-any.whl.
File metadata
- Download URL: vyntri-1.1.5-py3-none-any.whl
- Upload date:
- Size: 83.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30cbf982aa6df1af0076e2bd461c7b5d3ef752b7d9c38e2277b49f946e3de622
|
|
| MD5 |
a2fb0cc760ea86d062a99ae9f702c21a
|
|
| BLAKE2b-256 |
f30189399a336047871b653697a2f43619771349ac48bc953d3c8a49e8f8eddf
|
Provenance
The following attestation bundles were made for vyntri-1.1.5-py3-none-any.whl:
Publisher:
workflow.yml on AreebShahid07/vyntri
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vyntri-1.1.5-py3-none-any.whl -
Subject digest:
30cbf982aa6df1af0076e2bd461c7b5d3ef752b7d9c38e2277b49f946e3de622 - Sigstore transparency entry: 2518240463
- Sigstore integration time:
-
Permalink:
AreebShahid07/vyntri@df6135d763319f9871e71abafdd3d3745ac0b38b -
Branch / Tag:
refs/tags/v1.1.5 - Owner: https://github.com/AreebShahid07
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@df6135d763319f9871e71abafdd3d3745ac0b38b -
Trigger Event:
push
-
Statement type: