Neural networks used for unsupervised learning that aim to learn efficient codings of input data. They consist of an encoder that maps the input to a latent space and a decoder that reconstructs the input from the latent space.
Author
Benedict Thekkel
Intuition
An autoencoder learns to copy its input to its output, but it is forced to do so through a narrow bottleneck. Because the network cannot simply pass the data through unchanged, it must discover a compact, informative representation of the data.
The architecture has two halves:
Encoder\(f\): maps the input \(x\) to a lower-dimensional latent code \(z = f(x)\).
Decoder\(g\): reconstructs the input from the code, \(\hat{x} = g(z)\).
The model is trained to minimize the difference between \(x\) and \(\hat{x}\). Training is self-supervised: the target is the input itself, so no labels are required.
x --> [ Encoder ] --> z (latent / bottleneck) --> [ Decoder ] --> x_hat
The latent code \(z\) captures the salient structure of the data. If the bottleneck is smaller than the input (an undercomplete autoencoder), the network is forced to compress, which yields a useful representation for downstream tasks.
The Reconstruction Loss
The objective is to minimize a reconstruction loss measuring how far \(\hat{x}\) is from \(x\).
Mean Squared Error (MSE) is used for real-valued inputs (e.g. normalized pixels):
Binary Cross-Entropy (BCE) is used when inputs are in \([0, 1]\) and interpreted as probabilities (e.g. binarized MNIST). Here \(\hat{x}_i\) is a sigmoid output:
The full training objective is simply \(\min_{f, g} \; \mathbb{E}_{x}[\, L(x, g(f(x))) \,]\).
A Basic Fully-Connected Autoencoder
We flatten a 28x28 image into a 784-dim vector, compress it down to a small latent dimension, then reconstruct. Sigmoid on the output keeps reconstructions in \([0, 1]\).
For images, convolutions preserve spatial structure far better than fully-connected layers. The encoder uses strided Conv2d to downsample; the decoder uses ConvTranspose2d to upsample back to the original resolution.
A denoising autoencoder (DAE) corrupts the input with noise but trains the network to reconstruct the clean original. This prevents the model from learning a trivial identity mapping and forces it to capture robust, meaningful structure.
Key point: the loss compares the output to the clean target, not the noisy input.
def add_noise(x, noise_factor=0.3): noisy = x + noise_factor * torch.randn_like(x) # add Gaussian noisereturn torch.clamp(noisy, 0.0, 1.0) # keep valid pixel rangemodel = ConvAutoencoder()opt = torch.optim.Adam(model.parameters(), lr=1e-3)criterion = nn.MSELoss()fake_loader = [torch.rand(32, 1, 28, 28) for _ inrange(50)]model.train()for clean in fake_loader: noisy = add_noise(clean) # corrupt the input recon, _ = model(noisy) # feed the noisy version loss = criterion(recon, clean) # but compare against the CLEAN target opt.zero_grad(); loss.backward(); opt.step()print('final denoising loss:', loss.item())
final denoising loss: 0.0791814997792244
Sparse Autoencoders and the Bottleneck Idea
There are two ways to regularize an autoencoder so it learns something useful rather than the identity function:
Undercomplete: the latent dimension is smaller than the input. Compression itself is the regularizer. This is the standard bottleneck design above.
Overcomplete: the latent dimension is equal to or larger than the input. Without an extra constraint this can trivially copy the input, so we add a penalty.
A sparse autoencoder keeps a large hidden layer but penalizes activations so that only a few units fire for any given input. With an L1 penalty on the codes:
\[ L = L_{recon} + \lambda \, \sum_j |z_j| \]
This encourages a sparse, disentangled representation where each unit responds to a specific feature.
# Sparse penalty: add an L1 term on the latent activations to the reconstruction loss.model = Autoencoder(in_dim=784, latent_dim=128) # overcomplete-ish wide codeopt = torch.optim.Adam(model.parameters(), lr=1e-3)lam =1e-3# sparsity weightx = torch.rand(64, 784)x_hat, z = model(x)recon = F.mse_loss(x_hat, x)sparsity = z.abs().mean() # L1 on the codes -> encourages few active unitsloss = recon + lam * sparsityopt.zero_grad(); loss.backward(); opt.step()print(f'recon={recon.item():.4f} sparsity={sparsity.item():.4f}')
recon=0.0833 sparsity=0.0748
Variational Autoencoder (VAE)
A plain autoencoder learns arbitrary points in latent space, so sampling new data from it is unreliable. A Variational Autoencoder makes the latent space continuous and structured by having the encoder output a distribution instead of a single point.
The encoder predicts a mean \(\mu\) and log-variance \(\log \sigma^2\) for each input. We sample \(z \sim \mathcal{N}(\mu, \sigma^2)\) and decode it.
Reparameterization trick. Sampling is not differentiable, so we rewrite the sample as
The KL term pulls the latent distribution toward the prior, so we can later sample \(z \sim \mathcal{N}(0, I)\) and decode to generate brand new samples.
def vae_loss(x_hat, x, mu, logvar):# Reconstruction: sum BCE over pixels, averaged over the batch. recon = F.binary_cross_entropy(x_hat, x, reduction='sum')# KL divergence between N(mu, sigma^2) and N(0, I), closed form. kld =-0.5* torch.sum(1+ logvar - mu.pow(2) - logvar.exp())return (recon + kld) / x.size(0) # per-sample lossopt = torch.optim.Adam(model.parameters(), lr=1e-3)x = torch.rand(16, 784)x_hat, mu, logvar = model(x)loss = vae_loss(x_hat, x, mu, logvar)opt.zero_grad(); loss.backward(); opt.step()print('VAE loss:', loss.item())# Generate new samples: draw z from the prior and decode (no encoder needed).model.eval()with torch.no_grad(): z = torch.randn(8, 20) # z ~ N(0, I) samples = model.decode(z) # (8, 784) brand-new generated imagesprint(samples.shape)
VAE loss: 548.925048828125
torch.Size([8, 784])
Applications
Dimensionality reduction: the latent code is a compact feature vector for visualization or downstream classifiers (often better than PCA for nonlinear data).
Anomaly detection: train on normal data only; anomalies reconstruct poorly, so a high reconstruction error flags outliers.
Denoising / inpainting: recover clean signals from corrupted inputs.
Generation: VAEs (and their variants) sample new, realistic data from the prior.
Pretraining / representation learning: learn features without labels, then fine-tune.
Autoencoder vs PCA, and AE vs GAN
Autoencoder vs PCA. A linear autoencoder with a single layer and MSE loss recovers the same subspace as PCA. The power of autoencoders comes from nonlinear activations and depth, which let them capture curved manifolds that PCA (a linear projection) cannot. PCA is cheaper, deterministic, and yields orthogonal components; autoencoders are more flexible but need training and tuning.
AE/VAE vs GAN for generation. VAEs optimize a likelihood-based objective (the ELBO), train stably, and give an explicit encoder, but their samples tend to be blurry because of the averaging effect of pixel-wise reconstruction losses. GANs use an adversarial loss and produce sharper, more realistic samples, but training is unstable and they provide no encoder or explicit likelihood. In short: VAEs trade sample sharpness for stability and a structured latent space.
Key Takeaways
An autoencoder learns to reconstruct its input through a bottleneck, self-supervised.
The bottleneck (undercomplete) or a penalty (sparse) forces a useful representation.
Use MSE for continuous inputs, BCE for inputs in \([0, 1]\).
Convolutional AEs use Conv2d to downsample and ConvTranspose2d to upsample.
Denoising AEs reconstruct clean targets from corrupted inputs for robustness.
VAEs make the latent space continuous via the reparameterization trick and a KL term, enabling principled generation.
Autoencoders generalize PCA to nonlinear manifolds; VAEs trade sharpness for stable training versus GANs.