Module G · Lecture 23

Autoencoders: Architecture, Training, Sparse & Denoising Variants

Train a network to reconstruct its own input, and the bottleneck it must squeeze through becomes a compressed, self-discovered representation — no labels required.

⏱ ~80 min 🧩 Builds on: Lectures 6–8, 22 🎯 CO5
🧭 Why we're learning this now

Lecture 22's Boltzmann Machines learn an unlabeled data distribution through energy and sampling — powerful, but slow and awkward to train. There's a far simpler unsupervised idea available, using nothing but the ordinary backpropagation from Lecture 8: train a network to reconstruct its own input, and see what it's forced to learn in order to do that well through a bottleneck.

  • Explain autoencoders as unsupervised representation learning, and state precisely how a linear autoencoder relates to PCA.
  • Describe the encoder → bottleneck → decoder architecture and explain why an unconstrained, bottleneck-free autoencoder can trivially learn the identity function.
  • Write the MSE and BCE reconstruction losses and identify autoencoder training as self-supervised learning with target \(y=x\).
  • Hand-compute a full encode → decode → loss forward pass through a small linear autoencoder.
  • Formulate the L1 and KL-divergence sparsity penalties and connect the KL term back to Lecture 6's definition.
  • Explain denoising autoencoders — three standard noise models, and why training against the clean target (not the corrupted input) forces the network to learn robust features.

1. Why Autoencoders? Unsupervised Representation Learning

Every architecture in this course so far — from the perceptron (Lecture 3) through CNNs, RNNs, and Transformers — was trained in a supervised setting: a labeled pair \((x,y)\) told the network exactly what output to produce. An autoencoder breaks that pattern. It is trained to reconstruct its own input: given \(x\), produce \(\hat x \approx x\). No external label is required — the input itself supplies the target. This makes autoencoders a form of unsupervised representation learning: by forcing the network to compress and then reconstruct data, we make it discover, on its own, which features of the data are most informative.

Definition — Autoencoder

An unsupervised feedforward network trained to approximate the identity mapping \(g_\phi(f_\theta(x)) \approx x\) through a representation bottleneck, so that the internal code \(z\) becomes a compressed, informative summary of \(x\).

You already know one classical technique for compressing data while preserving as much information as possible: Principal Component Analysis (PCA). PCA finds a linear lower-dimensional subspace — the directions of maximum variance in the data, obtained via eigendecomposition of the covariance matrix (equivalently, SVD) — and projects each point onto that subspace. Every operation in PCA is linear.

An autoencoder generalizes this idea: because its encoder and decoder can include nonlinear activation functions (Lecture 5), it can learn a nonlinear lower-dimensional manifold — a curved, more flexible surface that a purely linear method like PCA cannot represent. This is precisely why autoencoders can capture structure PCA misses.

AspectPCAAutoencoder
MappingLinear projection onlyLinear or nonlinear, depending on activation choice
How it's foundEigendecomposition / SVD (closed-form, one shot)Gradient descent + backpropagation (iterative)
Structure capturedDirections of maximum varianceAny structure the network has the capacity to represent
ReversibilityExact linear inverse using the same componentsApproximate, learned decoder \(g_\phi\)
ObjectiveMaximize variance explainedMinimize reconstruction loss (MSE / BCE)
✅ A known, standard fact

A linear autoencoder — no nonlinear activation in the encoder or decoder — trained with mean-squared-error reconstruction loss learns to span the same subspace as PCA's top principal components. It is precisely the nonlinearity in \(\sigma,\sigma'\) that lets a nonlinear autoencoder capture structure PCA cannot.

2. Architecture: Encoder, Bottleneck, Decoder

An autoencoder has three logical parts, chained together:

  • Encoder \(f_\theta\): compresses the input into a lower-dimensional code. $$z = f_\theta(x) = \sigma(W_ex + b_e), \qquad x\in\mathbb R^n,\ z\in\mathbb R^m,\ m
  • Latent space / bottleneck: the code \(z\) itself — the narrowest point of the network.
  • Decoder \(g_\phi\): expands the code back toward the original input dimensionality. $$\hat x = g_\phi(z) = \sigma'(W_dz + b_d), \qquad \hat x \in \mathbb R^n$$

\(W_e,b_e\) and \(W_d,b_d\) are the encoder's and decoder's own weight matrix and bias — the same \(W,b\) notation family from Lecture 4, duplicated for two chained layers (or layer stacks). The activation functions \(\sigma,\sigma'\) are chosen per use case: \(\sigma'\) is often sigmoid when inputs are normalized to \([0,1]\) (e.g. pixel intensities), so \(\hat x\) is guaranteed to land in the same range as \(x\).

⚠ Why the bottleneck (m < n) matters

Without a bottleneck — or some other constraint — an autoencoder has a trivial, useless solution available: the identity function. If \(m \ge n\) and nothing else constrains the network, gradient descent can (and will) simply learn \(W_e,W_d\) such that \(z\) is an invertible transform of \(x\) and \(\hat x = x\) exactly — driving the reconstruction loss to zero without learning anything about the structure of the data. The bottleneck (\(m) rules this out directly: \(z\) physically cannot hold all the information in \(x\), so the network is forced to discard redundant information and keep only what's needed to reconstruct well on average. Sections 6–7 below (sparsity, denoising) — and Lecture 24's contractive penalty — are three further, independent ways of blocking this identity shortcut, useful even when a strict bottleneck isn't otherwise enforced.

The "hourglass" shape of an autoencoder: a wide input layer, a narrow bottleneck (the latent code), and a wide output layer of the same size as the input. Click "Animate forward pass" to watch signal flow through the compression and reconstruction.

3. Working Principle & Reconstruction Loss

The information flow is a straight pipeline: \(x \to \text{Encode} \to z \to \text{Decode} \to \hat x\). Training minimizes a reconstruction loss that measures how far \(\hat x\) is from \(x\). Two standard choices, both already familiar from Lecture 6:

$$L_{MSE} = \frac1n\sum_{i=1}^n (x_i-\hat x_i)^2 \qquad\text{(continuous-valued inputs)}$$ $$L_{BCE} = -\frac1n\sum_{i=1}^n\big[x_i\log\hat x_i + (1-x_i)\log(1-\hat x_i)\big] \qquad\text{(inputs normalized to }[0,1]\text{)}$$

This is exactly the binary cross-entropy formula from Lecture 8's backprop derivation — there it compared a single predicted probability \(Q\) against a single label \(y\); here it is applied per input dimension (e.g. per pixel) and averaged over all \(n\) dimensions of \(x\).

4. Training: Self-Supervision via Backpropagation

Nothing about the training procedure is new. Section 3's loss \(L\) is a differentiable scalar function of every weight in the encoder and decoder, so training an autoencoder is Lecture 7's gradient descent (or a mini-batch/Adam variant from Lecture 9) computing gradients via Lecture 8's backpropagation, applied end-to-end through the encoder and decoder as if they were one deeper network:

$$W \leftarrow W - \eta \nabla_W L \qquad \text{for every } W \in \{W_e,b_e,W_d,b_d\}$$

The one genuinely new idea is where the target comes from: instead of a separately supplied label \(y\), the target is the input itself, \(y = x\). This is called self-supervision — the network supervises itself using structure already present in unlabeled data, which is exactly what makes autoencoders useful whenever labels are scarce or unavailable.

5. Worked Numerical Example: A Linear Autoencoder

To make every equation above completely concrete, consider a linear autoencoder — no activation function in the encoder or decoder, so per Section 1's remark this specific example behaves like a 1-component PCA — compressing a 3-dimensional input down to a 1-dimensional latent code.

🔢 Initial parameters

$$x=[1.0,\ 0.5,\ -0.5] \qquad W_e=[0.4,\ 0.3,\ -0.2]\ (3\times1) \qquad b_e=0.1$$ $$W_d=[0.5,\ 0.4,\ -0.3]\ (1\times3) \qquad b_d=[0.05,\ 0.02,\ -0.01]$$

Step 1 — Encode: z = x·W_e + b_e

Three input features, three weights, one dot product plus a bias — every term written out (see Lecture 4, Section 6 for the general row/column mechanics of a dot product):

$$z = (1.0)(0.4) + (0.5)(0.3) + (-0.5)(-0.2) + 0.1 = 0.4 + 0.15 + 0.1 + 0.1 = \mathbf{0.75}$$

This single number \(z=0.75\) is the entire compressed representation of the 3-dimensional input \(x\) — the bottleneck in action.

Step 2 — Decode: x̂ = z·W_d + b_d

Here \(z\) is a single scalar and \(W_d\) is a \(1\times3\) row vector, so this is not really a "many-term" dot product — each output entry is just \(z\) times that entry of \(W_d\), plus that entry's own bias. Every term is spelled out:

$$\hat x_1 = 0.75(0.5)+0.05 = 0.375+0.05=\mathbf{0.425} \qquad \hat x_2 = 0.75(0.4)+0.02=0.3+0.02=\mathbf{0.32} \qquad \hat x_3 = 0.75(-0.3)-0.01=-0.225-0.01=\mathbf{-0.235}$$

So \(\hat x = [0.425,\ 0.32,\ -0.235]\) — the decoder's attempt to reconstruct the original 3-dimensional \(x\) from the single number \(z\).

Step 3 — Reconstruction error: x − x̂

$$x-\hat x = [1.0-0.425,\ \ 0.5-0.32,\ \ -0.5-(-0.235)] = [\mathbf{0.575},\ \mathbf{0.18},\ \mathbf{-0.265}]$$

Step 4 — MSE loss

Square every error entry, then average over the \(n=3\) dimensions:

$$\text{squared errors} = [0.575^2,\ 0.18^2,\ (-0.265)^2] = [0.330625,\ 0.0324,\ 0.070225]$$

$$L_{MSE} = \frac{0.330625+0.0324+0.070225}{3} = \frac{0.43325}{3} \approx \mathbf{0.14442}$$

This one number summarizes how well the network reconstructed \(x\) after squeezing it through a 1-dimensional bottleneck — training simply pushes \(W_e,b_e,W_d,b_d\) to make this number smaller, exactly as the cross-reference box below describes.

You can replay the same four steps interactively below — useful for testing yourself before moving on:

🔗 Cross-reference: how would we reduce this loss?

Exactly as in Lecture 8: compute \(\partial L/\partial W_d, \partial L/\partial b_d\) using the chain rule (for MSE, \(\delta = \hat x - x\) plays the role that \(Q-y\) played for BCE), propagate the error back through \(W_d\) into \(z\), then into \(W_e, b_e\). We don't repeat the mechanical backward-pass derivation here — it is identical in structure to Lecture 8, just applied to a reconstruction target instead of a classification target. Running that update over many training examples pushes \(W_e,W_d,b_e,b_d\) to steadily reduce the reconstruction error above.

6. Sparse Autoencoders

Even with a bottleneck, an autoencoder is not automatically safe from degenerate solutions — with enough capacity in \(W_e,W_d\), a network can learn a diffuse code that reconstructs adequately without any individual latent unit meaning anything in particular. A sparse autoencoder adds an explicit sparsity constraint: for any given input, most latent units should be inactive (near zero), and only a small subset should fire. This is loosely inspired by biological neurons — for any single stimulus, only a small fraction of the neurons in a cortical region are believed to be highly active — and the resulting representations tend to be more feature-selective (each active unit responds to something specific) and less prone to overfitting.

Two standard ways to enforce sparsity:

(1) L1 penalty on latent activations

$$L_{total} = L_{reconstruction} + \lambda\sum_j |z_j|$$ \(\lambda\) controls sparsity strength — larger \(\lambda\) pushes more latent units toward exactly zero.

(2) KL-divergence sparsity penalty

Pick a small target average activation \(\rho\) (e.g. \(\rho=0.05\) — "on average, each latent unit should be active only 5% of the time across the training set"). Let \(\hat\rho_j\) be unit \(j\)'s actual average activation over a batch. Penalize the divergence between the desired and actual Bernoulli firing distributions:

$$L_{total} = L_{reconstruction} + \beta\sum_jD_{KL}(\rho\,\|\,\hat\rho_j), \qquad D_{KL}(\rho\|\hat\rho_j)=\rho\log\frac\rho{\hat\rho_j}+(1-\rho)\log\frac{1-\rho}{1-\hat\rho_j}$$

This is exactly Lecture 6's KL-divergence definition — there it measured the distance between two general probability distributions; here it is applied to the specific case of two Bernoulli distributions ("fires" vs. "doesn't fire"), one per latent unit.

🔢 Worked example: computing one KL penalty by hand

Target \(\rho=0.05\). Take latent unit z1 from the table below, whose actual average activation over a batch turned out to be \(\hat\rho_1=0.03\). Plug directly into the formula, one term at a time:

$$D_{KL}(0.05\|0.03)=\underbrace{0.05\log\frac{0.05}{0.03}}_{\text{term 1}}\ +\ \underbrace{0.95\log\frac{0.95}{0.97}}_{\text{term 2}}$$

$$\text{term 1} = 0.05\log(1.6667) = 0.05(0.5108) \approx 0.02554 \qquad\qquad \text{term 2} = 0.95\log(0.9794) = 0.95(-0.0208) \approx -0.01979$$

$$D_{KL}(0.05\|0.03) \approx 0.02554 - 0.01979 = \mathbf{0.00575}$$

Matches the table's z1 row exactly. Now contrast with the over-active unit z3 (\(\hat\rho_3=0.30\)) — same formula, much larger penalty, because 0.30 is far from the 0.05 target:

$$\text{term 1}=0.05\log\frac{0.05}{0.30}=0.05(-1.7918)\approx-0.08959 \qquad\qquad \text{term 2}=0.95\log\frac{0.95}{0.70}=0.95(0.3054)\approx0.29011$$

$$D_{KL}(0.05\|0.30) \approx -0.08959+0.29011=\mathbf{0.20052}$$

z3's penalty is roughly 35× larger than z1's — exactly the strong gradient signal needed to push an over-firing unit back toward the target sparsity during training.

The full picture, all four latent units, target \(\rho=0.05\):

UnitActual avg. activation \(\hat\rho_j\)KL penalty \(D_{KL}(\rho\|\hat\rho_j)\)
z10.030.00575
z20.060.00094
z30.300.20052
z40.080.00698
KL-divergence sparsity penalty per latent unit (target \(\rho=0.05\)). Unit z3 fires far more often than desired and is penalized tens to hundreds of times more heavily than the well-behaved units — exactly the gradient signal that pushes an over-active unit back toward sparsity during training.

7. Denoising Autoencoders

Sparsity constrains the latent space directly. Denoising autoencoders take a different approach to blocking the trivial identity shortcut: instead of restricting what the network can do internally, corrupt the input itself. The encoder–decoder is trained to strip the corruption back out:

$$\tilde x = x + \text{noise} \qquad L = \|x - g_\phi(f_\theta(\tilde x))\|^2 \quad\text{(loss measured against the ORIGINAL clean } x\text{)}$$

Three standard ways to corrupt \(x\):

  • Gaussian noise: add \(\epsilon\sim\mathcal N(0,\sigma^2)\) independently to every feature/pixel.
  • Masking noise: randomly zero out a fraction of the input's features/pixels (a stand-in for missing data).
  • Salt-and-pepper noise: randomly flip individual pixels to the two extreme intensity values (pure black/white).

Because the loss is measured against the clean \(x\), not \(\tilde x\), simply copying the corrupted input straight through would not minimize the loss — the corruption would still be present, and the loss would still be large. The only way to genuinely reduce the loss is to learn what the underlying clean structure of the data looks like and reconstruct that, discarding the noise. This is what makes the learned features robust.

🔢 Extending the worked example: masking noise

Corrupt \(x=[1.0,0.5,-0.5]\) with masking noise that zeros the second feature: \(\tilde x = [1.0,\ 0,\ -0.5]\). Push \(\tilde x\) through the same encoder/decoder weights as Section 5 — \(W_e=[0.4,0.3,-0.2]\), \(b_e=0.1\), \(W_d=[0.5,0.4,-0.3]\), \(b_d=[0.05,0.02,-0.01]\).

Step 1 — Encode the corrupted input: z̃ = x̃·W_e + b_e

$$\tilde z = (1.0)(0.4) + (0)(0.3) + (-0.5)(-0.2) + 0.1 = 0.4+0+0.1+0.1 = \mathbf{0.6}$$

Compare with Section 5's clean-input code \(z=0.75\) — losing feature \(x_2\) shifted the latent code, because the encoder has no way to know that entry was artificially zeroed rather than genuinely small.

Step 2 — Decode: x̂ = z̃·W_d + b_d

$$\hat x_1=0.6(0.5)+0.05=\mathbf{0.35} \qquad \hat x_2=0.6(0.4)+0.02=\mathbf{0.26} \qquad \hat x_3=0.6(-0.3)-0.01=\mathbf{-0.19}$$

So \(\hat x = [0.35,\ 0.26,\ -0.19]\) — reconstructed entirely from the corrupted input \(\tilde x\).

Step 3 — Compare against the CLEAN target (not x̃!)

The loss is always measured against the original clean \(x=[1.0,0.5,-0.5]\), never against \(\tilde x\) — that's the whole point of denoising training:

$$x-\hat x = [1.0-0.35,\ 0.5-0.26,\ -0.5-(-0.19)] = [\mathbf{0.65},\ \mathbf{0.24},\ \mathbf{-0.31}]$$

$$\text{squared} = [0.4225,\ 0.0576,\ 0.0961] \qquad L_{MSE}=\frac{0.4225+0.0576+0.0961}{3}=\frac{0.5762}{3}\approx\mathbf{0.1921}$$

Higher than the clean-input loss (≈0.14442) from Section 5, exactly as expected — see the result box below for why.

The grid and stepper below replay the same steps interactively — useful for testing yourself before moving on:

Clean input → corrupted input (masking noise zeroes \(x_2\)) → reconstruction, compared against the clean target — use the stepper below to walk through it.
✅ Result

The reconstruction loss here (≈0.1921) is higher than the clean-input loss from Section 5 (≈0.14442) — expected and correct: reconstructing from a corrupted, information-poorer input is a strictly harder task. Minimizing this higher loss during training is precisely what forces the encoder to rely on the other, uncorrupted features (\(x_1,x_3\)) to infer what the missing \(x_2\) should have been — exactly the robust, structure-aware behaviour denoising autoencoders are designed to induce.

8. Pitfalls & Practical Notes

⚠ Things that silently break autoencoder training
  • Over-regularizing. Too large \(\lambda\) or \(\beta\), or overly aggressive noise, can make training difficult and hurt reconstruction quality — sparsity/denoising strength is a hyperparameter to tune, not maximize.
  • Wrong loss for the data type. BCE assumes inputs are genuinely bounded in \([0,1]\); it is not well defined for unbounded real-valued data like this lecture's example (which includes a negative value) — that needs MSE, not BCE.
  • A bottleneck alone isn't a guarantee. Section 2 explains why \(m blocks pure identity mapping, but with enough decoder capacity a network can still memorize training examples rather than learning general structure — this is why sparsity, denoising, and (Lecture 24) contractive constraints remain useful even with a bottleneck.
  • Denoising loss must target the clean input. Comparing the reconstruction against \(\tilde x\) instead of \(x\) would simply teach the network to reproduce noise.

9. Summary

Key takeaways
  • Autoencoders learn compressed representations by being trained to reconstruct their own input (self-supervision, \(y=x\)); a linear AE with MSE loss recovers PCA's subspace, and nonlinear AEs go further.
  • The bottleneck (\(m) is the base defense against the trivial identity solution; sparsity (L1/KL) and denoising are two further, independent strategies that work by constraining the latent code and corrupting the input, respectively.
  • Training is mechanically unchanged from Lectures 7–8 (gradient descent + backpropagation) — only the loss and the self-supervised target (\(y=x\)) are new.
  • Lecture 24 continues this theme with contractive autoencoders (a third defense, via the Jacobian), then scales autoencoders to many layers, convolutional architectures, and finally variational autoencoders.

10. Code: Autoencoder Forward Pass, Denoising & Sparsity

The NumPy implementation below reproduces the worked examples above exactly — run it and confirm the printed MSE values match the hand-derived numbers (≈0.14442 clean, ≈0.19207 denoised), plus the KL-divergence sparsity penalties from the table.

lecture-23-autoencoder.py
import numpy as np

# ---- linear autoencoder: 3 -> 1 -> 3 (matches the lecture's worked example) ----
x  = np.array([1.0, 0.5, -0.5])
We = np.array([0.4, 0.3, -0.2])          # encoder weights (3,)
be = 0.1
Wd = np.array([0.5, 0.4, -0.3])          # decoder weights (3,)
bd = np.array([0.05, 0.02, -0.01])

def encode(inp): return inp @ We + be
def decode(z):    return z * Wd + bd
def mse(a, b):     return np.mean((a - b) ** 2)

# ---- clean forward pass ----
z     = encode(x)
x_hat = decode(z)
loss  = mse(x, x_hat)
print("Clean:    z =", z, " x_hat =", x_hat, " MSE =", round(loss, 5))

# ---- denoising: masking noise zeroes feature x2 (index 1) ----
x_tilde     = x.copy(); x_tilde[1] = 0.0
z_tilde     = encode(x_tilde)
x_hat_noisy = decode(z_tilde)
loss_noisy  = mse(x, x_hat_noisy)          # <-- against the CLEAN x, not x_tilde
print("Denoised: z =", z_tilde, " x_hat =", x_hat_noisy, " MSE =", round(loss_noisy, 5))
# expected: Clean MSE ~ 0.14442, Denoised MSE ~ 0.19207

# ---- sparse autoencoder: KL-divergence sparsity penalty ----
def kl_sparsity(rho, rho_hat):
    return rho * np.log(rho / rho_hat) + (1 - rho) * np.log((1 - rho) / (1 - rho_hat))

rho       = 0.05                                    # target average activation
rho_hats  = np.array([0.03, 0.06, 0.30, 0.08])       # 4 latent units' actual average activation
penalties = kl_sparsity(rho, rho_hats)
for j, (rh, p) in enumerate(zip(rho_hats, penalties), start=1):
    print(f"  unit z{j}: rho_hat={rh:.2f}  KL penalty={p:.5f}")
print("Total sparsity penalty (sum over units):", round(penalties.sum(), 5))

⬇ Download lecture-23-autoencoder.py   More resources for this lecture →