La detección de anomalías (outliers) con autoencoders es una estrategia no supervisada para identificar anomalías cuando los datos no están etiquetados, es decir, no se conoce la clasificación real (anomalía - no anomalía) de las observaciones.
Si bien esta estrategia hace uso de autoencoders, no utiliza directamente su resultado como forma de detectar anomalías, sino que emplea el error de reconstrucción producido al revertir la reducción de dimensionalidad. El error de reconstrucción como estrategia para detectar anomalías se basa en la siguiente idea: los métodos de reducción de dimensionalidad permiten proyectar las observaciones en un espacio de menor dimensión que el espacio original, a la vez que tratan de conservar la mayor información posible. La forma en que consiguen minimizar la pérdida global de información es buscando un nuevo espacio en el que la mayoría de observaciones puedan ser bien representadas.
El método autoencoders crea una función que mapea la posición que ocupa cada observación en el espacio original con el que ocupa en el nuevo espacio generado. Este mapeo funciona en ambas direcciones, por lo que también se puede ir desde el nuevo espacio al espacio original. Solo aquellas observaciones que hayan sido bien proyectadas podrán volver a la posición que ocupaban en el espacio original con una precisión elevada.
Dado que la búsqueda de ese nuevo espacio ha sido guiada por la mayoría de las observaciones, serán las observaciones más próximas al promedio las que mejor puedan ser proyectadas y en consecuencia mejor reconstruidas. Las observaciones anómalas, por el contrario, serán mal proyectadas y su reconstrucción será peor. Es este error de reconstrucción (elevado al cuadrado) el que puede emplearse para identificar anomalías.
La detección de anomalías con autoencoders es muy similar a la detección de anomalías con PCA. La diferencia reside en que, el PCA, solo es capaz de aprender transformaciones lineales, mientras que los autoencoders no tienen esta restricción y pueden aprender transformaciones no lineales.
Los autoencoders son un tipo de redes neuronales en las que la entra y salida del modelo es la misma, es decir, redes entrenadas para predecir un resultado igual a los datos de entrada. Para conseguir este tipo de comportamiento, la arquitectura de los autoencoders es simétrica, con una región llamada encoder y otra decoder. ¿Cómo sirve esto para reducir la dimensionalidad? Los autoencoders siguen una arquitectura de cuello de botella, la región encoder está formada por una o varias capas, cada una con menos neuronas que su capa precedente, obligando así a que la información de entrada se vaya comprimiendo. En la región decoder esta compresión se revierte siguiendo la misma estructura pero esta vez de menos a más neuronas.
Para conseguir que la salida reconstruida sea lo más parecida posible a la entrada, el modelo debe aprender a capturar toda la información posible en la zona intermedia. Una vez entrenado, la salida de la capa central del autoencoder (la capa con menos neuronas) es una representación de los datos de entrada pero con una dimensionalidad igual el número de neuronas de esta capa.
La principal ventaja de los autoencoders es que no tienen ninguna restricción en cuanto al tipo de relaciones que pueden aprender, por lo tanto, a diferencia del PCA, la reducción de dimensionalidad puede incluir relaciones no lineales. La desventaja es su alto riesgo de sobreentrenamiento (overfitting), por lo que se recomienda emplear muy pocas épocas y siempre evaluar la evolución del error con un conjunto de validación.
En el caso de utilizar funciones de activación lineales, las variables generadas en el cuello de botella (la capa con menos neuronas), son muy similares a las componentes principales de un PCA pero sin que necesariamente tengan que ser ortogonales entre ellas.
# Instalación
# ==============================================================================
#!pip install requests
#!pip install tabulate
#!pip install "colorama>=0.3.8"
#!pip install future
#!pip uninstall h2o
#!pip install -f http://h2o-release.s3.amazonaws.com/h2o/latest_stable_Py.html h2o
# Tratamiento de datos
# ==============================================================================
import numpy as np
import pandas as pd
from mat4py import loadmat
# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
from matplotlib import style
import seaborn as sns
style.use('ggplot') or plt.style.use('ggplot')
# Preprocesado y modelado
# ==============================================================================
import h2o
from h2o.estimators.deeplearning import H2OAutoEncoderEstimator
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('ignore')
Los datos empleados en este documento se han obtenido de Outlier Detection DataSets (ODDS), un repositorio con datos comúnmente empleados para comparar la capacidad que tienen diferentes algoritmos a la hora de identificar anomalías (outliers). Shebuti Rayana (2016). ODDS Library [http://odds.cs.stonybrook.edu]. Stony Brook, NY: Stony Brook University, Department of Computer Science.
Todos estos conjuntos de datos están etiquetados, se conoce si las observaciones son o no anomalías (variable y). Aunque los métodos que se describen en el documento son no supervisados, es decir, no hacen uso de la variable respuesta, conocer la verdadera clasificación permite evaluar su capacidad para identificar correctamente las anomalías.
Los datos están disponibles en formato matlab (.mat). Para leer su contenido se emplea la función loadmat()
del paquete mat4py 0.1.0
.
# Lectura de datos
# ==============================================================================
cardio = loadmat(filename='cardio.mat')
datos_X = pd.DataFrame(cardio['X'])
datos_X.columns = ["col_" + str(i) for i in datos_X.columns]
datos_y = pd.DataFrame(cardio['y'], columns = ['y'])
datos = pd.concat((datos_X, datos_y), axis=1)
# Creación de un cluster local H2O
# ==============================================================================
h2o.init(
ip = "localhost",
# -1 indica que se empleen todos los cores disponibles.
nthreads = 1,
# Máxima memoria disponible para el cluster.
max_mem_size = "4g",
)
# Se eliminan los datos del cluster por si ya había sido iniciado.
# ==============================================================================
h2o.remove_all()
h2o.no_progress()
# Se transfieren los datos al cluster de h2o
# ==============================================================================
datos_h2o = h2o.H2OFrame(
python_obj = datos,
destination_frame = 'datos_h2o'
)
# División de las observaciones en conjunto de entrenamiento y test
# ==============================================================================
datos_train_h2o, datos_test_h2o = datos_h2o.split_frame(
ratios=[0.8],
destination_frames= ["datos_train_H2O",
"datos_test_H2O"],
seed = 123
)
# Entrenamiento del modelo autoencoder
# ==============================================================================
var_respuesta = 'y'
predictores = datos_h2o.columns
predictores.remove(var_respuesta)
autoencoder = H2OAutoEncoderEstimator(
activation = "Tanh",
standardize = True,
l1 = 0.01,
l2 = 0.01,
hidden = [10, 3, 10],
epochs = 100,
ignore_const_cols = False,
score_each_iteration = True,
# Seed solo válido cuando se emplea un único core
seed = 12345
)
autoencoder.train(
x = predictores,
training_frame = datos_train_h2o,
validation_frame = datos_test_h2o,
max_runtime_secs = None,
ignored_columns = None,
verbose = False
)
autoencoder.summary()
Para identificar el número de épocas adecuado se emplea la evolución del error de entrenamiento y validación.
fig, ax = plt.subplots(1, 1, figsize=(6, 3))
autoencoder.scoring_history().plot(x='epochs', y='training_rmse', ax=ax)
autoencoder.scoring_history().plot(x='epochs', y='validation_rmse', ax=ax)
ax.set_title('Evolución del error de entrenamiento y validación');
A partir de las 15 épocas, la reducción en el rmse es mínima. Una vez identificado el número óptimo de épocas, se reentrena el modelo, esta vez con todos los datos.
# Entrenamiento del modelo final
# ==============================================================================
autoencoder = H2OAutoEncoderEstimator(
activation = "Tanh",
standardize = True,
l1 = 0.01,
l2 = 0.01,
hidden = [10, 3, 10],
epochs = 15,
ignore_const_cols = False,
score_each_iteration = True,
seed = 12345
)
autoencoder.train(
x = predictores,
training_frame = datos_h2o,
verbose = False
)
El método anomaly()
de un modelo H2OAutoEncoderEstimator
permite obtener el error de reconstrucción. Para ello, realiza automáticamente la codificación, decodificación y la comparación de los valores reconstruidos con los valores originales.
El error cuadrático medio de reconstrucción de una observación se calcula como el promedio de las diferencias al cuadrado entre el valor original de sus variables y el valor reconstruido, es decir, el promedio de los errores de reconstrucción de todas sus variables elevados al cuadrado.
# Cálculo error de reconstrucción
# ==============================================================================
error_reconstruccion = autoencoder.anomaly(test_data = datos_h2o)
error_reconstruccion = error_reconstruccion.as_data_frame()
error_reconstruccion = error_reconstruccion['Reconstruction.MSE']
Una vez que el error de reconstrucción ha sido calculado, se puede emplear como criterio para identificar anomalías. Asumiendo que la reducción de dimensionalidad se ha realizado de forma que la mayoría de los datos (los normales) queden bien representados, aquellas observaciones con mayor error de reconstrucción deberían ser las más atípicas.
En la práctica, si se está empleando esta estrategia de detección es porque no se dispone de datos etiquetados, es decir, no se conoce qué observaciones son realmente anomalías. Sin embargo, como en este ejemplo se dispone de la clasificación real, se puede verificar si realmente los datos anómalos tienen errores de reconstrucción más elevados.
# Distribución del error de reconstrucción en anomalías y no anomalías
# ==============================================================================
df_resultados = pd.DataFrame({
'error_reconstruccion' : error_reconstruccion,
'anomalia' : datos_y['y'].astype(str)
})
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(7, 3.5))
sns.boxplot(
x = 'error_reconstruccion',
y = 'anomalia',
data = df_resultados,
palette = 'tab10',
ax = ax
)
ax.set_xscale("log")
ax.set_title('Distribución de los errores de reconstrucción')
ax.set_xlabel('log(Error de reconstrucción)')
ax.set_ylabel('clasificación (0 = normal, 1 = anomalía)');
La distribución de los errores de reconstrucción en el grupo de las anomalías (1) es claramente superior. Sin embargo, al existir solapamiento, si se clasifican las n observaciones con mayor error de reconstrucción como anomalías, se incurriría en errores de falsos positivos.
Acorde a la documentación, el set de datos Cardiotocogrpahy contiene 176 anomalías. Véase la matriz de confusión resultante si se clasifican como anomalías las 176 observaciones con mayor error de reconstrucción.
# Matriz de confusión de la clasificación final
# ==============================================================================
df_resultados = df_resultados \
.sort_values('error_reconstruccion', ascending=False) \
.reset_index(drop=True)
df_resultados['clasificacion'] = np.where(df_resultados.index <= 176, 1, 0)
pd.crosstab(
df_resultados.anomalia,
df_resultados.clasificacion
)
De las 176 observaciones identificadas como anomalías, solo el 65% (115/176) lo son realmente. El porcentaje de falsos positivos es bastante alto.
El autoencoder anterior se ha entrenado empleando todas las observaciones, incluyendo las potenciales anomalías. Dado que el objetivo es generar un espacio de proyección para datos “normales”, se puede mejorar el resultado reentrenando el modelo pero esta vez excluyendo las $n$ observaciones con mayor error de reconstrucción (potenciales anomalías).
Se repite la detección de anomalías pero, esta vez, descartando las observaciones con un error de reconstrucción superior al cuantil 0.8.
# Eliminación observaciones con error de reconstrucción superior al cuantil 0.8
# ==============================================================================
cuantil = np.quantile(a=error_reconstruccion, q=0.8)
datos_trimmed = datos.loc[error_reconstruccion < cuantil, :].copy()
datos_trimmed_h2o = h2o.H2OFrame(
python_obj = datos_trimmed,
destination_frame = 'datos_trimmed_h2o'
)
# Entrenamiento del modelo
# ==============================================================================
autoencoder.train(
x = predictores,
training_frame = datos_trimmed_h2o,
verbose = False
)
# Error de recostrucción
# ==============================================================================
error_reconstruccion = autoencoder.anomaly(test_data = datos_h2o)
error_reconstruccion = error_reconstruccion.as_data_frame()
error_reconstruccion = error_reconstruccion['Reconstruction.MSE']
# Matriz de confusión de la clasificación final
# ==============================================================================
df_resultados = pd.DataFrame({
'error_reconstruccion' : error_reconstruccion,
'anomalia' : datos_y['y']
})
df_resultados = df_resultados \
.sort_values('error_reconstruccion', ascending=False) \
.reset_index(drop=True)
df_resultados['clasificacion'] = np.where(df_resultados.index <= 176, 1, 0)
pd.crosstab(
df_resultados.anomalia,
df_resultados.clasificacion
)
Tras descartar el 20% de las observaciones con mayor error y reentrenando el autoencoder, se ha conseguido reducir el porcentaje de falsos positivos.
from sinfo import sinfo
sinfo()
Outlier Analysis Aggarwal, Charu C.
Outlier Ensembles: An Introduction by Charu C. Aggarwal, Saket Sathe
Introduction to Machine Learning with Python: A Guide for Data Scientists
Python Data Science Handbook by Jake VanderPlas
¿Cómo citar este documento?
Detección de anomalías con autoencoders y python por Joaquín Amat Rodrigo, disponible con licencia CC BY-NC-SA 4.0 en https://www.cienciadedatos.net/documentos/py32-deteccion-anomalias-autoencoder-python.html
¿Te ha gustado el artículo? Tu ayuda es importante
Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este contenido, creado por Joaquín Amat Rodrigo, tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
Compartir: copiar y redistribuir el material en cualquier medio o formato.
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
NoComercial: No puedes utilizar el material para fines comerciales.
CompartirIgual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.