Metadata-Version: 2.4
Name: vyntri
Version: 1.3.2
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

**Classify images without training.** Extract features from a pretrained backbone, project them analytically, and classify -- all in 4 lines of Python.

```python
from vyntri import Vyntri
from vyntri.data import split

s = split("./my-dataset", train=0.7, test=0.2, seed=42)
model = Vyntri()
model.fit(s)
model.predict("./image.jpg")  # Prediction(label=cat, confidence=0.94)
```

No GPU required. No training loops. Useful defaults require little or no tuning.

---

## Table of Contents

- [Why Vyntri?](#why-vyntri)
- [Quick Start](#quick-start)
- [How It Works](#how-it-works)
- [After Fitting](#after-fitting)
- [Configuration](#configuration)
- [Custom Backbones](#custom-backbones)
- [Fine-Tuning](#fine-tuning)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Installation](#installation)

---

## Why Vyntri?

| | Training-based | Vyntri |
|---|---|---|
| **Time to first prediction** | Minutes to hours | ~4 seconds |
| **Data needed** | Hundreds+ per class | Works with 10-50 per class |
| **GPU required** | Essentially yes | No |
| **Hyperparameters to tune** | Dozens | Few (sensible defaults) |
| **Overfitting risk** | High on small data | Low (analytic, no gradient loops) |

Vyntri replaces the training loop with analytic (closed-form) projection and classification. The result is **fast to fit**, **resistant to overfitting** (no gradient loops to memorize noise), and **works on small datasets** where traditional training fails.

---

## Quick Start

### 1. Prepare your dataset

Organize images into folders by class name:

```
my-dataset/
  cats/
    cat_001.jpg
    cat_002.jpg
  dogs/
    dog_001.jpg
  birds/
    bird_001.jpg
```

**Recommended:** At least 10 images per class for good results.

### 2. Split, fit, and predict

```python
from vyntri import Vyntri
from vyntri.data import split

# Create an explicit train/test split (no files copied)
s = split("./my-dataset", train=0.7, test=0.2, seed=42)

model = Vyntri()
model.fit(s)

result = model.predict("./test-photo.jpg")
print(result.label)       # cats
print(result.confidence)  # 0.94
```

### 3. Evaluate on the held-out test set

```python
result = model.evaluate(s.test)
print(result.accuracy)    # 0.91
print(result.macro_f1)    # Per-class F1 (macro-averaged)
print(result.per_class)   # Per-class precision/recall/F1
```

### 4. Save and reload

```python
model.save("./my-model.vyntri")
model = Vyntri.load("./my-model.vyntri")
model.predict("./new-image.jpg")
```

---

## How It Works

```
Image
  |
Pretrained backbone (frozen)    <- extracts features, no training
  |
Analytic projection              <- closed-form dim reduction
  |
Shrinkage estimation             <- stabilizes covariance
  |
Analytic Ridge classifier        <- closed-form weights
  |
Prediction
```

Every step is **analytic** -- mathematically derived, not learned through gradient descent:

- **Low overfitting risk** -- analytic fitting avoids iterative gradient-based optimization
- **No GPU needed** -- CPU matrix ops are fast enough
- **Sensible defaults** -- few or no parameters to tune for common workflows
- **Designed for determinism** -- analytic fitting on fixed data and config produces consistent results

The pretrained backbone provides general visual features. Vyntri never modifies it -- it only learns the projection and classifier on top.

---

## After Fitting

### Model info

```python
model.classes_           # ["cats", "dogs", "birds"]
model.class_to_idx_      # {"cats": 0, "dogs": 1, "birds": 2}
model.feature_dim_       # 576 (backbone feature dimension)
model.state              # "fitted", "fine_tuned", etc.
```

### Predictions

```python
result = model.predict("./image.jpg")
result.label            # cats
result.confidence       # 0.94
```

### Batch predictions

```python
results = model.predict_batch(["./img1.jpg", "./img2.jpg", "./img3.jpg"])
for path, label, conf in zip(results.paths, results.labels, results.confidences):
    print(f"{label}: {conf:.1%}")
```

### Inspect internals

```python
model.config             # Current configuration
model.fitted_config      # Config that produced this fit
model.projection_        # Learned projection matrix
model.classifier_        # Learned classifier weights
model.validation_accuracy_
```

---

## Configuration

```python
from vyntri import Vyntri
from vyntri.data import split

# All parameters can be passed directly as kwargs
model = Vyntri(
    backbone="auto",
    whitening="fk",
    shrinkage="diagonal",
    cache_dir="./vyntri-cache",
)

s = split("./dataset", train=0.7, val=0.1, test=0.2, seed=42)
model.fit(s)
```

```python
Config.describe()  # Returns structured docs for all parameters
```

### Key configuration groups

| Group | Options | What they control |
|---|---|---|
| **Backbone** | backbone | Feature extraction model |
| **Projection** | whitening, shrinkage, shrinkage_alpha, projection_dim | Dim reduction |
| **Classifier** | regularization | Ridge classification strength |
| **Validation** | val_fraction | Legacy only — use `split()` instead |
| **Cache** | cache_dir, cache_enabled | Feature caching |
| **Execution** | batch_size, num_workers, device, dtype | Runtime behavior |

---

## Custom Backbones

```python
from vyntri import Vyntri
from vyntri.data import split

# Use a larger backbone by name
s = split("./dataset", train=0.7, test=0.2, seed=42)
model = Vyntri(backbone="resnet50")
model.fit(s)
```

---

## Fine-Tuning

For the highest accuracy on your specific domain:

```python
from vyntri.data import split

s = split("./dataset", train=0.7, test=0.2, seed=42)
model.fit(s)
model.fine_tune(s.train, epochs=10, lr=1e-3, scope="last_layer")
```

| Scope | What unfreezes | When to use |
|---|---|---|
| last_layer | Final classification head | Default -- safe, fast |
| last_block | Final feature extraction block | 50+ images/class |
| full | Entire backbone | 1000+ images/class |

**Note:** Fine-tuning requires a GPU for reasonable speed.

---

## API Reference

| Method | Description |
|---|---|
| `vyntri.data.split(path, train, test, seed)` | Create explicit train/val/test split |
| Vyntri(**kwargs) | Create model instance |
| model.fit(split_result or path) | Fit on dataset |
| model.predict(image) | Classify single image |
| model.predict_batch(images) | Batch classify |
| model.evaluate(path or FolderDataset) | Evaluate on test set |
| model.save(path) / Vyntri.load(path) | Serialize/deserialize |
| model.fine_tune(dataset, scope, epochs, lr) | Optional fine-tuning (accepts SplitResult/FolderDataset/path) |
| model.update(dataset) | Add new data/classes (accepts FolderDataset/Path/path) |
| model.analyze(dataset) | Inspect dataset (accepts FolderDataset/Path/path) |
| model.select_backbone(dataset) | Auto-select backbone |
| model.clear_cache() | Remove feature cache |

| Property | Description |
|---|---|
| model.classes_ | Class names list |
| model.class_to_idx_ | Class to index mapping |
| model.state | Current model state |
| model.config | Current configuration |
| model.fitted_config | Config used for last fit |
| model.feature_dim_ | Backbone feature dimension |
| model.validation_accuracy_ | Validation accuracy from fit |

---

## Examples

### Image classification

```python
from vyntri import Vyntri
from vyntri.data import split

s = split("./flowers", train=0.7, test=0.2, seed=42)
model = Vyntri()
model.fit(s)
print(model.classes_)  # ["daisy", "rose", "sunflower", "tulip"]

result = model.predict("./test-rose.jpg")
print(result.label, result.confidence)
```

### Incremental learning

```python
from vyntri import Vyntri
from vyntri.data import split
from vyntri.data.dataset import FolderDataset

s = split("./dataset-v1", train=0.7, test=0.2, seed=42)
model = Vyntri()
model.fit(s)

# update() accepts FolderDataset, Path, or string
new_data = FolderDataset("./dataset-v2")
model.update(new_data)  # Adds new data without forgetting
```

### Comparing backbones

```python
from vyntri import Vyntri
from vyntri.data import split

s = split("./dataset", train=0.7, test=0.2, seed=42)
for name, bb in [("mobile", "mobilenet_v3_small"), ("resnet", "resnet50")]:
    model = Vyntri(backbone=bb)
    model.fit(s)
    print(name, model.validation_accuracy_)
```

---

## Installation

```bash
pip install vyntri
```

**Requirements:** Python 3.9+, PyTorch, torchvision

**Optional for fine-tuning:** CUDA-capable GPU

---

## Migrating from v1.1.x

### v1.2.0: Explicit dataset splitting

`vyntri.data.split()` replaces implicit automatic splitting. Passing a folder-per-class path directly to `fit()` still works but emits a `DeprecationWarning`.

```python
# Old (deprecated)
model.fit("./my-dataset")

# New (recommended)
from vyntri.data import split
s = split("./my-dataset", train=0.7, test=0.2, seed=42)
model.fit(s)
```

### v1.3.0: API consistency and stability

- `update()`, `fine_tune()`, and `analyze()` now accept `FolderDataset`, `Path`, or path strings (matching `fit()`).
- `save()`/`load()` preserves training history and class registry across roundtrips.
- `loaded.state` now reflects the pre-save state (`"fitted"`, `"updated"`, or `"fine_tuned"`) instead of always `"loaded"`.
- Scheduled fit with no validation set no longer crashes (gracefully skips gradient refinement).
- Corrupt image files now raise a clear `DatasetError` instead of a raw PIL traceback.

---

*Vyntri -- classify images without training.*
