Metadata-Version: 2.4
Name: vyntri
Version: 1.1.5
Summary: Rapid analytic adaptation of pretrained vision representations, CPU-first.
Author: Vyntri Contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/AreebShahid07/vyntri
Project-URL: Source Code, https://github.com/AreebShahid07/vyntri
Project-URL: Research, https://github.com/AreebShahid07/vyntri-research
Project-URL: Bug Tracker, https://github.com/AreebShahid07/vyntri/issues
Keywords: image-classification,analytic-learning,few-shot,continual-learning,pretrained-features,cpu,machine-learning
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24.0
Requires-Dist: torch>=2.0.0
Requires-Dist: torchvision>=0.15.0
Requires-Dist: pillow>=10.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Dynamic: license-file

# 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.

```python
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.**

```python
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

```bash
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:**

```python
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):**

```python
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)

```python
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:

```python
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)

```python
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):

```python
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:

```bash
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:

```python
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](docs/api.md)** — every public method, parameter, return
  type, example, and warning (spec §63).
- **[Benchmark](benchmarks/README.md)** — protocol, fairness rules, and
  how to run the §81 pipeline comparison.
- **[Notebooks](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
