In the previous lesson we saw that a GAN's generator maps a latent space to images, but that space is built "blindly", pushed by an adversary. The autoencoder takes the direct route: it is a network that learns to compress its inputs and reconstruct them, and in the process builds an explicit latent space needing neither labels nor adversaries. This apparently modest idea — copying the input to the output through a funnel — is one of the most fruitful in deep learning: it gives rise to dimensionality reduction, noise removal and, above all for TecnoMarket, anomaly and fraud detection, the application we will set up here and develop as a project in 07-03.

Contents

  1. Unsupervised representation learning
  2. Anatomy: encoder, bottleneck, decoder
  3. What the latent space learns
  4. A dense autoencoder on MNIST in Keras
  5. Interpreting the reconstruction error
  6. Applications: dimensionality reduction and denoising
  7. Anomaly detection: why it works
  8. The variational autoencoder (VAE): a bridge to generation

Unsupervised representation learning

Every model you have trained so far was supervised: each example carried its label (the correct digit, the review's sentiment, the next day's sales). But labeling is expensive. TecnoMarket has millions of unlabeled transactions, photos and records, and that is where unsupervised learning comes in: extracting structure from data without anyone saying what each thing is.

The autoencoder's trick is to turn an unlabeled problem into a supervised one with an elegant twist: each example's label is the example itself. The network receives an image and must produce... that same image. It sounds trivial (just copy it), but we put an obstacle in the way: in the middle of the network there is a bottleneck, a layer much smaller than the input. To get through it, the network is forced to decide which information is essential and which is expendable. That — deciding what matters — is learning a representation.

Anatomy: encoder, bottleneck, decoder

An autoencoder has three parts:

flowchart LR
    X[Input<br/>784 values] --> E1[Dense 128]
    E1 --> E2[Dense 64]
    E2 --> Z[Bottleneck<br/>32 values]
    Z --> D1[Dense 64]
    D1 --> D2[Dense 128]
    D2 --> XR[Reconstruction<br/>784 values]

    subgraph Encoder
        E1
        E2
    end
    subgraph Decoder
        D1
        D2
    end
  • Encoder: progressively compresses the input down to the bottleneck. It is like a CNN's path towards its embedding (03-04), but here with no labels to guide it.
  • Bottleneck (code or latent vector z): the compressed representation. Its size is a design decision: too large and the network "copies" without learning anything; too small and it loses essential information.
  • Decoder: reconstructs the input from the code. Sound familiar? It is structurally the same as the GAN generator from 05-01: a map from latent vector → data. The difference is how it is trained.

The reconstruction loss measures how closely the output resembles the input. For normalized images the usual choice is the MSE you met in 02-04 (or per-pixel BCE):

loss = MSE(x, x_reconstructed) = average of (x_i - x̂_i)²

There is no adversary, no minimax game: it is an ordinary optimization with Adam and backpropagation, as stable as those in module 2.

What the latent space learns

Compressing 784 pixels into 32 numbers forces lossy compression, and that loss is selective: the network keeps what helps reconstruct many examples and discards the idiosyncratic noise. The useful analogy is the summary: if you condense a 500-word review into 20 words, you keep "unhappy customer, battery life is short, wants a refund" and discard the stylistic flourishes. The bottleneck does the same with the data.

The result is a latent space with the properties you already know from embeddings (03-04, 04-03) and from the GAN latent space (05-01):

  • Similar inputs → nearby codes (measurable with cosine similarity or Euclidean distance).
  • The latent axes capture factors of variation (stroke slant, thickness, shape, in the case of MNIST).
  • The code can be used as a compact "fingerprint" of the data point for search or clustering.

There is one important difference from the GAN: the latent space of a classic autoencoder is not designed for generation: if you feed the decoder a z made up at random, you will most likely get a smudge, because the network has only learned to reconstruct codes coming from real data. We will come back to this at the end with the VAE.

A dense autoencoder on MNIST in Keras

Following the course's methodology — public dataset first, then TecnoMarket — let's build the autoencoder from the diagram on MNIST:

import numpy as np
from tensorflow import keras
from tensorflow.keras import layers

# 1. Data: we only need the images, NOT the labels (unsupervised)
(x_train, _), (x_test, _) = keras.datasets.mnist.load_data()
x_train = x_train.astype("float32") / 255.0   # normalize to [0, 1]
x_test = x_test.astype("float32") / 255.0
x_train = x_train.reshape(-1, 784)            # flatten 28x28 -> 784
x_test = x_test.reshape(-1, 784)

LATENT_DIM = 32   # the bottleneck: 784 -> 32 (~24:1 compression)

# 2. Encoder: a narrowing funnel
encoder = keras.Sequential([
    layers.Dense(128, activation="relu", input_shape=(784,)),
    layers.Dense(64, activation="relu"),
    layers.Dense(LATENT_DIM, activation="relu"),  # the code z
], name="encoder")

# 3. Decoder: a widening funnel, mirror of the encoder
decoder = keras.Sequential([
    layers.Dense(64, activation="relu", input_shape=(LATENT_DIM,)),
    layers.Dense(128, activation="relu"),
    # final sigmoid because the pixels live in [0, 1]
    layers.Dense(784, activation="sigmoid"),
], name="decoder")

# 4. Autoencoder = encoder + decoder chained together
autoencoder = keras.Sequential([encoder, decoder], name="autoencoder")

# 5. The "label" is the input itself: fit(x_train, x_train)
autoencoder.compile(optimizer="adam", loss="mse")
history = autoencoder.fit(
    x_train, x_train,          # <- input and target are the same
    epochs=20,
    batch_size=256,
    validation_data=(x_test, x_test),
)

The details that matter:

  • fit(x_train, x_train): the entire autoencoder idea condensed into one line. The target is the input itself.
  • Sigmoid in the final layer because the normalized pixels live in [0, 1]; remember from 02-02 that the output activation must match the target's range.
  • Encoder/decoder symmetry: it is not mandatory, but it is the usual design and makes the network easier to reason about.

And now, the most instructive part: looking at the reconstructions.

import matplotlib.pyplot as plt

reconstructed = autoencoder.predict(x_test[:10])

fig, axes = plt.subplots(2, 10, figsize=(14, 3))
for i in range(10):
    axes[0, i].imshow(x_test[i].reshape(28, 28), cmap="gray")
    axes[0, i].set_title("original", fontsize=8)
    axes[0, i].axis("off")
    axes[1, i].imshow(reconstructed[i].reshape(28, 28), cmap="gray")
    axes[1, i].set_title("reconstructed", fontsize=8)
    axes[1, i].axis("off")
plt.show()

You will see recognizable but slightly smoothed digits: fine strokes get blurred and calligraphic quirks disappear. That smoothness is lossy compression in action — the network reconstructs the "typical" digit that summarizes yours.

Interpreting the reconstruction error

Besides looking at the images, we can measure each example's error:

errors = np.mean((x_test - autoencoder.predict(x_test)) ** 2, axis=1)
print(f"Mean error: {errors.mean():.4f}")
print(f"Worst example: {errors.max():.4f}  |  Best: {errors.min():.4f}")

This per-example number is the key to almost every practical application:

  • Low error → the example resembles what the network saw in training: it is "normal".
  • High error → the example has something the learned summary cannot express: it is odd, noisy... or anomalous.

Sort the test examples by descending error and look at the top ones: you will find the strangest digits in the dataset (illegible strokes, unusual styles). The autoencoder has just built, without us asking, a weirdness detector.

Applications

Dimensionality reduction (vs. PCA)

The encoder on its own (encoder.predict(x)) is a dimensionality reducer: 784 → 32. The classic technique for this is PCA (principal component analysis); let's compare:

Aspect PCA Autoencoder
Type of transformation Linear (projection) Nonlinear (as flexible as the network)
Capacity Limited to linear structure Captures curved, complex relationships
Training cost Very low (direct algebra) Gradient-based training
Interpretability High (components ordered by variance) Low (the code has no natural ordering)
When to choose it Quick baseline, ~linear data Complex data (images, signals)

The practical rule is the same as with the baselines in 04-04: start with PCA (cheap and honest) and move to the autoencoder if the structure of your data justifies it. In fact, an autoencoder with linear activations and MSE learns essentially the same subspace as PCA — the gain comes from the nonlinearities.

Denoising autoencoder

A variant with a minimal change and great utility: train with noisy input and a clean target.

# Add Gaussian noise to the inputs
noise_factor = 0.4
x_train_noisy = x_train + noise_factor * np.random.normal(size=x_train.shape)
x_test_noisy = x_test + noise_factor * np.random.normal(size=x_test.shape)
x_train_noisy = np.clip(x_train_noisy, 0.0, 1.0)  # stay within [0, 1]
x_test_noisy = np.clip(x_test_noisy, 0.0, 1.0)

# Same architecture; ONLY the (input, target) pair changes:
# noisy input -> clean target
autoencoder.fit(x_train_noisy, x_train,
                epochs=20, batch_size=256,
                validation_data=(x_test_noisy, x_test))

Since the network cannot memorize the noise (it is random and different every time), it is forced to learn the digit's underlying structure and discard the rest. For TecnoMarket, the same recipe cleans up product photos uploaded by sellers with poor lighting or aggressive compression before handing them to the 03-04 classifier.

Anomaly detection: why it works

This is the star application for TecnoMarket, and the reasoning deserves a precise statement:

  1. You train the autoencoder only on normal data (e.g., hundreds of thousands of legitimate transactions).
  2. The network learns to summarize and reconstruct normality well: its regularities fit in the bottleneck.
  3. An anomalous example arrives (a fraudulent transaction with an unusual combination of amount, time, device and shipping address). The learned summary has no vocabulary for that oddity: compressing it warps it towards normality, and the reconstruction comes out wrong.
  4. High reconstruction error → anomaly candidate. By setting a threshold on the error (once again the threshold + human review queue pattern from 03-04), suspicious transactions go to review.

The elegance of the approach: you need no fraud examples to train — just abundant normality, which is exactly what you have plenty of. This matters because fraud is rare, ever-changing and expensive to label. In 07-03 we will build this complete system on fictional TecnoMarket transaction data: dataset generation, threshold selection, suitable metrics for imbalanced classes and deployment. For now it is enough that the logic is clear to you.

The variational autoencoder (VAE)

Let's close the circle with the GAN from 05-01. We said the decoder of a classic autoencoder is no good for generation: its latent space has "holes" — regions with no data where the reconstruction is garbage. The variational autoencoder (VAE) fixes exactly that, with two conceptual changes (we will spare you the heavy math):

  1. The encoder does not produce a point z, but a small cloud of probability (a mean and a variance) from which z is sampled. Each example occupies a region, not a point.
  2. A term is added to the loss that pushes all those clouds towards a standard normal distribution, forcing them to overlap and fill the space without holes.

The result is a continuous, navigable latent space: sample any z from the normal distribution, run it through the decoder and you get a plausible new data point. In other words, a VAE is an autoencoder that does generate:

Classic autoencoder VAE GAN
Trained by Reconstruction Reconstruction + latent regularization Adversarial game
Stability High High Low (05-01)
Generates new samples? No (holes in the latent space) Yes (somewhat blurry) Yes (sharp)
Typical use Compression, anomalies Controlled generation, interpolation High-fidelity generation

The VAE is the conceptual bridge between the two lessons: reconstructing (autoencoder) and creating (GAN) are two faces of the same latent space.

Common Mistakes and Tips

  • Making the bottleneck too large. With LATENT_DIM=784 the network can learn the identity function and the error will be tiny without having learned anything useful. If your reconstructions are perfect, be suspicious: shrink the code until the compression "hurts" a little.
  • Training the anomaly detector on contaminated data. If the training set unknowingly includes fraud, the network will learn to reconstruct it too and it will stop standing out. Curate the training dataset as well as you can.
  • Forgetting that the reconstruction error is not a probability. It is a distance with no universal scale: the anomaly threshold must be calibrated on a validation set, not set "by eye" (we will do it rigorously in 07-03).
  • Using the same fixed noise for denoising. If you generate the noise only once, the network can memorize it. Ideally the noise is regenerated every epoch (with a data generator), or at least you accept the simple version knowing its limitation.
  • Comparing the autoencoder against nothing. As with the time series in 04-04: keep a baseline (PCA for reduction, distance to the mean for anomalies) and demand that the autoencoder beat it.

Exercises

Exercise 1. Using the summary analogy, explain why an autoencoder trained on legitimate TecnoMarket transactions reconstructs a fraudulent transaction poorly, and why this is an advantage over training a supervised fraud/no-fraud classifier.

Exercise 2. Modify the MNIST autoencoder so the bottleneck has 2 dimensions, train it, and write the code to draw a scatter plot of encoder.predict(x_test) colored by the true digit (the labels, which we didn't use for training, may still be used for visualization). What do you expect to see?

Exercise 3. True or false, with justification: (a) an autoencoder needs labels to train; (b) sampling a random z and running it through the decoder of a classic autoencoder produces good-quality new examples; (c) the denoising autoencoder uses the noisy input as its target; (d) a VAE adds to the reconstruction a term that organizes the latent space.

Solutions

Solution 1.

The autoencoder learns a "summary" of normality: the usual combinations of amount, timing, device and destination fit in the bottleneck because they repeat across hundreds of thousands of examples. A fraudulent transaction contains combinations the summary cannot express (e.g., high amount + freshly created account + shipping to a never-seen address); when compressing it, the network "normalizes" it and the reconstruction differs greatly from the original → high error → alarm. The advantage over the supervised classifier is twofold: (1) no labeled fraud examples are needed, and those are scarce and expensive; (2) it detects new kinds of fraud, whereas a classifier only recognizes patterns similar to the fraud it saw during training.

Solution 2.

LATENT_DIM = 2  # and retrain encoder/decoder/autoencoder as before

(_, _), (x_test_img, y_test) = keras.datasets.mnist.load_data()
codes = encoder.predict(x_test)            # shape (10000, 2)

plt.figure(figsize=(8, 8))
plt.scatter(codes[:, 0], codes[:, 1], c=y_test, cmap="tab10", s=3)
plt.colorbar(label="digit")
plt.xlabel("z1"); plt.ylabel("z2")
plt.show()

You should expect clusters by digit: although the network never saw labels, examples of the same digit resemble each other and end up in nearby regions of the latent plane (with overlaps between visually similar digits, such as 4/9 or 3/8). Reconstruction with only 2 dimensions will be noticeably worse than with 32: that is the price of such aggressive compression.

Solution 3.

  • (a) False: it uses the input itself as the target (fit(x, x)); it is unsupervised representation learning.
  • (b) False in general: the classic latent space has data-free holes; an arbitrary z usually decodes to a smudge. That is precisely what motivates the VAE.
  • (c) False: the input is the noisy version, but the target is the clean version; that is why it learns to remove noise.
  • (d) True: the VAE adds to the reconstruction loss a term that pushes the codes towards a normal distribution, leaving the latent space continuous and fit for generation.

Conclusion

The autoencoder turns the absence of labels into a virtue: by using each data point as its own target, it learns a latent space that summarizes the essentials, and from that summary its applications are born — reducing dimensionality better than a linear projection, cleaning noise, and flagging as anomalous anything that reconstructs poorly, which is the basis of the fraud detector TecnoMarket will build in 07-03. The VAE adds the probabilistic ingredient that turns reconstruction into generation, building the bridge back to GANs.

One uncomfortable constant runs through the module so far: training from scratch costs data, time and GPU. In the next lesson we will learn the most profitable shortcut in practical deep learning: transfer learning, or how to lean on the pretrained architectures we catalogued in 03-03 to solve TecnoMarket problems with just a few dozen images.

© Copyright 2026. All rights reserved