Latent space explorer

Walk the two-number map a variational autoencoder learned from handwritten digits: decode any point, morph between digits, place your own drawing, and try latent arithmetic.

IntermediateExplained in Autoencoders and latent spaces

Both models squeeze each 28 × 28 MNIST digit through just two numbers and rebuild it. Every point on the map is a pair of numbers, so every point decodes to an image. The variational autoencoder (VAE) and the plain autoencoder have the same layers; only the training objective differs. They run in your browser from a 629 KB weight file.

Explore a latent space

Two autoencoders squeezed 60,000 handwritten digits through two numbers. Every point on this map decodes to an image.

Tool

Loading the autoencoders (700 KB)...

Both axes run from −3 to 3: three standard deviations of the prior.

Decoded from the point

Model
Map shows
Point(0.00, 0.00)
Prior samples nearer the center<1%

Challenges

0 of 4 complete

  1. Find a border: a spot where the 10 nearest test digits are split between two digits, at least 4 of each. Look at what the decoder draws there. (not yet)
  2. Morph between two different digits whose straight path passes through a third digit's region. (not yet)
  3. Draw a digit that lands where at least 8 of its 10 nearest test digits are the same digit. (not yet)
  4. Switch to the plain autoencoder and find its 3s: a spot where at least 5 of the 10 nearest test digits are 3s. (not yet)

Try this

  • Drag the point across the decoded digits. Shapes change gradually; there are no hard edges between digits.
  • Switch the map to Test digits to see where the encoder put 2,000 real test digits, each drawn as its numeral.
  • Choose Morph, pick a start and an end, and press Walk the line. Try a 1 to a 0.
  • Choose Draw, draw a digit, and see where it lands and how it looks rebuilt from two numbers.
  • Choose Add and add the “more slant” direction to a 1, then to a 9.
  • Switch to the Plain autoencoder and compare its map.
The VAE's sampling step and loss in NumPyPython with NumPy
import numpy as np

rng = np.random.default_rng(0)

# What an encoder outputs for one image: a mean and a log-variance per latent axis.
mu = np.array([0.8, -1.2])
logvar = np.array([-2.0, -1.5])
sigma = np.exp(0.5 * logvar)

# Reparameterization trick: sample noise separately, then shift and scale it.
# The randomness sits outside the network, so gradients reach mu and sigma.
eps = rng.standard_normal(2)
z = mu + sigma * eps
print("z =", np.round(z, 3))

# KL term: how far N(mu, sigma^2) is from the prior N(0, 1), in nats.
kl = 0.5 * np.sum(mu**2 + sigma**2 - 1 - logvar)
print("KL =", round(kl, 3))

# Reconstruction term: binary cross-entropy between the image and the decoder's output.
x = np.array([0.0, 1.0, 0.9, 0.0])       # four pixel intensities
x_hat = np.array([0.1, 0.8, 0.7, 0.2])   # what the decoder drew
bce = -np.sum(x * np.log(x_hat) + (1 - x) * np.log(1 - x_hat))
print("reconstruction =", round(bce, 3))
print("loss =", round(bce + kl, 3))
z = [ 0.846 -1.262]
KL = 1.969
reconstruction = 0.993
loss = 2.962

Try "embedding", "softmax", "overfitting", or "backpropagation".