How to Implement an Autoencoder in TensorFlow

An autoencoder is an unsupervised neural network that learns to compress data into a lower-dimensional representation and then reconstruct the original input from that compressed form. The...

Key Insights

  • Autoencoders learn compressed representations through an encoder-decoder architecture trained to reconstruct their inputs, making them powerful for dimensionality reduction and feature learning without labeled data
  • The bottleneck layer size directly controls compression—too small loses information, too large defeats the purpose of learning meaningful representations
  • Use mean squared error for continuous data reconstruction and binary crossentropy for binary data; monitor both training loss and visual reconstruction quality to prevent overfitting

Understanding Autoencoders

An autoencoder is an unsupervised neural network that learns to compress data into a lower-dimensional representation and then reconstruct the original input from that compressed form. The architecture consists of two main components: an encoder that maps inputs to a latent space, and a decoder that reconstructs the input from this latent representation.

The magic happens in the bottleneck layer—the compressed representation between encoder and decoder. By forcing information through this narrow passage, the network learns to capture only the most salient features of the data. This makes autoencoders excellent for dimensionality reduction, anomaly detection, denoising, and feature learning.

Common applications include compressing images for storage, detecting fraudulent transactions (anomalies reconstruct poorly), removing noise from data, and pre-training networks for downstream tasks. Unlike PCA, autoencoders can learn non-linear transformations, making them significantly more powerful for complex data.

Environment Setup and Data Preparation

Let’s build a practical autoencoder using TensorFlow and Keras. We’ll use the MNIST dataset—handwritten digits that are perfect for demonstrating reconstruction quality visually.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt

# Load and preprocess MNIST
(x_train, _), (x_test, _) = keras.datasets.mnist.load_data()

# Normalize to [0, 1] range
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0

# Flatten images from 28x28 to 784
x_train = x_train.reshape((len(x_train), 784))
x_test = x_test.reshape((len(x_test), 784))

print(f"Training samples: {x_train.shape[0]}")
print(f"Input dimension: {x_train.shape[1]}")

We’re flattening the 28×28 images into vectors of 784 elements. For autoencoders, we don’t need labels—the input serves as both the input and the target output.

Building the Encoder

The encoder progressively reduces dimensionality through a series of dense layers. Each layer learns increasingly abstract representations of the input data. The final encoder layer outputs the latent representation—our compressed encoding.

# Hyperparameters
input_dim = 784
encoding_dim = 32  # Compression factor of ~24.5x

# Build encoder
encoder = keras.Sequential([
    layers.Input(shape=(input_dim,)),
    layers.Dense(128, activation='relu'),
    layers.Dense(64, activation='relu'),
    layers.Dense(encoding_dim, activation='relu', name='encoding_layer')
], name='encoder')

encoder.summary()

The architecture progressively compresses: 784 → 128 → 64 → 32. The ReLU activation introduces non-linearity, allowing the network to learn complex patterns. The bottleneck at 32 dimensions forces the network to learn an efficient representation.

Choosing the encoding dimension is critical. Too small (e.g., 8) and you’ll lose important information. Too large (e.g., 256) and the network won’t learn meaningful compression. Start with a compression factor of 10-25x and adjust based on reconstruction quality.

Building the Decoder

The decoder mirrors the encoder in reverse, progressively expanding the latent representation back to the original input dimensions. The final layer should match the input dimensionality exactly.

# Build decoder
decoder = keras.Sequential([
    layers.Input(shape=(encoding_dim,)),
    layers.Dense(64, activation='relu'),
    layers.Dense(128, activation='relu'),
    layers.Dense(input_dim, activation='sigmoid')
], name='decoder')

decoder.summary()

The decoder expands: 32 → 64 → 128 → 784. Notice the sigmoid activation on the final layer—this outputs values between 0 and 1, matching our normalized input range. If your data is normalized differently, adjust this activation accordingly (or use linear activation for unbounded outputs).

Complete Model and Training

Now we combine the encoder and decoder into a single autoencoder model and train it to minimize reconstruction error.

# Create the autoencoder by chaining encoder and decoder
autoencoder_input = layers.Input(shape=(input_dim,))
encoded = encoder(autoencoder_input)
decoded = decoder(encoded)
autoencoder = keras.Model(autoencoder_input, decoded, name='autoencoder')

# Compile with appropriate loss and optimizer
autoencoder.compile(
    optimizer='adam',
    loss='mse',  # Mean squared error for continuous data
    metrics=['mae']
)

# Train the model
history = autoencoder.fit(
    x_train, x_train,  # Input and target are the same
    epochs=50,
    batch_size=256,
    shuffle=True,
    validation_data=(x_test, x_test),
    verbose=1
)

We use mean squared error (MSE) because our pixel values are continuous. For binary data, use binary crossentropy instead. The Adam optimizer adapts learning rates automatically—a solid default choice.

Notice we’re passing x_train as both input and target. The network learns to reproduce its input through the bottleneck, forcing it to learn compressed representations.

# Plot training history
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss (MSE)')
plt.legend()
plt.title('Training and Validation Loss')

plt.subplot(1, 2, 2)
plt.plot(history.history['mae'], label='Training MAE')
plt.plot(history.history['val_mae'], label='Validation MAE')
plt.xlabel('Epoch')
plt.ylabel('MAE')
plt.legend()
plt.title('Mean Absolute Error')

plt.tight_layout()
plt.show()

Watch for overfitting—if validation loss starts increasing while training loss decreases, you’re memorizing rather than learning generalizable features. Add dropout layers or reduce model capacity if this occurs.

Evaluating Reconstruction Quality

The best way to evaluate an autoencoder is visually inspecting reconstructions. Quantitative metrics like MSE are useful, but seeing what the network learned is invaluable.

# Generate reconstructions on test set
encoded_imgs = encoder.predict(x_test)
decoded_imgs = autoencoder.predict(x_test)

# Visualize original vs reconstructed
n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
    # Original
    ax = plt.subplot(2, n, i + 1)
    plt.imshow(x_test[i].reshape(28, 28), cmap='gray')
    plt.title("Original")
    plt.axis('off')
    
    # Reconstructed
    ax = plt.subplot(2, n, i + 1 + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28), cmap='gray')
    plt.title("Reconstructed")
    plt.axis('off')

plt.show()

Good reconstructions should preserve the essential characteristics of digits while potentially smoothing out noise. Blurry reconstructions indicate the encoding dimension might be too small. Perfect reconstructions might mean it’s too large.

Visualizing the Latent Space

For encodings with 2-3 dimensions, you can directly visualize the latent space. For higher dimensions, use t-SNE or PCA to project down.

from sklearn.manifold import TSNE

# Get labels for visualization
(_, y_train), (_, y_test) = keras.datasets.mnist.load_data()

# Encode test images
encoded_test = encoder.predict(x_test)

# Reduce to 2D if encoding_dim > 2
if encoding_dim > 2:
    tsne = TSNE(n_components=2, random_state=42)
    encoded_2d = tsne.fit_transform(encoded_test)
else:
    encoded_2d = encoded_test

# Plot latent space colored by digit
plt.figure(figsize=(10, 8))
scatter = plt.scatter(encoded_2d[:, 0], encoded_2d[:, 1], 
                     c=y_test, cmap='tab10', alpha=0.6, s=5)
plt.colorbar(scatter)
plt.title('Latent Space Visualization')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')
plt.show()

Well-separated clusters indicate the autoencoder learned meaningful representations. Overlapping clusters suggest you might need more encoding dimensions or deeper networks.

Convolutional Autoencoders for Images

For image data, convolutional autoencoders preserve spatial structure better than dense layers. Here’s a quick implementation:

# Convolutional Encoder
conv_encoder = keras.Sequential([
    layers.Input(shape=(28, 28, 1)),
    layers.Conv2D(16, (3, 3), activation='relu', padding='same', strides=2),
    layers.Conv2D(8, (3, 3), activation='relu', padding='same', strides=2),
    layers.Flatten(),
    layers.Dense(encoding_dim, activation='relu')
], name='conv_encoder')

# Convolutional Decoder
conv_decoder = keras.Sequential([
    layers.Input(shape=(encoding_dim,)),
    layers.Dense(7 * 7 * 8, activation='relu'),
    layers.Reshape((7, 7, 8)),
    layers.Conv2DTranspose(8, (3, 3), activation='relu', padding='same', strides=2),
    layers.Conv2DTranspose(16, (3, 3), activation='relu', padding='same', strides=2),
    layers.Conv2D(1, (3, 3), activation='sigmoid', padding='same')
], name='conv_decoder')

Convolutional autoencoders use strided convolutions for downsampling and transposed convolutions for upsampling. They typically achieve better reconstruction quality on images with fewer parameters than dense autoencoders.

Practical Considerations

Start simple with dense layers, then move to convolutional architectures if needed. Monitor both training metrics and visual quality—numbers don’t tell the whole story. Experiment with encoding dimensions to find the sweet spot between compression and reconstruction quality.

For denoising, add noise to inputs during training but use clean images as targets. For anomaly detection, train on normal data only—anomalies will reconstruct poorly. For variational autoencoders (VAEs), add a KL divergence term to the loss and sample from learned distributions rather than deterministic encodings.

Autoencoders are powerful tools for unsupervised learning. With this foundation, you can tackle dimensionality reduction, feature learning, and anomaly detection across various domains.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.