Module G · Lecture 24

Contractive, Stacked, Deep & Convolutional Autoencoders + Applications

Three more ways to stop an autoencoder from cheating, a recipe for scaling it to many layers and images, a first glimpse of generative modeling, and where all of this actually gets used.

⏱ ~85 min 🧩 Builds on: Lecture 23; Lectures 12–14, 16, 22 🎯 CO5
🧭 Why we're learning this now

Lecture 23's basic autoencoder has one obvious weak point: nothing stops it from cheating, in principle, if the bottleneck isn't tight enough or the network is powerful enough to memorize shortcuts. Every variant in this final lecture — sparse, denoising, contractive, stacked — is a different engineered constraint that closes off a different version of that same loophole, before we turn to what these models are actually used for.

  • Derive and compute the contractive autoencoder penalty (Frobenius norm of the encoder Jacobian) and contrast it mechanistically with denoising autoencoders (Lecture 23).
  • Explain greedy layer-wise pretraining of stacked autoencoders and its direct parallel to DBN pretraining (Lecture 22).
  • Contrast deep (jointly trained) vs. stacked (greedy) autoencoders, and describe standard fixes for vanishing gradients, overfitting, and initialization sensitivity.
  • Describe convolutional autoencoders (Conv2D encoder, transposed-convolution decoder) and, at a conceptual level, variational autoencoders.
  • Compute and interpret MSE and PSNR as reconstruction-quality metrics, and describe SSIM and PCA/t-SNE latent-space visualization.
  • Identify three applications of autoencoders — anomaly detection, denoising, and data compression — with the concrete mechanism behind each.

1. Contractive Autoencoders

Lecture 23 blocked the identity-mapping shortcut two ways: sparsity constrained the latent code, denoising corrupted the input. Contractive autoencoders take a third, more direct route: penalize the encoder's local sensitivity itself. The goal is to make the encoder locally insensitive — "contractive" — to small perturbations of the input, so that nearby inputs map to nearby (or identical) latent codes. This gives a smoother, more robust latent space, since the representation doesn't jump around in response to noise-scale changes in the input.

The Jacobian penalty

Enforce this by penalizing the Frobenius norm of the encoder's Jacobian matrix:

$$L_{total} = L_{reconstruction} + \lambda\|J_f(x)\|_F^2, \qquad J_f(x)_{ji}=\frac{\partial z_j}{\partial x_i}, \qquad \|J_f(x)\|_F^2=\sum_{j,i}\left(\frac{\partial z_j}{\partial x_i}\right)^2$$

In words: \(J_f(x)_{ji}\) is how much latent unit \(z_j\) changes per unit change in input feature \(x_i\); the Frobenius norm sums the squares of every such sensitivity across all latent units and all input features.

For a single-layer sigmoid encoder \(z=\sigma(Wx+b)\), this has a clean closed form that requires no numerical differentiation — it is directly computable from the encoder's own weights and activations:

$$\|J_f(x)\|_F^2=\sum_j\big(z_j(1-z_j)\big)^2\sum_iW_{ji}^2$$
The sigmoid local-gradient factor \(z(1-z)\) that scales the contractive penalty per latent unit. It peaks at \(z=0.5\) (value 0.25) and shrinks toward 0 as \(z\) saturates near 0 or 1 — so a confident, saturated latent unit contributes almost nothing to the penalty, while an unsaturated, "undecided" unit is penalized most.
🔗 Denoising vs. contractive: same goal, different mechanism

Denoising achieves robustness indirectly and stochastically — a different random corruption is applied on every training step, so robustness emerges only in expectation over many steps. Contractive achieves robustness directly and analytically — an exact local-sensitivity penalty is computed at each real training point, every single step. Both aim at the same "robust to small input perturbations" goal, via different mechanisms.

AspectDenoising AE (Lecture 23)Contractive AE
MechanismCorrupt input, reconstruct cleanPenalize Jacobian norm directly
NatureStochastic — different noise each stepAnalytic — exact, deterministic at each \(x\)
What's regularizedWhole encode-decode pipeline's robustness to actual corruptionSpecifically the encoder's local sensitivity
Extra computationAn extra forward/backward pass on the corrupted inputA closed-form extra term added to the loss
🔢 Worked example: linear-encoder Jacobian

Reuse Lecture 23's linear-encoder weights \(W_e=[0.4,\ 0.3,\ -0.2]\). For a linear encoder (no sigmoid), the Jacobian is simply \(W_e\) itself — a constant that doesn't depend on \(x\):

$$\|J_f\|_F^2 = 0.4^2+0.3^2+(-0.2)^2 = 0.16+0.09+0.04 = \mathbf{0.29}$$

For a sigmoid encoder, this same value would additionally be multiplied by \(\big(z(1-z)\big)^2\) at the current activation — making the penalty adaptive to how "confident" (saturated) each latent unit currently is, exactly as the plot above shows. Concretely: suppose this same encoder instead used a sigmoid activation, and for this input its output happened to be \(z=0.75\) — the exact latent value Lecture 23, Section 5's clean forward pass produced. The local-gradient factor at that point is:

$$z(1-z) = 0.75(1-0.75) = 0.75(0.25) = \mathbf{0.1875}$$

Squaring it (the formula above uses \(\big(z_j(1-z_j)\big)^2\)) and multiplying by the linear part computed above:

$$\|J_f\|_F^2 = (0.1875)^2\times0.29 = 0.03516\times0.29\approx\mathbf{0.0102}$$

The sigmoid-encoder penalty (≈0.0102) is roughly 28× smaller than the raw linear-encoder value (0.29) — because at \(z=0.75\) the unit is already reasonably confident/saturated (away from the maximally-sensitive \(z=0.5\) point the plot above highlights), so a small input change barely moves its output, and the penalty correctly reflects that lower sensitivity.

2. Stacked Autoencoders: Greedy Layer-Wise Pretraining

Motivation: just like a deep network learns increasingly abstract features layer by layer (Lectures 12–14's CNN filters go from edges to object parts), we would like an autoencoder's latent representation to be built up hierarchically, across several stacked encoding stages rather than one. Training such a stack directly, end-to-end, from scratch can be unstable — so the classical solution is greedy layer-wise pretraining, a direct parallel to Lecture 22's DBN pretraining (there, RBMs were stacked and trained one at a time; here, the same idea is applied to autoencoders instead):

Greedy layer-wise pretraining, stage by stage
  1. Stage 1 — Train AE-1 on the raw input. Train a shallow autoencoder (encoder₁ + decoder₁) directly on the raw input \(x\), minimizing reconstruction loss exactly as in Lecture 23.
  2. Stage 2 — Discard decoder₁, keep the code. Throw away decoder₁. Encoder₁'s output \(z_1=f_1(x)\) becomes the new "input data" for the next stage — this is the move that makes the stacking greedy (one layer trained at a time, never all at once).
  3. Stage 3 — Train AE-2 on \(z_1\), not the raw data. A second autoencoder (encoder₂ + decoder₂) is trained with \(z_1\) as its input. Its own latent code \(z_2=f_2(z_1)\) captures more abstract structure than \(z_1\) did — the same "features of features" idea Lectures 12–14 showed for CNN filters.
  4. Stage 4 — Repeat, then stack the encoders. Repeat Stages 1–3 for as many layers as desired, then connect encoder₁ → encoder₂ → … end-to-end, optionally topped with a softmax classifier (Lecture 14's softmax + cross-entropy).
  5. Stage 5 — Fine-tune the whole stack jointly. Run standard backpropagation (Lecture 8) end-to-end through the entire connected stack on the labeled task. Greedy pretraining supplies a good starting point for every layer's weights; joint fine-tuning then polishes all of them together.

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

After greedy pretraining, the stacked encoders are connected end-to-end and, optionally, topped with a softmax classifier (Lecture 14's softmax + cross-entropy). The whole stack is then fine-tuned jointly via standard backpropagation (Lecture 8) on a labeled task. This was historically one of the earliest successful "unsupervised pretraining, then supervised fine-tuning" recipes for deep networks — the same historical role played by DBN pretraining (Lecture 22) — and while it has been largely superseded today by better initialization schemes, normalization, and architectures, it remains conceptually important and is still used for genuinely label-scarce problems.

3. Deep Autoencoders: Joint Training & Practical Challenges

Contrast Section 2's approach with a deep autoencoder: many encoder/decoder layers trained jointly, end-to-end, from random initialization — not greedily, layer by layer. Training a deep AE this way runs into exactly the challenges covered earlier in the course, just applied to a deep encoder–decoder pipeline instead of an RNN or a plain classifier:

  • Vanishing gradients through many stacked layers (Lecture 16's problem).
  • Overfitting, especially with a large bottleneck relative to dataset size.
  • Initialization sensitivity — a poor starting point can stall training before it gets anywhere.

The standard fixes should already sound familiar:

  • Dropout — randomly zero a fraction of units during training to prevent co-adaptation.
  • Batch normalization — Lecture 14's exact recipe applies here too: normalize each layer's pre-activations to zero mean and unit variance using the current mini-batch's statistics, then apply a learnable scale \(\gamma\) and shift \(\beta\) so the layer can still represent its ideal distribution. This keeps activations well-scaled through a deep encoder–decoder stack the same way it does through a deep CNN classifier.
  • Early stopping — halt training when validation reconstruction loss stops improving, before it starts overfitting to training-set noise.
A "deep" autoencoder — several encoder and decoder layers on each side of the bottleneck, all trained jointly (contrast with Lecture 23's single-hidden-layer hourglass). More layers means a richer hierarchy of features, but also more exposure to vanishing gradients and overfitting, hence the fixes above.

4. Convolutional Autoencoders

For image data, flattening pixels into a dense bottleneck throws away spatial structure. A convolutional autoencoder keeps that structure throughout: the encoder uses Conv2D layers (Lecture 13's convolution operation) to shrink the spatial resolution while increasing channel depth, and the decoder uses transposed convolution ("deconvolution") layers to expand back toward the original resolution.

Transposed convolution, briefly

The gradient-reversal operation used to upsample a smaller feature map back toward the original spatial resolution — conceptually, it runs a (learned) convolution "backward," spreading each input value out over a larger output region, the inverse spatial operation to the strided/pooled convolutions of the encoder.

Because every layer stays spatially organized (a 2D grid of activations, not a flat vector), convolutional autoencoders preserve local structure — edges, textures, shapes — far better than a dense encoder/decoder would, making them the standard choice for image reconstruction, denoising, and compression tasks.

5. Variational Autoencoders — A Conceptual Preview

One more variant, at a purely conceptual level (no derivation — this is intentionally out of scope here, consistent with the course's own treatment): a Variational Autoencoder (VAE) encodes an input not to a single fixed latent point \(z\), but to a probability distribution over latent space — typically a Gaussian, parameterized by a predicted mean and variance. It then decodes by sampling from that distribution rather than reading off a fixed code. This small change has a large consequence: it makes the latent space smooth and generative — you can sample a brand-new latent point that was never produced by encoding any real input, and decode it into a plausible new data sample. A standard autoencoder's latent space carries no such guarantee; nothing stops it from having "holes" that decode to nonsense.

⚠ Scope note

This section deliberately stops at the concept. The VAE loss (the evidence lower bound, ELBO) and the reparameterization trick are standard next steps in a course that continues beyond this one, but are not derived here.

6. Evaluating Reconstructions: MSE, PSNR & SSIM

MSE is both the usual training loss (Lecture 23, Section 3) and a perfectly valid evaluation metric on its own — lower is better. For images specifically, two further standard metrics are common:

PSNR — Peak Signal-to-Noise Ratio

$$PSNR = 10\log_{10}\left(\frac{MAX_I^2}{MSE}\right)$$ where \(MAX_I\) is the maximum possible pixel value (e.g. 255 for 8-bit images, or 1.0 for normalized images). Higher PSNR = better reconstruction.

SSIM (Structural Similarity Index) compares luminance, contrast, and structure between two images rather than raw pixelwise error, and is better correlated with human perceptual judgment than MSE or PSNR — the concept matters here, not the full formula.

Applying the PSNR formula mechanically to Lecture 23's worked-example MSE values (treating \(MAX_I=1\) for illustration, even though our toy 3-feature vector isn't literally pixel data):

🔢 Worked example: PSNR from Lecture 23's two MSE values

Clean reconstruction (\(MSE\approx0.14442\)):

$$PSNR = 10\log_{10}\left(\frac{1^2}{0.14442}\right) = 10\log_{10}(6.9243) = 10(0.8404) \approx \mathbf{8.40\ dB}$$

Denoised reconstruction (\(MSE\approx0.19207\)):

$$PSNR = 10\log_{10}\left(\frac{1^2}{0.19207}\right) = 10\log_{10}(5.2064) = 10(0.7166) \approx \mathbf{7.17\ dB}$$

Both numbers match the bar chart below. Notice the direction: a smaller MSE sits inside a larger fraction \(1/MSE\), which produces a larger log and therefore a higher PSNR — this is why "higher PSNR = better reconstruction," even though the metric is built directly out of an error term.

PSNR (dB) computed from Lecture 23's two reconstruction MSE values. The clean-input reconstruction (MSE≈0.14442 → PSNR≈8.40 dB) scores higher than the denoised reconstruction from a corrupted input (MSE≈0.19207 → PSNR≈7.17 dB) — lower MSE always means higher PSNR, since PSNR is just a rescaled, inverted, log view of the same error.

7. Visualizing the Latent Space & Applications

The latent code \(z\) is typically much lower-dimensional than the input, but often still more than 2 or 3 dimensions — too many to plot directly. Two standard tools project it down further, for human-interpretable plotting:

  • PCA (Lecture 23, Section 1) — a linear projection maximizing variance; fast, but only captures linear structure in the latent space.
  • t-SNE — nonlinear; preserves local neighborhood structure and similarity better than PCA does, which is why it's the more common choice specifically for visualization (not necessarily for downstream computation).
ApplicationMechanismExample
Anomaly detectionTrain an autoencoder only on "normal" data. At inference, an input that reconstructs poorly (high reconstruction error) is flagged as anomalous — the network never learned to compress/reconstruct that kind of pattern.Fraud detection, manufacturing defect detection
Image denoisingDeploy Lecture 23's denoising autoencoder directly as a noise-removal tool: feed a noisy image in, read the reconstruction out.Cleaning scanned documents, sensor noise removal
Data compressionThe latent code \(z\) is a compressed representation of \(x\) — unlike generic lossless compression (zip), this is lossy and learned specifically for the training data's distribution.Much higher compression ratios on in-distribution data, at the cost of imperfect reconstruction

8. Pitfalls & Practical Notes

⚠ Things that silently break these variants
  • Contractive: too large \(\lambda\) collapses the latent space (every \(z\) pulled toward the same point, killing reconstruction quality) — it must be balanced against \(L_{reconstruction}\), not maximized.
  • Stacked: forgetting to discard each stage's decoder before training the next encoder on top — feeding a decoder's output (not the encoder's code) into the next stage breaks the intended hierarchy.
  • Deep: skipping batch norm/dropout/early stopping and then blaming "autoencoders don't work," when the real cause is vanishing gradients or overfitting — exactly the Lecture 16 and Lecture 9 failure modes, unaddressed.
  • Convolutional: encoder/decoder shape mismatches — the output spatial dimensions of the Conv2DTranspose stack must exactly match the input image dimensions, or the reconstruction loss cannot even be computed elementwise.
  • Evaluation: relying on MSE/PSNR alone — two reconstructions can have near-identical MSE while looking very different perceptually; this is exactly why SSIM exists.

9. Summary

Module G takeaway

The bottleneck forces compression → sparse, denoising, and contractive constraints each prevent the trivial identity shortcut in a different way → stacked/deep training scales the idea to many layers (echoing Lecture 22's DBN pretraining) → convolutional variants adapt it to images (echoing Lectures 12–14) → variational autoencoders point the way toward true generative modeling.

🎓 The course, in one paragraph

This is the last lecture of a 24-lecture arc: from the single perceptron (Lecture 3) and backpropagation (Lecture 8), through CNNs (Lectures 12–14) that learn to see, RNNs and LSTMs (Lectures 15–18) that learn to remember, embeddings, attention, and Transformers (Lectures 19–20) that learn to relate, energy-based models and Boltzmann machines (Lectures 21–22) that learn without labels via a different mechanism, and finally autoencoders (Lectures 23–24) that learn compressed, useful representations by reconstructing their own input. Every lecture's downloadable code is collected in one place — visit resources.html to grab any of it.

10. Code: Contractive Penalty & Convolutional Autoencoder Skeleton

The NumPy portion below reproduces the contractive-penalty worked example exactly (0.29). The Keras portion is a runnable architecture skeleton for a convolutional autoencoder on MNIST-shaped input.

⚠ Train the real thing on Colab/GPU

The skeleton below defines the architecture only — actually training a convolutional autoencoder on real image data belongs on a GPU-backed notebook, not a lecture snippet. See the official Keras tutorial linked on the resources page for a complete, runnable, trained example.

lecture-24-contractive-conv.py
import numpy as np

# ==============================================================
# Part 1 -- Contractive penalty: Frobenius norm of the encoder Jacobian
#           (reproduces the lecture's linear-encoder example: 0.29)
# ==============================================================
We = np.array([0.4, 0.3, -0.2])          # same linear encoder as Lecture 23

def contractive_penalty_linear(W):
    """For a LINEAR encoder z = Wx + b, the Jacobian is just W itself
    (constant, independent of x), so ||J||_F^2 = sum(W_ji^2)."""
    return np.sum(W ** 2)

def contractive_penalty_sigmoid(W, z):
    """For a sigmoid encoder z = sigmoid(Wx + b):
    ||J||_F^2 = sum_j (z_j(1-z_j))^2 * sum_i W_ji^2   (per latent unit j)."""
    local_grad_sq = (z * (1 - z)) ** 2          # elementwise sigmoid'(z)^2
    row_norms_sq  = np.sum(W ** 2, axis=-1)     # sum_i W_ji^2 per row j
    return np.sum(local_grad_sq * row_norms_sq)

jf_linear = contractive_penalty_linear(We)
print("Linear-encoder contractive penalty ||J_f||_F^2 =", round(jf_linear, 5))
# expected: 0.29

# same weights, imagining instead a sigmoid encoder currently outputting z=0.75
z_example  = np.array([0.75])
jf_sigmoid = contractive_penalty_sigmoid(We.reshape(1, -1), z_example)
print("Sigmoid-encoder contractive penalty at z=0.75:", round(jf_sigmoid, 5))

# ==============================================================
# Part 2 -- Convolutional autoencoder skeleton (Keras), MNIST-shaped input
#           Full training belongs on Colab/GPU -- see the box above.
# ==============================================================
def build_conv_autoencoder():
    from tensorflow.keras import layers, models
    inp = layers.Input(shape=(28, 28, 1))

    # ---- encoder: Conv2D with stride to shrink spatial size ----
    x = layers.Conv2D(16, 3, activation='relu', padding='same', strides=2)(inp)   # 28->14
    x = layers.Conv2D(8,  3, activation='relu', padding='same', strides=2)(x)     # 14->7
    encoded = x                                                                    # bottleneck feature map

    # ---- decoder: Conv2DTranspose ("deconvolution") to upsample back ----
    x = layers.Conv2DTranspose(8,  3, activation='relu', padding='same', strides=2)(encoded)  # 7->14
    x = layers.Conv2DTranspose(16, 3, activation='relu', padding='same', strides=2)(x)         # 14->28
    decoded = layers.Conv2D(1, 3, activation='sigmoid', padding='same')(x)                     # 28x28x1

    autoencoder = models.Model(inp, decoded)
    autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
    return autoencoder

if __name__ == "__main__":
    try:
        model = build_conv_autoencoder()
        model.summary()
    except ImportError:
        print("TensorFlow/Keras not installed -- run this part on Colab (see the lecture's resource link).")

⬇ Download lecture-24-contractive-conv.py   More resources for this lecture →