Detección de anomalías con autoencoders y python

Detección de anomalías con autoencoders y python

Joaquín Amat Rodrigo
Abril, 2021

Introducción


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.

Autoencoders


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.

Representación de un autoencoder. Fuente: Computer Age Statistical Inference 2016

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.

Librerías


Una de las implementaciones de autoencoders disponible en python se encuentra en la librería H2O. Esta librería permite generar, entre otros modelos de machine learning, redes neuronales con arquitectura de autoencoder y extraer de forma sencilla los errores de reconstrucción.

In [3]:
# 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
In [4]:
# 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')

Datos


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.

  • Cardiotocogrpahy dataset link:
    • Número de observaciones: 1831
    • Número de variables: 21
    • Número de outliers: 176 (9.6%)
    • y: 1 = outliers, 0 = inliers
    • Observaciones: todas las variables están centradas y escaladas (media 0, sd 1).
    • Referencia: C. C. Aggarwal and S. Sathe, “Theoretical foundations and algorithms for outlier ensembles.” ACM SIGKDD Explorations Newsletter, vol. 17, no. 1, pp. 24–47, 2015. Saket Sathe and Charu C. Aggarwal. LODES: Local Density meets Spectral Outlier Detection. SIAM Conference on Data Mining, 2016.

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.

In [5]:
# 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)
In [22]:
# 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",
)
Checking whether there is an H2O instance running at http://localhost:54321 . connected.
H2O_cluster_uptime: 1 min 01 secs
H2O_cluster_timezone: Etc/UTC
H2O_data_parsing_timezone: UTC
H2O_cluster_version: 3.32.1.1
H2O_cluster_version_age: 1 month and 1 day
H2O_cluster_name: H2O_from_python_ubuntu_3kpg2r
H2O_cluster_total_nodes: 1
H2O_cluster_free_memory: 3.546 Gb
H2O_cluster_total_cores: 2
H2O_cluster_allowed_cores: 1
H2O_cluster_status: locked, healthy
H2O_connection_url: http://localhost:54321
H2O_connection_proxy: {"http": null, "https": null}
H2O_internal_security: False
H2O_API_Extensions: Amazon S3, XGBoost, Algos, AutoML, Core V3, TargetEncoder, Core V4
Python_version: 3.7.9 final
In [7]:
# Se eliminan los datos del cluster por si ya había sido iniciado.
# ==============================================================================
h2o.remove_all()
h2o.no_progress()
In [8]:
# Se transfieren los datos al cluster de h2o
# ==============================================================================
datos_h2o = h2o.H2OFrame(
                python_obj = datos,
                destination_frame = 'datos_h2o'
            )
In [9]:
# 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
                                  )

Modelo


In [10]:
# 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()
Status of Neuron Layers: auto-encoder, gaussian distribution, Quadratic loss, 524 weights/biases, 13.7 KB, 146,000 training samples, mini-batch size 1
layer units type dropout l1 l2 mean_rate rate_rms momentum mean_weight weight_rms mean_bias bias_rms
0 1 21 Input 0.0
1 2 10 Tanh 0.0 0.01 0.01 0.055926 0.015956 0.0 -0.006857 0.085 0.000701 0.006415
2 3 3 Tanh 0.0 0.01 0.01 0.056557 0.01714 0.0 -0.004242 0.27266 -0.002755 0.002292
3 4 10 Tanh 0.0 0.01 0.01 0.056564 0.017131 0.0 0.078494 0.291363 0.000229 0.000765
4 5 21 Tanh 0.01 0.01 0.053891 0.021108 0.0 0.001987 0.101033 -0.008322 0.040856
Out[10]:

Diagnostico


Para identificar el número de épocas adecuado se emplea la evolución del error de entrenamiento y validación.

In [11]:
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.

In [12]:
# 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
)

Error de reconstrucción


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.

In [13]:
# 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']

Detección de anomalías


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.

In [14]:
# 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.

In [15]:
# 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
)
Out[15]:
clasificacion 0 1
anomalia
0.0 1593 62
1.0 61 115

De las 176 observaciones identificadas como anomalías, solo el 65% (115/176) lo son realmente. El porcentaje de falsos positivos es bastante alto.

Reentrenamiento iterativo


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.

In [16]:
# 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'
                    )
In [17]:
# Entrenamiento del modelo
# ==============================================================================
autoencoder.train(
    x                = predictores,
    training_frame   = datos_trimmed_h2o,
    verbose          = False
)
In [18]:
# 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']
In [19]:
# 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
)
Out[19]:
clasificacion 0 1
anomalia
0.0 1607 48
1.0 47 129

Tras descartar el 20% de las observaciones con mayor error y reentrenando el autoencoder, se ha conseguido reducir el porcentaje de falsos positivos.

Información de sesión

In [20]:
from sinfo import sinfo
sinfo()
-----
h2o         3.32.1.1
mat4py      0.5.0
matplotlib  3.3.2
numpy       1.19.5
pandas      1.2.3
seaborn     0.11.0
sinfo       0.3.1
-----
IPython             7.20.0
jupyter_client      6.1.11
jupyter_core        4.7.1
notebook            6.2.0
-----
Python 3.7.9 (default, Aug 31 2020, 12:42:55) [GCC 7.3.0]
Linux-5.4.0-1045-aws-x86_64-with-debian-buster-sid
2 logical CPU cores, x86_64
-----
Session information updated at 2021-04-27 08:59

Bibliografía


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 DOI


¿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! 😊


Creative Commons Licence
Este contenido, creado por Joaquín Amat Rodrigo, tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.