¡Mayday Autoencoders!

¡Mayday Autoencoders!

Viendo la muy buena recepción que tuvo nuestro anterior artículo sobre Self-Organizing Maps (https://bit.ly/2Rp9iVr), hemos decidido traer esta semana los Autoencoders, otro uso particular de las redes neuronales artificiales (ANN, por sus siglas en inglés).

Los autoencoders (AE) son una variación específica de las estructuras y el uso de las ANN donde lo que se busca es reconstruir el input, es decir, dado un vector de valores (o una imagen), buscamos que la red neuronal nos devuelva el mismo vector, o al menos uno muy parecido; por lo tanto, se trata de un método no supervisado. Hasta aquí suena muy fácil ¿verdad? Basta sólo con que la red aprenda la función identidad. Pero claro, esto no representa ninguna utilidad, ni mucho menos un reto matemático.

Entonces ¿para qué usar autoencoders?

Hay dos respuestas: la primera tiene que ver con encontrar un número menor de atributos o variables que expliquen los datos adecuadamente, tal y como hemos venido enseñando con los artículos de manifold learning; la segunda, por otro lado, es para entrenar redes neuronales profundas, sí, eso que está tan de moda, el tan aclamado “Deep learning”. En este artículo, sin embargo, nos concentraremos tan sólo en la primera respuesta, aunque todo el conocimiento asienta las bases de ambas.

La estructura de un AE se compone de dos partes bien distinguidas: el codificador, encargado de expresar los inputs a través de una nueva representación, denominada código, y el decodificador, que se encarga de reconstruir los inputs a partir de dicho código. Una arquitectura posible es la que se muestra en la siguiente imagen:

Hemos dicho que la anterior es una estructura posible porque existen tantas opciones como tonalidades de color hay en el universo. Es muy importante adecuar la arquitectura en función de los datos con los que la vayamos a entrenar y del objetivo último para el que se va a emplear la red. Por ejemplo, si contamos con una gran cantidad de datos y éstos tienen pocas etiquetas, aumentar el número de capas ocultas, el de neuronas por capa y el tamaño del código (el cual es una capa oculta más), sólo conseguirá que el AE aprenda a copiar el input sin aprender ninguna característica relevante de los datos; en contraposición, si nuestros datos tienen, por ejemplo, una mayor cantidad de etiquetas y necesitan un modelo más complejo, entonces es adecuado tener arquitecturas más profundas que cuenten con un mayor número de parámetros.

Como buscamos encontrar un conjunto de variables latentes, menor a las visibles, que capturen la máxima cantidad de información, tiene sentido que el código tenga una menor dimensionalidad, de manera que si, tras entrenar la red con el clásico backpropagation, conseguimos reconstruir el input significará que los datos se pueden explicar de forma más compacta a través de atributos más representativos. Así pues, con la red entrenada, se utiliza el codificador para representar el conjunto de datos a través de las variables latentes aprendidas por el AE. A este tipo de AE se les denomina “undercomplete” o subcompletos.

Otra manera de empujar la red a encontrar una representación compacta es introduciendo el concepto de dispersión, en el cual exigiremos que una parte de las neuronas del código no tengan activación alguna la mayor parte del tiempo, es decir, que devuelvan valores cercanos al 0. Por lo tanto, la capa oculta que hace de código podría, incluso, tener un mayor número de neuronas que dimensiones tiene el input, ya que penalizaremos que la mayoría estén usualmente activas; a este tipo de AE cuyo código tiene mayor dimensionalidad que el input se le denomina “overcomplete” o supracompleto. Supongamos que tenemos un AE con una sola capa oculta, el código. Denotemos a_j la activación de la neurona j en la capa de código; también, definamos ρ*_j como la activación promedio de esta neurona:

Donde a_j(x_i) es la activación de la neurona dado el input x_i; recordad que la activación es un valor continuo entre 0 y 1. Entonces, buscaremos que ρ*_j sea lo más similar al parámetro de dispersidad ρ definido por nosotros, como por ejemplo 0.05, buscando que la neurona se considere activada sólo en un 5% de los casos. Para conseguir que se cumpla la restricción, introduciremos el siguiente término de penalización en la función objetivo (de coste):

Si os fijáis, no es más que la divergencia de Kullback-Leibler, una medida de cuán diferentes son dos distribuciones; en este caso mide cuán diferente resultan las activaciones promedio de las neuronas que componen el código respecto al parámetro de dispersidad fijado por nosotros. El término puede ser escrito como:

La divergencia de Kullback-Leibler tiene la siguiente forma (suponiendo que el parámetro de dispersión se haya fijado en 0.2):

Como vemos, crece monótonamente a medida que se aleja del parámetro, tendiendo a infinito cuando la activación promedio de las neuronas es cercana a la unidad o cuando es cercana a 0, penalizándose con más gravedad activaciones inferiores a la esperada.

Por lo tanto, la función de coste quedará:

Donde β será el coeficiente (hiperparámetro) que nos permitirá controlar el efecto que tenga la divergencia.

Hay una segunda forma de introducir dispersidad y es durante la fase de entrenamiento: cuando se alimenta la red (feedforward) con los inputs, simplemente se ponen a cero algunas neuronas, sea cual sea su activación. Se trata de una alternativa menos elegante, matemáticamente hablando, pero igual de útil.

A estos AE se les denomina Sparse AutoEncoders.

Llegados a este punto, tú lector, te puedes preguntar si es posible combinar un código reducido (undercomplete) con la dispersidad forzada. La respuesta es que sí, se puede, aunque a priori no parece lo más conveniente, por la lógica seguida conviene más darle flexibilidad a la red y que al entrenar se determine qué neuronas acaban siendo realmente útiles para la codificación.

Una tercera vía se abre paso al intentar que el AE sea invariante en gran medida al input, con la intención de que variaciones en la información debidas a cualquier fenómeno externo al evento registrado, como el ruido o errores en la recepción de un mensaje, no provoquen cambios notables en el output. En otras palabras, queremos que el AE sea robusto ante pequeñas variaciones en los datos.

Para conseguirlo, también se introduce un término de penalización en la función de coste. Este término es la norma de Frobenius para el Jacobiano:

Así, sin más, acaba de caer una fórmula de esas que parecen sacadas de la chistera y que no parece tener relación evidente con lo que buscamos conseguir.

La derivada parcial dentro del sumatorio evalúa cómo varía la activación de las neuronas en el código en función de los distintos valores que adquieren los atributos visibles a lo largo de todos los inputs. Como queremos que sea invariante, significa que introduciremos el término en la función de coste e intentaremos minimizarlo también:

Con esto llegamos a una contradicción y es que si el AE se vuelve invariante ante los atributos visibles, y como consecuencia ante el input, devolverá siempre el mismo código y no será útil. El punto, como habréis deducido, se encuentra en el equilibrio conseguido entre una buena reconstrucción y una cierta robustez ante cambios en el input, teniendo en cuenta sólo la información necesaria para dicha reconstrucción y obviando cualquier desviación debida a factores externos o a información poco relevante.

A éstos autoencoders se les denomina Contractive AutoEncoders (CAE), dónde el término contractive hace referencia a esa robustez ante cambios poco relevantes. Los SAE (dispersos) tienen una cierta relación con los CAE, y es que al intentar que las neuronas tengan activación nula, lo que se consigue de forma indirecta es que la activación se encuentre en la zona más plana a la izquierda en la función de activación, como pasaría usando un sigmoide o las ReLu, de manera que la primera derivada sería también prácticamente nula y, por lo tanto, el Jacobiano tendría un valor similar al de los CAE.

Una opción similar, algo menos estable pero más simple de implementar, son los denoising autoencoders (AE con eliminación de ruido). El concepto es extremadamente simple, se trata de entrenar un autoencoder para que reconstruya un input que está corrompido por ruido, es decir, lo alimentaremos con un input ruidoso y debe devolvernos el original. Un ejemplo sería el desarrollado por opendeep.org:

En la imagen podemos ver los inputs originales (izquierda), su versión corrompida con la que se alimentará el AE (centro) y la reconstrucción conseguida (derecha). Básicamente lo que está haciendo el AE es capturar la estructura del manifold y buscando la representación de las instancias corruptas sobre él, tal y como se muestra en la siguiente imagen:

Se puede observar cómo el AE busca la localización original del input corrupto (puntos rojos) y encuentra su posición sobre el manifold (línea negra que representa el espacio de soluciones) a través de la función de reconstrucción (flechas moradas).

Aunque parezca que hacen la misma tarea los DAE que los CAE, en realidad es ligeramente distinta. Los CAE buscan una mayor robustez en la representación, mientras que los DAE buscan robustez en la reconstrucción, con lo cual los primero tienen una aproximación analítica gracias a penalizar las primeras derivadas, mientras que en los DAE se trata de un proceso estocástico por hacerse a través de alimentar la red con versiones explícitamente corruptas de un mismo punto y perseguir una misma reconstrucción (y así para cada instancia de nuestro conjunto de datos).

Sin embargo, estos AE presentan un problema fundamental si lo que se persigue es encontrar un espacio de variables latente que sea continuo en el que se pueda interpolar para generar nuevos puntos de datos o tener una mayor comprensión de ciertas variaciones dentro de nuestro conjunto de datos. Por ejemplo, entrenando un AE para que identifique distintas cifras escritas a mano, encontrando una representación en 2D, nos muestra distintas agrupaciones que son extremadamente útiles para replicar el input y realizar tareas de clasificación, pero poco útiles para generar nuevas soluciones, tal y cómo se muestra en la siguiente imagen:

Si la nueva representación tiene vacíos, como los espacios entre agrupaciones, e intentas generar una variación situada en esa región, el decodificador generará un output no realista porque realmente no tiene idea de cómo tratar con ese pedazo del espacio latente, principalmente porque durante el entrenamiento nunca vio instancias codificadas en esa zona.

Para dar respuesta a esta cuestión llegaron los Variational AutoEncoders (VAE), una nueva versión de AE que introduce la propiedad de continuidad en el espacio de variables latentes; se comprenderá mejor si os enseño primero la arquitectura que tiene:

En esta arquitectura, el codificador devuelve dos vectores de menor dimensionalidad (m y s) en lugar de uno sólo (que antes era el código); el primer vector es un vector de valores promedios y el segundo un vector de desviaciones estándar. Así pues, para los n nuevos atributos latentes, tenemos un valor promedio y su respectiva desviación, con lo que para cada específico input podemos generar un conjunto de códigos a partir de la distribución estándar establecida por ambos vectores. Por lo tanto, el AE no se concentra en aprender una codificación específica, si no más bien un espacio, tal y como muestra la siguiente imagen:

La zona en azul indica todas las posibles codificaciones que podría obtener un input en particular, siendo más probables aquellas donde el color es más intenso (en el centro).

Entonces, después de que el codificador nos haya devuelvo los dos vectores, se generará automáticamente un conjunto de codificaciones para cada input, que luego serán utilizados por el decodificador para recuperar los inputs originales.

Idealmente, nos gustaría que instancias de nuestro conjunto de datos tuvieran una cierta intersección en el espacio latente, aunque pertenezcan, por ejemplo, a clases distintas, de tal forma que podamos interpolar soluciones que se sitúen “en medio”. Sin embargo, al no haber ningún tipo de limitación en los vectores μ y σ, el codificador puede aprender a generar valores de m muy diferentes para distintas clases, agrupándolas muy lejos unas de otras y, por lo tanto, persistiendo el problema que intentamos evitar con los VAE.

Para forzar que distintas clases no estén muy alejadas entre sí, reintroduciremos la divergencia de Kullback-Leibler, como en los SAE, pero en esta ocasión lo haremos para penalizar que los atributos latentes no sigan una distribución normal N(0,1), la cual empuja al codificador a distribuir uniformemente todas las codificaciones alrededor del centro del espacio latente; con esto penalizamos que el AE intente agrupar en zonas alejadas del espacio distintas clases.

Como en el caso de los CAE, el truco reside en el equilibrio entre conseguir reconstruir los input, lo cual intenta juntar aquellos que son similares, y mantener una distribución normal en los atributos latentes, el cual empuja a que haya una distribución normal alrededor del centro del nuevo espacio sin importar el tipo de input; de hecho, si sólo contásemos con el segundo término, los puntos se distribuirían aleatoriamente de forma uniforme y el decodificador sería incapaz de reconstruir nada, simplemente porque no se estaría capturando ninguna información útil.

Tras aplicar VAE sobre cifras escritas a mano obtendríamos:

Aplicación industrial

Es bien sabido que los costes de mantenimiento suelen ser más elevados de lo que deberían, principalmente porque en la mayoría de los casos es muy difícil saber cuándo una pieza específica va a fallar ya que entran en juego muchísimos factores y los distintos modelos, tanto basados en la física como los basados en los datos, suelen requerir capturar comportamientos extremadamente complejos. Todo esto se vuelve particularmente importante cuando se trata del sector de la aviación, un campo de la ingeniería extremadamente costoso a la par que delicado puesto que cualquier fallo podría resultar mortal.

En el artículo de J.Ma et al. “Predicting the remaining useful life of an aircraft engine using a stacked sparse autoencoder with multilayer self-learning” (2018), los autores diseñan un modelo para predecir el tiempo de vida útil que le queda al motor de un avión. Para ello, se sirven de la información generada por simulación de cinco componentes móviles pertenecientes a la turbina, a la cual se le introduce un proceso de degradación en un ciclo de trabajo aleatorio; en total se recogen 21 señales a lo largo de cada simulación. Para el experimento, se generaron 200 simulaciones, 100 para entrenar, las cuales se llevaron hasta el fallo mecánico, y 100 para testear, las cuales se detuvieron antes de que se produjera éste.

El modelo utilizado consiste en un AE con dos capas ocultas, pero construido a partir de dos AE de una sola capa oculta; a esto se le denomina stacked autoencoder y es lo que se utiliza como método para pre-entrenar redes profundas (sí, se trata de Deep learning). La idea reside en entrenar primero un AE y, una vez es capaz de reconstruir el input, usar el código que genera para alimentar al segundo; una vez el segundo AE también es capaz de reconstruir su input, entonces podemos juntar los dos y tener un AE profundo. Los AE utilizados son SAE, para la primera capa, y DAE, para la segunda, y son undercomplete, con lo que el código tiene un menor tamaño que el input. Así, utilizan el código generado por el AE profundo para entrenar un modelo de regresión logística (a partir de máximum likelihood) la cual devuelve un valor entre 0 y 1 que está relacionado con la esperanza de vida del motor normalizada a esa escala. De esta manera, los autores consiguen predecir la vida útil con una efectividad del 85%.

Moraleja: vuestro avión nunca os dejará tirados si cuenta con un autoencoder que lo monitorice.

¡Buen viaje!

Para contactar con nosotros:

Web: www.maichinery.com

E-Mail: info@maichinery.com

Maichinery Data Driven Engineering

Inicia sesión para ver o añadir un comentario.

Más artículos de Massimo Angelini

Otros usuarios han visto

Ver temas