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

    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 hay datos que se le transmiten desde los servidores.

    Esto no sería un problema para un solo usuario. Pero imagine manejar miles, si no millones, de solicitudes con grandes cantidades de datos al mismo tiempo. Estos flujos de datos deben reducirse de alguna manera para que podamos proporcionárselos 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 nos «recortamos» mucho
    • Con pérdida: La integridad y precisión de los datos no es tan importante como la rapidez con la que los podemos entregar. Imagine una transferencia de video en tiempo real, donde es más importante estar «en vivo» que tener video de alta calidad

    Por ejemplo, utilizando Autoencoders, 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.

    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:

    ¿Qué son los Autoencoders?

    Un autoencoder es, por definición, una técnica para codificar algo automáticamente. Al usar 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, usar esa representación para reconstruir los datos originales lo más cerca posible del original.

    Hay dos componentes clave en esta tarea:

    • Codificador: Aprende a comprimir la entrada original en una pequeña codificación
    • Descifrador: Aprende cómo 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 tanto.

    Crédito: Puerta de la investigación

    Codificador

    El codificador tiene la tarea de encontrar la representación de datos más pequeña posible que pueda 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 gran número: intenta encontrar un patrón en él que pueda memorizar y restaurar la secuencia completa 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 redes neuronales artificiales (ANN) simples. 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 ellos: 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 LFW ejemplo de conjunto de datos:

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

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

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

    Descifrador

    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 generar 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, nos propagamos hacia atrás y actualizamos todos los parámetros del decodificador al codificador. Por lo tanto, en función de las diferencias entre las imágenes de entrada y salida, tanto el decodificador como el codificador son evaluados en sus trabajos y actualizan sus parámetros para mejorar.

    Construyendo un Autoencoder

    Keras es un marco de Python que simplifica la construcció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:

    $ pip install keras
    

    Procesamiento previo de datos

    Nuevamente, usaremos el Conjunto de datos de LFW. Como es habitual, 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:

    # 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:

    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 realmente el conjunto de datos y adaptarlo a nuestras necesidades:

    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
    

    Implementando el Autoencoder

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

    Nuestros datos están en el X matrix, en forma de matriz 3D, que es la representación predeterminada para 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 Machine Learning, tendemos a hacer valores pequeños y centrados en 0, ya que esto ayuda a que nuestro modelo se entrene más rápido y obtenga mejores resultados, así que normalicemos nuestras imágenes:

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

    A estas alturas, si probamos el X matriz para el mínimo y el máximo será -.5 y .5, que puede verificar:

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

    Para poder ver la imagen, creemos un show_image función. Se agregará 0.5 a las imágenes ya que el valor de píxel no puede ser negativo:

    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:

    show_image(X[6])
    

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

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

    El sklearn train_test_split() La función puede dividir los datos dándole la proporción de prueba y el resto es, por supuesto, el tamaño del entrenamiento. los random_state, que verá mucho en el Machine Learning, se utiliza para producir los mismos resultados sin importar cuántas veces ejecute el código.

    Ahora toca el modelo:

    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 un 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) dónde 32 representan el ancho y el alto, y 3 representa las matrices de canales de color. Dicho esto, nuestra imagen ha 3072 dimensiones.

    Lógicamente, cuanto menor sea el code_size es decir, 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 utiliza básicamente para agregar capas secuencialmente y profundizar nuestra red. Cada capa alimenta a la siguiente, y aquí, simplemente comenzamos con la InputLayer (un marcador de posición para la entrada) con el tamaño del vector de entrada – image_shape.

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

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

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

    Ahora conectémoslos y comencemos nuestro modelo:

    # 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: nuestro code La variable es la salida del codificador, que colocamos en el decodificador y generamos el reconstruction variable.

    Posteriormente, los vinculamos a ambos creando un Model con el inp y reconstruction parámetros y compílelos con el adamax optimizador y mse función de pérdida.

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

    En este punto, podemos resumir los resultados:

    _________________________________________________________________
    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. Nota la None aquí se refiere al índice de instancia, ya que le damos los datos al modelo, tendrá una forma de (m, 32,32,3), dónde m es el número de instancias, por lo que lo mantenemos como None.

    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 ves es (32,32,3).

    Ahora, intercambiemos el modelo:

    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 y y son iguales a X_train. Idealmente, la entrada es igual a la salida.

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

    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 por épocas para obtener una visión general sobre el número de épocas.

    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()
    

    Podemos ver que después de la tercera época, no hay un progreso significativo en la pérdida. Visualizar así puede ayudarte a tener una mejor idea de cuántas épocas son realmente suficientes para entrenar tu 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 llevar 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:

    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)
    

     

    Puede 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 visto en el medio, esto no está nada mal. A través de la compresión de 3072 dimensiones a solo 32 perdemos muchos datos.

    Ahora, aumentemos el code_size a 1000:

     

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

    Nota: La codificación no es bidimensional, como se representa arriba. Esto es solo para 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 características generando nuevas características que son más pequeñas, pero aún capturan la información importante.

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

    Denoising de imagen

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

    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 sigma valores:

    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])
    

    Como podemos ver, como 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:

    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:

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

     

    Aplicaciones del 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 utilizar en aplicaciones como Deepfakes, donde tienes un codificador y decodificador de diferentes modelos.

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

    Crédito: AlanZucconi

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

    Crédito: PapersWithCode

    Conclusión

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

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

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *