
0. Requisitos
El éxito no se mide en cuántos ceros tengas en tu cuenta. Solo busca algo que te vuelva loco, y dedicate a ello
— Marcelo Guital 2017

1. Introducción
Hemos llegado a la parte final de los conceptos básicos en Deep Learning, hoy veremos los Autoencoders.
¿que es un autoencoder?. Bueno así se ve un autoencoder.

Donde los nodos Amarillos son la entrada y los Rojos son la salida, esto significa que nuestra red está direccionada, es decir que tiene entradas y salidas. ¿Cual es el propósito del autoencoder?. Bueno como el nombre lo dice un Autoencoder se codifica a si mismo, esta es la filosofía detrás del autoencoder. Toma una serie de entradas los pasa por una capa oculta y produce salidas, siempre intentando que las salidas sean iguales a las entradas.
Se puede decir que los Autoencoders no son un proceso de aprendizaje no supervisado en toda la regla, es realmente un algoritmo de aprendizaje autosupervisado, porque las salidas se están comparando con algo. Recordemos que las en Máquinas de Boltzmann, no teníamos salidas no podíamos comparar las salidas a ni un tipo de etiquetas, aprendía solo de los datos de entrada, y los SOMs era lo mismo no teníamos como comparar si estaba realizando un buen o mal trabajo. En cambio aquí estamos comparando los valores de salida con los valores de entrada. Si has seguido los artículos anteriores esta parte de la serie será muy fácil de entender para ti.
Los autoencoders se utilizan para detectar propiedades (Feature Detection). Una vez que codifiques tus datos los nodos ocultos van a representar ciertas características de tus datos, y estas características se pueden usar luego en otros proyectos. Básicamente se usan para codificar miles de datos que puedas tener, codificarlos en una representación mucho más pequeña y luego puedes rehacer tus valores originales desde la capa del decodificador.
Ahora que vimos un poco de la arquitectura básica de los Autoencoders, vamos a ver un pequeño ejemplo de cómo funcionan, así lo podemos entender un poco mejor.

Aquí tengo una versión a simplificada para poder representar mejor este ejemplo. Imagina que tengo unos valores de entrada que representar si a una persona le gusto o no una película, el número 1 representa que le gusto y el número 0 que no le gusto.
Lo que quiero probar aquí es que tomar 4 valores y codificarlo en 2 es posible, no nos preocupamos de cómo se entrena ni nada de eso, porque esto suena como magia, y veamos como esto es posible.

Primero vamos a colorear estos pesos de color gris y color celeste, el celeste significa que voy a multiplicar mi valor por 1 y el gris es una multiplicación por -1. Así es como vamos a estructurar este auto encoder predefiniendo los pesos con esos valores, ahora veamos las entradas.

Digamos que tenemos a una persona que solo le gusto la pelicula 1, en este caso los nodos ocultos. Este nodo que tiene 1 va a encender a estos dos nodos del medio y los ceros solamente van a sumar cero. Ahora vamos a calcular la primera salida

Aquí sumamos los valores de la capa oculta y obtenemos 2, ahora haré este proceso para el resto de los nodos de salida.

Aquí como vemos el ultimo es -2 porque se multiplicamos ambos 1 de la capa oculta por -1 y se sumó y quedó en -2, pero como hacemos que esto se paresca a mi entrada. Bueno aquí es cuando la magia ocurre, una vez terminado el proceso de codificado, aplicamos la función softmax.

Y así es como finalmente quedan nuestros inputs.
La función softmax la revisamos en la sección 4 del segundo capitulo
[embed]https://medium.com/@jcrispis56/introducci%C3%B3n-al-deep-learning-parte-3-redes-neuronales-recurrentes-7da543c3b181[/embed]
También podemos ver representados a los autoencoders de esta manera

Esas dos neuronas que hay a la entrada y la salida es la representación gráfica de los biases que es la b en la fórmula de arriba, que no son más que nodos que siempre suman 1.
Y así es como este algoritmo funciona. Los ejemplos de esta sección los saqué de este blog que te recomiendo leer Probably Dance.

2. Entrenando a un AutoEncoder
Ahora vamos a destacar los pasos requeridos para entrenar a un autoencoder primero tenemos que tener una entradas, que en el caso del ejemplo en el que trabajaremos luego van a ser muchas películas con las calificaciones de muchos usuarios y al pasarla por nuestro Autoencoder vamos a comprimir esa información con ayuda de nuestros pesos, en ciertas características que nos ayudarán a reconstruir las salidas entonces ahora vamos a los pasos
- Tenemos que comenzar con un Array donde los datos las filas sean los usuarios y las columnas correspondan a la puntuación de cada una de las películas y cada una de las columnas tengan puntuaciones del 1 al 5. Algo importante a entender es que cada fila de nuestros datos corresponden a un usuario único.
- El primer usuario va a ser procesado por el modelo, el vector de entrada recordemos va a tener todas las puntuaciones que ese usuario dio a esas películas
- El valor de entrada correspondiente al primer usuario va a codificado y se reducirán las dimensiones de sus datos a través de una función de mapeo (Ej: Función Sigmoide) y el parámetro que pasaremos a la función sigmoide va a ser el siguiente

donde “W” van a ser nuestros pesos y “b” va a ser el bias y “f” nuestra función de mapeo
4. Z va a ser decodificado, en un vector de salida con las mismas dimensiones que nuestro vector de entrada, como ya habíamos dicho la red a través de nuestros pesos y nuestra capa oculta va a tratar de replicar nuestros datos de entrada, hasta converger, en ese momento nuestros datos de entrada serán igual que los datos de salida
5. La error de reconstruccion d(x,y) = ||x-y|| va a ser calculado. El objetivo siempre será la de minimizar esa función de error de reconstrucción.
6. Propagación hacia atrás (BackPropagation): De derecha a izquierda es decir, de los nodos de salida a los de entrada, nuestra función de error se va a propagar. Y los pesos va a ser recalculados en función de cuán grande sea ese número. La razón de aprendizaje (Learning Rate) va a decidir cuánto se deben actualizar los pesos.
7. Repetir los pasos del 1 al 6 y actualizar los pesos después de cada dato que se pase a la red, es decir, este proceso se hace para cada dato individualmente o para un batch, si no cada vez que yo paso un dato a mi modelo tengo que repetir todos los pasos anteriores.
8. Después de que todos los datos pasen por la red se cumplirá una época y debemos ir a la siguiente época
Lo importante a considerar es que cada vez que completo un batch

Como aqui que estamos pasado un dato, o un batch de datos, tenemos que hacer la propagación hacia atrás

Igual esto lo iremos viendo en más detalle en la parte practica del articulo, pero igual es importante rescatar eso.
Adicionalmente, como siempre dejo fuentes de donde saco mi conocimiento en forma de papers, te recomiendo leer esto blog de Francois Chollet el creador de Keras de que se llama “Construyendo un autoencoder con Keras” por si quieres leerlo haz click aquí.

3. Capa oculta excesiva
Algo que quiero mencionar es que en las siguientes secciones, van a ser cortas porque son partes que quiero revisar a un nivel más alto, y no entrar tanto en detalles, porque hay muchos tipos de Autoencoders, que fácilmente cada uno se merece un articulo distinto, igual los veremos muy por encima. Los Autoencoders son un método avanzado dentro del Deep Learning, y son muy populares por lo que hay demasiadas variantes de los mismos. Te mostraré los conceptos claves de cada uno sin entrar en mucho detalle y te dejare los papers y los blogs donde podrás encontrar la información de aquellos si quieres implementarlos, o saber mucho más de los mismos.
Lo que vamos a hablar en esta sección es sobre las capas ocultas excesivas, es decir capas ocultas con muchas neuronas, que este es el concepto más importante dentro de todas las variaciones de los autoencoders que mencione anteriormente.

Bueno aqui tengo mi AutoEncoder con cuatro neuronas de entrada, dos neuronas en la capa oculta y cuatro neuronas en la capa de salida. La pregunta aquí es ¿que pasa si quiero incrementar el número de neuronas en la capa oculta, si quiero tener más neuronas en mi capa oculta que en la de entrada?.

Algo como esto. y la pregunta que nace aquí es. ¿Por que querria hacer eso? si estoy tratando de reducir mis datos, ¿por que quiero hacer mas grande mi capa oculta?. Y la respuesta es ¿por que no?. Habíamos hablado de que los AutoEncoders se utilizaban como una técnica de extracción de características de nuestros datos, ¿y que pasa si quiero mas caracteristicas que las que tengo originalmente?. Cuando hablamos de las Redes Neuronales Artificiales, era muy fácil para nosotros agregar x cantidad de neuronas y capas para obtener más características de nuestros datos y poder clasificarlos de manera más precisa, no estabamos restringiendonos a la cantidad de nodos que quería tener en mi capa oculta. Y también es una buena idea en los AutoEncoders tener más neuronas intermedias, podemos extraer muchas más características.
Todo está bien hasta ahora pero esto puede ocasionar un problema, y quiero que hagas un stop aquí y pensar en ese problema.
Bueno el problema es que si hacemos esto nuestro AutoEncoder podría hacer trampa, porque lo que busca este algoritmo es que la entrada sea igual a la salida, y si le damos más neuronas intermedias este algoritmo va a ser capaz de copiar los valores de entrada y ponerlos en nuestros valores de salida. la información va a pasar directamente por nuestro algoritmo y tendrá dos nodos que no va a utilizar.

De este modo lo que hará es copiar la información de un lado a otro y no va a extraer nada importante. Ahora ¿Cómo vamos a resolver este problema?

4. Autoencoders Dispersos
Nuestro objetivo aquí es. Crear un AutoEncoder donde la capa oculta sea más grande que la capa de entrada y la razón para hacer eso es que queremos extraer más características de nuestros datos y como sabemos los AutoEncoders están diseñados para esto, pero eso le da la capacidad a nuestro AutoEncoder de hacer trampa y la forma de resolver este problema que vamos a discutir se llama AutoEncoders Dispersos (Sparse Autoencoders).
Algo que quiero aclarar es que escucharas mucho de este término en los papers relacionados con AutoEncoders. Literalmente en cada uno de los artículos que veas sobre distintas arquitectura de AutoEncoders lo escucharas si es que aun no lo has escuchas, y muy pocas veces te preguntas a que se refiere y también muy pocas veces se explica a que se refiere ese término.
Entonces. ¿Qué es un AutoEncoder Disperso?. Bueno los AutoEncoders dispersos son AutoEncoders donde la capa oculta es más grande que la capa de entrada. Y la técnica de regularización que se usa es la dispersión, la regularización de cualquier tipo es una técnica que previene el OverFitting, en este caso copiar los valores de entrada a los de salida se considera OverFitting, y necesita técnicas de regularización y en este caso es la técnica de dispersión.
Lo que hace esta técnica de regularización es introducir una penalización dentro de la función de pérdida, donde va a desactivar ciertas neuronas poniendo valores insignificantes en ellas.

Entonces nuestros valores pasan y la red desactiva los valores. Todos los nodos grises tienen un valor muy bajo insignificante para la operación que hacemos, entonces es como si no contaran, y la red hace que esos nodos solamente extraigan información durante el batch luego se propaga el error.

Y hace que estos otros nodos tengan la capacidad de extraer datos y apaga los otros para evitar que nuestro autoencoder haga trampa
Y es es lo que es un AutoEncoder Disperso cuando oigas de ese concepto recuerda que es a esto a lo que se refiere.
En cierto modo está comprimiendo la información pero siempre usando distintos nodos.
Y te dejo un tutorial interesante de Chris McCormick que se llama, que en cierta manera de ahi saque el conocimiento de las dos secciones anteriores y si quieres informarte mas al respecto te dejo el link
Deep Learning Tutorial — Sparse Autoencoder
Y este es un tutorial de MatLab así que si eres fan de MatLab o lo utilizas esto te vendrá bastante bien y si quieres entender que ecuaciones son las que se ven involucradas en este proceso adelante.
Otra fuente que use para estas dos secciones es Deep Learning: Sparse Autoencoders y es de Eric Wilkinson y es un blog muy breve sobre la esencia de esta técnica no te llevará mucho tiempo leerla y en realidad puedes disfrutarla bastante si te interesa saber mas del tema
Deep Learning: Sparse Autoencoder
Y por último un paper más pesado sobre esta técnica de la mano de Alireza Makhzani se llama k-Sparse Autoencoders. Y básicamente lleva este concepto a otro nivel donde habla que el parámetro k nos permite controlar el nivel de dispersión de este AutoEncoder y te da algunos ejemplos con distintos valores para este parámetro y es interesante si quieres aprender más sobre este tema
https://arxiv.org/pdf/1312.5663.pdf

4.(Mini Revisión) Denoising Autoencoders
Otra técnica que se usa con el fin de que nuestro AutoEncoder no haga trampa es el Denoising Autoencoder. Básicamente esta regularización toma nuestras entradas originales, y las copia y las aparta. Y en vez de poner todas las características, las va activando y desactivando poniendo ceros en algunas de las entradas y en las otras nuestros datos

De esta forma se aseguramos que no pueda copiar nuestros valores porque van desactivando aleatoriamente durante el paso de un batch. Entonces de esta forma no se propagan los mismos datos todo el tiempo. Pero al momento de propagar hacia atrás nuestras salidas se van a comparar con los valores completos, no con los valores incompletos.

De esa forma nos aseguramos de que nuestros valores sirvan sobre los valores completos del dataset y no sobre los valores incompletos.
Esta informacion la saque de un paper de Pascal Vincent que se llama Extracting and Composing Robust Features with Denoising Autoencoders.
Te lo dejo aquí abajo como de costumbre, por si te interesa aprender del tema más a profundidad
https://www.cs.toronto.edu/~larocheh/publications/icml-2008-denoising-autoencoders.pdf

5. (Mini-Revisión) Autoencoders Apilados
Entonces qué son estos AutoEncoders Apilados. Es cuando agregamos otra capa oculta de codificación a nuestro AutoEncoder

Entonces tenemos dos capas de codificación y una de decodificación y esto es un algoritmo muy poderoso este modelo puede superar en resultados a las DBN que vimos en el articulo pasado, y es un modelo totalmente distinto las DBNs están basadas en las Máquinas De Boltzmann Restringidas y son redes neuronales sin dirección y los AutoEncoder son redes neuronales dirigidas y es curioso que este algoritmo pueda superar a las DBNs.
Y un paper para profundizar sobre este tema es de Pascal Vincent y otros que te dejo a continuación.
http://www.jmlr.org/papers/volume11/vincent10a/vincent10a.pdf

6. (Aclaración) Deep Autoencoders
Quiero hacer hincapié en que los Deep Autoencoders no son lo mismo que los AutoEncoders Apilados. Y se va a hacer muy obvio cuando veas esta imagen

Y es lo que piensas una Máquina de Boltzmann Restringida Apilada y son pre entrenadas capa por capa para dar una salida y se dan los resultados con una Propagación Hacia Atrás, es como una cruza entre aprendizaje supervisado y aprendizaje no supervisado

La naturaleza nos ha dado las semillas del conocimiento, no el conocimiento mismo.

7. Preprocesado de datos
Vamos a usar el mismo dataset que en el articulo anterior, que lo puedes encontrar aqui
dataset
Te recomiendo correr este algoritmo si o si en google colab, ya que se tardará en entrenar y demanda mucho hardware

Esto es lo que hacemos siempre, y el preprocesado va a ser igual que la vez anterior solo que con unas pequeñas modificaciones

Vamos a cargar los mismos dataset que la vez anterior el parámetro delimiter es para el separador, por lo general los csv se separan por “,” pero en este caso se separa por tabs.
y los pasaremos como vimos en la exploración la vez anterior todos los valores del dataset se corresponden con el tipo Int64 asique los pasamos a arrays de numpy, con ese formato.

Ahora lo que deseamos es obtener el indice del ultimo usuario y el índice de la última película con el fin de utilizarlos luego, para pasar nuestros registros a una matriz donde dividamos por filas a los usuarios y por columnas a las películas, y el número de usuarios para usarlos en el entrenamiento

Ahora haremos la función convert, por lo que iniciaremos un array vacío donde vamos a ir almacenando nuestros valores para luego convertirlos a Tensores de pyTorch. Lo siguiente que haremos es recorrer nuestros datos con un iterador que será nuestra id de usuario, presente en la primera columna de nuestros datos, y recorremos desde el 1 hasta el máximo de usuarios + 1 porque el último usuario no lo tomara en cuenta, asique necesitamos sumarle 1
Lo siguiente es comprobar que la ID de ese usuario exista en el dataset, y obtener todas las puntuaciones de ese usuario para cada película en el dataset.

Lo siguiente que vamos a hacer, es un array del porte de el número máximo de películas de todo el dataset, con el fin de que cada fila represente un usuario y cada número de esa columna sea una película a la que el usuario haya puntuado. Luego le pasaremos la lista de índices que nos retorno numpy de las películas que nos retorno numpy en la operación anterior, y cada uno de esos índices lo reemplazamos con la puntuación de la película.
Y por último vamos a poner esa lista de puntuaciones en nuestra lista new_data y la vamos a retornar.

Y convertiremos nuestros dos sets a ese formato y ya estaríamos listos para la siguiente parte

8. Armando y Entrenando nuestro AutoEncoder con PyTorch
Primero si has visto todos los capítulos de la serie felicitaciones, ahora vamos a prepararnos para el jefe final, que no es tan complejo como en la parte anterior pero no bajes el perfil a este modelo, es muy poderoso

Vamos a importar las dependencias que usamos hoy para entrenar a nuestro modelo

Y el primer paso es convertir nuestros datos a tensores de pyTorch o si no nuestro programa no será capaz de trabajar con ellos. ¿Por que estamos usando pyTorch?. Porque hacer autoencoders en otras librerías es mucho más complejo, puedes usar la librería que quieras, Tensorflow, Keras, o la que se te ocurra, pero a la hora de la verdad PyTorch da mejores resultados en los AutoEncoders que las otras librerías de Deep Learning disponibles.
Ahora vamos a comenzar a armar la Arquitectura de nuestro AutoEncoder

Lo que vamos a hacer es usar la herencia de Python para heredar todos los elementos de la clase Module de PyTorch, y para heredar todos los métodos clases de la clase Module necesitamos llamar al init de la clase con super, que es una función nativa de python que hace referencia a todos los métodos y funciones del padre de esa herencia. por eso se llama Super, porque hace referencia a la Clase Superior o Superclase.
Y uno de los objetos que heredamos del padre es decir Module este objeto fc o Fully Connected, que hace referencia a las capas neuronales tradicionales, o las dense. y para la primera capa es lo mismo que con nuestras redes neuronales tradicionales, que necesitamos decirle la forma de la matriz que va a recibir como primer parámetro y el número de neuronas de la capa, en este caso la forma de la matriz va a ser el número de películas y la primera capa tendrá 20 neuronas de salida, igual este modelo eres libre de cambiarlo y experimentar con el.

Lo siguiente es apilar capas como solíamos hacerlo en nuestra Red Neuronal Tradicional, pero algo que notar que el segundo numero tiene que ser el mismo número que el primero de la siguiente capa, porque el segundo número corresponde a la salida, y si las salidas no calzan con la entrada de la siguiente capa va a lanzar un error, mientras respetemos esa regla (En tensorflow es exactamente lo mismo), todo marchara bien.
Y como dijimos en nuestro bloque teórico los autoencoders tienen la misma cantidad de neuronas de salida que de entrada, entonces la salida de la última capa tiene que ser igual que la entrada de la primera. Y por último definimos nuestra función de activación

Y como habíamos dicho en nuestro bloque teórico lo unico calculo que teníamos que hacer en nuestro AutoEncoder era el siguiente

Aplicar una función de activación a nuestra suma ponderada, asique es exactamente lo que hacemos en esta parte, lo único que debemos cuidar es que a nuestra última capa no debemos de aplicar la funcion de activacion o si no nos lanzara un error.

Por último vamos a instanciar a nuestro modelo y vamos a definir la función de pérdida, que en este caso va a ser el Promedio de los Errores al Cuadrado, y nuestro optimizador, que por lo general va bien el RMSprop a el optimizador recibe como parámetro nuestra arquitectura de la Red que vamos a entrenar, la razón de aprendizaje que mientras más baja sea se va a demorar mas en aprender pero va a encontrar un mejor punto de convergencia. y el weght_decay reduce nuestra razón de aprendizaje al pasar de las épocas para encontrar un punto de convergencia aún mejor.

Ahora comenzaremos a hacer la lógica del entrenamiento, primero vamos a definir las épocas y el valor de pérdida, y el valor con el cual vamos a normalizar la pérdida del entrenamiento.

Ahora vamos a tomar de a una la id de cada usuario y para usar este valor como entrada debemos convertirlo a la clase Variable de pyTorch y crear una dimensión adicional para que los valores de entrada calcen con los que espera nuestro modelo.

Ahora vamos a clonar nuestra entrada, para que también sea la salida de nuestro AutoEncoder, recordemos que el AutoEncoder, tiene la misma salida y la misma entrada, y lo que haremos con este if es comprobar que nuestro array de puntuaciones tenga a lo menos una puntuación, si no tiene ni una no nos vamos a preocupar de este dato porque no sería relevante para nuestra red. Y pasaremos nuestro dato de entrada a nuestra red y almacenaremos la salida

Ahora le diremos a PyTorch, que no calcule la gradiente con nuestro valor objetivo, o valor esperado por eso pasamos el comando target.require_grad=False, porque así no aseguramos que ese valor no va a contar para calcular el error, y además de eso reemplazamos todos los valores de la salida cuyos valores dentro de los datos reales sean 0, porque eso afectara al calculo del error, porque aunque estos valores existan, no queremos que eso sea lo que se propague a nuestra red

Ahora vamos a calcular la pérdida, y la condición que la entrada nunca sea igual a 0, es por esta razón, porque tengo que corregir mi error para mostrarlo de forma adecuada, si mi denominador fuera cero no podría saber si mi red está avanzando o no, porque luego este valor nos servirá para calcular nuestra error finalmente.

lo siguiente es usar la función inversa de nuestra pérdida que nos dirá en qué dirección se deben de actualizar nuestros pesos, si necesitamos hacer los valores más grandes o pequeños. Por último extraemos el valor de nuestra pérdida en el entrenamiento, que va a estar elevado al cuadrado y por eso aplicamos la raíz cuadrada, y luego lo multiplicamos por la corrección de nuestro error

Por último actualizamos nuestro valor s, porque es el que lleva la cuenta de la cantidad de usuarios que ya hemos calculado, y actualizamos los pesos.

Nos salimos de ese bucle for, imprimimos nuestros valores y ya estamos listos para entrenar
Luego de 2000 épocas obtuve este resultado:
“Epoca: 2000 Perdida: 0.4805447864237684”
Lo cual es un resultado bastante decente para este tipo de algoritmo, pero ahora necesitamos correr el algoritmo en el set de prueba, y esto es similar al correr el algoritmo en el set de entrenamiento por lo que lo explicare paso por paso

En este caso no necesitamos épocas porque eso es solo para el entrenamiento, ahora pasare a relatar los cambios que le hice con respecto al entrenamiento
- Nuestra variable del set de entrenamiento se queda, porque nos servirá para reconstruir nuestro valor de prueba
- Nuestro objetivo ahora es la variable del set de prueba
- El cambio fundamental aquí es que no estamos actualizando nuestros pesos porque como no es el entrenamiento no necesitamos hacer esos cálculos, y lo demás sigue siendo igual
El resultado que me dio a mi fue el siguiente:
“Perdida en el set de prueba 0.42683314598480343”

9. Agradecimientos
Llegamos al final de la serie, pero veo necesario para seguir avanzando dentro de la misma, y cubrir conceptos avanzados, es ir a lo básico y aprender todo lo que nos puede ofrecer el Machine Learning. Por eso ahora comenzare una serie dentro de esta práctica, donde iré desde lo más básico a lo más complejos, veremos las Matemáticas detrás de esto veremos lo básico que Python para pasar por todo lo que el Machine Learning tiene por ofrecer.
Hasta la proxima, espero hayas disfrutado esta serie tanto como yo disfrute hacerla, espero que juntos podamos crecer, y entender el mundo de la informática desde otro espejo, no lo clasico, si no cosas mas avanzadas, que no están disponibles en español, ese es mi caballo de batalla, acercar el contenido abundante en el habla inglesa a la comunidad en español.

Comentarios
Publicar un comentario