Autocodificadores para reconstrucción de imágenes en Python y Keras

En un mundo basado en datos, optimizar su tamaño es primordial. Los codificadores automáticos codifican y decodifican automáticamente la información para facilitar el transporte. En este artículo, usaremos Python y Keras para crear un codificador automático mediante el aprendizaje profundo.

Introducción

Hoy en día, tenemos enormes cantidades de datos en casi todas las aplicaciones que usamos: escuchar música en Spotify, buscar imágenes de amigos en Instagram o tal vez ver un nuevo avance en YouTube. Siempre se transmiten datos desde los servidores hacia usted.

Esto no sería un problema para un solo usuario. Pero imagine manejar miles, si no millones, de solicitudes con grandes datos al mismo tiempo. Estos flujos de datos deben reducirse de alguna manera para que podamos proporcionarlos físicamente a los usuarios; aquí es donde entra en juego la compresión de datos.

Hay muchas técnicas de compresión, y varían en su uso y compatibilidad. Por ejemplo, algunas técnicas de compresión solo funcionan en archivos de audio, como el famoso códec MPEG-2 Audio Layer III (MP3).

Hay dos tipos principales de compresión:

  • Sin pérdida: se prefiere la integridad y precisión de los datos, incluso si no “recortamos” mucho
  • Lossy: la integridad y la precisión de los datos no son tan importantes como la rapidez con la que podemos entregarlos. Imagine una transferencia de video en tiempo real, donde es más importante estar "en vivo" que tener vídeo de alta calidad

Por ejemplo, usando Codificadores automáticos, podemos descomponer esta imagen y representarla como el código de 32 vectores a continuación. Usándolo, podemos reconstruir la imagen. Por supuesto, este es un ejemplo de compresión con pérdida, ya que hemos perdido bastante información.

low dimension autoencoder

Sin embargo, podemos usar exactamente la misma técnica para hacer esto con mucha más precisión, asignando más espacio para la representación:

high dimension autoencoder

¿Qué son los codificadores automáticos?

Un autocodificador es, por definición, una técnica para codificar algo automáticamente. Mediante el uso de una red neuronal, el codificador automático puede aprender a descomponer datos (en nuestro caso, imágenes) en bits de datos bastante pequeños y, luego, usando esa representación, reconstruir los datos originales lo más cerca posible del original.

Hay dos componentes clave en esta tarea:

  • Codificador: aprende cómo comprimir la entrada original en una codificación pequeña
  • Decodificador: Aprende a restaurar los datos originales de esa codificación generada por el Codificador

Estos dos se entrenan juntos en simbiosis para obtener la representación más eficiente de los datos a partir de los cuales podemos reconstruir los datos originales, sin perder gran parte de ellos.

autoencoder diagram

[Crédito: [Puerta de la investigación](https://www.researchgate.net/profile/Jeremie_Sublime/publication/333038461/figure/fig3/AS:[email protected]/Basic-architecture-of-a-single-layer- codificador-automático-hecho-de-un-codificador-que-va-desde-la-entrada.ppm)]{.pequeño}

Codificador

El codificador tiene la tarea de encontrar la representación más pequeña posible de los datos que puede almacenar, extrayendo las características más destacadas de los datos originales y representándolos de una manera que el decodificador pueda entender.

Piense en ello como si estuviera tratando de memorizar algo, como por ejemplo memorizar un número grande: intente encontrar un patrón en él que pueda memorizar y restaurar toda la secuencia a partir de ese patrón, ya que será fácil recordar un patrón más corto que el número entero.

Los codificadores en su forma más simple son simples Redes neuronales artificiales (ANN). Sin embargo, hay ciertos codificadores que utilizan Redes neuronales convolucionales (CNN), que es un tipo muy específico de ANN.

El codificador toma los datos de entrada y genera una versión codificada de los mismos: los datos comprimidos. Luego podemos usar esos datos comprimidos para enviarlos al usuario, donde serán decodificados y reconstruidos. Echemos un vistazo a la codificación de un ejemplo de conjunto de datos LFW:

generated encodings

La codificación aquí no tiene mucho sentido para nosotros, pero es suficiente para el decodificador. Ahora, es válido plantear la pregunta:

"¿Pero cómo el codificador aprendió a comprimir imágenes como esta?

Aquí es donde entra en juego la simbiosis durante el entrenamiento.

Decodificador

El Decodificador funciona de manera similar al codificador, pero al revés. Aprende a leer, en lugar de generar, estas representaciones de código comprimido y genera imágenes basadas en esa información. Su objetivo es minimizar la pérdida mientras se reconstruye, obviamente.

La salida se evalúa comparando la imagen reconstruida con la original, utilizando un error cuadrático medio (MSE): cuanto más similar es al original, menor es el error.

En este punto, propagamos hacia atrás y actualizamos todos los parámetros desde el decodificador hasta el codificador. Por lo tanto, en función de las diferencias entre las imágenes de entrada y salida, tanto el decodificador como el codificador se evalúan en sus trabajos y actualizan sus parámetros para mejorar.

Creación de un codificador automático {#construcción de un codificador automático}

Keras es un marco de Python que simplifica la creación de redes neuronales. Nos permite apilar capas de diferentes tipos para crear una red neuronal profunda, lo que haremos para construir un codificador automático.

Primero, instalemos Keras usando pip:

1
$ pip install keras

Preprocesamiento de datos {#preprocesamiento de datos}

De nuevo, usaremos el Conjunto de datos LFW. Como de costumbre, con proyectos como estos, preprocesaremos los datos para facilitar que nuestro codificador automático haga su trabajo.

Para esto, primero definiremos un par de rutas que conducen al conjunto de datos que estamos usando:

1
2
3
4
5
6
7
8
# http://www.cs.columbia.edu/CAVE/databases/pubfig/download/lfw_attributes.txt
ATTRS_NAME = "lfw_attributes.txt"

# http://vis-www.cs.umass.edu/lfw/lfw-deepfunneled.tgz
IMAGES_NAME = "lfw-deepfunneled.tgz"

# http://vis-www.cs.umass.edu/lfw/lfw.tgz
RAW_IMAGES_NAME = "lfw.tgz"

Luego, emplearemos dos funciones: una para convertir la matriz sin procesar en una imagen y cambiar el sistema de color a RGB:

1
2
3
4
def decode_image_from_raw_bytes(raw_bytes):
    img = cv2.imdecode(np.asarray(bytearray(raw_bytes), dtype=np.uint8), 1)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    return img

Y el otro para cargar el conjunto de datos y adaptarlo a nuestras necesidades:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def load_lfw_dataset(
        use_raw=False,
        dx=80, dy=80,
        dimx=45, dimy=45):

    # Read attrs
    df_attrs = pd.read_csv(ATTRS_NAME, sep='\t', skiprows=1)
    df_attrs = pd.DataFrame(df_attrs.iloc[:, :-1].values, columns=df_attrs.columns[1:])
    imgs_with_attrs = set(map(tuple, df_attrs[["person", "imagenum"]].values))

    # Read photos
    all_photos = []
    photo_ids = []

    # tqdm in used to show progress bar while reading the data in a notebook here, you can change
    # tqdm_notebook to use it outside a notebook
    with tarfile.open(RAW_IMAGES_NAME if use_raw else IMAGES_NAME) as f:
        for m in tqdm.tqdm_notebook(f.getmembers()):
            # Only process image files from the compressed data
            if m.isfile() and m.name.endswith(".jpg"):
                # Prepare image
                img = decode_image_from_raw_bytes(f.extractfile(m).read())

                # Crop only faces and resize it
                img = img[dy:-dy, dx:-dx]
                img = cv2.resize(img, (dimx, dimy))

                # Parse person and append it to the collected data
                fname = os.path.split(m.name)[-1]
                fname_splitted = fname[:-4].replace('_', ' ').split()
                person_id = ' '.join(fname_splitted[:-1])
                photo_number = int(fname_splitted[-1])
                if (person_id, photo_number) in imgs_with_attrs:
                    all_photos.append(img)
                    photo_ids.append({'person': person_id, 'imagenum': photo_number})

    photo_ids = pd.DataFrame(photo_ids)
    all_photos = np.stack(all_photos).astype('uint8')

    # Preserve photo_ids order!
    all_attrs = photo_ids.merge(df_attrs, on=('person', 'imagenum')).drop(["person", "imagenum"], axis=1)

    return all_photos, all_attrs

Implementación del codificador automático

1
2
import numpy as np
X, attr = load_lfw_dataset(use_raw=True, dimx=32, dimy=32)

Nuestros datos están en la matriz X, en forma de matriz 3D, que es la representación predeterminada para las imágenes RGB. Al proporcionar tres matrices: rojo, verde y azul, la combinación de estos tres genera el color de la imagen.

Estas imágenes tendrán valores grandes para cada píxel, que van de 0 a 255. En general, en el aprendizaje automático tendemos a hacer que los valores sean pequeños y centrados alrededor de 0, ya que esto ayuda a que nuestro modelo entrene más rápido y obtenga mejores resultados, así que normalicemos nuestras imágenes:

1
X = X.astype('float32') / 255.0 - 0.5

Por ahora, si probamos la matriz X para el mínimo y el máximo, será -.5 y .5, que puede verificar:

1
print(X.max(), X.min())
1
0.5 -0.5

Para poder ver la imagen, vamos a crear una función show_image. Agregará 0.5 a las imágenes ya que el valor del píxel no puede ser negativo:

1
2
3
import matplotlib.pyplot as plt
def show_image(x):
    plt.imshow(np.clip(x + 0.5, 0, 1))

Ahora echemos un vistazo rápido a nuestros datos:

1
show_image(X[6])

preprocessed data face

Genial, ahora dividamos nuestros datos en un conjunto de entrenamiento y prueba:

1
2
from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(X, test_size=0.1, random_state=42)

La función sklearn train_test_split() es capaz de dividir los datos dándoles la relación de prueba y el resto es, por supuesto, el tamaño de entrenamiento. El random_state, que verá mucho en el aprendizaje automático, se usa para producir los mismos resultados sin importar cuántas veces ejecute el código.

Ahora el tiempo para el modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from keras.layers import Dense, Flatten, Reshape, Input, InputLayer
from keras.models import Sequential, Model

def build_autoencoder(img_shape, code_size):
    # The encoder
    encoder = Sequential()
    encoder.add(InputLayer(img_shape))
    encoder.add(Flatten())
    encoder.add(Dense(code_size))

    # The decoder
    decoder = Sequential()
    decoder.add(InputLayer((code_size,)))
    decoder.add(Dense(np.prod(img_shape))) # np.prod(img_shape) is the same as 32*32*3, it's more generic than saying 3072
    decoder.add(Reshape(img_shape))

    return encoder, decoder

Esta función toma image_shape (dimensiones de la imagen) y code_size (el tamaño de la representación de salida) como parámetros. La forma de la imagen, en nuestro caso, será (32, 32, 3) donde 32 representa el ancho y el alto, y 3 representa las matrices de canales de color. Dicho esto, nuestra imagen tiene dimensiones 3072.

Lógicamente, cuanto más pequeño sea code_size, más se comprimirá la imagen, pero se guardarán menos características y la imagen reproducida será mucho más diferente de la original.

Un modelo secuencial de Keras se usa básicamente para agregar capas secuencialmente y profundizar nuestra red. Cada capa alimenta a la siguiente, y aquí, simplemente comenzamos con InputLayer (un marcador de posición para la entrada) con el tamaño del vector de entrada - image_shape.

El trabajo de la capa Flatten es aplanar la matriz (32,32,3) en una matriz 1D (3072) ya que la arquitectura de red no acepta matrices 3D.

La última capa del codificador es la capa Densa, que es la red neuronal real aquí. Intenta encontrar los parámetros óptimos que logran el mejor resultado; en nuestro caso, es la codificación, y estableceremos el tamaño de salida (también el número de neuronas que contiene) en code_size.

El decodificador es también un modelo secuencial. Acepta la entrada (la codificación) e intenta reconstruirla en forma de fila. Luego, lo apila en una matriz 32x32x3 a través de la capa Densa. La capa final Reformar la remodelará en una imagen.

Ahora conectémoslos y comencemos nuestro modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Same as (32,32,3), we neglect the number of instances from shape
IMG_SHAPE = X.shape[1:]
encoder, decoder = build_autoencoder(IMG_SHAPE, 32)

inp = Input(IMG_SHAPE)
code = encoder(inp)
reconstruction = decoder(code)

autoencoder = Model(inp,reconstruction)
autoencoder.compile(optimizer='adamax', loss='mse')

print(autoencoder.summary())

Este código es bastante sencillo: nuestra variable código es la salida del codificador, que ponemos en el decodificador y generamos la variable reconstrucción.

Luego, los vinculamos creando un ‘Modelo’ con los parámetros ‘inp’ y ‘reconstrucción’ y los compilamos con el optimizador ‘adamax’ y la función de pérdida ‘mse’.

Recopilar el modelo aquí significa definir su objetivo y cómo alcanzarlo. El objetivo en nuestro contexto es minimizar el mse y lo logramos mediante el uso de un optimizador, que es básicamente un algoritmo ajustado para encontrar el mínimo global.

En este punto, podemos resumir los resultados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_6 (InputLayer)         (None, 32, 32, 3)         0
_________________________________________________________________
sequential_3 (Sequential)    (None, 32)                98336
_________________________________________________________________
sequential_4 (Sequential)    (None, 32, 32, 3)         101376
=================================================================
Total params: 199,712
Trainable params: 199,712
Non-trainable params: 0
_________________________________________________________________

Aquí podemos ver que la entrada es 32,32,3. Tenga en cuenta que ‘Ninguno’ aquí se refiere al índice de instancias, ya que le damos los datos al modelo, tendrá una forma de ‘(m, 32,32,3)’, donde ’m’ es el número de instancias, por lo que manténgalo como Ninguno.

La capa oculta es 32, que es de hecho el tamaño de codificación que elegimos y, por último, la salida del decodificador, como puede ver, es (32,32,3).

Ahora, cambiemos el modelo:

1
2
history = autoencoder.fit(x=X_train, y=X_train, epochs=20,
                validation_data=[X_test, X_test])

En nuestro caso, compararemos las imágenes construidas con las originales, por lo que tanto x como y son iguales a X_train. Idealmente, la entrada es igual a la salida.

La variable epochs define cuántas veces queremos que los datos de entrenamiento pasen por el modelo y validation_data es el conjunto de validación que usamos para evaluar el modelo después del entrenamiento:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Train on 11828 samples, validate on 1315 samples
Epoch 1/20
11828/11828 [==============================] - 3s 272us/step - loss: 0.0128 - val_loss: 0.0087
Epoch 2/20
11828/11828 [==============================] - 3s 227us/step - loss: 0.0078 - val_loss: 0.0071
.
.
.
Epoch 20/20
11828/11828 [==============================] - 3s 237us/step - loss: 0.0067 - val_loss: 0.0066

Podemos visualizar la pérdida a lo largo de las épocas para obtener una visión general del número de épocas.

1
2
3
4
5
6
7
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

loss/epoch

Podemos ver que después de la tercera época, no hay un progreso significativo en la pérdida. Visualizar de esta manera puede ayudarlo a tener una mejor idea de cuántas épocas son realmente suficientes para entrenar su modelo. En este caso, simplemente no hay necesidad de entrenarlo para “20” épocas, y la mayor parte del entrenamiento es redundante.

Esto también puede conducir a un ajuste excesivo del modelo, lo que hará que su rendimiento sea deficiente en datos nuevos fuera de los conjuntos de datos de entrenamiento y prueba.

Ahora, la parte más esperada - visualicemos los resultados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def visualize(img,encoder,decoder):
    """Draws original, encoded and decoded images"""
    # img[None] will have shape of (1, 32, 32, 3) which is the same as the model input
    code = encoder.predict(img[None])[0]
    reco = decoder.predict(code[None])[0]

    plt.subplot(1,3,1)
    plt.title("Original")
    show_image(img)

    plt.subplot(1,3,2)
    plt.title("Code")
    plt.imshow(code.reshape([code.shape[-1]//2,-1]))

    plt.subplot(1,3,3)
    plt.title("Reconstructed")
    show_image(reco)
    plt.show()

for i in range(5):
    img = X_test[i]
    visualize(img,encoder,decoder)

pca encoder results
pca encoder results 2
pca encoder results 3
pca encoder results 4\

pca encoder results 5

Puedes ver que los resultados no son realmente buenos. Sin embargo, si tenemos en cuenta que toda la imagen está codificada en el vector extremadamente pequeño de 32 que se ve en el medio, esto no está nada mal. A través de la compresión de las dimensiones 3072 a solo 32, perdemos muchos datos.

Ahora, aumentemos code_size a 1000:

pca 1000 results
pca 1000 results
pca 1000 results
pca 1000 results
pca 1000 results

¿Ver la diferencia? A medida que le da al modelo más espacio para trabajar, guarda más información importante sobre la imagen.

Nota: La codificación no es bidimensional, como se representa arriba. Esto es solo con fines ilustrativos. En realidad, es una matriz unidimensional de 1000 dimensiones.

Lo que acabamos de hacer se llama Análisis de componentes principales (PCA), que es una técnica de reducción de dimensionalidad. Podemos usarlo para reducir el tamaño del conjunto de funciones generando nuevas funciones que son más pequeñas, pero aún así capturan la información importante.

El análisis de componentes principales es un uso muy popular de los codificadores automáticos.

Eliminación de ruido de imagen {#eliminación de ruido de imagen}

Otro uso popular de los codificadores automáticos es la eliminación de ruido. Agreguemos un poco de ruido aleatorio a nuestras imágenes:

1
2
3
def apply_gaussian_noise(X, sigma=0.1):
    noise = np.random.normal(loc=0.0, scale=sigma, size=X.shape)
    return X + noise

Aquí agregamos algo de ruido aleatorio de la distribución normal estándar con una escala de sigma, que por defecto es 0.1.

Como referencia, así es como se ve el ruido con diferentes valores sigma:

1
2
3
4
5
6
7
8
plt.subplot(1,4,1)
show_image(X_train[0])
plt.subplot(1,4,2)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.01)[0])
plt.subplot(1,4,3)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.1)[0])
plt.subplot(1,4,4)
show_image(apply_gaussian_noise(X_train[:1],sigma=0.5)[0])

image with noise

Como podemos ver, a medida que sigma aumenta a 0.5 la imagen apenas se ve. Intentaremos regenerar la imagen original de las ruidosas con sigma de 0.1.

El modelo que generaremos para esto es el mismo que el anterior, aunque lo entrenaremos de manera diferente. Esta vez, lo entrenaremos con las imágenes ruidosas originales y correspondientes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
code_size = 100

# We can use bigger code size for better quality
encoder, decoder = build_autoencoder(IMG_SHAPE, code_size=code_size)

inp = Input(IMG_SHAPE)
code = encoder(inp)
reconstruction = decoder(code)

autoencoder = Model(inp, reconstruction)
autoencoder.compile('adamax', 'mse')

for i in range(25):
    print("Epoch %i/25, Generating corrupted samples..."%(i+1))
    X_train_noise = apply_gaussian_noise(X_train)
    X_test_noise = apply_gaussian_noise(X_test)

    # We continue to train our model with new noise-augmented data
    autoencoder.fit(x=X_train_noise, y=X_train, epochs=1,
                    validation_data=[X_test_noise, X_test])

Ahora veamos los resultados del modelo:

1
2
3
4
X_test_noise = apply_gaussian_noise(X_test)
for i in range(5):
    img = X_test_noise[i]
    visualize(img,encoder,decoder)

denoising an image with autoencoder
denoising an image with autoencoder
denoising an image with autoencoder
denoising an image with autoencoder
denoising an image with autoencoder

Aplicaciones de codificador automático {#aplicaciones de codificador automático}

Hay muchos más usos para los codificadores automáticos, además de los que hemos explorado hasta ahora.

Autoencoder se puede usar en aplicaciones como falsificaciones profundas, donde tienes codificador y decodificador de diferentes modelos.

Por ejemplo, digamos que tenemos dos codificadores automáticos para Persona X y uno para Persona Y. No hay nada que nos impida usar el codificador de ‘Persona X’ y el decodificador de ‘Persona Y’ y luego generar imágenes de ‘Persona Y’ con las características destacadas de ‘Persona X’:

face swap

[Crédito: alanzucconi]{.small}

Los codificadores automáticos también se pueden usar para la segmentación de imágenes, como en los vehículos autónomos, donde necesita segmentar diferentes elementos para que el vehículo tome una decisión:

image segmentation

[Crédito: PapelesConCódigo]{.small}

Conclusión

Los codificadores automáticos se pueden utilizar para el análisis de componentes principales, que es una técnica de reducción de dimensionalidad, eliminación de ruido de imagen y mucho más.

Puede probarlo usted mismo con diferentes conjuntos de datos, como por ejemplo el conjunto de datos MNIST y ver qué resultados obtiene. .