Metadata-Version: 2.4
Name: dual-vade
Version: 0.1.0.post1
Summary: Unsupervised image clustering with trainable VaDE, Freq-VaDE, and Dual-VaDE
Author: Wugeng Zheng
Maintainer: Wugeng Zheng
License-Expression: MIT
Project-URL: Repository, https://github.com/ZhengWugeng/Dual-VaDE
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: torch>=2.6
Requires-Dist: scikit-learn>=1.3
Dynamic: license-file

# Dual-VaDE

Unsupervised image clustering with VaDE, Freq-VaDE, and Dual-VaDE.
Train on your own grayscale images without labels, save the model, and use it
to cluster new images.

## Installation

```sh
python -m pip install dual-vade
```

Requires Python 3.10 or newer. CPU and CUDA operation are supported. For CUDA,
install a PyTorch build compatible with your GPU and driver before installing
`dual-vade`.

## Quick start

Prepare your images as a NumPy array and save it as `train_images.npy`.
This example expects shape `(N, 128, 128)` and values in `[0, 1]`.

```python
import numpy as np
import torch
from dual_vade import DualVaDE

images = np.load("train_images.npy", allow_pickle=False)
device = "cuda" if torch.cuda.is_available() else "cpu"

model = DualVaDE(n_clusters=3, device=device)
model.fit(images, epochs=200, verbose=True)
labels = model.predict(images)  # Shape: (N,)
model.save("my_model")
```

`fit()` trains the neural network and then fits a GMM to the learned embeddings.
It returns the model itself; call `predict()` to obtain cluster IDs.
No ground-truth labels are needed. Choose `n_clusters` yourself: the model does
not determine the number of groups or assign names to them.

## Input format

All training and prediction inputs must be prepared grayscale image arrays:

- Shape: `(N, H, H)` or `(N, 1, H, H)`.
- Values: real, finite, and in `[0, 1]`.
- Image dimensions: match `image_size`, which defaults to `128`.
- Other supported sizes: powers of two at least `64`.
- Training set size: at least `max(2, n_clusters)` images. This is an API minimum,
  not a recommended dataset size.

The same requirements apply to medical and nonmedical images; no specific
dataset is required. Resize, convert to grayscale, and normalize your images
before passing them to the model. Folder paths, RGB images, and 3D volumes are
not accepted directly. Use the same image preparation for training and inference.
When evaluating on held-out data, fit only on the training split.

## Choose a model

```python
from dual_vade import DualVaDE, FreqVaDE, VaDE

spatial_model = VaDE(n_clusters=3)
frequency_model = FreqVaDE(n_clusters=3, spectral="reim")
dual_model = DualVaDE(n_clusters=3, spectral="reim")
```

| Class | Image representation | `spectral` choices |
| --- | --- | --- |
| `VaDE` | Spatial only | Omit this argument |
| `FreqVaDE` | Frequency only | `fullfreq`, `lowfreq`, `polarfreq`, `reim` |
| `DualVaDE` | Spatial and frequency | `fullfreq`, `lowfreq`, `polarfreq`, `reim` |

`FreqVaDE` and `DualVaDE` default to `spectral="reim"`. These choices give nine
configurations: one spatial-only, four frequency-only, and four dual-domain.
Each new model starts with random neural weights; fit it before inference.

### Constructor parameters

All three classes share these parameters:

| Parameter | Default | Purpose |
| --- | --- | --- |
| `n_clusters` | `3` | Number of clusters; positive integer |
| `spectral` | `None` | Frequency representation; defaults to `reim` for frequency/dual models |
| `latent_dim` | `32` | Embedding dimension; positive integer |
| `image_size` | `128` | Square image size; power of two, at least `64` |
| `device` | `"cpu"` | Execution device, such as `"cpu"` or `"cuda"` |
| `batch_size` | `16` | Training and inference batch size; positive integer |
| `random_state` | `123` | Random seed, or `None` |

For example, to use 64-by-64 images and five clusters:

```python
model = DualVaDE(n_clusters=5, image_size=64, batch_size=32, device=device)
```

## Public methods

Here, `N` is the number of input images, `K` is `n_clusters`, `D` is
`latent_dim`, and `H` is `image_size`.

| Method | Result |
| --- | --- |
| `fit(images, epochs=200, learning_rate=1.5e-4, verbose=False)` | Train the neural network, fit the final GMM, and return `self` |
| `predict(images)` | NumPy cluster IDs, shape `(N,)` |
| `predict_proba(images)` | NumPy cluster-membership probabilities, shape `(N, K)` |
| `transform(images)` | NumPy embeddings, shape `(N, D)` |
| `reconstruct(images)` | NumPy reconstructed images, shape `(N, 1, H, H)` |
| `save(directory)` | Save the complete model to a new directory; return its `Path` |
| `Class.load(directory, device="cpu", batch_size=16)` | Load a saved model without retraining |

`epochs` must be a positive integer. Prediction, probabilities, embeddings,
reconstruction, and saving require a fitted or loaded model.
After fitting, `model.history_` contains the epoch history and `model.n_iter_`
contains the number of epochs from the most recent successful fit. Both are
preserved when saving and loading.

## Load a model and cluster new images

Use the class that matches your saved model: `DualVaDE.load()`,
`FreqVaDE.load()`, or `VaDE.load()`.

```python
import numpy as np
from dual_vade import DualVaDE

restored = DualVaDE.load("my_model", device="cpu", batch_size=16)
new_images = np.load("new_images.npy", allow_pickle=False)

labels = restored.predict(new_images)
probabilities = restored.predict_proba(new_images)
embeddings = restored.transform(new_images)
reconstructions = restored.reconstruct(new_images)
```

Loading and inference do not train the network or refit the GMM. You do not
need the original training images, and a GPU-trained model can be loaded for
CPU inference. Cluster IDs refer to the groups learned by that saved model;
they are not automatically named categories.

### Keep the complete saved directory

```text
my_model/
    config.json
    model_state.pth
    clusterer.npz
    manifest.json
```

Keep or share all four files together. The `.pth` file alone is not enough to
restore the clustering model. `save()` creates a new directory and will not
overwrite an existing one. Training and saving do not upload your images or models.

## Continue training

Call `fit()` again on an existing or loaded model to continue from its current
neural weights, then save to a different directory:

```python
restored.fit(new_images, epochs=50, learning_rate=1e-4, verbose=True)
restored.save("my_model_updated")
```

Each `fit()` call starts a new optimizer and refits the GMM using that call's
training images. This continues neural training but does not resume the previous
optimizer state. Cluster assignments may change after refitting. To train a
separate model from scratch, create a new instance instead.
