En este tutorial, aprenderás más sobre autoencoders y cómo construir autoencoders convolucionales y de eliminación de ruido con el dataset notMNIST en Keras.
En general, los autoencoders se consideran una técnica de aprendizaje no supervisado, ya que no necesitas etiquetas explícitas para entrenar el modelo. Todo lo que necesitas es datos de entrada en bruto.
En este tutorial, aprenderás qué son los autoencoders en deep learning e implementarás un autoencoder convolucional y uno de eliminación de ruido en Python con Keras. Trabajarás con el dataset de alfabetos NotMNIST como ejemplo.
En pocas palabras, hoy verás:
Primero, qué es un autoencoder, cómo se compara con las técnicas de reducción de la dimensionalidad y los distintos tipos que existen de este algoritmo;
Luego, te centrarás en el autoencoder convolucional: verás qué hace este tipo y cómo puedes construirlo. Después, ¡lo implementarás tú mismo! Aprenderás a cargar datos en formato ubyte gzip, luego a explorarlos, preprocesarlos, ajustarlos a un modelo, visualizar las curvas de pérdida de entrenamiento y validación y, por último, predecir sobre el conjunto de prueba.
A continuación, te presentaremos el autoencoder de eliminación de ruido y verás cómo implementarlo: aprenderás a añadir ruido a las imágenes. Después, introducirás estas imágenes en tu modelo de deep learning y lo entrenarás. Finalmente, harás predicciones sobre las imágenes de prueba con ruido.
Autoencoder
Como leíste en la introducción, un autoencoder es un algoritmo de aprendizaje automático no supervisado que toma una imagen como entrada e intenta reconstruirla usando un menor número de bits en el cuello de botella, también llamado espacio latente. La imagen se comprime en gran medida en ese cuello de botella. La compresión en los autoencoders se logra entrenando la red durante un tiempo; a medida que aprende, intenta representar lo mejor posible la imagen de entrada en el cuello de botella. Los algoritmos generales de compresión de imágenes como JPEG o JPEG lossless comprimen sin necesidad de entrenamiento y suelen funcionar bastante bien.
Los autoencoders son similares a técnicas de reducción de dimensionalidad como el Análisis de Componentes Principales (PCA). Proyectan los datos de una dimensión mayor a otra menor mediante transformaciones lineales e intentan preservar las características importantes eliminando las partes no esenciales.
Sin embargo, la gran diferencia entre los autoencoders y PCA está en la transformación: como ya viste, PCA usa transformaciones lineales, mientras que los autoencoders emplean transformaciones no lineales.
Ahora que ya tienes una idea de qué son los autoencoders, ¡vamos a desglosar el término para ganar más intuición!
La figura anterior muestra un autoencoder sencillo de dos capas con una capa oculta. En la jerga del deep learning, verás a menudo que la capa de entrada no se tiene en cuenta al contar el número total de capas de una arquitectura. El total de capas incluye únicamente las capas ocultas y la capa de salida.
Como se muestra en la imagen, las capas de entrada y salida tienen el mismo número de neuronas.
Veamos un ejemplo. Introduces una imagen con cinco píxeles en el autoencoder; el codificador la comprime en tres valores de píxel en el cuello de botella (capa intermedia) o espacio latente. Con esos tres valores, el decodificador intenta reconstruir los cinco píxeles, es decir, la imagen original que diste como entrada a la red.
En la práctica, suele haber más capas ocultas entre la entrada y la salida.
Un autoencoder se divide en tres partes
Encoder (codificador): esta parte de la red comprime o reduce la resolución de la entrada a un menor número de bits. Al espacio representado por esos pocos bits se le llama espacio latente o cuello de botella. También se denomina "punto máximo de compresión" porque ahí es donde la entrada se comprime al máximo. Estos bits comprimidos que representan la entrada original constituyen la "codificación" de la entrada.
Decoder (decodificador): esta parte de la red intenta reconstruir la entrada usando solo su codificación. Cuando el decodificador logra reconstruir la entrada exactamente como se dio al codificador, puedes decir que el codificador está generando las mejores codificaciones para que el decodificador reconstruya bien.
Existen distintos tipos de autoencoders, como el convolucional, el de eliminación de ruido, el variacional o el disperso (sparse). Sin embargo, como viste en la introducción, en este tutorial te centrarás únicamente en los convolucionales y los de eliminación de ruido.Convolutional Autoencoders in Python with Keras
Dado que tus datos de entrada son imágenes, es buena idea usar un autoencoder convolucional. No es una variante distinta, sino un autoencoder tradicional apilado con capas de convolución: básicamente sustituyes las capas totalmente conectadas por capas convolucionales. Las capas de convolución junto con las de max-pooling convierten la entrada de ancha (una imagen 28 x 28) y fina (un solo canal o escala de grises) a pequeña (imagen 7 x 7 en el espacio latente) y gruesa (128 canales).
¡No te preocupes si la idea anterior no te ha quedado clara del todo! La segunda parte del tutorial, donde te centrarás en la implementación, debería despejar tus dudas.
Consejo: si quieres saber más sobre redes neuronales convolucionales, echa un vistazo a este tutorial.
Esto ayuda a la red a extraer rasgos visuales de las imágenes y, por tanto, a obtener una representación del espacio latente más precisa. El proceso de reconstrucción utiliza upsampling y convoluciones, que conforman el decodificador. La reducción (downsampling) es el proceso por el cual la imagen se comprime a baja dimensión, y corresponde al codificador.
Es importante notar que el codificador principalmente comprime la imagen de entrada; por ejemplo: si tu imagen de entrada tiene dimensión 176 x 176 x 1 (~30976), el punto de compresión máxima puede tener dimensión 22 x 22 x 512 (~247808). En este caso, partiste de una imagen en escala de grises 176 x 176 y, tras pasarla por varias capas convolucionales y exactamente tres capas de max-pooling, la imagen se reduce a 22 x 22, pero el número de canales aumenta de 1 a 512. Como decíamos: pasas de una entrada ancha (176 x 176) y fina (1) a una pequeña (22 x 22) y gruesa (512).
Carga de datos
El dataset notMNIST es un conjunto de reconocimiento de imágenes de glifos de letras de la A a la J. Es muy similar al clásico MNIST, que contiene imágenes de dígitos manuscritos del 0 al 9: en este caso, encontrarás que NotMNIST incluye imágenes en escala de grises de 28x28 con 70.000 letras de la A a la J, en total 10 categorías y 6.000 imágenes por categoría.
Consejo: si quieres aprender a implementar un perceptrón multicapa (MLP) para tareas de clasificación con el dataset MNIST, echa un vistazo a este tutorial.
El dataset NotMNIST no está predefinido en Keras ni en TensorFlow, así que tendrás que descargar los datos desde esta fuente. Los datos se descargarán en formato ubyte.gzip, ¡pero no te preocupes! Pronto verás cómo leer flujos de bytes y convertirlos en un array de NumPy. ¡Vamos allá!
La red se entrenará en una Nvidia Tesla K40, así que si entrenas en GPU y usas Jupyter Notebook, tendrás que añadir tres líneas más para especificar el orden de dispositivos CUDA y los dispositivos visibles CUDA usando el módulo os.
En el código siguiente, estableces variables de entorno en el notebook con os.environ. Es buena práctica hacerlo antes de inicializar Keras para limitar el backend TensorFlow a usar la primera GPU. Si la máquina en la que entrenas tiene la GPU en 0, asegúrate de usar 0 en lugar de 1. Puedes comprobarlo ejecutando un comando sencillo en tu terminal, por ejemplo, nvidia-smi
import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"]="1"#model will be trained on GPU 1Con la tecnología de
A continuación, importa los módulos necesarios como numpy, matplotlib y, lo más importante, keras, ya que será el framework que usarás hoy.
import keras
from matplotlib import pyplot as plt
import numpy as np
import gzip
%matplotlib inline
from keras.layers import Input,Conv2D,MaxPooling2D,UpSampling2D
from keras.models import Model
from keras.optimizers import RMSprop
Con la tecnología de
Aquí defines una función que abre el archivo gzip y lo lee con bytestream.read(). Le pasas la dimensión de la imagen y el número total de imágenes. Luego, con np.frombuffer(), conviertes la cadena almacenada en la variable buf en un array de NumPy de tipo float32.
Después, reconfiguras el array a uno tridimensional (tensor) donde la primera dimensión es el número de imágenes y la segunda y tercera son la dimensión de la imagen. Por último, devuelves el array de NumPy data.
defextract_data(filename, num_images):with gzip.open(filename)as bytestream:
bytestream.read(16)
buf = bytestream.read(28*28* num_images)
data = np.frombuffer(buf, dtype=np.uint8).astype(np.float32)
data = data.reshape(num_images,28,28)return data
Con la tecnología de
Ahora llamarás a la función extract_data() pasando los archivos de entrenamiento y prueba junto con su número de imágenes correspondiente:
train_data = extract_data('train-images-idx3-ubyte.gz',60000)
test_data = extract_data('t10k-images-idx3-ubyte.gz',10000)Con la tecnología de
De forma similar, defines una función para extraer etiquetas que abre el archivo gzip, lo lee con bytestream.read(), a la que pasas la dimensión de la etiqueta (1) y el número total de imágenes. Luego, con np.frombuffer(), conviertes la cadena almacenada en la variable buf en un array de NumPy de tipo int64.
Esta vez no necesitas reconfigurar el array, ya que la variable labels devolverá un vector columna de dimensión 60.000 x 1. Por último, devuelves el array de NumPy labels.
defextract_labels(filename, num_images):with gzip.open(filename)as bytestream:
bytestream.read(8)
buf = bytestream.read(1* num_images)
labels = np.frombuffer(buf, dtype=np.uint8).astype(np.int64)return labels
Con la tecnología de
Ahora llamarás a la función de extracción de etiquetas pasando los archivos de etiquetas de entrenamiento y prueba junto con su número de imágenes correspondiente:
train_labels = extract_labels('train-labels-idx1-ubyte.gz',60000)
test_labels = extract_labels('t10k-labels-idx1-ubyte.gz',10000)Con la tecnología de
Una vez cargados los datos de entrenamiento y prueba, ya puedes analizarlos para hacerte una idea del dataset con el que vas a trabajar hoy.
Exploración de datos
Analicemos cómo son las imágenes del dataset y veamos también su dimensión con el atributo de NumPy .shape:
# Shapes of training setprint("Training set (images) shape: {shape}".format(shape=train_data.shape))# Shapes of test setprint("Test set (images) shape: {shape}".format(shape=test_data.shape))Con la tecnología de
Training set(images) shape:(60000,28,28)
Test set(images) shape:(10000,28,28)Con la tecnología de
Por la salida anterior, puedes ver que los datos de entrenamiento tienen forma 60000 x 28 x 28, ya que hay 60.000 muestras de entrenamiento, cada una con una matriz 28 x 28. De forma similar, los datos de prueba tienen forma 10000 x 28 x 28.
Nota: en esta tarea no usarás las etiquetas de entrenamiento y prueba. Solo trabajarás con las imágenes. Sin embargo, para explorar los datos y ganar intuición, usaremos las etiquetas.
Creemos ahora un diccionario con los nombres de clase y sus etiquetas categóricas correspondientes:
# Create dictionary of target classes
label_dict ={0:'A',1:'B',2:'C',3:'D',4:'E',5:'F',6:'G',7:'H',8:'I',9:'J',}Con la tecnología de
Ahora, echemos un vistazo a un par de imágenes del dataset:
plt.figure(figsize=[5,5])# Display the first image in training data
plt.subplot(121)
curr_img = np.reshape(train_data[0],(28,28))
curr_lbl = train_labels[0]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: "+str(label_dict[curr_lbl])+")")# Display the first image in testing data
plt.subplot(122)
curr_img = np.reshape(test_data[0],(28,28))
curr_lbl = test_labels[0]
plt.imshow(curr_img, cmap='gray')
plt.title("(Label: "+str(label_dict[curr_lbl])+")")Con la tecnología de
La salida de las dos gráficas anteriores muestra una de las imágenes de ejemplo de entrenamiento y prueba, a las que se les asignan las etiquetas 5 o F, por un lado, y 3 o D, por otro. De forma análoga, el resto de letras tienen distintas etiquetas, y las iguales comparten la misma. Es decir, las 6.000 imágenes de la clase F tendrán etiqueta 5.
Preprocesamiento de datos
Las imágenes del dataset son en efecto en escala de grises, con valores de píxel entre 0 y 255 y dimensión 28 x 28, así que antes de alimentar el modelo es importante preprocesarlas. Primero convertirás cada imagen 28 x 28 de train y test en una matriz de 28 x 28 x 1, que podrás pasar a la red:
train_data = train_data.reshape(-1,28,28,1)
test_data = test_data.reshape(-1,28,28,1)
train_data.shape, test_data.shape
Con la tecnología de
Después, asegúrate de comprobar el tipo de datos de los arrays de entrenamiento y prueba: debe ser float32; si no, conviértelo (ya lo hiciste al leer los datos). Además, debes reescalar los valores de píxel al rango 0 - 1. ¡Hagámoslo!
No olvides verificar los tipos de datos de entrenamiento y prueba:
Hecho esto, toca particionar los datos. Para que tu modelo generalice bien, divide el conjunto de entrenamiento en dos: entrenamiento y validación. Entrenarás con el 80% y validarás con el 20% restante.
Esto también ayuda a reducir el sobreajuste, ya que validarás con datos que el modelo no ve durante el entrenamiento.
Puedes usar el módulo train_test_split de scikit-learn para dividir correctamente:
from sklearn.model_selection import train_test_split
train_X,valid_X,train_ground,valid_ground = train_test_split(train_data,
train_data,
test_size=0.2,
random_state=13)Con la tecnología de
Nota: para esta tarea no necesitas etiquetas de entrenamiento ni de prueba. Por eso pasarás las imágenes de entrenamiento dos veces. Tus imágenes de entrenamiento actuarán tanto como entrada como verdad terreno, igual que las etiquetas en una tarea de clasificación.
Ahora ya puedes definir la red y alimentar los datos. ¡Vamos al siguiente paso!
El autoencoder convolucional
Las imágenes son de tamaño 28 x 28 x 1, o un vector de 784 dimensiones. Convertirás la matriz de imagen a un array, la reescalarás entre 0 y 1, le darás forma 28 x 28 x 1 y la pasarás como entrada a la red.
Usarás un tamaño de lote de 128; tamaños mayores como 256 o 512 también son válidos: depende del sistema con el que entrenes. Influyen mucho en el aprendizaje y en la precisión de las predicciones. Entrenarás durante 50 épocas.
batch_size =128
epochs =50
inChannel =1
x, y =28,28
input_img = Input(shape =(x, y, inChannel))Con la tecnología de
Como comentamos, el autoencoder se divide en dos partes: codificador y decodificador.
Encoder
La primera capa tendrá 32 filtros de 3 x 3, seguida de una capa de reducción (max-pooling).
La segunda capa tendrá 64 filtros de 3 x 3, seguida de otra capa de reducción.
La última capa del codificador tendrá 128 filtros de 3 x 3.
Decoder
La primera capa tendrá 128 filtros de 3 x 3 seguida de una capa de upsampling,/li>
La segunda capa tendrá 64 filtros de 3 x 3 seguida de otra capa de upsampling,
La última capa del codificador tendrá 1 filtro de 3 x 3.
La capa de max-pooling reducirá a la mitad la entrada cada vez que la uses, mientras que la de upsampling la duplicará cada vez.
Nota: el número de filtros, su tamaño, el número de capas o de épocas, son hiperparámetros y deberías decidirlos con tu propia intuición. Eres libre de probar variaciones y medir el rendimiento de tu modelo. ¡Así es como irás dominando el arte del deep learning!
defautoencoder(input_img):#encoder#input = 28 x 28 x 1 (wide and thin)
conv1 = Conv2D(32,(3,3), activation='relu', padding='same')(input_img)#28 x 28 x 32
pool1 = MaxPooling2D(pool_size=(2,2))(conv1)#14 x 14 x 32
conv2 = Conv2D(64,(3,3), activation='relu', padding='same')(pool1)#14 x 14 x 64
pool2 = MaxPooling2D(pool_size=(2,2))(conv2)#7 x 7 x 64
conv3 = Conv2D(128,(3,3), activation='relu', padding='same')(pool2)#7 x 7 x 128 (small and thick)#decoder
conv4 = Conv2D(128,(3,3), activation='relu', padding='same')(conv3)#7 x 7 x 128
up1 = UpSampling2D((2,2))(conv4)# 14 x 14 x 128
conv5 = Conv2D(64,(3,3), activation='relu', padding='same')(up1)# 14 x 14 x 64
up2 = UpSampling2D((2,2))(conv5)# 28 x 28 x 64
decoded = Conv2D(1,(3,3), activation='sigmoid', padding='same')(up2)# 28 x 28 x 1return decoded
Con la tecnología de
Una vez creado el modelo, compílalo usando el optimizador RMSProp.
Consejo: mira este tutorial de Andrew Ng si quieres saber más sobre RMSProp.
Ten en cuenta que también debes especificar el tipo de pérdida con el argumento loss. En este caso, es el error cuadrático medio, ya que la pérdida tras cada lote se calcula entre la salida predicha y la verdad terreno, píxel a píxel, con el error cuadrático medio:
autoencoder = Model(input_img, autoencoder(input_img))
autoencoder.compile(loss='mean_squared_error', optimizer = RMSprop())Con la tecnología de
Visualicemos ahora las capas creadas con la función summary; verás el número de parámetros (pesos y sesgos) de cada capa y el total del modelo.
_________________________________________________________________
Layer(type) Output Shape Param #
=================================================================input_6(InputLayer)(None,28,28,1)0
_________________________________________________________________
conv2d_25(Conv2D)(None,28,28,32)320
_________________________________________________________________
max_pooling2d_9(MaxPooling2(None,14,14,32)0
_________________________________________________________________
conv2d_26(Conv2D)(None,14,14,64)18496
_________________________________________________________________
max_pooling2d_10(MaxPooling(None,7,7,64)0
_________________________________________________________________
conv2d_27(Conv2D)(None,7,7,128)73856
_________________________________________________________________
conv2d_28(Conv2D)(None,7,7,128)147584
_________________________________________________________________
up_sampling2d_9(UpSampling2(None,14,14,128)0
_________________________________________________________________
conv2d_29(Conv2D)(None,14,14,64)73792
_________________________________________________________________
up_sampling2d_10(UpSampling(None,28,28,64)0
_________________________________________________________________
conv2d_30(Conv2D)(None,28,28,1)577=================================================================
Total params:314,625
Trainable params:314,625
Non-trainable params:0
_________________________________________________________________
Con la tecnología de
¡Por fin toca entrenar el modelo con la función fit() de Keras! El modelo se entrena durante 50 épocas. La función fit() devuelve un objeto history; si guardas el resultado en fashion_train, luego podrás trazar la curva de pérdida de entrenamiento y validación para analizar visualmente el rendimiento.
Entrena el modelo
autoencoder_train = autoencoder.fit(train_X, train_ground, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(valid_X, valid_ground))Con la tecnología de
¡Listo! Has entrenado el modelo en Not-MNIST durante 50 épocas. Ahora, representemos la pérdida de entrenamiento frente a la de validación para visualizar el rendimiento.
Gráfica de pérdida: entrenamiento vs validación
loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs =range(epochs)
plt.figure()
plt.plot(epochs, loss,'bo', label='Training loss')
plt.plot(epochs, val_loss,'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()Con la tecnología de
Como ves, las pérdidas de validación y entrenamiento van a la par. Indica que tu modelo no está sobreajustando: la pérdida de validación disminuye y no aumenta, y apenas hay brecha entre ambas curvas.
Por lo tanto, puedes decir que la capacidad de generalización de tu modelo es buena.
Por último, es el momento de reconstruir las imágenes de prueba usando la función predict() de Keras y comprobar qué tal reconstruye tu modelo en los datos de test.
Predicción sobre los datos de prueba
Harás predicciones con el modelo entrenado sobre las 10.000 imágenes de test y representarás algunas reconstrucciones para visualizar la calidad.
plt.figure(figsize=(20,4))print("Test Images")for i inrange(10):
plt.subplot(2,10, i+1)
plt.imshow(test_data[i,...,0], cmap='gray')
curr_lbl = test_labels[i]
plt.title("(Label: "+str(label_dict[curr_lbl])+")")
plt.show()
plt.figure(figsize=(20,4))print("Reconstruction of Test Images")for i inrange(10):
plt.subplot(2,10, i+1)
plt.imshow(pred[i,...,0], cmap='gray')
plt.show()Con la tecnología de
En las figuras anteriores puedes observar que tu modelo ha hecho un gran trabajo reconstruyendo las imágenes de prueba. Al menos visualmente, las originales y las reconstruidas son casi idénticas.
Autoencoder de eliminación de ruido
Un autoencoder de eliminación de ruido intenta aprender una representación (espacio latente o cuello de botella) robusta al ruido. Añades ruido a una imagen y pasas esa imagen ruidosa como entrada a la parte de codificación de tu red. El codificador transforma la imagen a un espacio que intenta preservar las letras pero eliminar el ruido.
Pero, ¿cómo elimina exactamente el ruido?
Durante el entrenamiento, defines una función de pérdida, similar al error cuadrático medio que usaste antes en el autoencoder convolucional. En cada iteración, la red calculará la pérdida entre la imagen con ruido que sale del decodificador y la verdad terreno (imagen sin ruido) e intentará minimizar esa diferencia entre la reconstrucción y la imagen original libre de ruido. En otras palabras, la red aprenderá un espacio 7 x 7 x 128 que contendrá codificaciones sin ruido de los datos con los que entrenes.
Implementación del autoencoder de eliminación de ruido
Para ver cómo funciona en Python, usarás el mismo dataset NotMNIST que en la primera parte. Eso significa que no necesitas repetir el preprocesamiento. Sin embargo, un paso clave aquí será añadir ruido a las imágenes de entrenamiento, validación y prueba. ¡Hagámoslo!
Añadir ruido a las imágenes
Primero definamos un factor de ruido, que es un hiperparámetro. Ese factor se multiplica por una matriz aleatoria con media 0,0 y desviación típica 1,0. Esta matriz toma muestras de una distribución normal (gaussiana). La forma del array normal aleatorio será la misma que la de los datos a los que añadirás el ruido.
Por simplicidad, veámoslo con un ejemplo: la variable train_X tiene forma 48000 x 28 x 28 x 1. Por lo tanto, el array normal aleatorio tendrá la misma forma que train_X; solo así podrás sumar ambos arrays.
np.clip() saturará todos los valores negativos a cero y todos los mayores que uno a uno, ya que quieres que los píxeles estén entre cero y uno. Como al introducir ruido el rango puede variar, es buena práctica recortar los valores para mantenerlos en el rango deseado.
Visualización de imágenes con ruido
plt.figure(figsize=[5,5])# Display the first image in training data
plt.subplot(121)
curr_img = np.reshape(x_train_noisy[1],(28,28))
plt.imshow(curr_img, cmap='gray')# Display the first image in testing data
plt.subplot(122)
curr_img = np.reshape(x_test_noisy[1],(28,28))
plt.imshow(curr_img, cmap='gray')Con la tecnología de
autoencoder = Model(input_img, autoencoder(input_img))
autoencoder.compile(loss='mean_squared_error', optimizer = RMSprop())Con la tecnología de
Entrenamiento
Si recuerdas, al entrenar el autoencoder convolucional pasabas las imágenes de entrenamiento dos veces, ya que la entrada y la verdad terreno eran iguales. En el autoencoder de eliminación de ruido, en cambio, pasas las imágenes con ruido como entrada, mientras que la verdad terreno son las imágenes originales (sin ruido) a las que habías aplicado ese ruido. Solo así la red puede calcular la pérdida entre la imagen ruidosa y la imagen verdadera sin ruido.
autoencoder_train = autoencoder.fit(x_train_noisy, train_X, batch_size=batch_size,epochs=epochs,verbose=1,validation_data=(x_valid_noisy, valid_X))Con la tecnología de
loss = autoencoder_train.history['loss']
val_loss = autoencoder_train.history['val_loss']
epochs =range(epochs)
plt.figure()
plt.plot(epochs, loss,'bo', label='Training loss')
plt.plot(epochs, val_loss,'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
plt.show()Con la tecnología de
De la gráfica anterior puedes intuir que el modelo sobreajusta en algunas épocas, aunque en la mayor parte se mantiene estable. Puedes intentar mejorar el rendimiento introduciendo algo más de complejidad para reducir la pérdida o entrenando más épocas, y decidir después.
plt.figure(figsize=(20,4))print("Test Images")for i inrange(10,20,1):
plt.subplot(2,10, i+1)
plt.imshow(test_data[i,...,0], cmap='gray')
curr_lbl = test_labels[i]
plt.title("(Label: "+str(label_dict[curr_lbl])+")")
plt.show()
plt.figure(figsize=(20,4))print("Test Images with Noise")for i inrange(10,20,1):
plt.subplot(2,10, i+1)
plt.imshow(x_test_noisy[i,...,0], cmap='gray')
plt.show()
plt.figure(figsize=(20,4))print("Reconstruction of Noisy Test Images")for i inrange(10,20,1):
plt.subplot(2,10, i+1)
plt.imshow(pred[i,...,0], cmap='gray')
plt.show()Con la tecnología de
Parece que, en solo 20 épocas, una arquitectura bastante sencilla ha hecho un buen trabajo eliminando el ruido de las imágenes de prueba, ¿verdad?
Sigue aprendiendo
Este tutorial es un buen punto de partida para entender los autoencoders tanto en teoría como en la práctica. Si has podido seguirlo sin problemas, o incluso con un poco de esfuerzo extra, ¡bien hecho! Echa un vistazo al tutorial Keras Tutorial: Deep Learning in Pythonde DataCamp y al curso Introduction to Deep Learning in Python.
En el próximo tutorial aprenderás a leer imágenes desde cero, analizarlas, preprocesarlas y alimentarlas al modelo usando un dataset de huellas dactilares. ¡También verás cómo leer imágenes médicas de modalidad T-1 y reconstruirlas con un autoencoder!
Todavía queda mucho por cubrir, así que ¿por qué no haces el curso Deep Learning in Python de DataCamp si aún no lo has hecho? Aprenderás desde lo básico y poco a poco llegarás a dominar el deep learning; sin duda será un recurso imprescindible para aprender a trabajar con redes convolucionales en Python, detectar caras, objetos, etc.
Una guía completa para entender las CNN, su impacto en el análisis de imágenes y algunas estrategias clave para combatir el sobreajuste en aplicaciones robustas de CNN frente al aprendizaje profundo.
Aprende los principios fundamentales, las aplicaciones y las ventajas prácticas de los autocodificadores variacionales y sigue una implementación paso a paso con PyTorch.
En general, los autoencoders se consideran una técnica de aprendizaje no supervisado, ya que no necesitas etiquetas explícitas para entrenar el modelo. Todo lo que necesitas es datos de entrada en bruto.
En este tutorial, aprenderás qué son los autoencoders en deep learning e implementarás un autoencoder convolucional y uno de eliminación de ruido en Python con Keras. Trabajarás con el dataset de alfabetos NotMNIST como ejemplo.
En pocas palabras, hoy verás:
Autoencoder
Como leíste en la introducción, un autoencoder es un algoritmo de aprendizaje automático no supervisado que toma una imagen como entrada e intenta reconstruirla usando un menor número de bits en el cuello de botella, también llamado espacio latente. La imagen se comprime en gran medida en ese cuello de botella. La compresión en los autoencoders se logra entrenando la red durante un tiempo; a medida que aprende, intenta representar lo mejor posible la imagen de entrada en el cuello de botella. Los algoritmos generales de compresión de imágenes como JPEG o JPEG lossless comprimen sin necesidad de entrenamiento y suelen funcionar bastante bien.
Los autoencoders son similares a técnicas de reducción de dimensionalidad como el Análisis de Componentes Principales (PCA). Proyectan los datos de una dimensión mayor a otra menor mediante transformaciones lineales e intentan preservar las características importantes eliminando las partes no esenciales.
Sin embargo, la gran diferencia entre los autoencoders y PCA está en la transformación: como ya viste, PCA usa transformaciones lineales, mientras que los autoencoders emplean transformaciones no lineales.
Ahora que ya tienes una idea de qué son los autoencoders, ¡vamos a desglosar el término para ganar más intuición!
La figura anterior muestra un autoencoder sencillo de dos capas con una capa oculta. En la jerga del deep learning, verás a menudo que la capa de entrada no se tiene en cuenta al contar el número total de capas de una arquitectura. El total de capas incluye únicamente las capas ocultas y la capa de salida.
Como se muestra en la imagen, las capas de entrada y salida tienen el mismo número de neuronas.
Veamos un ejemplo. Introduces una imagen con cinco píxeles en el autoencoder; el codificador la comprime en tres valores de píxel en el cuello de botella (capa intermedia) o espacio latente. Con esos tres valores, el decodificador intenta reconstruir los cinco píxeles, es decir, la imagen original que diste como entrada a la red.
En la práctica, suele haber más capas ocultas entre la entrada y la salida.
Un autoencoder se divide en tres partes
Existen distintos tipos de autoencoders, como el convolucional, el de eliminación de ruido, el variacional o el disperso (sparse). Sin embargo, como viste en la introducción, en este tutorial te centrarás únicamente en los convolucionales y los de eliminación de ruido.Convolutional Autoencoders in Python with Keras
Dado que tus datos de entrada son imágenes, es buena idea usar un autoencoder convolucional. No es una variante distinta, sino un autoencoder tradicional apilado con capas de convolución: básicamente sustituyes las capas totalmente conectadas por capas convolucionales. Las capas de convolución junto con las de max-pooling convierten la entrada de ancha (una imagen 28 x 28) y fina (un solo canal o escala de grises) a pequeña (imagen 7 x 7 en el espacio latente) y gruesa (128 canales).
¡No te preocupes si la idea anterior no te ha quedado clara del todo! La segunda parte del tutorial, donde te centrarás en la implementación, debería despejar tus dudas.
Consejo: si quieres saber más sobre redes neuronales convolucionales, echa un vistazo a este tutorial.
Esto ayuda a la red a extraer rasgos visuales de las imágenes y, por tanto, a obtener una representación del espacio latente más precisa. El proceso de reconstrucción utiliza upsampling y convoluciones, que conforman el decodificador. La reducción (downsampling) es el proceso por el cual la imagen se comprime a baja dimensión, y corresponde al codificador.
Es importante notar que el codificador principalmente comprime la imagen de entrada; por ejemplo: si tu imagen de entrada tiene dimensión 176 x 176 x 1 (~30976), el punto de compresión máxima puede tener dimensión 22 x 22 x 512 (~247808). En este caso, partiste de una imagen en escala de grises 176 x 176 y, tras pasarla por varias capas convolucionales y exactamente tres capas de max-pooling, la imagen se reduce a 22 x 22, pero el número de canales aumenta de 1 a 512. Como decíamos: pasas de una entrada ancha (176 x 176) y fina (1) a una pequeña (22 x 22) y gruesa (512).
Carga de datos
El dataset notMNIST es un conjunto de reconocimiento de imágenes de glifos de letras de la A a la J. Es muy similar al clásico MNIST, que contiene imágenes de dígitos manuscritos del 0 al 9: en este caso, encontrarás que NotMNIST incluye imágenes en escala de grises de 28x28 con 70.000 letras de la A a la J, en total 10 categorías y 6.000 imágenes por categoría.
Consejo: si quieres aprender a implementar un perceptrón multicapa (MLP) para tareas de clasificación con el dataset MNIST, echa un vistazo a este tutorial.
El dataset NotMNIST no está predefinido en Keras ni en TensorFlow, así que tendrás que descargar los datos desde esta fuente. Los datos se descargarán en formato
ubyte.gzip, ¡pero no te preocupes! Pronto verás cómo leer flujos de bytes y convertirlos en un array de NumPy. ¡Vamos allá!La red se entrenará en una Nvidia Tesla K40, así que si entrenas en GPU y usas Jupyter Notebook, tendrás que añadir tres líneas más para especificar el orden de dispositivos CUDA y los dispositivos visibles CUDA usando el módulo
os.En el código siguiente, estableces variables de entorno en el notebook con
os.environ. Es buena práctica hacerlo antes de inicializar Keras para limitar el backend TensorFlow a usar la primera GPU. Si la máquina en la que entrenas tiene la GPU en0, asegúrate de usar0en lugar de1. Puedes comprobarlo ejecutando un comando sencillo en tu terminal, por ejemplo,nvidia-smiA continuación, importa los módulos necesarios como
numpy,matplotliby, lo más importante,keras, ya que será el framework que usarás hoy.Aquí defines una función que abre el archivo
gzipy lo lee conbytestream.read(). Le pasas la dimensión de la imagen y el número total de imágenes. Luego, connp.frombuffer(), conviertes la cadena almacenada en la variablebufen un array de NumPy de tipofloat32.Después, reconfiguras el array a uno tridimensional (tensor) donde la primera dimensión es el número de imágenes y la segunda y tercera son la dimensión de la imagen. Por último, devuelves el array de NumPy
data.Ahora llamarás a la función
extract_data()pasando los archivos de entrenamiento y prueba junto con su número de imágenes correspondiente:De forma similar, defines una función para extraer etiquetas que abre el archivo
gzip, lo lee conbytestream.read(), a la que pasas la dimensión de la etiqueta (1) y el número total de imágenes. Luego, connp.frombuffer(), conviertes la cadena almacenada en la variablebufen un array de NumPy de tipoint64.Esta vez no necesitas reconfigurar el array, ya que la variable
labelsdevolverá un vector columna de dimensión 60.000 x 1. Por último, devuelves el array de NumPylabels.Ahora llamarás a la función de extracción de etiquetas pasando los archivos de etiquetas de entrenamiento y prueba junto con su número de imágenes correspondiente:
Una vez cargados los datos de entrenamiento y prueba, ya puedes analizarlos para hacerte una idea del dataset con el que vas a trabajar hoy.
Exploración de datos
Analicemos cómo son las imágenes del dataset y veamos también su dimensión con el atributo de NumPy
.shape:Por la salida anterior, puedes ver que los datos de entrenamiento tienen forma 60000 x 28 x 28, ya que hay 60.000 muestras de entrenamiento, cada una con una matriz 28 x 28. De forma similar, los datos de prueba tienen forma 10000 x 28 x 28.
Nota: en esta tarea no usarás las etiquetas de entrenamiento y prueba. Solo trabajarás con las imágenes. Sin embargo, para explorar los datos y ganar intuición, usaremos las etiquetas.
Creemos ahora un diccionario con los nombres de clase y sus etiquetas categóricas correspondientes:
Ahora, echemos un vistazo a un par de imágenes del dataset:
La salida de las dos gráficas anteriores muestra una de las imágenes de ejemplo de entrenamiento y prueba, a las que se les asignan las etiquetas
5oF, por un lado, y3oD, por otro. De forma análoga, el resto de letras tienen distintas etiquetas, y las iguales comparten la misma. Es decir, las 6.000 imágenes de la claseFtendrán etiqueta5.Preprocesamiento de datos
Las imágenes del dataset son en efecto en escala de grises, con valores de píxel entre 0 y 255 y dimensión 28 x 28, así que antes de alimentar el modelo es importante preprocesarlas. Primero convertirás cada imagen 28 x 28 de train y test en una matriz de 28 x 28 x 1, que podrás pasar a la red:
Después, asegúrate de comprobar el tipo de datos de los arrays de entrenamiento y prueba: debe ser float32; si no, conviértelo (ya lo hiciste al leer los datos). Además, debes reescalar los valores de píxel al rango 0 - 1. ¡Hagámoslo!
No olvides verificar los tipos de datos de entrenamiento y prueba:
Ahora, reescala los datos de entrenamiento y prueba con el valor máximo de píxel de cada conjunto:
Verifiquemos que el valor máximo de train y test sea 1.0 tras el reescalado:
Hecho esto, toca particionar los datos. Para que tu modelo generalice bien, divide el conjunto de entrenamiento en dos: entrenamiento y validación. Entrenarás con el 80% y validarás con el 20% restante.
Esto también ayuda a reducir el sobreajuste, ya que validarás con datos que el modelo no ve durante el entrenamiento.
Puedes usar el módulo
train_test_splitde scikit-learn para dividir correctamente:Nota: para esta tarea no necesitas etiquetas de entrenamiento ni de prueba. Por eso pasarás las imágenes de entrenamiento dos veces. Tus imágenes de entrenamiento actuarán tanto como entrada como verdad terreno, igual que las etiquetas en una tarea de clasificación.
Ahora ya puedes definir la red y alimentar los datos. ¡Vamos al siguiente paso!
El autoencoder convolucional
Las imágenes son de tamaño 28 x 28 x 1, o un vector de 784 dimensiones. Convertirás la matriz de imagen a un array, la reescalarás entre 0 y 1, le darás forma 28 x 28 x 1 y la pasarás como entrada a la red.
Usarás un tamaño de lote de 128; tamaños mayores como 256 o 512 también son válidos: depende del sistema con el que entrenes. Influyen mucho en el aprendizaje y en la precisión de las predicciones. Entrenarás durante 50 épocas.
Como comentamos, el autoencoder se divide en dos partes: codificador y decodificador.
Encoder
Decoder
La capa de max-pooling reducirá a la mitad la entrada cada vez que la uses, mientras que la de upsampling la duplicará cada vez.
Nota: el número de filtros, su tamaño, el número de capas o de épocas, son hiperparámetros y deberías decidirlos con tu propia intuición. Eres libre de probar variaciones y medir el rendimiento de tu modelo. ¡Así es como irás dominando el arte del deep learning!
Una vez creado el modelo, compílalo usando el optimizador RMSProp.
Consejo: mira este tutorial de Andrew Ng si quieres saber más sobre RMSProp.
Ten en cuenta que también debes especificar el tipo de pérdida con el argumento
loss. En este caso, es el error cuadrático medio, ya que la pérdida tras cada lote se calcula entre la salida predicha y la verdad terreno, píxel a píxel, con el error cuadrático medio:Visualicemos ahora las capas creadas con la función summary; verás el número de parámetros (pesos y sesgos) de cada capa y el total del modelo.
¡Por fin toca entrenar el modelo con la función
fit()de Keras! El modelo se entrena durante 50 épocas. La funciónfit()devuelve un objeto history; si guardas el resultado en fashion_train, luego podrás trazar la curva de pérdida de entrenamiento y validación para analizar visualmente el rendimiento.Entrena el modelo
¡Listo! Has entrenado el modelo en Not-MNIST durante 50 épocas. Ahora, representemos la pérdida de entrenamiento frente a la de validación para visualizar el rendimiento.
Gráfica de pérdida: entrenamiento vs validación
Como ves, las pérdidas de validación y entrenamiento van a la par. Indica que tu modelo no está sobreajustando: la pérdida de validación disminuye y no aumenta, y apenas hay brecha entre ambas curvas.
Por lo tanto, puedes decir que la capacidad de generalización de tu modelo es buena.
Por último, es el momento de reconstruir las imágenes de prueba usando la función
predict()de Keras y comprobar qué tal reconstruye tu modelo en los datos de test.Predicción sobre los datos de prueba
Harás predicciones con el modelo entrenado sobre las 10.000 imágenes de test y representarás algunas reconstrucciones para visualizar la calidad.
En las figuras anteriores puedes observar que tu modelo ha hecho un gran trabajo reconstruyendo las imágenes de prueba. Al menos visualmente, las originales y las reconstruidas son casi idénticas.
Autoencoder de eliminación de ruido
Un autoencoder de eliminación de ruido intenta aprender una representación (espacio latente o cuello de botella) robusta al ruido. Añades ruido a una imagen y pasas esa imagen ruidosa como entrada a la parte de codificación de tu red. El codificador transforma la imagen a un espacio que intenta preservar las letras pero eliminar el ruido.
Pero, ¿cómo elimina exactamente el ruido?
Durante el entrenamiento, defines una función de pérdida, similar al error cuadrático medio que usaste antes en el autoencoder convolucional. En cada iteración, la red calculará la pérdida entre la imagen con ruido que sale del decodificador y la verdad terreno (imagen sin ruido) e intentará minimizar esa diferencia entre la reconstrucción y la imagen original libre de ruido. En otras palabras, la red aprenderá un espacio 7 x 7 x 128 que contendrá codificaciones sin ruido de los datos con los que entrenes.
Implementación del autoencoder de eliminación de ruido
Para ver cómo funciona en Python, usarás el mismo dataset NotMNIST que en la primera parte. Eso significa que no necesitas repetir el preprocesamiento. Sin embargo, un paso clave aquí será añadir ruido a las imágenes de entrenamiento, validación y prueba. ¡Hagámoslo!
Añadir ruido a las imágenes
Primero definamos un factor de ruido, que es un hiperparámetro. Ese factor se multiplica por una matriz aleatoria con media 0,0 y desviación típica 1,0. Esta matriz toma muestras de una distribución normal (gaussiana). La forma del array normal aleatorio será la misma que la de los datos a los que añadirás el ruido.
Por simplicidad, veámoslo con un ejemplo: la variable
train_Xtiene forma 48000 x 28 x 28 x 1. Por lo tanto, el array normal aleatorio tendrá la misma forma quetrain_X; solo así podrás sumar ambos arrays.np.clip()saturará todos los valores negativos a cero y todos los mayores que uno a uno, ya que quieres que los píxeles estén entre cero y uno. Como al introducir ruido el rango puede variar, es buena práctica recortar los valores para mantenerlos en el rango deseado.Visualización de imágenes con ruido
Ahora que ya tienes los datos con ruido, puedes pasarlos por la red y ver cómo el ruido se elimina "mágicamente" de las imágenes.
Red de autoencoder de eliminación de ruido
Como ya dijimos, el autoencoder se divide en dos partes: codificador y decodificador. La arquitectura que vas a construir tendrá este aspecto:
Encoder
Decoder
Entrenamiento
Si recuerdas, al entrenar el autoencoder convolucional pasabas las imágenes de entrenamiento dos veces, ya que la entrada y la verdad terreno eran iguales. En el autoencoder de eliminación de ruido, en cambio, pasas las imágenes con ruido como entrada, mientras que la verdad terreno son las imágenes originales (sin ruido) a las que habías aplicado ese ruido. Solo así la red puede calcular la pérdida entre la imagen ruidosa y la imagen verdadera sin ruido.
Gráfica de pérdida: entrenamiento vs validación
De la gráfica anterior puedes intuir que el modelo sobreajusta en algunas épocas, aunque en la mayor parte se mantiene estable. Puedes intentar mejorar el rendimiento introduciendo algo más de complejidad para reducir la pérdida o entrenando más épocas, y decidir después.
Predicción sobre el conjunto de prueba
Parece que, en solo 20 épocas, una arquitectura bastante sencilla ha hecho un buen trabajo eliminando el ruido de las imágenes de prueba, ¿verdad?
Sigue aprendiendo
Este tutorial es un buen punto de partida para entender los autoencoders tanto en teoría como en la práctica. Si has podido seguirlo sin problemas, o incluso con un poco de esfuerzo extra, ¡bien hecho! Echa un vistazo al tutorial Keras Tutorial: Deep Learning in Python de DataCamp y al curso Introduction to Deep Learning in Python.
En el próximo tutorial aprenderás a leer imágenes desde cero, analizarlas, preprocesarlas y alimentarlas al modelo usando un dataset de huellas dactilares. ¡También verás cómo leer imágenes médicas de modalidad T-1 y reconstruirlas con un autoencoder!
Todavía queda mucho por cubrir, así que ¿por qué no haces el curso Deep Learning in Python de DataCamp si aún no lo has hecho? Aprenderás desde lo básico y poco a poco llegarás a dominar el deep learning; sin duda será un recurso imprescindible para aprender a trabajar con redes convolucionales en Python, detectar caras, objetos, etc.