Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

Autoencoders Explained: Encoder, Bottleneck and Anomaly Use

Lesson 8 of 9 · 11 min · Updated 13 Nov 2026

Autoencoders explained: train a tiny encoder-decoder in NumPy, compare with PCA, detect anomalies with reconstruction error and meet variational autoencoders.

A tailor on MG Road takes your measurements for a kurta. He does not photograph you from every angle. He writes about eight numbers on a slip: chest, waist, shoulder, length, sleeve and a few more. From that slip, a cutter in the back room rebuilds the whole pattern.

The slip works because bodies are not random. Someone with long arms is usually tall too, so a handful of numbers captures most of what matters. The tailor has, in effect, learned a compressed code for human shapes.

Now imagine a customer wants a costume with one sleeve twice as long as the other. The standard slip has no box for that, and the cutter's pattern comes out wrong. That failure is useful: it tells you this order is unusual.

What is an autoencoder?

Map the tailor's shop to the network:

  • You, the customer, are the input x.
  • Measuring and writing the slip is the encoder.
  • The slip itself is the code, also called the latent representation or bottleneck.
  • The cutter rebuilding the pattern is the decoder, producing the reconstruction x̂.
  • How far the pattern is from your real shape is the reconstruction error.

An autoencoder is an unsupervised neural network made of an encoder that compresses the input into a low-dimensional code and a decoder that reconstructs the input from that code, trained to make the reconstruction as close to the original as possible.

The target is the input itself, so you need no labels. That makes autoencoders handy when you have plenty of data but nobody has tagged it.

Autoencoder architecture: an 8-value input is squeezed by the encoder into a 2-value bottleneck code and rebuilt by the decoder into an 8-value reconstruction

The hourglass shape of an autoencoder. The bottleneck is narrower than the input, so the network cannot simply copy; it must learn which patterns to keep.

How does an autoencoder learn?

Training is the same loop you used in neural network from scratch, with one twist: the target is x itself.

  1. Encode: code = f(W1 · x + b1), with fewer outputs than inputs.
  2. Decode: x̂ = W2 · code + b2, back to the original size.
  3. Loss: mean squared error between x̂ and x.
  4. Backpropagate through decoder and encoder together, then update with an optimiser such as Adam from gradient descent and optimizers.

The bottleneck is what forces learning. If the code were as wide as the input, the network could learn the identity function and copy everything, learning nothing.

Train a tiny autoencoder in NumPy

The data below mimics the tailor's problem: 500 customers, 8 measurements each, all driven by just 2 hidden factors (think height and build) plus a little noise. The autoencoder squeezes 8 numbers into 2 and rebuilds them.

Python
import numpy as np
rng = np.random.default_rng(0)

factors = rng.normal(size=(500, 2))                       # hidden: height, build
mix = rng.normal(size=(2, 8))
X = np.tanh(factors @ mix) + rng.normal(scale=0.05, size=(500, 8))
X = (X - X.mean(0)) / X.std(0)                            # standardise measurements

W1 = rng.normal(scale=0.3, size=(8, 2)); b1 = np.zeros(2)     # encoder: 8 -> 2
W2 = rng.normal(scale=0.3, size=(2, 8)); b2 = np.zeros(8)     # decoder: 2 -> 8
params = [W1, b1, W2, b2]
m = [np.zeros_like(p) for p in params]; v = [np.zeros_like(p) for p in params]
for step in range(1, 1501):
    code = np.tanh(X @ W1 + b1)                  # encode: squeeze to 2 numbers
    X_hat = code @ W2 + b2                       # decode: rebuild 8 numbers
    err = X_hat - X
    d_out = 2 * err / err.size                   # backprop through decoder and encoder
    d_code = (d_out @ W2.T) * (1 - code ** 2)
    grads = [X.T @ d_code, d_code.sum(0), code.T @ d_out, d_out.sum(0)]
    for k, (p, g) in enumerate(zip(params, grads)):   # Adam update
        m[k] = 0.9 * m[k] + 0.1 * g; v[k] = 0.999 * v[k] + 0.001 * g ** 2
        p -= 0.01 * (m[k] / (1 - 0.9 ** step)) / (np.sqrt(v[k] / (1 - 0.999 ** step)) + 1e-8)
    if step in (1, 100, 500, 1500):
        print(f"step {step:4d}  reconstruction MSE {np.mean(err ** 2):.4f}")
print("baseline (predict the average customer): MSE 1.0")
print("first customer's 2-number code:", np.round(code[0], 3))

The error falls from about 0.99 to about 0.04, so 2 numbers rebuild 8 measurements with roughly 96% of the variation explained. Guessing the average customer for everyone gives an error of 1.0, because the data is standardised.

How is an autoencoder different from PCA?

PCA also compresses data into a few numbers, but only with straight-line (linear) projections. In fact, an autoencoder with no activation functions and squared-error loss learns the same subspace as PCA. Add non-linear activations and extra layers, and an autoencoder can follow curved structure that PCA flattens.

PCA Autoencoder
Mapping Linear only Linear or non-linear
Training Closed form, seconds Gradient descent, needs tuning
Interpretability Components are ranked and orthogonal Code dimensions have no fixed order
Data needed Works on small data Needs more data to avoid overfitting
Good first choice for Tabular data, quick reduction Images, audio, complex sensor data

Try PCA first. Reach for an autoencoder when PCA's reconstruction is clearly too poor.

Quick check

What is the training target of a basic autoencoder?

Autoencoders for anomaly detection

Back to the costume with mismatched sleeves. An autoencoder trained only on normal data learns to rebuild normal patterns well. Feed it something unusual and the reconstruction is poor. The reconstruction error becomes an anomaly score.

Autoencoder anomaly detection flow: normal records rebuild with low error, unusual records rebuild with high error and are flagged above a threshold

Train on normal rows only, then set a threshold from their errors. New rows whose reconstruction error crosses it are flagged for review.

The steps:

  1. Train the autoencoder on data you believe is normal.
  2. Compute the reconstruction error for every training row.
  3. Set a threshold, for example the 99th percentile of those errors.
  4. Flag any new row whose error is above the threshold.
Python
import numpy as np
rng = np.random.default_rng(1)

mix = rng.normal(size=(2, 8))
def normal_rows(n):                               # 8 readings driven by 2 hidden factors
    return np.tanh(rng.normal(size=(n, 2)) @ mix) + rng.normal(scale=0.05, size=(n, 8))
X_train, X_new = normal_rows(1000), normal_rows(200)
odd = rng.uniform(-1, 1, size=(10, 8))            # 10 records that break the pattern

P = [rng.normal(scale=0.3, size=(8, 2)), np.zeros(2), rng.normal(scale=0.3, size=(2, 8)), np.zeros(8)]
def rebuild(X):
    code = np.tanh(X @ P[0] + P[1]);  return code, code @ P[2] + P[3]
M = [np.zeros_like(p) for p in P]; V = [np.zeros_like(p) for p in P]
for t in range(1, 1501):                          # same autoencoder + Adam as before
    code, X_hat = rebuild(X_train)
    d_out = 2 * (X_hat - X_train) / X_train.size
    d_code = (d_out @ P[2].T) * (1 - code ** 2)
    for k, g in enumerate([X_train.T @ d_code, d_code.sum(0), code.T @ d_out, d_out.sum(0)]):
        M[k] = 0.9 * M[k] + 0.1 * g;  V[k] = 0.999 * V[k] + 0.001 * g ** 2
        P[k] -= 0.01 * (M[k] / (1 - 0.9 ** t)) / (np.sqrt(V[k] / (1 - 0.999 ** t)) + 1e-8)

score = lambda X: ((X - rebuild(X)[1]) ** 2).mean(axis=1)   # reconstruction error per row
threshold = np.percentile(score(X_train), 99)
print(f"threshold (99th percentile of normal errors): {threshold:.4f}")
print(f"new normal rows flagged: {(score(X_new) > threshold).sum()} of 200")
print(f"odd rows flagged:        {(score(odd) > threshold).sum()} of 10")
print("median error  normal:", round(float(np.median(score(X_new))), 4), " odd:", round(float(np.median(score(odd))), 4))

All 10 odd rows are flagged and none of the 200 new normal rows are. The median error for odd rows is about 75 times higher than for normal ones. Real systems use this idea for card fraud, machine sensor faults and network intrusion, alongside the methods in anomaly detection techniques.

Quick check

In anomaly detection with an autoencoder, which rows get flagged?

Types of autoencoders

  • Undercomplete autoencoder: the basic hourglass above, with a code smaller than the input.
  • Denoising autoencoder: you add noise to the input but ask it to rebuild the clean original. It learns to remove noise from scans, audio or sensor readings.
  • Sparse autoencoder: the code can be wide, but a penalty keeps most code values near zero, so each input activates only a few features.
  • Convolutional autoencoder: uses CNN layers in the encoder and transposed convolutions in the decoder, for images.
  • Variational autoencoder (VAE): the encoder outputs a distribution instead of a single point, which lets you generate new samples.

Variational autoencoders in one block

A VAE's encoder outputs a mean μ and a log-variance for each code dimension. Training samples a code with the reparameterisation trick, z = μ + σ · ε with random ε, so gradients can still flow through μ and σ.

A KL divergence penalty pulls every code distribution towards a standard normal, which keeps the code space smooth. After training, you sample z from a standard normal and decode it to create new data.

Python
import numpy as np
rng = np.random.default_rng(3)

mu = np.array([0.8, -1.2])            # encoder output for one customer: centre
log_var = np.array([-2.0, -0.5])      # and log-variance (log keeps variance positive)

std = np.exp(0.5 * log_var)
for _ in range(3):                    # reparameterisation trick: z = mu + std * noise
    z = mu + std * rng.normal(size=2)
    print("sampled code z:", np.round(z, 3))

def kl_to_standard_normal(mu, log_var):
    return -0.5 * np.sum(1 + log_var - mu ** 2 - np.exp(log_var))
print(f"KL penalty for this customer : {kl_to_standard_normal(mu, log_var):.3f}")
print(f"KL penalty if mu=0, var=1    : {abs(kl_to_standard_normal(np.zeros(2), np.zeros(2))):.3f}")
print("random code for generation   :", np.round(rng.normal(size=2), 3))

The KL penalty is zero only when the code distribution is exactly a standard normal, so the model balances rebuilding well against keeping codes tidy. VAEs are an ancestor of today's image generators; their adversarial rival is covered in GANs explained.

Building an autoencoder in PyTorch

Python
import torch
from torch import nn

class AutoEncoder(nn.Module):
    def __init__(self, n_features=8, code_size=2):
        super().__init__()
        self.encoder = nn.Sequential(nn.Linear(n_features, 16), nn.ReLU(),
                                     nn.Linear(16, code_size))
        self.decoder = nn.Sequential(nn.Linear(code_size, 16), nn.ReLU(),
                                     nn.Linear(16, n_features))

    def forward(self, x):
        return self.decoder(self.encoder(x))

model = AutoEncoder()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
x = torch.randn(64, 8)                       # a batch of normal rows
loss = nn.functional.mse_loss(model(x), x)   # target is the input itself
optimizer.zero_grad(); loss.backward(); optimizer.step()

Once trained, model.encoder(x) gives you the compressed code for clustering or plotting, and the per-row MSE gives you anomaly scores. To build projects like a fraud detector around this idea with mentors, see an ML course in Indore where you build autoencoders from scratch at Debugshala.

Common mistakes with autoencoders

  • A bottleneck that is too wide. The network learns to copy and the code is useless. Shrink it until reconstruction starts to suffer, then back off a little.
  • Training on data full of anomalies. The model learns to rebuild them too, and the error stops separating them.
  • Unscaled features. One large-valued column dominates the squared error. Standardise first.
  • Setting the threshold on test data. Pick it from training or validation errors only.
  • Expecting good samples from a plain autoencoder. Random codes decode to junk; use a VAE if you want to generate.

Key takeaways

  • An autoencoder = encoder + bottleneck code + decoder, trained to rebuild its input.
  • The narrow bottleneck forces it to learn the structure of the data, without labels.
  • A linear autoencoder matches PCA; non-linear ones capture curved structure.
  • Reconstruction error is a ready-made anomaly score.
  • Denoising, sparse, convolutional and variational versions suit different jobs; VAEs can generate.

For more hands-on array work like the code above, test your NumPy reshaping skills on Debugshala Test.

Take the free Deep Learning quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

FAQ

Is an autoencoder supervised or unsupervised?

An autoencoder is usually called unsupervised, or more precisely self-supervised. It needs no human labels because the input serves as its own target. The training itself uses the same loss and backpropagation as a supervised network, which is why the "self-supervised" label is common.

What is the bottleneck in an autoencoder?

The bottleneck is the narrowest layer in the middle of the network, holding the compressed code. Its size controls how much the network must compress: too wide and it simply copies the input, too narrow and the reconstruction loses important detail. You choose it by checking reconstruction error.

What is the difference between an autoencoder and a VAE?

A plain autoencoder maps each input to one fixed code. A variational autoencoder maps each input to a probability distribution and adds a KL penalty that keeps the code space smooth and close to a standard normal. That smoothness lets you sample new codes and decode them into realistic new data.

Can autoencoders be used for image compression?

They can learn compact codes for images, and research systems use them for learned compression. Everyday formats like JPEG still dominate because they are fast and standard. In practice, autoencoders are more often used for denoising images, finding defects in product photos and creating features for other models.

Where can I learn autoencoders and deep learning in Indore?

Debugshala's Machine Learning course at the Old Palasia campus in Indore covers neural networks and unsupervised deep learning ideas such as autoencoders. Teaching is in English and Hindi, fresh graduates and non-IT learners can join, placement assistance is offered, and a free demo class is available before you pay.

From the team behind this site

Go further with the Data Science & AI Course at Debugshala

These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.

  • Offline classes at Old Palasia, Indore
  • Free demo class before you pay
  • Non-IT, fresher and year-gap friendly
  • Placement assistance

700+ students placed · 200+ hiring partners · 4.9★ Google rating

RNN and LSTM Explained: Recurrent Neural Networks in NumPyGANs Explained: Train a Generative Adversarial Network in NumPy