Autoencoders excel at learning compressed representations of data by minimizing the reconstruction error between the input and the output. This characteristic makes them well-suited for anomaly detection. The core idea is straightforward: train an autoencoder on data representing the normal state or behavior. When presented with data that deviates significantly from this normality (an anomaly), the autoencoder, being trained only on normal patterns, will struggle to reconstruct it accurately. This results in a higher reconstruction error compared to normal data points. By setting a threshold on this error, we can distinguish between normal instances and potential anomalies.
Let's illustrate this with a conceptual example using image data, like MNIST digits, where we might consider one digit class (e.g., '1') as normal and others as anomalies.
1. Data Preparation
Assume we load the MNIST dataset. We'll designate images of the digit '1' as our normal data and use them for training. Other digits ('0', '2'-'9') will serve as anomalies during evaluation. Preprocess the images by normalizing pixel values (e.g., scaling to [0, 1]) and potentially flattening them if using a dense autoencoder, or keeping the 2D structure for a convolutional one.
# Conceptual Data Loading (using TensorFlow/Keras for illustration)
import tensorflow as tf
import numpy as np
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
# Normalize and reshape
x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
# Select 'normal' data (e.g., digit 1) for training
x_train_normal = x_train[y_train == 1]
x_val_normal = x_test[y_test == 1][:500] # Use part of test '1's for validation threshold
x_test_normal = x_test[y_test == 1][500:]
x_test_anomalies = x_test[y_test != 1]
print(f"Training normal shape: {x_train_normal.shape}")
print(f"Validation normal shape: {x_val_normal.shape}")
print(f"Test normal shape: {x_test_normal.shape}")
print(f"Test anomalies shape: {x_test_anomalies.shape}")
# Example: Flatten images for a dense AE
# x_train_normal = x_train_normal.reshape((len(x_train_normal), np.prod(x_train_normal.shape[1:])))
# x_val_normal = x_val_normal.reshape((len(x_val_normal), np.prod(x_val_normal.shape[1:])))
# x_test_normal = x_test_normal.reshape((len(x_test_normal), np.prod(x_test_normal.shape[1:])))
# x_test_anomalies = x_test_anomalies.reshape((len(x_test_anomalies), np.prod(x_test_anomalies.shape[1:])))
# input_shape = x_train_normal.shape[1]
# Example: Reshape for Convolutional AE (add channel dimension)
x_train_normal = np.expand_dims(x_train_normal, axis=-1)
x_val_normal = np.expand_dims(x_val_normal, axis=-1)
x_test_normal = np.expand_dims(x_test_normal, axis=-1)
x_test_anomalies = np.expand_dims(x_test_anomalies, axis=-1)
input_shape = x_train_normal.shape[1:]
2. Model Building (Convolutional AE Example)
A convolutional autoencoder (CAE) is often suitable for image data.
# Conceptual CAE Model Definition (TensorFlow/Keras)
from tensorflow.keras import layers, models
latent_dim = 32
encoder_inputs = tf.keras.Input(shape=input_shape)
x = layers.Conv2D(32, (3, 3), activation='relu', padding='same', strides=2)(encoder_inputs)
x = layers.Conv2D(64, (3, 3), activation='relu', padding='same', strides=2)(x)
# Shape info needed: output shape of last Conv2D layer
# Assuming input 28x28, after two strides=2 convs, shape is 7x7x64
shape_before_flattening = tf.keras.backend.int_shape(x)[1:]
x = layers.Flatten()(x)
encoder_outputs = layers.Dense(latent_dim, name='encoder_output')(x)
encoder = models.Model(encoder_inputs, encoder_outputs, name='encoder')
decoder_inputs = tf.keras.Input(shape=(latent_dim,))
# Need to match the shape before flattening in the encoder
x = layers.Dense(np.prod(shape_before_flattening))(decoder_inputs)
x = layers.Reshape(shape_before_flattening)(x)
x = layers.Conv2DTranspose(64, (3, 3), activation='relu', padding='same', strides=2)(x)
x = layers.Conv2DTranspose(32, (3, 3), activation='relu', padding='same', strides=2)(x)
decoder_outputs = layers.Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x) # Output channel = 1 for grayscale
decoder = models.Model(decoder_inputs, decoder_outputs, name='decoder')
autoencoder = models.Model(encoder_inputs, decoder(encoder_outputs), name='autoencoder')
autoencoder.compile(optimizer='adam', loss='mse')
autoencoder.summary()
3. Training
Train the model only on the x_train_normal
data.
# Conceptual Training Call
history = autoencoder.fit(x_train_normal, x_train_normal,
epochs=20, # Adjust epochs as needed
batch_size=128,
shuffle=True,
validation_data=(x_val_normal, x_val_normal)) # Use normal validation data here too
4. Calculating Reconstruction Errors
Use the trained model to predict (reconstruct) samples and calculate the MSE for each.
# Calculate reconstruction errors for validation normal data
reconstructions_val = autoencoder.predict(x_val_normal)
val_errors = tf.keras.losses.mse(
tf.reshape(x_val_normal, (len(x_val_normal), -1)),
tf.reshape(reconstructions_val, (len(reconstructions_val), -1))
) # Calculate MSE per sample
# Calculate errors for test normal data
reconstructions_test_normal = autoencoder.predict(x_test_normal)
test_normal_errors = tf.keras.losses.mse(
tf.reshape(x_test_normal, (len(x_test_normal), -1)),
tf.reshape(reconstructions_test_normal, (len(reconstructions_test_normal), -1))
)
# Calculate errors for test anomaly data
reconstructions_test_anomalies = autoencoder.predict(x_test_anomalies)
test_anomaly_errors = tf.keras.losses.mse(
tf.reshape(x_test_anomalies, (len(x_test_anomalies), -1)),
tf.reshape(reconstructions_test_anomalies, (len(reconstructions_test_anomalies), -1))
)
5. Threshold Selection
Determine the threshold using the errors from the normal validation set (val_errors
).
# Example: Use the 99th percentile of validation errors as threshold
threshold = np.percentile(val_errors.numpy(), 99)
print(f"Anomaly Threshold (99th percentile): {threshold:.6f}")
# Alternative: Use mean + k * stddev
# threshold = np.mean(val_errors.numpy()) + 3 * np.std(val_errors.numpy())
# print(f"Anomaly Threshold (Mean + 3*StdDev): {threshold:.6f}")
6. Evaluation
Classify test samples based on the threshold and evaluate.
# Classify test samples
normal_predictions = test_normal_errors <= threshold
anomaly_predictions = test_anomaly_errors > threshold
# Combine results for evaluation (example)
true_positives = np.sum(anomaly_predictions)
false_negatives = len(test_anomaly_errors) - true_positives
true_negatives = np.sum(normal_predictions)
false_positives = len(test_normal_errors) - true_negatives
print(f"Detected Anomalies (True Positives): {true_positives} / {len(test_anomaly_errors)}")
print(f"Correctly Classified Normal (True Negatives): {true_negatives} / {len(test_normal_errors)}")
print(f"Incorrectly Classified Normal as Anomaly (False Positives): {false_positives}")
print(f"Missed Anomalies (False Negatives): {false_negatives}")
# Calculate metrics like Precision, Recall, F1-score
precision = true_positives / (true_positives + false_positives) if (true_positives + false_positives) > 0 else 0
recall = true_positives / (true_positives + false_negatives) if (true_positives + false_negatives) > 0 else 0
f1_score = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1_score:.4f}")
Plotting histograms of the reconstruction errors for normal and anomalous test data can provide visual confirmation of the separation.
Distribution of reconstruction errors for normal (blue) vs. anomalous (red) test samples. Anomalies generally exhibit higher errors, allowing separation via a threshold. (Note: Values are illustrative).
This practical approach demonstrates how autoencoders, trained appropriately, can serve as effective tools for identifying unusual patterns or outliers within datasets across various domains, from image processing to time-series analysis and cybersecurity.
© 2025 ApX Machine Learning