Note: This is a development version of the code. The code in the
mainbranch is a more stable version.
Affinity-VAE for disentanglement, clustering and classification of objects in
multidimensional image data
Mirecka J, Famili M, Kotanska A, Juraschko N, Costa-Gomes B, Palmer CM,
Thiyagalingam J, Burnley T, Basham M & Lowe AR
- What Affinity-VAE does
- Installation
- Preparing your data
- Quick start: a minimal run
- Expanded runs: configuration in full
- All parameters
- Output directory structure
- Evaluating a trained model
- Multi-GPU and cluster runs
- Inspecting results with Napari
- Repository layout
- Development: tests and linting
- Known limitations
Affinity-VAE is a variational autoencoder for 2D images and 3D volumes (for example cryo-ET subtomograms). Each input is encoded into:
- a latent vector (
latent_dims) that describes the object's shape or identity, and - an optional pose vector (
pose_dims) that captures pose (such as rotation), so the latent vector can be pose-invariant.
The model is trained with a loss made of three parts:
| Term | What it does | Weight |
|---|---|---|
| Reconstruction | MSE (or BCE for binary data) between input and decoded output | 1 |
| KL divergence | Regularises the latent distribution (beta-VAE term) | beta |
| Affinity | L1 distance between the cosine similarity of each pair of latent vectors in a batch and the pre-computed similarity of their two classes, read from an affinity matrix you provide | gamma |
The affinity term organises the latent space so that classes you know to be
similar end up close together. Set gamma: 0 and pose_dims: 0 to get a
plain beta-VAE. beta and gamma can be constant or follow a cyclical
schedule over epochs.
Around the model, the pipeline:
- loads
.mrc/.npydata with the caked library, takes class labels from the file names, splits the data into training and validation sets, and can also encode an optional held-out test set, - trains on CPU, one GPU or several GPUs (through Lightning Fabric),
- regularly trains a classifier (MLP, k-nearest neighbours or logistic regression) on the training latents and scores it on the validation latents,
- writes plots (loss, confusion matrices, F1, reconstructions, t-SNE embeddings, similarity matrices, interpolations, latent and pose traversals), an interactive HTML latent-space explorer, model checkpoints and latent metadata,
- tracks the best validation loss and best validation accuracy epochs and saves a full set of plots for each,
- evaluates a trained model on new data, including classes it never saw in training.
Two decoder families are available: cnn (convolutional encoder and decoder)
and gsd (convolutional encoder with a differentiable Gaussian-splat decoder).
Requirements: Python 3.10 or later, git (one dependency, caked, is installed
straight from GitHub), and a CUDA GPU if you want GPU training (it is
optional). The current code has been run with Python 3.12, PyTorch 2.13,
Lightning 2.6 and pydantic 2.13.
git clone https://github.com/alan-turing-institute/affinity-vae.git
cd affinity-vae
python -m venv env
source env/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[all]"The all extra installs torch, torchvision, lightning and pydantic>2.
These are needed to train and are not included in a plain pip install -e ..
If you are developing, install the test extra and the pre-commit hooks instead:
python -m pip install -e ".[test]"
python -m pip install pre-commit
pre-commit installCheck the install by printing the options:
python run.py --helpmodule purge
module load baskerville
module load bask-apps/live
module load PyTorch/2.0.1-foss-2022a-CUDA-11.7.0
module load torchvision/0.15.2-foss-2022a-CUDA-11.7.0
python -m venv pyenv_affinity
source pyenv_affinity/bin/activate
git clone https://github.com/alan-turing-institute/affinity-vae.git
cd affinity-vae/
python -m pip install -e ".[baskerville]"See Multi-GPU and cluster runs for submitting jobs.
The Napari viewer needs pydantic<2, which the training code cannot use, so
install it in its own environment with pip install -e ".[napari]". See
Inspecting results with Napari.
A training run needs three things: a data directory, an affinity matrix and (optionally) a class list.
One flat directory of files, all of the same type:
datatype: mrcreads any file whose name contains.mrc(so.mrcand.mrcsboth work);datatype: npyreads.npyfiles.- Each file holds one 2D image or 3D volume. All files must be the same
size, or use
rescaleto resize them. Each side must be divisible by2^depth(by 8 with the defaultdepth: 3; withfilters, by 2 to the number of layers). - The class label is the part of the file name before the first underscore.
1b23_12-321-215.mrcshas class1b23;2_0_img.npyhas class2. The rest of the name is kept as metadata.
Optionally, add a test/ subdirectory with the same layout. Test data is never
used for training. It is encoded every freq_eval epochs and shown in the
embedding plots, so test points only appear in plots and checkpoints from
epochs that are a multiple of freq_eval. Test classes do not need to appear
in the class list or the affinity matrix.
In plots, test samples are labelled <class>_test during training and
<class>_eval during evaluation (--evaluate), so they are not confused with
training classes of the same name. If test files have no known class, name
them test_<anything> (class test); they are then labelled plain test or
eval. Accuracy, confusion matrices and the saved metadata always use the
plain class names.
my_data/
├── 1b23_12-321-215.mrcs
├── 1b23_12-78-36.mrcs
├── 1dfo_10-119-219.mrcs
├── ...
├── classes.csv # optional, can live anywhere
├── affinity.csv # required for training, can live anywhere
└── test/ # optional held-out data
├── 1fcj_3-10-20.mrcs
└── ...
A square CSV file. The header row holds the class names, and the rows follow
the same order as the columns. Value (i, j) is the similarity between classes
i and j, in the range -1 to 1, with 1 on the diagonal:
a,b,c
1.0,0.14,-0.28
0.14,1.0,0.71
-0.28,0.71,1.0
It can contain more classes than you train on. Every training class must be in it, or the run stops with an error naming the missing classes.
A CSV file whose header row is the list of classes to train on, e.g.
1b23,1dfo,1dkg,1e3p. Files of other classes in datapath are ignored (a
good way to hold classes back for evaluation). If you leave this out, every
class found in datapath is used.
Example datasets ship with the repository: tests/testdata_mrc (3D, 4 training
classes, plus a test/ folder that includes two unseen classes) and
tests/testdata_npy (2D). The MNIST tutorial shows how
to build a dataset from scratch.
From the repository root, with the environment active, train for 10 epochs on the bundled 3D test data:
python run.py \
--datapath tests/testdata_mrc \
--affinity tests/testdata_mrc/affinity_fsc_10.csv \
--classes tests/testdata_mrc/classes.csv \
--epochs 10 --batch 8 \
--out ~/avae_runs --new_outThis takes about 20 seconds on one GPU. With no GPU it falls back to the CPU
and logs a warning. On a machine with several GPUs it uses all of them by
default; add --gpu_devices 0 to use just one.
What the options do:
--datapath,--affinity,--classes: the inputs described above.--epochs 10 --batch 8: a short run with small batches (the test set is tiny).--out ~/avae_runs: write all outputs under this directory (it is created if missing). Without--out, outputs go to the current directory.--new_out: create a fresh, descriptively named run directory inside--out.
Everything else uses its default: a cnn model with 7 latent and 3 pose
dimensions, beta=0.1, gamma=2, an 80/20 train/validation split, and
loss, accuracy, reconstruction and embedding plots every 10 epochs.
While it runs, each epoch logs its losses, for example:
Training : Epoch: [10/10] | Loss: ... | Recon: ... | KLdiv: ... | Affin: ... | Beta: 0.100000 | Gamma: 2.000000
Validation : Epoch: [10/10] | Loss: 0.499 | Recon: 0.009 | KLdiv: 0.182 | Affin: 0.236 | ...
------------------->>> Overall accuracy: Train: 1.000000 | Val: 0.666667
When it finishes you will find (with <ts> the run timestamp, YYMMDD-HHMM):
~/avae_runs/
└── results_<ts>_model_cnn_lat7_pose3_lr0.001_beta0.1_gamma2.0/
├── configs/config_<ts>.yaml # the full resolved config: re-run with --config_file
├── logs/avae_run_log_<ts>.log # everything printed to the console
├── latents/latent_epoch_10.html # interactive latent-space explorer (open in a browser)
├── states/
│ ├── avae_<ts>_E9_7_3.pt # model checkpoint (epoch index 9 = 10th epoch)
│ └── meta_<ts>_E9_L7_P3.pkl # latents + metadata of every sample, needed for evaluation
└── plots/
├── loss.png, loss_total.png # loss curves (all terms / total only)
├── confusion_trn_norm.png # normalised confusion matrices (+ .csv, raw and normalised)
├── confusion_val_norm.png
├── f1.png, f1_trn.csv, f1_val.csv
├── recon_trn_in.png / _out.png # input vs reconstruction, central slices
├── recon_trn_in.mrc, recon_trn_out.mrc, recon_trn.csv # 3D input and reconstruction grids (same layout) and which sample is where
├── recon_val_* # same for validation
├── embedding_TSNE.png # t-SNE of the latent space
├── embedding_TSNE_pose.png # t-SNE of the pose space
├── best_loss_epoch0010_<ts>/ # per-epoch plots (not loss/F1) from the best-validation-loss epoch
└── best_acc_epoch0010_<ts>/ # per-epoch plots (not loss/F1) from the best-validation-accuracy epoch
The Output directory structure section lists every file a run can produce.
To evaluate the trained model on the test/ data (including two classes it has
never seen):
python run.py --evaluate \
--datapath tests/testdata_mrc \
--out ~/avae_runs/results_<ts>_model_cnn_lat7_pose3_lr0.001_beta0.1_gamma2.0This writes *_eval plots next to the training plots. See
Evaluating a trained model.
Every parameter in All parameters can be set in three places. Later ones override earlier ones:
- Built-in defaults, from
avae/config.py. - A YAML config file, given with
--config_file path.yml. - The command line, as
--name value(or just--namefor an on/off option).
# all from a config file
python run.py --config_file tests/avae-test-config.yml
# config file, with a few values overridden
python run.py --config_file my_config.yml --epochs 200 --beta 0.5 --new_outWhen the command line overrides a value from the file, the log shows a
Command line argument ... is overwriting config value warning. Unknown keys and
invalid values (for example model: xyz or vis_class: nosuchclass) stop the run
before training begins, with a message naming the problem.
On/off options can only be switched on from the command line. Writing
--vis_recsets it to true. To turn off an option that is on by default (gpu,shift_min,no_val_drop,vis_best,vis_los,vis_acc,vis_rec,vis_emb,vis_dynamic), set it tofalsein a config file.filters(a list) can also only be set in a config file.
Each run saves the fully resolved config (file, command line and defaults
combined, with absolute paths) to configs/config_<ts>.yaml. Passing that file
back with --config_file reproduces the run.
Start from tests/avae-test-config.yml or tutorials/mnist_data/mnist_config.yml, or from this one, which sets the commonly changed options:
#### Data
datapath: /data/subtomos # directory of .mrc/.npy files, optional test/ subdir
affinity: /data/affinity.csv # required for training
classes: /data/classes.csv # optional: train only on these classes
datatype: mrc # mrc or npy
split: 20 # % of the data used for validation
limit: null # cap on the number of files loaded (null = all)
#### Pre-processing
shift_min: true # min-max scale each sample to [0, 1]
normalise: false
gaussian_blur: false
rescale: null # e.g. 32 resizes every axis to 32 pixels
#### Network
model: cnn # cnn or gsd
latent_dims: 8
pose_dims: 1 # 0 disables the pose channel
depth: 3 # number of conv layers ...
channels: 64 # ... and channels in the first one
# filters: [8, 16, 32, 64] # or give channels per layer explicitly
bnorm_encoder: true
bnorm_decoder: true
#### Training
epochs: 500
batch: 128
learning: 0.001
opt_method: adam
classifier: NN
gpu: true
gpu_devices: "0,1" # null = all visible GPUs
freq_sta: 10 # save a checkpoint every N epochs
freq_eval: 10 # encode test/ every N epochs
#### Loss
beta: 1
gamma: 2
recon_loss: MSE
cyc_method_beta: cycle_sigmoid # anneal beta from beta_min to beta
beta_min: 0
beta_cycle: 5
cyc_method_gamma: flat # constant gamma
#### Visualisation
vis_all: true # switch on every plot type ...
freq_all: 20 # ... every 20 epochs (0 = only at best epochs)
vis_format: png
vis_best: true
best_start: 50 # start tracking best epochs at epoch 50
vis_class: "1b23,1dfo" # extra latent/pose traversals per listed classPaths can be relative, but they are resolved against the directory you run from, so absolute paths are safest.
python run.py \
--datapath tests/testdata_npy --datatype npy \
--affinity tests/testdata_npy/affinity_an.csv \
--classes tests/testdata_npy/classes.csv \
--epochs 12 --batch 16 --latent_dims 8 --pose_dims 1 \
--vis_all --freq_all 6 --best_start 10 --vis_epochs \
--vis_class a,e \
--out ~/avae_runs --new_outThis turns on every plot at epochs 6 and 12, plus at each new best epoch from
epoch 10. It also keeps a copy of each epoch's plots under
plots/epochs_<ts>/ (--vis_epochs) and checkpoints at epochs 6 and 12 (--freq_all also sets
freq_sta and freq_eval).
All of these are available as --name on the command line and as name: in a
config file. Run python run.py --help for the same list with defaults. The
On column marks on/off options and their defaults.
| Parameter | Default | Description |
|---|---|---|
datapath |
required | Data directory (see Preparing your data). If it has a test/ subdirectory, that is used as the test set. |
affinity |
none | Affinity matrix CSV. Required for training, ignored by evaluation. |
classes |
none | CSV whose header row lists the classes to train on. Default: all classes in datapath. Evaluation uses it to order the classes in its plots. |
datatype |
mrc |
mrc or npy. |
limit |
none | Load at most this many files (taken after class filtering and a random shuffle). Applies to the test set too. |
split |
20 |
Percentage of the data held out for validation. |
no_val_drop |
on | Keep the last validation batch even if it is smaller than batch. |
out |
none | Directory for all outputs, created if missing. Default: the current directory. For evaluation, set it to the training run directory. |
new_out |
off | Create results_<ts>_model_<model>_lat<L>_pose<P>_lr<lr>_beta<b>_gamma<g>/ (inside out if set) and write everything there. |
date_time_run |
now (YYMMDD-HHMM) |
Run identifier used in all output file names. Set it to give a run a fixed name (e.g. mnist-tutorial). Two runs started in the same minute in the same directory share it. |
Applied to each sample as it is loaded, in this order: rescale, shift, blur, normalise.
| Parameter | Default | Description |
|---|---|---|
gaussian_blur |
off | Gaussian blur to reduce noise (fixed kernel). |
normalise |
off | Normalise each sample. |
shift_min |
on | Min-max scale each sample to [0, 1]. Keep this on with recon_loss: BCE. |
rescale |
none | Resize every axis to this many pixels (e.g. 32). |
| Parameter | Default | Description |
|---|---|---|
model |
cnn |
cnn (convolutional decoder) or gsd (Gaussian-splat decoder). u, a and b are accepted only for evaluating or restarting old checkpoints. |
latent_dims |
7 |
Size of the latent (shape) vector. |
pose_dims |
3 |
Size of the pose vector. 0 turns pose off. |
depth |
3 |
Number of convolutional layers. |
channels |
64 |
Channels in the first layer (doubling with each layer). |
filters |
none | Channels per layer, e.g. [8, 16, 32, 64]. Overrides depth and channels. Config file only. |
bnorm_encoder |
off | Batch normalisation in the encoder. |
bnorm_decoder |
off | Batch normalisation in the decoder. |
n_splats |
128 |
gsd only: number of Gaussian splats. |
gsd_conv_layers |
0 |
gsd only: number of output channels of the convolution layers added after the splat decoder (0 = no convolution layers). |
| Parameter | Default | Description |
|---|---|---|
epochs |
100 |
Number of epochs. |
batch |
16 |
Batch size (per GPU when using several GPUs). |
learning |
0.001 |
Learning rate. |
opt_method |
adam |
adam, sgd or asgd. |
classifier |
NN |
Classifier fitted on the training latents to measure accuracy: NN (scikit-learn MLP, adam solver), KNN (k-nearest neighbours) or LR (logistic regression). All are tuned by grid search on macro F1. |
gpu |
on | Use CUDA GPUs if present, otherwise CPU. |
gpu_devices |
none | Comma-separated GPU indices, e.g. 0,1,3. Default: all visible, working GPUs. |
strategy |
auto |
Lightning multi-GPU strategy: auto, ddp, deepspeed or fsdp. Falls back to auto on a single GPU. |
freq_sta |
10 |
Save a checkpoint and metadata file to states/ every N epochs. |
freq_eval |
10 |
Encode the test/ set every N epochs (if there is one). |
| Parameter | Default | Description |
|---|---|---|
beta |
0.1 |
Weight of the KL term (the maximum when annealing). |
gamma |
2 |
Weight of the affinity term (the maximum when annealing). |
recon_loss |
MSE |
MSE, or BCE for data in [0, 1]. |
klreduction |
mean |
mean or sum reduction of the KL term. |
The same options exist for gamma: replace beta with gamma in each name.
| Parameter | Default | Description |
|---|---|---|
cyc_method_beta |
flat |
flat (constant at beta), cycle_linear, cycle_sigmoid, cycle_cosine (repeat a rise from beta_min to beta beta_cycle times), or ramp (a single rise lasting epochs / beta_cycle × beta_ratio epochs, then constant). |
beta_min |
0 |
Starting value of each cycle. |
beta_cycle |
4 |
Number of cycles over the run. |
beta_ratio |
0.5 |
Fraction of each cycle spent rising; the rest stays at beta. |
beta_load |
none | .npy file of per-epoch values (length must equal epochs). Overrides all the above. tools/create_scheduling_func.ipynb helps build one. |
Use --vis_cyc to plot the resulting schedules.
| Parameter | Default | Description |
|---|---|---|
evaluate |
off | Evaluate a trained model instead of training (see below). |
restart |
off | Resume training from a checkpoint. Currently broken, see Known limitations. |
state |
none | Checkpoint (.pt) to load. Default: the newest one in states/. |
meta |
none | Metadata file (.pkl) to load for evaluation. Default: the newest one in states/. |
Plots marked per-epoch are drawn every freq_<name> epochs (counting from
1), at the best epochs when vis_best is on, and at no other time. With
freq_<name>: 0 a plot is only drawn at the best epochs (see
Visualisation: frequencies).
| Parameter | Default | When | Output (in plots/ unless noted) |
|---|---|---|---|
vis_los |
on | every epoch from epoch 2 | loss, loss_total |
vis_acc |
on | per-epoch (freq_acc) |
confusion_{trn,val}_norm + CSVs, f1 + f1_{trn,val}.csv |
vis_rec |
on | per-epoch (freq_rec) |
recon_{trn,val}_in/_out; for 3D also recon_{trn,val}_in.mrc / _out.mrc (same grid layout) + .csv |
vis_emb |
on | per-epoch (freq_emb) |
embedding_TSNE, embedding_TSNE_pose |
vis_dynamic |
on | per-epoch (freq_dynamic) |
latents/latent_epoch_<N>.html (interactive; large for big datasets) |
vis_sim |
off | per-epoch (freq_sim) |
similarity_mean_{trn,val}: mean cosine similarity between classes in latent space |
vis_int |
off | per-epoch (freq_int) |
interpolations_trn: 10x10 latent grid between real encodings of 4 random classes; with only 2 or 3 classes, a row of 10 steps between 2 classes (skipped with 1 class). Pose fixed to the first corner's real pose |
vis_dis |
off | per-epoch (freq_dis) |
disentanglement-latent_avg_trn: traversal of each latent dimension, from -2 to +2 standard deviations in 15 steps (+ disentanglement-latent_<class>_trn with vis_class) |
vis_pos |
off | per-epoch (freq_pos) |
disentanglement-pose_avg_trn: traversal of each pose dimension, same range (+ disentanglement-pose_<class>_trn with vis_class) |
vis_his |
off | once | classes_distribution_{train,validation,evaluation} |
vis_aff |
off | once | affinity_matrix (training classes highlighted) |
vis_cyc |
off | once | hyperaparam_beta, hyperaparam_gamma |
vis_all |
none | — | Set to on/off to switch every option above at once. Options you give on the command line take precedence. |
For 3D data, the interpolation, traversal and pose plots are saved as .mrc
grids (open them in Napari or ChimeraX), plus a PNG (vis_format) of the central
slice of every volume in the same grid, with PNG_EDGE_CROP (3) pixels cropped
from each in-plane edge. The .mrc is not cropped.
| Parameter | Default | Description |
|---|---|---|
vis_format |
png |
png, pdf or svg. Vector formats stay sharp and are fastest for large matrices. |
vis_print |
off | Render matrix plots at 300 dpi (print quality) instead of screen resolution. |
vis_best |
on | Whenever the validation loss or validation accuracy reaches a new best (from best_start on), draw every enabled per-epoch plot regardless of freq_*, into plots/best_loss_epochXXXX_<ts>/ or plots/best_acc_epochXXXX_<ts>/. Only the latest folder of each kind per run is kept. Validation accuracy is computed every epoch from best_start on, which costs some time per epoch. |
best_start |
10 |
First epoch considered by vis_best. A warning is logged if it is later than epochs. |
vis_epochs |
off | Also keep every epoch's plots in plots/epochs_<ts>/epoch_XXXX/ (plots/ still holds the latest). |
vis_class |
none | Comma-separated classes, e.g. 1b23,1dfo. With vis_dis, one latent traversal figure per class; with vis_pos, one pose traversal figure per class (from the class mean latent and the class pose mean and spread). A class not in the data stops the run when the plot is first made. |
The classes in vis_class are checked when the first per-class plot is made; a class that is not in the data stops the run with an error listing the available classes. A class with a single sample or no spread is still plotted, with a warning.
freq_acc, freq_rec, freq_emb, freq_dynamic, freq_sim, freq_int,
freq_dis and freq_pos (all default 10) set how often, in epochs, each
per-epoch plot is drawn. freq_all sets all of them, and freq_sta and
freq_eval, to one value. As with vis_all, values given explicitly on the
command line take precedence.
A frequency of 0 turns the periodic plot off. With vis_best on, that plot
is then drawn only when the validation loss or accuracy reaches a new best
(from best_start). To get only best-epoch plots:
vis_best: true
best_start: 10
freq_all: 0 # per-epoch plots only at best epochsfreq_all: 0 does not touch freq_sta or freq_eval, so checkpoints and test
encoding keep their own schedule (test points only appear in a best-epoch
embedding if freq_eval lands on that epoch). The loss plot (vis_los) has no
frequency and is redrawn every epoch. With vis_best off, a plot with
frequency 0 is never drawn and a warning is logged.
| Parameter | Default | Description |
|---|---|---|
config_file |
none | YAML config file (command line only). |
debug |
off | Verbose (DEBUG) logging. |
All outputs go to the run directory: --out, then the results_... folder if
--new_out is set, otherwise the current directory. A full training run
produces:
<run dir>/
├── configs/
│ └── config_<ts>.yaml # resolved config of each run (one file per <ts>)
├── logs/
│ └── avae_run_log_<ts>.log # full log (appended if <ts> repeats)
├── states/ # every freq_sta epochs
│ ├── avae_<ts>_E<e>_<L>_<P>.pt # checkpoint: weights, model object, optimiser, loss
│ │ # history, best epochs. <e> is 0-based: E9 = epoch 10
│ └── meta_<ts>_E<e>_L<L>_P<P>.pkl # DataFrame: one row per train/val/test sample with
│ # file name, class, mode, latent + pose vectors, images
├── latents/
│ └── latent_epoch_<N>.html # interactive Altair latent explorer (hover for images)
└── plots/
├── loss.<fmt>, loss_total.<fmt> # updated every epoch
├── f1.<fmt>, f1_trn.csv, f1_val.csv
├── affinity_matrix.<fmt> # vis_aff (+ affinity_matrix_classes.txt for caching)
├── classes_distribution_*.<fmt> # vis_his
├── hyperaparam_{beta,gamma}.<fmt> # vis_cyc
├── <latest per-epoch plots> # confusion_*, recon_*, embedding_*, similarity_*,
│ # interpolations_*, disentanglement-*
├── best_loss_epochXXXX_<ts>/ # vis_best: per-epoch plots at the best val-loss epoch
├── best_acc_epochXXXX_<ts>/ # vis_best: per-epoch plots at the best val-accuracy epoch
└── epochs_<ts>/epoch_XXXX/ # vis_epochs: per-epoch plots of every plotted epoch
Notes:
<ts>isdate_time_run;<L>/<P>arelatent_dims/pose_dims;<fmt>isvis_format;XXXXis the 1-based epoch, zero-padded.- Per-epoch plots are overwritten in
plots/each time they are drawn, soplots/always holds the latest set. Usevis_epochsto keep every epoch's plots. - Loss, F1 and the once-per-run plots are only written to
plots/, never into thebest_*orepochs_*folders. - Running several trainings in the same directory is safe: each run's
best_*andepochs_*folders carry its own<ts>and are not removed by other runs. - Evaluation adds
*_evalfiles to the same directory (see below). latents/*.htmlfiles embed a thumbnail of every sample and can become very large; considervis_dynamic: falsefor big training runs and switch it on for evaluation.
Evaluation encodes new data with a trained model, compares it with the
training latent space and writes evaluation plots. Run it in the training run
directory, either with --out <run dir> or by changing into it:
python run.py --evaluate \
--datapath /data/new_subtomos \
--out ~/avae_runs/results_<ts>_...To reuse every training setting (data type, pre-processing, vis_*), pass the
saved config and override what changes:
cd ~/avae_runs/results_<ts>_...
python /path/to/affinity-vae/run.py --config_file configs/config_<ts>.yaml \
--evaluate --datapath /data/new_subtomosThe saved config keeps the training date_time_run, out and new_out, so
this evaluation overwrites configs/config_<ts>.yaml and appends to the
training log. When it uses new_out, it finds the run directory by rebuilding
its name, so do not override any parameter that is part of that name
(date_time_run, model, latent_dims, pose_dims, learning, beta,
gamma). Otherwise it opens a new, empty directory with no states/.
How it works:
- Data: everything in
datapath. Ifdatapathhas atest/subdirectory, that subdirectory is used instead. Labels come from file names as usual. Classes unseen in training are allowed and reported separately. - Model:
--state, or by default the neweststates/*.pt(files withevalin the name are skipped). The number of pose dimensions is read from the checkpoint file name, so keep the original names. - Training latents:
--meta, or by default the neweststates/*.pkl. - Pre-processing options (
shift_min,normalise, ...) must match training. The saved config takes care of this. - GPU: evaluation runs on one device: the first index in
gpu_devices, or GPU 0.
Outputs (in the same run directory):
plots/confusion_trn_eval_norm.<fmt>, confusion_eval_norm.<fmt> (+ .csv)
plots/f1_eval.<fmt>, f1_trn_eval.csv, f1_eval.csv
plots/embedding_TSNE_eval.<fmt> # evaluation data only
plots/embedding_TSNE_trn_eval_comparison.<fmt> # training + evaluation together
plots/recon_eval_in/_out.<fmt> (+ recon_eval_in.mrc/_out.mrc/.csv for 3D)
plots/*_eval* # dis/pos/int/sim plots if enabled
latents/latent_epoch_1_eval.html
states/meta_<ts>_E<e>_L<L>_P<P>_eval.pkl # training metadata + evaluation rows
The log reports accuracy on the training set, on seen classes only, and on all classes including unseen ones:
Overall accuracy: Train: 1.000000 | Val : 1.000000 | Val with unseen labels: 0.300000
With gpu: true and no gpu_devices, all visible GPUs are checked and every
working one is used, in a data-parallel run through Lightning Fabric
(strategy: auto normally selects DDP). batch is per GPU, so the training set
must hold at least batch × number of GPUs samples. Plots and checkpoints are
written by rank 0 only. More than 4 devices are spread over nodes assuming 4
GPUs per node (as on Baskerville).
On a SLURM cluster, edit and submit tools/slurm_run.sh:
sbatch tools/slurm_run.shIt loads the Baskerville modules and runs
srun python run.py --config_file <config> --new_out. Change the account,
QoS, environment path and config path at the top of the script first.
scripts/run_napari_model_view.py opens an interactive Napari viewer for a trained model: decode any point of the latent embedding, and move along individual latent and pose dimensions to see the decoder's reconstruction.
Napari needs pydantic<2, while the training code needs pydantic>2. They
cannot share one environment, so install the viewer in its own virtual
environment. Do not use it for training, and do not install [napari] into
your training environment. From the repository root:
python -m venv env_napari
source env_napari/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[napari]"The napari extra also installs umap-learn, which is not a dependency of the
training environment.
python scripts/run_napari_model_view.py \
--model_file <run>/states/<model>.pt \
--meta_file <run>/states/<meta>.pkl--model_file: a checkpoint from the run'sstates/directory (the best or the latest one).--meta_file: the metadata.pklfrom the same run (also instates/). It holds each sample'sid, latents (lat*), poses (pos*) and stored 2D embedding (emb*).--manifold umap|load(defaultumap):umapfits a UMAP on the stored latents when the viewer starts. A click anywhere on the map is decoded through UMAP'sinverse_transform, so you can explore regions with no data. It is slower, and the map differs from the t-SNE plots made in training. t-SNE cannot be inverted, so it is not offered.loaduses the embedding stored in the meta file and snaps a click to the nearest stored sample, whose latent is decoded. It is fast and needs noumap-learn, but only reproduces latents that exist in the data.
--latent_dims,--pose_dims: optional integers that override the values read from the model (encoder.mu/encoder.pose_fc). Set them if the model has no such attributes.
- Latents tab: one slider per latent dimension, ranging over the min and max of that dimension in the meta file. Moving one re-decodes the volume.
- Pose group, depending on
pose_dims:4: axis-angle control. Athetaslider (0 to 2 pi) and anaxeschoice (X, Y or Z) for the rotation axis.1(or any other value): one slider per pose dimension (pose0, ...), ranging over the values of that pose column in the meta file. With the splat decoder, a single pose value is a rotation about z.0: no pose controls.
- Manifold tab: a density map of the embedding with class labels at the class centroids. Click to decode that point; the latent sliders jump to the decoded latent.
Here is the interface on a model trained on the Shrec protein dataset:

ImportError: ... umap-learn: you are in an environment without thenapariextra. Runpip install -e ".[napari]", or use--manifold load.pydanticornapariimport errors, or training failing in this environment: you mixed the environments. Use the Napari one only for the viewer.Model does not have pose/latent attributes: pass--pose_dimsand--latent_dims.- Blank or wrong-shaped reconstruction:
--pose_dims/--latent_dimsdo not match the trained model.
affinity-vae/
├── run.py # command-line entry point (train or evaluate)
├── avae/ # the package
│ ├── config.py # AffinityConfig: every parameter, default and check
│ ├── train.py # training loop, best-epoch tracking, checkpoints
│ ├── evaluate.py # evaluation of a trained model
│ ├── data.py # data loading (via caked), affinity matrix, labels
│ ├── models.py # model selection (cnn / gsd)
│ ├── base.py # model base classes
│ ├── encoders/, decoders/ # network building blocks
│ ├── loss.py # reconstruction + KL + affinity loss
│ ├── cyc_annealing.py # beta / gamma schedules
│ ├── utils_learning.py # classifiers, accuracy, t-SNE, metadata tables
│ ├── utils_gpu.py # CPU / GPU / multi-GPU setup
│ ├── vis.py # all plots
│ ├── utils_vis.py # plot folders, saving, grids
│ └── napari.py # Napari viewer widgets
├── configs/ # example config for tools/create_subtomo.py
├── tests/ # pytest suite, example config, test datasets
│ ├── avae-test-config.yml
│ ├── testdata_mrc/ # small 3D dataset (+ test/ with unseen classes)
│ └── testdata_npy/ # small 2D dataset
├── tutorials/ # MNIST tutorial (README, data builder, config)
├── scripts/ # Napari viewer, subtomogram creation
├── tools/ # SLURM script, data prep, schedule and plot notebooks
└── images/ # README images
tools/create_subtomo.py (run through scripts/run_create_subtomo.py with
configs/avae-create_subtomo_test_config.yml) cuts, filters and augments
subtomograms from full tomograms.
tools/plotting_post_process.ipynb helps
analyse run outputs.
From the repository root with the [test] extra installed:
python -m pytest -q tests # full suite (~2.5 min)
python -m pytest -q tests/test_vis.py -k <name> # a subset
pre-commit run --files <changed files> # black, isort, mypy, file checksTests marked integration run short end-to-end trainings. Pytest is set up in
pyproject.toml to turn warnings into errors, apart from a list of known ones.
--restartis broken: resuming training stops with aNameError(thestatevariable is not set inavae/train.py). Until this is fixed, start a new run instead.- On/off options that default to on, and
filters, can only be changed in a config file (see Three ways to set parameters). - Class labels must be the file-name prefix before the first
_. - Training needs an affinity matrix. Without one, the run stops with an error.