Generative Adversarial Networks

Contenido abierto del capítulo 8 del libro DEEP LEARNING Introducción práctica con Keras

Este post es una versión preliminar del capítulo 8 del libro “Deep Learning – Introducción práctica con Keras (SEGUNDA PARTE)” que se publicará este otoño en Kindle Direct Publishing con ISBN 978-1-687-47399-8 en la colección WATCH THIS SPACE – Barcelona (Book 6). Como autor agradeceré que cualquier error o comentario que el lector o lectora encuentre en este texto me lo comunique por email a libro.keras@gmail.com para mejorar su contenido. Muchas gracias de antemano.

En este capítulo 8 introducimos las redes conocidas como Generative Adversarial Network (GAN), que están generando un gran interés puesto que son las responsables en gran medida de las Deepfakes . Estoy seguro de que resultará interesante para el lector despedirse del libro con este tema tan actual. Aprovecharemos la explicación de las GAN para introducir otros tipos de capas de redes no vistas hasta ahora, o alternativas de programación a la usada hasta ahora, en el entorno TensorFlow 2.0. Creemos que es una buena oportunidad para abrir la curiosidad del lector a TensorFlow 2.0.

Generative Adversarial Networks

La manipulación de fotos, videos y edición de audio ha sido posible durante mucho tiempo, pero todos hemos observado que desde hace un tiempo hacia esta parte esto ha tomado una magnitud muy impresionante. Por ejemplo, investigadores de la Universidad de Washington[1]han creado una técnica de manipulación de video de alta cualidad que permite poner cualquier discurso en la boca del expresidente Barack Obama de manera que resulta absolutamente creible durante el visionado del video[2].

Motivación por las GAN

Con los recientes avances en los llamados modelos generativos ahora es posible generar videos con caras sintéticas extremadamente realistas, incluso en tiempo real[3]. Los modelos generativos no solo reproducen los datos en los que están entrenados (un acto de memorización poco interesante), sino más bien construyen un modelo de la clase subyacente de la que se extrajeron esos datos, y de esta manera pueden generar nuevas imágenes a partir de estos modelos.

Para las imágenes, uno de los modelos generativos más exitosos hasta el momento ha sido lo que se conoce como Generative Adversarial Network(GANs), donde dos redes neuronales, una “generadora” y otra “discriminadora”, participan en un concurso de discernimiento similar al de un falsificador artístico y un detective, por hacer un símil muy habitual. La red neuronal generadora produce imágenes con el objetivo de engañar a la red discriminadora para que crea que son reales. Mientras, la red discriminadora tiene por objetivo detectar las falsificaciones.

Las imágenes generadas por la red neuronal generadora, primero desordenadas y aleatorias, se van refinando a medida que va pasando el tiempo, y la dinámica contínua entre las dos redes neuronales conduce a que esta red generadora sintetice imágenes cada vez más realistas que, en muchos casos no son distinguibles de imagenes reales. El proceso llega a su equilibrio cuando la red neuronal discriminadora ya no puede distinguir entre imágenes reales e imágenes falsas. En este momento tenemos una red generadora de imágenes que es capaz de inventarse imágenes “reales”.

Le propongo al lector que mire las dos imágenes a continuación. ¿Puede decir cuál es una fotografía y cuál es una imagen sintética generada por un algoritmo basado en GAN?

Pues lo cierto es que ambas imágenes son falsificaciones generadas por algoritmos GAN creados por un grupo de investigación de NVDIA en el artículo A Style-Based Generator Architecture for Generative Adversarial Networks[4], que propone un nuevo modelo en la generación de caras basado en TensorFlow[5].

Aunque el descubrimiento de las GANs es reciente, la tecnología ha sido rápidamente adoptada en la generación y transferencia de imágenes con grandes avances siendo sus aplicaciones innumerables. Pero una preocupación generalizada en torno a los modelos generativos es su uso indebido en la generación de, por ejemplo, lo que se conoce como Deepfakes[6]. De hecho, distinguir entre el video original y el manipulado puede ser un desafío para los humanos e incluso para los propios computadores, especialmente cuando los videos están comprimidos o tienen baja resolución, como suele ocurrir en las redes sociales.

Ahora bien, ya se están realizando esfuerzos importantes para enfrentarse a estos desafíos en el mundo de la investigación, creando nuevas herramientas forenses automáticas basadas a su vez en GANs como FaceForensics[7]que generan grandes volúmenes de falsificaciones de caras y luego utilizan estas imágenes falsas como datos de entrenamiento para redes neuronales que hacen detección de imágenes falsa. O también con los mismos modelos GANs se pueden detectar medios sintéticos fraudulentos, por ejemplo, detectan discursos de audio fake[8].

Arquitectura de las GAN

Generative Adversarial Networks(GANs) son una clase de algoritmos dentro de la categoría de Deep Learningintroducidas por primera vez por Ian Goodfellow en 2014 en el artículo Generative Adversarial Networks[9], y han generado una gran expectación en la comunidad de investigación porque permiten generar nuevos datos sintéticos que resultan indistinguibles de datos reales.

La base de una GANs consiste en dos modelos de redes neuronales, denominados Generadory Discriminador. La función del Generador es generar instancias de datos a partir de “nada” (consideraremos el ruido aleatorio como entrada), mientras que el Discriminador comprueba si los datos aleatorios son consistentes con las instancias de datos reales, y devuelve probabilidades entre 0 y 1, donde 1 indica una predicción de realidad (real) y 0 indica una falsificación (fake). Esquematicamente se podría resumir visualmente de la siguiente manera:

Proceso de entrenamiento

El entrenamiento de estas redes consiste en un proceso iterativo en el que el Discriminador trata de distinguir los datos genuinos de las falsificaciones creadas por el Generador, mientras que el Generador convierte el ruido aleatorio que tiene de entrada en imitaciones lo más perfectas posibles de los datos, en un intento de engañar al Discriminador.

En el siguiente diagrama se muestra la arquitectura general que tiene una GAN mostrada en el anterior apartado, añadiendo al diagrama cómo el cálculo de la Lossde la clasificación interacciona entre las dos redes neuronales que intervienen:

Vemos que hay como entrada dos conjuntos de datos. Por un lado, los datos reales (1) que queremos que el Generador aprenda a emular lo mejor posible. Estos datos sirven de entrada a la red neuronal del Discriminador. Por otro lado (2), hay una entrada de datos aleatorios (ruido) que el Generador utiliza como punto de partida para sintetizar ejemplos (3). La red del Discriminador toma como entrada (4) un dato real o un ejemplo fakeproducido por el Generador, y determina la probabilidad (en una escala de 0 a 1) de que el ejemplo sea real. Finalmente, para cada una de las conjeturas del Discriminador se debe determinar cuán bueno fue, y usamos los resultados para ajustar iterativamente a través del mecanismo de backpropagationlos parámetros de la red del Discriminador (5) y de la red del Generador (6).

En el proceso de entrenamiento, los pesos y sesgos de la red del Discriminador se actualizan para maximizar su precisión de clasificación; es decir, maximizando la probabilidad de predecir que el “dato real” es “real” y el “dato fake” es fake. En concreto, el proceso de entrenamiento del Discriminador se podría resumir en ejecutar interativamente estos pasos (usaremos mini-batches):

  1. Coger un mini-batchde “datos reales” aleatorio del conjunto de datos de entrenamiento.
  2. Generar un mini-batchnuevo de vectores de ruido aleatorio, que se pasa como entrada al Generador y este sintetiza un mini-batchde “datos fake”.
  3. El Discriminador classifica tanto los “datos reales” como los “datos fake”.
  4. Se calculan los errores de clasificación y propaga la Losstotal al Discriminador para actualizar los pesos y sesgos de este a fin de minimizar los errores de clasificación.

Por otro lado, los pesos y sesgos del Generador se actualizan también iterativamente para maximizar la probabilidad de que el Discriminador clasifique incorrectamente, es decir, clasifique un “dato fake” como “real”. El proceso de entrenamiento del Generador se hace siguiendo los pasos anteriores, pero solo se fija en cómo ha clasificado el Discriminador losmini-batchesde “datos fake”. Con solo esta parte de datos, calcula el error de clasificación teniendo en cuenta que su objetivo es maximizar el error de clasificación del Discriminador (como hemos dicho, que clasifique como “real” un “dato fake”) y propagar la Losstotal sobre estos datos para actualizar los pesos y sesgos del Generador.

Es importante notar que la función de Lossde una red neuronal tradicional se define únicamente en términos de sus parámetros entrenables. Por el contrario, una GAN consta de dos redes cuyas funciones de Lossdependen de los parámetros de ambas redes, aunque cada red solo puede controlar los suyos propios.

Programando una GAN

En esta sección vamos a presentar los pasos para implementar una GAN que permite entrenar un Generador que sintetiza dígitos escritos a mano que parecen realesusando Deep Convolutional Generative Adversarial Networks(DCGAN)[10].

El código está parcialmente basado en el tutorial de la página web de TensorFlow[11], que además de usar la API Keras también aprovecha las utililidades avanzadas que proporciona el nuevo TensorFlow 2.0, requeridas para poder programar códigos complejos como el de este caso de estudio de GAN.

Algunas de estas nuevas prestaciones de TensorFlow 2.0 se escapan de lo que podríamos considerar contenido apropiado de un libro introductorio, pero hemos decidido incluir su uso porque estamos seguros de que va a ser muy interesante para el lector poderse hacer una idea por encima de estas nuevas posibilidades que nos abre TensorFlow 2.0.

Guiaremos al lector a través de todos los pasos que se realizan en el código, como se ha hecho habitualmente, y hemos puesto a su disposición el notebooken el GitHub del libro para que experimente él mismo con el código. Pero no entraremos en detalle en algunas partes del código, por ejemplo con las nuevas capas requeridas por estas redes neuronales que no se han presentado anteriormente en este libro, el modelo de programación Eager en el entorno TensorFlow 2.0 o la manera de especificar una programación más eficiente en tiempo de ejecución.

Para empezar y poder facilitar la explicación del código, avancemos el resultado que esperamos obtener con nuestro código. En la siguiente figura se muestra las capturas de las imágenes producidas por el Generador en diferentes epochsdurante su proceso de entrenamiento (se indica en qué número de epochse ha generado cada imagen). En concreto, en este ejemplo, se usa el Generador para generar 16 imágenes (en el código del GitHub correspondiente a este capítulo se generan 100 números para visualizar el aprendizaje, pero por espacio aquí lo hemos reducido) que se presenta en esta forma de malla de 4×4 para facilitar su visión. Podemos observar fácilmente que al inicio del entrenamiento, las imágenes aparecen como ruido aleatorio, y a medida que va avanzando el entrenamiento (va avanzando el número de epochs) los dígitos generados se parecen cada vez más a dígitos reales escritos a mano. El objetivo final en un caso real es que el Generador sintetize imágenes de 28×28 que se confundan como datos reales del conjunto MNIST (conjunto de imágenes que hemos cosiderado como “reales”).

Preparación del entorno y descarga de datos

Pasemos a codificar nuestro ejemplo comenzando por preparar el entono importando todos los paquetes y librerías necesarias para ejecutar el modelo propuesto:

Debemos descargar las imágenes que consideraremos “reales”. Lo podemos hacer a partir del conjunto de datos MNIST de dígitos escritos a mano directamente desde keras.datasetsy preparar las imagenes para ser usadas por las redes (los barajamos y preparamos en lotes):

En este caso solo nos interesan las imágenes y por tanto no descargamos ni las labelsni los datos de test. Podemos ver en el código que estas imágenes se han normalizado en el rango [-1, 1]

para poder usar como función de activación en la capa final del Generador la función tanhcomo veremos a continuación.

Creación de los models

A continuación ya podemos pasar a crear las redes neuronales que actuaran de Generador y Discriminador. Debido al carácter introductorio del libro no entraremos en detalle con las capas (algunas nuevas) de los modelos usados, pues para coger una idea global del funcionamiento de las GAN no hace falta.

Generador

Siguiendo el esquema que habíamos descrito, el Generador recibe como entrada ruido, que lo puede obtener por ejemplo con tf.random.normal, y de este ruido debe crear una imagen de 28×28 píxeles:

El generador que obtiene una imagen de estas características a partir del vector de ruido que hemos indicado podría ser[12]:

Podemos comprobar que funciona como pensábamos con el siguiente código en el que el Generador genera una instancia de datos fake:

Este modelo empieza con una capa densa que recoge el vector de ruido de entrada y lo transforma en un tensor tridimensional, que en las sucesivas capas lo va transformando hasta llegar a una salida de 28 × 28 × 1. Sin entrar en detalle, comentaremos los 3 tipos de capas que usa este modelo que no hemos definido anteriormente en este libro:

  • Conv2DTranspose[13]: es una capa que realiza una transformación en dirección opuesta a una convolución normal.
  • BatchNormalization[14]: sirve para acelerar la etapa de entrenamiento de una capa con normalización de los pesos en esta.
  • LeakyReLU[15]: una versión modificada de la ReLu que se utiliza en cada capa excepto en la última capa[16].

En la última capa se ha usado una función de activación tanh. La razón para usar tanh(en lugar de por ejemplo una sigmoide, que generaría valores en el rango más típico de 0 a 1) es que tanhtiende a producir imágenes más nítidas[17].

 

Discriminador

Por otra parte, el Discriminator recibe imágenes de 28 × 28 × 1 píxeles y saca una probabilidad que indica si esta imagen de entrada la considera real (en lugar de fake). Esta red se construye con dos capas convolucionales de 64 y 128 neuronas con función de activación LeakyReLU. La última capa es una capa densa con una función de activación sigmoide:

El Discriminador se usará para clasificar las imágenes generadas como reales o fake, generando valores próximos al 1 para imágenes que considera reales y próximas a 0 para imágenes que considera fake.Podemos comprobar que el Discriminador funciona como pensábamos con el siguiente código[18]:

 Funciones de Loss y Optimizadores

Con los modelos del Generador y Discriminador creados, el siguiente paso para entrenar las redes es establecer hiperparámetros de entrenamiento como la función de Lossy el optimizador que se usará para el proceso de backpropagation. En anteriores ejemplos de código esto se indicado con los argumentos del método compile()que luego se usan cuando se entrena el modelo al ejecutar el método fit()del modelo.

Pero aquí no lo haremos usando estos métodos, y aprovecharemos este ejemplo avanzado para mostrar al lector otras opciones que ofrece TensorFlow para especificar estos pasos. Por un lado, mostraremos cómo se puede especificar el entrenamiento de nuestra red en el modelo de ejecución Eager[19]que TensorFlow 2.0 tiene por defecto. Por otro lado, mostraremos cómo se puede acelerar, computacionalmente hablando, el entrenamiento de un modelo.

Funciones de Loss

En este ejemplo de GAN, para las funciones de Loss de ambas redes neuronales utilizaremos la binary cross entropy, que es una medida de la diferencia entre las probabilidades calculadas y las probabilidades reales de predicciones en los casos donde solo hay dos clases posibles en las que pueden ser clasificados los datos de entrada. En concreto, usaremos el método tf.keras.losses.BinaryCrossentropyque retorna una función auxiliar para calcular labinary cross entropy:

Con esta función auxiliar definiremos el método discriminator_loss()para cuantificar una Lossdel Discriminador que cuantifique cómo de bien el Discriminador consigue distinguir imágenes reales de imágenes fakeen una interación (para una imagen real y una imagen fake). Concretamente el método recibe en el primer argumento (real_output)la predicción que ha hecho el Discriminador de un batchde imagenes reales y en el segundo argumento (fake_output) la predicción que ha hechos de un batchuna imagens fake.

Si la predicción fuera la correcta, para una imagen real la predicción debería de ser 1 y para una imagen fakedebería de ser 0. Por ello este método compara el batchde imágenes reales predecidas por el Discriminador con un arrayde unos (tf.ones_like(real_output)) y el batchde imágenes fakecon un arrayde ceros (tf.zeros_like(real_output)). La Losspara esta iteración está compuesta tanto por los errores de predicción de imágenes reales como los errores de las imágenes fake. El siguiente código sintetiza este método:

La manera de construir el método generator_losspara cuantificar una Lossdel Generador será muy parecida. Pero en este caso la Lossdel Generador deberá quantificar qué tan bien fue capaz de engañar al Discriminador. Intuitivamente, si el Generador funciona bien, el Discriminador clasificará las imágenes fakecomo reales (es decir, como 1). Aquí, compararemos las decisiones del Discriminador sobre las imágenes generadas por el Generador con una matriz de 1s. El siguiente código sintetiza este método:

Optimizadores

Los optimizadores del Discriminador y del Generador son diferentes y requerimos dos, ya que entrenaremos dos redes por separado:

Para la optimización de ambas redes, utilizamos el algoritmo Adamque mencionamos en la sección 3.3. Se trata de un optimizador cuyo funcionamiento interno está fuera del alcance de este libro, pero es relevante indicar que Adamse ha convertido en el optimizador para la mayoría de las implementaciones de GANs porque se ha demostrado que en la práctica tiene un rendimiento superior a los otros en este tipo de redes.

Estrenamiento de las redes

Con todas las piezas en su lugar, ¡el modelo está listo para entrenar! Siguiendo el código en el GitHub correspondiente a este ejemplo, vemos que este consiste simplemente en llamar al método train()pasándole en los argumentos los datos de entrenamiento y el número de epochsque queremos ejecutar para entrenar simultaneamente el Generador y el Discriminador:

Este método ejecuta un bucle, de tantas iteraciones como epochs hemos indicado en el argumento, que básicamente llama al método train_step, que realiza todo el trabajo requerido para calcular las Lossy actualizar los parámetros de las redes, para todas las imágenes de un batch:

Pero en lector observará que el método contiene más código. El resto de código del bucle simplemente sirve para monitorizar visualmente el avance del entrenamiento, de tal manera que después de cada epochse muestra por pantalla cómo va el avance del entrenamiento del Generador:

Para facilitar la explicación, el lector solo debe saber que con generate_imagesse visualiza por pantalla datos generados por el Generador, y dejamos para un apartado posterior comentar en más detalle este método y las variables seed, grid_size_xy grid_size_y.

Por fin hemos llegado al método train_stepque se encuentra en el corazón del entrenamiento de este problema. Lo presentamos a continuación y aprovechamos el ejemplo de código para presentar por encima algunos de los temas que les avanzábamos sobre el modelo de programación Eager en el entorno TensorFlow 2.0 y una programación más eficiente en tiempo de ejecución:

Por un lado, en este código podemos ver el uso de una herramienta interesante que ya ofrece TensorFlow 2.0, el decorador (decorator)@tf.funtion[20]. Cuando se anota una función con @tf.function, esta es optimizada a nivel interno (compilada a nivel de grafo interno) para poder ser acelerada en el hardwaredisponible, aunque se puede continuar invocando desde cualquier parte del programa escrito en Python.

Recordemos que esta etapa de entrenamiento es la más costosa computacionalmente hablando y por ello es importante intentar conseguir que esta parte de código se ejecute lo más rápido posible. La aceleración de las aplicaciones es un tema estratégico en Deep Learningy es uno de los aspectos que preocupan en estos momentos a la comunidad que desarrolla TensorFlow 2.0, dado que el lenguaje Python en el que describimos nuestra aplicación es un lenguaje interpretado. Por ello, como avanzaremos en la clausura de esta SEGUNDA PARTE del libro, aparece en escena otro lenguaje de programación llamado Swift.

Por otro lado, vamos a ver que la manera de especificar el entrenamiento de la red es diferente de como lo hemos hecho hasta ahora. Veamos intuitivamente cómo se especifica con TensorFlow 2.0 el entrenamiento de nuestra red comentando brevemente las líneas del código de este método. Esperamos que de esta manera el lector pueda quedarse con la idea global que hay detrás.

Este método train_steprepresenta un paso del bucle de entrenamiento, y empieza con la creación de un conjunto de semillas de ruido con el que el Generador pueda generar las imágenes correspondientes (recordemos que estamos procesando las imágenes por batchesde tamaño BATCH_SIZE):

Con este vector de ruido el Generador genera un batchde imágenes fake:

El siguiente paso es usar el Discriminador para clasificar un batchde imágenes reales extraídas del conjunto MNIST (recibidas como argumento) y unbatchde imágenes fakeacabadas de producir por el Generador:

Aquí nos fijamos que el argumento training recibe el valor true, puesto que en este momento queremos entrenar estos modelos. Mientras que cuando se ha usado anteriormente el Generador para generar un ejemplo de imagen fakehemos instanciado el argumento a false. Sin entrar en detalle, indicar que se expresa de esta manera porque se está asumiendo que los modelos son de la clase tf.keras.Model[21]y con este argumento booleano se puede especificar si usamos el modelo para entrenamiento o para inferencia.

A continuación se calcula la Lossde ambos modelos con los métodos que hemos definido anteriormente:

Vemos que los anteriores pasos descritos se ejecutan dentro del contexto (context) de tf.GradientTape:

En el entorno de ejecución con Eager[22]TensorFlow propone usar un contexto con tf.GradientTape[23]en el proceso de cálculo de los gradientes usados en la optimización del modelo. Se trata de la API tf.GradientTape que facilita la diferenciación automática[24]a partir de calcular el gradiente de una operación con respecto a sus variables de entrada mediante el registro en una “cinta” conceptual (de aquí el nombre de tape) de todas las operaciones ejecutadas dentro de un determinado contexto tf.GradientTape.

Esto se realiza mediante el uso un with, en el que las operaciones ejecutadas en el contexto de tf.GradientTapeson grabadas en un objeto, y unas vez grabadas las operaciones, se aplica el método gradient()para obtener los gradientes[25]con respecto a la Loss, tanto para el Generador como para el Discriminador. Una vez tenemos los gradientes, con el método apply_gradients()de los optimizador definidos anteriormente, se aplican los gradientes a las variables.

Evaluación de los resultados

Finalmente nos queda comentar la visualización por pantalla que hace el método generate_images. Como hemos indicado en un apartado anterior, a cada epochse ha mostrado por pantalla cómo evolucionaban las predicciones que realizaba el Generador. Para ello usaremos este método que definimos a continuación:

Con este método se consigue mostrar por pantalla tantas predicciones como se le han indicado mediante las variables grid_size_xy grid_size_y que se han definido antes. Para garantizar que el Generador genera las mismas instancias cada vez, se pasa al Generador la misma semilla de ruido seeden cada una de las epochs:

En resumen, a medida que van avanzando las epochsde esta manera podemos ir viendo por pantalla cómo va mejorando el Generador. Al principio del entrenamiento, las imágenes generadas (100 en este código) parecen ruino aleatorio como se muestra en la siguiente figura:

Pero a medida que el proceso de entrenamiento va avanzando, los dígitos generados van pareciéndose cada vez más a dígitos del conjunto de datos MNIST. Por ejemplo en la epoch50 se muestra por pantalla estos resultados:

Aunque las imágenes que ha generado el Generador no son perfectas, muchas de ellas son fácilmente reconocibles como números reales. Si tenemos en cuenta que se han definido unas arquitecturas de red simples para ambas redes en este caso de estudio, los resultados los podemos considerar impresionantes, teniendo en cuenta que la red Generadora al final está generando estos números desde “nada” (ruido aleatorio).

Hasta aquí hemos mostrado un ejemplo sencillo de GANs con todo detalle que debe permitir al lector comprender el funcionamiento de estas redes que están generando tanto interés. Esperamos que le haya resultado interesante al lector finalizar el libro con este tema tan actual y el haber conocido de paso algunos de los aspectos de TensorFlow 2.0 que lo hacen realmente interesante.

Referencias del capítulo

[1]Véase http://grail.cs.washington.edu/projects/AudioToObama/siggraph17_obama.pdf[Accedido: 18/08/2019]

[2]Véase https://www.youtube.com/watch?time_continue=130&v=9Yq67CjDqvw [Accedido: 18/08/2019]

[3]Véase  https://nirkin.com/fsgan/  [Accedido: 26/08/2019]

[4]Véase http://openaccess.thecvf.com/content_CVPR_2019/papers/Karras_A_Style-Based_Generator_Architecture_for_Generative_Adversarial_Networks_CVPR_2019_paper.pdf[Accedido: 18/08/2019]

[5]  Véase https://github.com/NVlabs/stylegan [Accedido: 18/08/2019]

[6]Véase https://blog.witness.org/2018/07/deepfakes-and-solutions/ [Accedido: 18/08/2019]

[7]Véase https://arxiv.org/pdf/1803.09179.pdf [Accedido: 18/08/2019]

[8]Véase https://www.blog.google/outreach-initiatives/google-news-initiative/advancing-research-fake-audio-detection [Accedido: 18/08/2019]

[9]Véase https://arxiv.org/pdf/1406.2661.pdf [Accedido: 18/08/2019]

[10]Véasehttps://arxiv.org/pdf/1511.06434.pdf  [Accedido: 18/08/2019]

[11]Véasehttps://www.tensorflow.org/beta/tutorials/generative/dcgan  [Accedido: 18/08/2019]

[12]La estructura de las dos redes neuronales está inspirada en las propuestas en el libro GANs in Actionde J Langr y Vladimir Bok. Manning Publications. September 2019.  https://github.com/GANs-in-Action/gans-in-action[Accedido: 18/08/2019]

[13]Véasehttps://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/Conv2DTranspose  [Accedido: 18/08/2019]

[14]Véasehttps://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/

BatchNormalization [Accedido: 18/08/2019]

[15]Véasehttps://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/layers/LeakyReLU[Accedido: 18/08/2019]

[16]Las funciones de activación avanzadas en Keras, incluida LeakyReLU, están disponibles como capas y no como funciones de activación.

[17]Véasehttps://stats.stackexchange.com/questions/330559/why-is-tanh-almost-always-better-than-sigmoid-as-an-activation-function[Accedido: 18/08/2019]

[18]Recordemos, para interpretar el resultado, que el Discriminador aun no está entrenado.

[19]Véasehttps://www.tensorflow.org/beta/guide/eager[Accedido: 18/08/2019]

[20]Véasehttps://github.com/tensorflow/docs/blob/master/site/en/r2/guide/effective_tf2.md  [Accedido: 18/08/2019]

[21]Véase https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/Model [Accedido: 18/08/2019]

[22]Véase https://www.tensorflow.org/beta/guide/eager [Accedido: 18/08/2019]

[23]Véase https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/GradientTape [Accedido: 18/08/2019]

[24]Véase https://es.wikipedia.org/wiki/Diferenciación_automática  [Accedido: 18/08/2019]

[25]Por defecto, se realiza un seguimiento de las variables entrenables, aunque se puede realizar el seguimiento de otras variables usando el método watch().