Autoencoders
Force data through a layer too narrow to hold it, then ask for the original back. Whatever survives the squeeze is the structure the data actually has.
Why Does This Exist?
A camera above a kiln photographs every ceramic floor tile coming off the line. Sixty-four pixels square, greyscale, 200,000 photos a week, none of them labelled. An inspector tagged 400 as cracked or fine, months ago, and that's your entire supervised dataset.
So 4096 numbers describe a tile. But tiles aren't 4096 different things. Same glaze, same grout spacing, same camera height — what varies is brightness, a little rotation, speckle density, and whether there's a crack. A dozen quantities; 4084 dimensions along for the ride.
An encoder here is nothing exotic: a stack of layers mapping numbers to numbers, a multi-layer perceptron. And dropping dimensions is already solved — PCA finds the flat subspace keeping the most variance and projects onto it.
Here's where flat runs out. Rotate a tile two degrees and every pixel changes, along a curve through pixel space, not a straight line. A flat subspace spends dimensions approximating that bend. You wanted the curve.
Think of It Like This
The card through the hatch
Two draughtsmen, a wall between them, one narrow slot. The first looks at a tile and writes 32 numbers on a card. The second, who never sees the tile, redraws it from the card alone. Score how close the redrawing comes, and repeat on this factory's tiles for a month.
They'll invent a shorthand. Number 3 becomes grout offset, number 11 becomes speckle density, because there's room for nothing else. Nobody handed them that vocabulary — the scoring produced it.
Two things follow, and both matter later. Widen the slot to a 4096-number card and they stop inventing anything: the first copies the pixels across, the score is perfect, the shorthand is gone. And if you invent 32 numbers yourself and post the card through, the second draughtsman draws something — it just isn't a tile.
How It Actually Works
Two halves with a pinch in the middle. The encoder maps 4096 pixels down to a code of 32 numbers. The decoder maps back out to 4096. Training minimises how far the round trip lands from where it started:
is one tile photo, is the network's attempt at rebuilding it, and the bars mean squared difference summed over all 4096 pixels. Look at what isn't there: a label. The target is the input, which is what makes this self-supervised learning.
The bottleneck does the work
Thirty-two isn't a knob you tune for accuracy. It's the constraint that makes learning happen. With 4096 slots the cheapest solution is a copy, and a copy teaches nothing. With 32, every slot has to earn its place. Glaze brightness earns it. Pixel (17, 40) does not.
Where PCA ends and this starts
The relationship is exact, not a family resemblance. Make both halves single matrices with no activation, keep squared-error loss, and the optimum spans the same subspace as the top principal components. Same subspace, not the same axes — nothing in that loss asks for orthogonal or ordered directions.
So a linear autoencoder buys nothing over PCA except a slower way to compute it. The payoff arrives with the nonlinearities: the decoder traces a curved surface through pixel space instead of a flat slice, so two degrees of rotation becomes a short move along that surface. That surface is the latent manifold, and following it is the only reason to reach for a network.
Variants, and the hole none of them fill
Denoising is the one to reach for first. Knock out 30% of the input pixels at random and ask for the clean tile back. Copying is now actively wrong, so the network has to infer missing grout lines from the surrounding ones. Sparse autoencoders go the other way: keep the code wide, penalise how many units switch on, let activity be the limit instead of width. Both block the lazy path — and a roomy bottleneck really does let a network approximate the identity function, reconstruct beautifully, and teach nothing.
Now the limit the next page exists to fix. That loss says nothing about the space between codes. It scores 200,000 round trips and never asks what sits between two of them, so codes land wherever training put them and decoding a point you didn't encode gives mush. You can't sample either — you don't know what distribution the codes follow. Compression model, not a generative one. (Discrete-latent variants sidestep that by fitting a separate model over code sequences, the route to autoregressive generation.)
Show Me the Code
Fit a linear autoencoder by gradient descent, then take the principal angles between its encoder subspace and the top two principal components. Cosines near 1 mean the same subspace.
import numpy as np
def fit_linear_autoencoder(x: np.ndarray, width: int, steps: int = 40_000) -> np.ndarray: rng = np.random.default_rng(0) enc = rng.normal(0.0, 0.1, (x.shape[1], width)) dec = rng.normal(0.0, 0.1, (width, x.shape[1])) for _ in range(steps): # no activation anywhere, so this is the linear case err = x @ enc @ dec - x # squared error against the input itself enc -= 2e-2 * (x.T @ err @ dec.T) / len(x) dec -= 2e-2 * ((x @ enc).T @ err) / len(x) return enc
rng = np.random.default_rng(1)tilt = rng.normal(0.0, 1.0, (2, 6)) # 500 points on a 2-D plane sitting inside 6-Dx = rng.normal(0.0, 1.0, (500, 2)) @ tilt + rng.normal(0.0, 0.05, (500, 6))x -= x.mean(axis=0)pca = np.linalg.svd(x, full_matrices=False)[2][:2]overlap = np.linalg.svd(np.linalg.qr(fit_linear_autoencoder(x, 2))[0].T @ pca.T)[1]print(np.round(overlap, 4)) # -> [0.9999 0.9998]Two matrices found the plane PCA gets in closed form. Add a nonlinearity and that guarantee is gone, which is the point.
Watch Out For
A bottleneck wide enough to cheat through
Reconstruction error drops to almost nothing, so you ship it. Then the defect classifier trained on those 32 numbers performs like it's reading noise. The bottleneck had room to pass the input through nearly unchanged, so the code is a lightly scrambled copy of the pixels.
Reconstruction error can't catch it — that's what a cheating network optimises. Two things can. Check whether the code's dimensions carry independent information rather than a few dominating, and narrow the bottleneck on purpose: if downstream accuracy goes up when you cut 64 numbers to 16, the wide version was passing pixels.
Treating the latent space as something you can sample
Someone interpolates between two tile codes to make training examples, or draws 32 Gaussian numbers and decodes them. Out come smears and grey fog. The instinct is that the model undertrained.
It didn't. Nothing in the loss constrained the region between two encoded points, so a decoder asked about it returns garbage with full confidence. Test it: encode two real tiles, decode ten points along the line between their codes, look. If the midpoints aren't plausible tiles, closing those holes is what a variational autoencoder is for.
The Quick Version
- An encoder squeezes the input to a short code, a decoder rebuilds it, and the loss is reconstruction error against the input. No labels.
- The bottleneck is the mechanism. Too wide and the network learns the identity function and teaches you nothing.
- Linear layers plus squared error span the same subspace as PCA, exactly. Nonlinearity earns its keep on curved structure.
- Denoising asks for the clean version of a corrupted input. Sparse penalises activations instead of narrowing the layer.
- Nothing structures the latent space, so unvisited points decode to nothing and sampling has no basis.
What to Read Next
- Variational Autoencoders adds the term that makes this latent space samplable.
- Principal Component Analysis is the linear case in closed form. Try it first.
- Self-Supervised Learning is the wider family reconstruction belongs to.
- Multi-Layer Perceptron covers the layers both halves are built from.
- Latent Spaces and Manifold Learning is what the code space is, and why curvature matters.
- Definitions worth a look: Latent Space, Manifold Hypothesis, and Embedding.