Autoencoders
Train an encoder-decoder pair to reconstruct inputs through a bottleneck, including the denoising variant.
The simplest generative-style neural network: an autoencoder compresses inputs through a low-dimensional bottleneck and reconstructs them. The reconstruction loss forces the bottleneck to capture whatever structure is useful for reproducing the data[1]. This lesson grounds the generative-modeling track by establishing the encoder-decoder pattern that VAEs and diffusion models will build on[2].
The architecture
An autoencoder has two halves:
- Encoder maps an input to a low-dimensional latent code .
- Decoder maps the code back to a reconstruction .
Train end-to-end on the reconstruction loss:
The bottleneck dimension is the central design choice: small enough that the network cannot just pass the input through, large enough that the structure of the data can be captured.
Linear autoencoder = PCA
When the encoder and decoder are linear and the loss is squared error, the autoencoder reduces to PCA. The optimal encoder is the projection onto the top- principal components, and the optimal decoder is its transpose. This was the anomaly-detection lesson's setup.
The autoencoder framing matters when the structure is nonlinear: encoder and decoder become MLPs (or CNNs for images), and the latent space can capture curved manifolds that PCA cannot.
What the latent encodes
After training, the latent code captures what the decoder needs to reconstruct :
- Discarded: any input variation the decoder cannot exploit (pure noise, irrelevant detail).
- Kept: anything that systematically helps reconstruction (object identity, pose, color, structural features).
This makes autoencoders useful for representation learning even when generation is not the goal. The latent space often has interpretable structure: similar inputs are nearby, distinct classes occupy distinct regions.
Denoising autoencoder (DAE)
Vincent et al. (2008). Train the autoencoder to reconstruct the clean input from a corrupted version:
where is with noise added (Gaussian, masked pixels, etc.).
Why corrupt the input? Two reasons:
- Prevents identity learning: an unconstrained autoencoder with a large enough bottleneck can learn the identity function, which is useless. Denoising forces the model to learn the underlying manifold.
- Implicit manifold learning: the model must map corrupted inputs back to the data manifold. The reconstruction error is the "energy" of corruption, and minimizing it teaches the network the shape of the manifold.
The denoising recipe seeded modern generative models:
- Diffusion models are sequences of denoising autoencoders at different noise levels (last lessons in D4).
- Masked autoencoders (MAE) are denoising in the masked-token form for vision (D2 lesson).
- BERT is essentially a masked denoising autoencoder for text.
What autoencoders cannot do
Plain autoencoders are not proper generative models:
- The latent space is unstructured. Sampling a random and decoding it usually gives garbage, because the encoder has only populated some regions of the latent space.
- There is no notion of likelihood. The model can reconstruct inputs but not generate new ones in a principled way.
The next lesson fixes both with the variational autoencoder, which adds a probabilistic structure to the latent space.
Practical uses (beyond stepping-stone to VAE)
- Dimensionality reduction: replace PCA when nonlinear structure matters (e.g., t-SNE / UMAP precomputation).
- Anomaly detection (C6 lesson): high reconstruction error flags out-of-distribution points.
- Feature extraction: use the encoder's output as features for a downstream classifier.
- Image compression: trained autoencoders can outperform JPEG at low bit rates.
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
# Synthetic 2D data on a curve in 10D ambient space
n = 500
t = rng.uniform(0, 2 * np.pi, n)
X = np.column_stack([
np.cos(t), np.sin(t),
0.5 * np.cos(2 * t), 0.5 * np.sin(2 * t),
rng.normal(scale=0.05, size=(n, 6)) # noise dims
])
d = X.shape[1]
# Tiny MLP autoencoder: d -> 16 -> 2 -> 16 -> d
def init(in_d, out_d): return rng.normal(scale=0.5 / np.sqrt(in_d), size=(in_d, out_d))
W1 = init(d, 16); b1 = np.zeros(16)
W2 = init(16, 2); b2 = np.zeros(2)
W3 = init(2, 16); b3 = np.zeros(16)
W4 = init(16, d); b4 = np.zeros(d)
def relu(x): return np.maximum(0, x)
def encode(X): return relu(X @ W1 + b1) @ W2 + b2 # signed bottleneck (no ReLU on top)
def decode(z): return relu(z @ W3 + b3) @ W4 + b4
def forward(X): return decode(encode(X))
# Train with SGD on the reconstruction loss
lr = 0.1
for epoch in range(500):
h_pre = X @ W1 + b1
h_enc = relu(h_pre) # encoder hidden (post-relu)
Z = h_enc @ W2 + b2 # bottleneck (signed)
d_pre = Z @ W3 + b3
d_hid = relu(d_pre) # decoder hidden
Xhat = d_hid @ W4 + b4
err = Xhat - X
# Manual backprop through the chain
grad_W4 = d_hid.T @ err / n
grad_b4 = err.mean(axis=0)
grad_d_pre = (err @ W4.T) * (d_pre > 0)
grad_W3 = Z.T @ grad_d_pre / n
grad_b3 = grad_d_pre.mean(axis=0)
grad_z = grad_d_pre @ W3.T # no ReLU on Z, so this is grad wrt Z directly
grad_W2 = h_enc.T @ grad_z / n
grad_b2 = grad_z.mean(axis=0)
grad_h = grad_z @ W2.T
grad_W1 = X.T @ (grad_h * (h_pre > 0)) / n
grad_b1 = (grad_h * (h_pre > 0)).mean(axis=0)
W4 -= lr * grad_W4; b4 -= lr * grad_b4
W3 -= lr * grad_W3; b3 -= lr * grad_b3
W2 -= lr * grad_W2; b2 -= lr * grad_b2
W1 -= lr * grad_W1; b1 -= lr * grad_b1
# Reconstruction quality
Xhat = forward(X)
rmse = np.sqrt(np.mean((Xhat - X)**2))
print(f"autoencoder RMSE on training data: {rmse:.4f}")
# Compare to PCA at the same bottleneck dimension
mean = X.mean(axis=0); Xc = X - mean
U, s, Vt = np.linalg.svd(Xc, full_matrices=False)
V_k = Vt[:2]
X_pca = (Xc @ V_k.T) @ V_k + mean
rmse_pca = np.sqrt(np.mean((X_pca - X)**2))
print(f"PCA k=2 RMSE on the same data: {rmse_pca:.4f}")The nonlinear autoencoder beats linear PCA at the same bottleneck because it can model the curved structure.
Exercises
Exercise 1. A linear autoencoder has encoder and decoder with and , trained on centered data with squared reconstruction error. Show that if is fixed to the top- PCA projection (rows orthonormal), the decoder that minimizes reconstruction error is , so the optimum reconstructs by projecting onto the principal subspace.
Solution
Step 1. With , the code of an input is , a vector of coordinates in the principal basis. The reconstruction is .
Step 2. The per-sample error is . Write in the full orthonormal eigenbasis , so with and . Then and .
Step 3. The error splits because and are orthogonal:
The second term does not depend on . The first is minimized to zero by setting for all codes , i.e. .
Step 4. At the reconstruction is , the orthogonal projection onto the top- principal subspace, leaving only the residual energy that PCA also leaves. The linear autoencoder optimum is the PCA reconstruction.
Exercise 2. In the denoising autoencoder, the corruption with is independent of . For a fixed map , show that the expected denoising loss is minimized, over all measurable maps, by , the posterior mean of the clean input given the corrupted one.
Solution
Step 1. Let be the reconstruction as a function of the corrupted input. Condition on and use the tower rule:
Step 2. For each fixed , the inner expectation is a squared error of the random clean input against a constant vector . The constant that minimizes is the conditional mean , since and only the first term depends on .
Step 3. Minimizing pointwise in minimizes the outer expectation, so the optimal reconstruction is . The denoising target is the posterior mean, which is why a trained DAE maps corrupted points back toward the data manifold rather than learning the identity.
Exercise 3 (code). Build a linear autoencoder with by hand on 3D data lying near a 2D plane. Confirm that closing the loop with the PCA projection , gives exactly the PCA reconstruction error, matching Exercise 1.
Solution
import numpy as np
rng = np.random.default_rng(0)
# 3D data near a 2D plane: two latent directions plus small off-plane noise
n = 400
basis = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) # spanning plane
coeffs = rng.normal(size=(n, 2)) * np.array([3.0, 1.0])
X = coeffs @ basis + rng.normal(scale=0.05, size=(n, 3)) # off-plane noise on dim 3
X = X - X.mean(axis=0) # center
# PCA: top-2 principal directions
U_svd, s, Vt = np.linalg.svd(X, full_matrices=False)
V_k = Vt[:2].T # d x k, orthonormal cols
# Linear autoencoder with W = V_k^T (encoder), U = V_k (decoder)
W = V_k.T # k x d
U = V_k # d x k
Z = X @ W.T # codes
Xhat_ae = Z @ U.T # reconstruction
# Direct PCA reconstruction
Xhat_pca = (X @ V_k) @ V_k.T
err_ae = np.mean(np.sum((X - Xhat_ae)**2, axis=1))
err_pca = np.mean(np.sum((X - Xhat_pca)**2, axis=1))
print(f"linear AE error: {err_ae:.6f} PCA error: {err_pca:.6f}")
assert np.allclose(Xhat_ae, Xhat_pca)
assert np.isclose(err_ae, err_pca)The linear autoencoder closed with the PCA projection reconstructs identically to PCA, the equality Exercise 1 derived. The residual error is the small off-plane noise PCA also discards.
The next lesson upgrades this to a proper generative model: the variational autoencoder.
References
- [1]Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, §14 (Autoencoders). MIT Press. The encoder-decoder bottleneck and the denoising variant.↩
- [2]Kingma, D. P., & Welling, M. (2014). "Auto-encoding variational bayes". ICLR. arXiv:1312.6114. The variational autoencoder built on the encoder-decoder pattern this lesson establishes.↩