VAE User Guide
September 9, 2026 · View on GitHub
Complete guide to building, training, and using Variational Autoencoders with Artifex.
Overview
This guide covers practical usage of VAEs in Artifex, from basic setup to advanced techniques. You'll learn how to:
-
:material-cog: Configure VAEs
Set up encoder/decoder architectures and configure hyperparameters
-
:material-play: Train Models
Train VAEs with proper loss functions and monitoring
-
:material-creation: Generate Samples
Sample from the prior and manipulate latent representations
-
:material-tune: Tune & Debug
Optimize hyperparameters and troubleshoot common issues
Quick Start
Basic VAE Example
import jax.numpy as jnp
from flax import nnx
from artifex.generative_models.core.configuration.network_configs import (
EncoderConfig,
DecoderConfig,
)
from artifex.generative_models.core.configuration.vae_config import VAEConfig
from artifex.generative_models.models.vae import VAE
# Initialize RNGs
rngs = nnx.Rngs(params=0, dropout=1, sample=2)
# Configuration
latent_dim = 20
# Create encoder config
encoder_config = EncoderConfig(
name="mlp_encoder",
hidden_dims=(256, 128),
latent_dim=latent_dim,
activation="relu",
input_shape=(28, 28, 1), # Image shape
)
# Create decoder config
decoder_config = DecoderConfig(
name="mlp_decoder",
hidden_dims=(128, 256),
output_shape=(28, 28, 1),
latent_dim=latent_dim,
activation="relu",
)
# Create VAE config
vae_config = VAEConfig(
name="basic_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense", # Use MLP encoder/decoder
kl_weight=1.0,
)
# Create model
vae = VAE(config=vae_config, rngs=rngs)
# Forward pass
x = jnp.ones((32, 28, 28, 1))
outputs = vae(x) # Model uses internal RNGs
# Get outputs
reconstructed = outputs["reconstructed"]
mean = outputs["mean"]
log_var = outputs["log_var"]
latent = outputs["z"]
print(f"Reconstruction shape: {reconstructed.shape}")
print(f"Latent shape: {latent.shape}")
Creating VAE Models
Artifex uses a config-based API where you define configurations first, then create models from them. This provides type safety, validation, and easy serialization.
1. Encoder Configurations
MLP Encoder (Fully-Connected)
Best for tabular data and flattened images:
from artifex.generative_models.core.configuration.network_configs import EncoderConfig
from artifex.generative_models.models.vae.encoders import MLPEncoder
# Define encoder configuration
encoder_config = EncoderConfig(
name="mlp_encoder",
hidden_dims=(512, 256, 128), # Network depth (use tuples)
latent_dim=32, # Latent space dimension
activation="relu", # Activation function
input_shape=(784,), # Flattened input size
)
# Create encoder from config
encoder = MLPEncoder(config=encoder_config, rngs=rngs)
# Forward pass returns (mean, log_var)
mean, log_var = encoder(x)
CNN Encoder (Convolutional)
Best for image data with spatial structure:
from artifex.generative_models.core.configuration.network_configs import EncoderConfig
from artifex.generative_models.models.vae.encoders import CNNEncoder
encoder_config = EncoderConfig(
name="cnn_encoder",
hidden_dims=(32, 64, 128, 256), # Channel progression
latent_dim=64,
activation="relu",
input_shape=(28, 28, 1), # (H, W, C)
)
encoder = CNNEncoder(config=encoder_config, rngs=rngs)
# Preserves spatial information through convolutions
mean, log_var = encoder(x)
Conditional Encoder
Add class conditioning using ConditionalVAEConfig:
from artifex.generative_models.core.configuration.network_configs import EncoderConfig
from artifex.generative_models.core.configuration.vae_config import ConditionalVAEConfig
from artifex.generative_models.models.vae import ConditionalVAE
# ConditionalVAE handles label embedding and conditioning automatically.
# The encoder and decoder are created internally with conditional=True,
# so you only need to provide the base encoder/decoder configs:
cvae_config = ConditionalVAEConfig(
name="conditional_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_classes=10,
kl_weight=1.0,
)
cvae = ConditionalVAE(config=cvae_config, rngs=rngs)
# Integer labels are automatically one-hot encoded:
labels = jnp.array([0, 1, 2, 3]) # No need for manual one_hot
outputs = cvae(x, y=labels)
2. Decoder Configurations
MLP Decoder
from artifex.generative_models.core.configuration.network_configs import DecoderConfig
from artifex.generative_models.models.vae.decoders import MLPDecoder
decoder_config = DecoderConfig(
name="mlp_decoder",
hidden_dims=(128, 256, 512), # Reversed from encoder
output_shape=(784,), # Reconstruction size
latent_dim=32,
activation="relu",
)
decoder = MLPDecoder(config=decoder_config, rngs=rngs)
reconstructed = decoder(z) # Returns JAX array
CNN Decoder (Transposed Convolutions)
from artifex.generative_models.core.configuration.network_configs import DecoderConfig
from artifex.generative_models.models.vae.decoders import CNNDecoder
decoder_config = DecoderConfig(
name="cnn_decoder",
hidden_dims=(256, 128, 64, 32), # Reversed channel progression
output_shape=(28, 28, 1), # Output image shape
latent_dim=64,
activation="relu",
)
decoder = CNNDecoder(config=decoder_config, rngs=rngs)
reconstructed = decoder(z) # Returns (batch, 28, 28, 1)
Conditional Decoder
For conditional generation, use the full ConditionalVAE model which handles
conditioning in both encoder and decoder:
# See "Conditional VAE" section below for the full config-based approach
3. Complete VAE Models
Standard VAE
from artifex.generative_models.core.configuration.network_configs import (
EncoderConfig,
DecoderConfig,
)
from artifex.generative_models.core.configuration.vae_config import VAEConfig
from artifex.generative_models.models.vae import VAE
# Define configurations
encoder_config = EncoderConfig(
name="encoder",
hidden_dims=(256, 128),
latent_dim=32,
activation="relu",
input_shape=(28, 28, 1),
)
decoder_config = DecoderConfig(
name="decoder",
hidden_dims=(128, 256),
output_shape=(28, 28, 1),
latent_dim=32,
activation="relu",
)
vae_config = VAEConfig(
name="standard_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense", # "dense" for MLP, "cnn" for convolutional
kl_weight=1.0, # Beta parameter (1.0 = standard VAE)
)
# Create model
vae = VAE(config=vae_config, rngs=rngs)
β-VAE (Disentangled Representations)
from artifex.generative_models.core.configuration.vae_config import BetaVAEConfig
from artifex.generative_models.models.vae import BetaVAE
beta_config = BetaVAEConfig(
name="beta_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
beta_default=4.0, # Higher beta = more disentanglement
beta_warmup_steps=10000, # Gradual beta annealing
reconstruction_loss_type="mse", # "mse" or "bce"
)
beta_vae = BetaVAE(config=beta_config, rngs=rngs)
Conditional VAE
from artifex.generative_models.core.configuration.vae_config import ConditionalVAEConfig
from artifex.generative_models.models.vae import ConditionalVAE
cvae_config = ConditionalVAEConfig(
name="conditional_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_classes=10, # Number of classes for conditioning
kl_weight=1.0,
)
cvae = ConditionalVAE(config=cvae_config, rngs=rngs)
# Forward pass with condition (one-hot encoded labels)
labels = jax.nn.one_hot(jnp.array([0, 1, 2]), num_classes=10)
outputs = cvae(x, y=labels)
VQ-VAE (Discrete Latents)
from artifex.generative_models.core.configuration.vae_config import VQVAEConfig
from artifex.generative_models.models.vae import VQVAE
vqvae_config = VQVAEConfig(
name="vqvae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_embeddings=512, # Codebook size
embedding_dim=64, # Embedding dimension
commitment_cost=0.25, # Commitment loss weight
)
vqvae = VQVAE(config=vqvae_config, rngs=rngs)
Training VAEs
Basic Training Loop
import jax
import jax.numpy as jnp
import optax
from flax import nnx
from artifex.generative_models.core.configuration.network_configs import (
EncoderConfig,
DecoderConfig,
)
from artifex.generative_models.core.configuration.vae_config import VAEConfig
from artifex.generative_models.models.vae import VAE
# 1. Create synthetic data (replace with real data loading)
key = jax.random.key(42)
train_data = jax.random.uniform(key, (1000, 28, 28, 1))
# 2. Create model configuration
rngs = nnx.Rngs(params=0, dropout=1, sample=2)
encoder_config = EncoderConfig(
name="encoder",
hidden_dims=(256, 128),
latent_dim=32,
activation="relu",
input_shape=(28, 28, 1),
)
decoder_config = DecoderConfig(
name="decoder",
hidden_dims=(128, 256),
output_shape=(28, 28, 1),
latent_dim=32,
activation="relu",
)
vae_config = VAEConfig(
name="mnist_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
kl_weight=1.0,
)
# 3. Initialize model and optimizer
vae = VAE(config=vae_config, rngs=rngs)
optimizer = nnx.Optimizer(vae, optax.adam(learning_rate=1e-3), wrt=nnx.Param)
# 4. Training step (JIT-compiled for speed)
@nnx.jit
def train_step(model, optimizer, batch):
def loss_fn(model):
outputs = model(batch) # Model uses internal RNGs
losses = model.loss_fn(batch, outputs)
return losses["total_loss"], losses
(loss, losses), grads = nnx.value_and_grad(loss_fn, has_aux=True)(model)
optimizer.update(model, grads)
return losses
# 5. Training loop
batch_size = 32
num_epochs = 5
for epoch in range(num_epochs):
# Simple batching
for i in range(0, len(train_data), batch_size):
batch = train_data[i : i + batch_size]
losses = train_step(vae, optimizer, batch)
print(f"Epoch {epoch + 1} | Loss: {losses['total_loss']:.4f}")
Training β-VAE with Annealing
from artifex.generative_models.core.configuration.vae_config import BetaVAEConfig
from artifex.generative_models.models.vae import BetaVAE
beta_config = BetaVAEConfig(
name="beta_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
beta_default=4.0,
beta_warmup_steps=10000,
reconstruction_loss_type="mse",
)
beta_vae = BetaVAE(config=beta_config, rngs=rngs)
optimizer = nnx.Optimizer(beta_vae, optax.adam(learning_rate=1e-3), wrt=nnx.Param)
step = 0
for epoch in range(num_epochs):
for i in range(0, len(train_data), batch_size):
batch = train_data[i : i + batch_size]
def loss_fn(model):
outputs = model(batch)
# Pass current step for beta annealing
losses = model.loss_fn(batch, outputs, step=step)
return losses["total_loss"], losses
(loss, losses), grads = nnx.value_and_grad(loss_fn, has_aux=True)(beta_vae)
optimizer.update(beta_vae, grads)
step += 1
print(f"Epoch {epoch + 1}, Beta: {losses.get('beta', 1.0):.4f}")
Training Conditional VAE
from artifex.generative_models.core.configuration.vae_config import ConditionalVAEConfig
from artifex.generative_models.models.vae import ConditionalVAE
cvae_config = ConditionalVAEConfig(
name="conditional_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_classes=10,
kl_weight=1.0,
)
cvae = ConditionalVAE(config=cvae_config, rngs=rngs)
optimizer = nnx.Optimizer(cvae, optax.adam(learning_rate=1e-3), wrt=nnx.Param)
# Create synthetic labels (replace with real labels)
train_labels = jax.random.randint(jax.random.key(0), (1000,), 0, 10)
train_labels_onehot = jax.nn.one_hot(train_labels, num_classes=10)
for epoch in range(num_epochs):
for i in range(0, len(train_data), batch_size):
batch_x = train_data[i : i + batch_size]
batch_y = train_labels_onehot[i : i + batch_size]
def loss_fn(model):
outputs = model(batch_x, y=batch_y) # Condition on labels
losses = model.loss_fn(batch_x, outputs)
return losses["total_loss"], losses
(loss, losses), grads = nnx.value_and_grad(loss_fn, has_aux=True)(cvae)
optimizer.update(cvae, grads)
print(f"Epoch {epoch + 1} | Loss: {losses['total_loss']:.4f}")
Training VQ-VAE
from artifex.generative_models.core.configuration.vae_config import VQVAEConfig
from artifex.generative_models.models.vae import VQVAE
vqvae_config = VQVAEConfig(
name="vqvae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_embeddings=512,
embedding_dim=64,
commitment_cost=0.25,
)
vqvae = VQVAE(config=vqvae_config, rngs=rngs)
optimizer = nnx.Optimizer(vqvae, optax.adam(learning_rate=1e-3), wrt=nnx.Param)
for epoch in range(num_epochs):
for i in range(0, len(train_data), batch_size):
batch = train_data[i : i + batch_size]
def loss_fn(model):
outputs = model(batch)
losses = model.loss_fn(batch, outputs)
return losses["total_loss"], losses
(loss, losses), grads = nnx.value_and_grad(loss_fn, has_aux=True)(vqvae)
optimizer.update(vqvae, grads)
# VQ-VAE specific metrics
print(f"Epoch {epoch + 1} | Recon: {losses.get('reconstruction_loss', 0.0):.4f}")
Generating and Sampling
Generate New Samples
# Sample from prior distribution
n_samples = 16
samples = vae.sample(n_samples, temperature=1.0)
# Temperature controls diversity
hot_samples = vae.sample(n_samples, temperature=2.0) # More diverse
cold_samples = vae.sample(n_samples, temperature=0.5) # More focused
# Using generate() method (alias for sample)
samples = vae.generate(n_samples, temperature=1.0)
Conditional Generation
# Generate samples for specific classes
target_classes = jnp.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) # One of each digit
labels = jax.nn.one_hot(target_classes, num_classes=10)
samples = cvae.sample(n_samples=10, y=labels, temperature=1.0)
Reconstruction
# Stochastic reconstruction (uses internal RNGs)
reconstructed = vae.reconstruct(x, deterministic=False)
# Deterministic reconstruction (use mean of latent distribution)
deterministic_recon = vae.reconstruct(x, deterministic=True)
Latent Space Manipulation
Interpolation Between Images
# Linear interpolation in latent space
x1 = test_images[0:1] # First image (keep batch dim)
x2 = test_images[1:2] # Second image
interpolated = vae.interpolate(
x1=x1,
x2=x2,
steps=10, # Number of interpolation steps
)
# interpolated.shape = (10, *input_shape)
Latent Traversal (Disentanglement Analysis)
# Traverse a single latent dimension
x = test_images[0:1]
dim_to_traverse = 3 # Which latent dimension to vary
traversal = vae.latent_traversal(
x=x,
dim=dim_to_traverse,
range_vals=(-3.0, 3.0), # Range of values
steps=10, # Number of steps
)
# traversal.shape = (10, *input_shape)
Manual Latent Manipulation
# Encode image to latent space
mean, log_var = vae.encode(x)
# Manipulate specific dimensions
modified_mean = mean.at[:, 5].set(2.0) # Increase dimension 5
modified_mean = modified_mean.at[:, 10].set(-1.5) # Decrease dimension 10
# Decode modified latent
modified_image = vae.decode(modified_mean)
Evaluation and Analysis
Reconstruction Quality
# Calculate reconstruction error
test_batch = test_images[:100]
reconstructed = vae.reconstruct(test_batch, deterministic=True)
mse = jnp.mean((test_batch - reconstructed) ** 2)
print(f"Reconstruction MSE: {mse:.4f}")
ELBO (Evidence Lower Bound)
# Full ELBO calculation
outputs = vae(test_batch) # Model uses internal RNGs
losses = vae.loss_fn(test_batch, outputs)
elbo = -(losses['reconstruction_loss'] + losses['kl_loss'])
print(f"ELBO: {elbo:.4f}")
Latent Space Statistics
# Encode test set
all_means = []
all_logvars = []
batch_size = 32
for i in range(0, len(test_images), batch_size):
batch = test_images[i : i + batch_size]
mean, log_var = vae.encode(batch) # Uses internal RNGs
all_means.append(mean)
all_logvars.append(log_var)
all_means = jnp.concatenate(all_means, axis=0)
all_logvars = jnp.concatenate(all_logvars, axis=0)
# Statistics per dimension
mean_per_dim = jnp.mean(all_means, axis=0)
std_per_dim = jnp.std(all_means, axis=0)
variance_per_dim = jnp.exp(jnp.mean(all_logvars, axis=0))
print(f"Latent mean: {mean_per_dim}")
print(f"Latent std: {std_per_dim}")
print(f"Average variance: {variance_per_dim}")
Disentanglement Metrics
# Per-dimension KL divergence (detect posterior collapse)
def per_dim_kl(mean, log_var):
"""Calculate KL divergence per dimension."""
kl_per_dim = -0.5 * (1 + log_var - mean**2 - jnp.exp(log_var))
return jnp.mean(kl_per_dim, axis=0)
kl_per_dimension = per_dim_kl(all_means, all_logvars)
# Dimensions with very low KL likely collapsed
inactive_dims = jnp.sum(kl_per_dimension < 0.01)
print(f"Inactive dimensions: {inactive_dims}/{vae.latent_dim}")
Hyperparameter Tuning
Key Hyperparameters
# Architecture
config = {
# Network architecture
"latent_dim": 64, # 10-100 for images, 2-20 for simple data
"hidden_dims": [512, 256, 128], # Deeper for complex data
"activation": "relu", # or "gelu", "swish"
# Training
"learning_rate": 1e-3, # 1e-4 to 1e-3 typical
"batch_size": 128, # Larger is more stable
"num_epochs": 100,
# VAE-specific
"kl_weight": 1.0, # Beta parameter
"reconstruction_loss": "mse", # "mse" or "bce"
}
Beta Tuning for β-VAE
from artifex.generative_models.core.configuration.vae_config import BetaVAEConfig
from artifex.generative_models.models.vae import BetaVAE
# Grid search over beta values
beta_values = [0.5, 1.0, 2.0, 4.0, 8.0]
results = {}
for beta in beta_values:
# Create config with different beta
beta_config = BetaVAEConfig(
name=f"beta_vae_{beta}",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
beta_default=beta,
)
rngs = nnx.Rngs(params=0, dropout=1, sample=2)
model = BetaVAE(config=beta_config, rngs=rngs)
# Train and evaluate (implement your train/evaluate functions)
# trained_model = train(model, train_data, num_epochs=50)
# recon_error = evaluate_reconstruction(trained_model, test_data)
results[beta] = {"beta": beta}
# Find best trade-off
print(results)
Learning Rate Scheduling
import optax
# Cosine decay schedule
schedule = optax.cosine_decay_schedule(
init_value=1e-3,
decay_steps=num_train_steps,
alpha=0.1, # Final learning rate = 0.1 * init_value
)
optimizer = nnx.Optimizer(vae, optax.adam(learning_rate=schedule), wrt=nnx.Param)
Common Issues and Solutions
Problem 1: Posterior Collapse
Symptoms: KL divergence near zero, poor generation quality
Solutions:
from artifex.generative_models.core.configuration.vae_config import BetaVAEConfig
from artifex.generative_models.models.vae import BetaVAE
# Solution 1: Beta annealing - start with β=0, gradually increase
beta_config = BetaVAEConfig(
name="beta_annealing_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
beta_default=1.0,
beta_warmup_steps=10000, # Gradual warmup
)
beta_vae = BetaVAE(config=beta_config, rngs=rngs)
# Solution 2: Weaker decoder (make it harder to ignore latent)
# Use smaller hidden_dims in decoder than encoder
weak_decoder_config = DecoderConfig(
name="weak_decoder",
hidden_dims=(64, 128), # Smaller than encoder
output_shape=(28, 28, 1),
latent_dim=32,
activation="relu",
)
Problem 2: Blurry Reconstructions
Symptoms: Overly smooth outputs, lack of detail
Solutions:
# Solution 1: Lower kl_weight (emphasize reconstruction)
vae_config = VAEConfig(
name="sharp_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
kl_weight=0.5, # Lower than default 1.0
)
vae = VAE(config=vae_config, rngs=rngs)
# Solution 2: Use VQ-VAE (discrete latents often produce sharper outputs)
from artifex.generative_models.core.configuration.vae_config import VQVAEConfig
from artifex.generative_models.models.vae import VQVAE
vqvae_config = VQVAEConfig(
name="vqvae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
num_embeddings=512,
embedding_dim=64,
)
vqvae = VQVAE(config=vqvae_config, rngs=rngs)
Problem 3: Unstable Training
Symptoms: Loss oscillations, NaN values
Solutions:
# Solution 1: Gradient clipping
import optax
optimizer = nnx.Optimizer(
vae,
optax.chain(
optax.clip_by_global_norm(1.0), # Clip gradients
optax.adam(learning_rate=1e-3),
),
wrt=nnx.Param
)
# Solution 2: Lower learning rate
optimizer = nnx.Optimizer(vae, optax.adam(learning_rate=1e-4), wrt=nnx.Param)
# Solution 3: Batch normalization in encoder/decoder
# (implement custom encoder/decoder with normalization)
Problem 4: Poor Disentanglement
Symptoms: Latent dimensions don't correspond to interpretable factors
Solutions:
# Solution 1: Increase beta for more disentanglement
from artifex.generative_models.core.configuration.vae_config import BetaVAEConfig
from artifex.generative_models.models.vae import BetaVAE
beta_config = BetaVAEConfig(
name="high_beta_vae",
encoder=encoder_config,
decoder=decoder_config,
encoder_type="dense",
beta_default=4.0, # Higher beta encourages disentanglement
)
beta_vae = BetaVAE(config=beta_config, rngs=rngs)
# Solution 2: More latent dimensions - give model more capacity
# Update encoder_config with larger latent_dim
encoder_config_large = EncoderConfig(
name="encoder_large_latent",
hidden_dims=(256, 128),
latent_dim=128, # Increased from 32
activation="relu",
input_shape=(28, 28, 1),
)
Advanced Techniques
Custom Loss Functions
def custom_loss_fn(predictions, targets):
"""Custom reconstruction loss combining multiple terms.
Note: follows JAX/Optax convention — (predictions, targets) order.
"""
# L1 loss for sparsity
l1_loss = jnp.mean(jnp.abs(predictions - targets))
# L2 loss for overall quality
l2_loss = jnp.mean((predictions - targets) ** 2)
# Combine
return 0.5 * l1_loss + 0.5 * l2_loss
# Use in training
losses = vae.loss_fn(
batch,
outputs,
reconstruction_loss_fn=custom_loss_fn,
)
Multi-GPU Training
import jax
from jax import devices
# Check available devices
print(f"Available devices: {jax.devices()}")
# For multi-GPU training, build a mesh and shard batches with substrax
from substrax.mesh import DeviceMeshManager
from substrax.spmd import create_data_parallel_sharding, place_batch_on_shards
mesh = DeviceMeshManager.create_data_parallel_mesh()
sharding = create_data_parallel_sharding(mesh)
See the Distributed Training guide for the full loop.
Checkpointing
import orbax.checkpoint as ocp
# Create checkpointer
checkpointer = ocp.StandardCheckpointer()
# Save model state
graphdef, state = nnx.split(vae)
checkpointer.save("/tmp/vae_checkpoint", state)
# Load model state
restored_state = checkpointer.restore("/tmp/vae_checkpoint")
# Create new model and merge state
new_vae = VAE(config=vae_config, rngs=nnx.Rngs(0))
_, new_state = nnx.split(new_vae)
# Merge restored state into new model
nnx.update(new_vae, restored_state)
Best Practices
DO ✅
- Start simple: Begin with standard VAE before trying variants
- Monitor both losses: Track reconstruction AND KL divergence
- Use appropriate loss: MSE for continuous, BCE for binary data
- Visualize latent space: Plot 2D projections to check structure
- Test interpolation: Smooth interpolation indicates good latent space
- Check per-dim KL: Detect posterior collapse early
- Use beta annealing: Helps avoid posterior collapse
- Larger batch size: More stable training (128+ recommended)
DON'T ❌
- Don't ignore KL: Zero KL means model ignores latent code
- Don't use too small latent: Leads to underfitting
- Don't overtrain: Can lead to posterior collapse
- Don't skip validation: Regular evaluation prevents surprises
- Don't forget temperature: Use temperature for diverse sampling
- Don't compare different betas directly: Higher beta trades reconstruction for disentanglement
Performance Tips
Memory Optimization
# Use gradient checkpointing for large models
from jax import checkpoint
@checkpoint
def encoder_forward(encoder, x):
return encoder(x)
# Use lower precision for faster training
# Set precision in config or at JAX level
import jax
jax.config.update("jax_default_matmul_precision", "float32") # or "bfloat16"
Speed Optimization
# JIT compile training step
@nnx.jit
def fast_train_step(model, optimizer, batch):
def loss_fn(model):
outputs = model(batch) # Model uses internal RNGs
losses = model.loss_fn(batch, outputs)
return losses["total_loss"], losses
(loss, losses), grads = nnx.value_and_grad(loss_fn, has_aux=True)(model)
optimizer.update(model, grads)
return losses
# Vectorize sampling
vmapped_decode = jax.vmap(lambda z: vae.decode(z))
samples = vmapped_decode(latent_vectors)
Summary
This guide covered:
- ✅ Creating encoders, decoders, and VAE models
- ✅ Training standard VAE, β-VAE, CVAE, and VQ-VAE
- ✅ Generating samples and manipulating latent space
- ✅ Evaluation metrics and diagnostics
- ✅ Hyperparameter tuning strategies
- ✅ Troubleshooting common issues
- ✅ Advanced techniques and optimizations
Next Steps
- VAE Concepts — Deep dive into theory
- VAE API Reference — Complete API documentation
- VAE MNIST Example — Hands-on tutorial
- Trainer API — Low-level trainer interface
- Benchmarking — Evaluate your models