Cómo crear una Red Generativa Antagónica (GAN) en Python

Share on linkedin
Share on twitter
Share on email

Las redes neuronales son muy potentes. En este blog ya las hemos programado una red neuronal desde cero en Python, y enseñado las redes neuronales convolucionales para clasificar imágenes. Hoy vamos a ir más allá. En este post vamos a aprender cómo programar una red generativa antagónica (GAN) en Python con la que crear imágenes que no existen. ¿Te suena interesante? ¡Pues vamos a ello!

Preparando nuestro script en Google Colab

Nota: si ya conoces Google Colab y sabes cómo trabajar en Colab sobre GPU y guardar/leer archivos de Drive en Colab, sáltate este apartado 😉

Como supongo que ya sabrás, Google Colab es un servicio freemium de Google para aprender data science. Básicamente permite ejecutar Jupyter Notebooks de Python en servidores de Google.

En este sentido, Google Colab ofrece varias ventajas de cara a entrenar nuestras redes neuronales, pero, sin duda alguna, lo mejor de Google Colab es más importante es la capacidad de trabajar sobre GPU de forma grauita. Y es que, el entrenamiento de Redes Neuronales es muchísimo más rápido sobre GPU que CPU, pero claro, no todos tenemos una GPU potente en nuestro ordenador…

Para poder habilitar la GPU en Colab debes:

1. Ir al apartado «Cambiar de Entorno de Ejecución» dentro de la sección Entorno de ejecución en el menú.

Cómo cambiar de entorno de ejecución a uno basado en GPU en Google Colab

2. Seleccionar GPU como acelerador por hardware.

Cómo elegir un entorno basado en GPU en Google Colab

Con esto, ya tendremos acceso a una GPU. Ahora solo tenemos que hacer que Tensorflow entienda que trabajamos sobre GPU para utilizarla.

Para ello, debemos ejectura el siguiente código:

import tensorflow as tf
device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU device not found')
print('Found GPU at: {}'.format(device_name))
Found GPU at: /device:GPU:0

Pues… ¡ya está! Todos los cálculos que pidamos a Tensorflow a partir de ahora se harán sobre GPU.

Por otro lado, recomendaría también conectarse con Google Drive para ir guardando en Drive los checkpoints y resultados (imágenes) de nuestro modelo.

Y es que, una de las desventajas de usar Google Colab es que al de cierto tiempo de inactividad te desconectan del servidor.

Claro, cuando dejemos entrenar a nuestra red, que llevará un tiempo, pues… seguramente nos vayamos a otro lado, por lo que sería una pena perder cualquier progreso que hagamos durante el entrenamiento, ¿no crees?

Para conectarse a Drive, simplemente basta con correr el siguiente código, entrar a la URL y pegar el código que nos devuelva Google.

from google.colab import drive
import os
drive.mount('/content/gdrive/')
Go to this URL in a browser: https://accounts.google.com/o/oauth2/auth?client_id=123456789123-6bn6qk8qdgf4n4g3pfee6491hc0brc4i.apps.googleusercontent.com&redirect_uri=urn%3aietf%3awg%3aoauth%3a2.0%3aoob&response_type=code&scope=email%20https%3a%2f%2fwww.googleapis.com%2fauth%2fdocs.test%20https%3a%2f%2fwww.googleapis.com%2fauth%2fdrive%20https%3a%2f%2fwww.googleapis.com%2fauth%2fdrive.photos.readonly%20https%3a%2f%2fwww.googleapis.com%2fauth%2fpeopleapi.readonly

Enter your authorization code:
··········
Mounted at /content/gdrive/

Además, yo selecciono una carpeta de Drive específica para guardar los resultados. Esto lo puedes hacer de la siguiente manera:

%cd /content/gdrive/My\ Drive/Red \Neuronal \Generativa \Antagonica
/content/gdrive/My Drive/Red Neuronal Generativa Antagonica

Con esto ya tendríamos nuestros entorno preparado, así ahora… ¡a crear una red generativa antagónica (GAN) en Python!

Generador/Discriminador como base de las redes generativas adversarias

Para conseguir crear una red neuronal que genere imágenes vamos a necesitar dos redes neuronales diferentes:

  • Red Generadora: se trata de una red neuronal que genera imágenes. En un principio, esta red generará ruido, así que la tendremos que ir entrenando poco a poco para que aprenda a generar imágenes.
  • Red Discriminadora: esta es una red que clasifica si una imagen es real o no. De esta manera, nos servirá como entrenadora de nuestra red neuronal generadora.

Para crear una red neuronal con esta estructura usaremos keras. Sin embargo, debido a las peculiaridades del caso, esta vez no será tan sencillo como indicar que el modelo es secuencial e ir añadiendo capas.

En su lugar, tendremos que ir haciendo cada paso por partes, de tal manera que más adelante podamos conectar las dos redes.

Al fin y al cabo, los resultados de la red discriminadora son usados por la red generadora para ajustar sus parámetros, como en el ejemplo de abajo:

Estructura de una red generativa antagónica (GAN)

Ahora bien, ¿cómo se programa esto? Vamos a verlo con el dataset cifar10, que contiene 50.000 imágenes de 10 tipos de objetos diferentes con un tamaño de 32×32 píxeles. Podéis ver más del dataset aquí.

En nuestro caso nos centraremos en las imágenes de aviones de ese dataset, pero el modelo serviría también para otros tipos de imágenes.

Dicho esto, ¿quieres aprender a programar una rede generativa antagonica en Python? ¡Pues vamos a ello!

Programando una red generativa adversaria

Programando la red generativa

Estructura de la red generativa

Lo primero para programar nuestra red generativa antagónica es saber qué estructura deben tener tanto el generador como el clasificador. Para la red generativa su punto de partida es un vector de ruido. Este vector lo vamos a transformar e ir agrandando hasta terminar con una imagen con las mismas dimensiones que nuestras imágenes reales, es decir, 32x32x3.

La idea de esta red es sencilla: partiendo de datos aleatorios (ruido) ir realizando convoluciones hasta generar una imagen. En un principio, estas imágenes no tendrán sentido, serán ruido y el discriminador dará un error elevado. Poco a poco, la red generativa irá mejorando, reduciendo su error a medida que genera imágenes más realistas.

Ahora que ya tenemos clara la estructura de esta red generativa, ¡vamos a programarla!

Programando la red generativa

Lo primero para programar nuestra red generativa es cargar las funciones que necesitaremos, que en este caso son:

  • Dense: será la capa de ruido de nuestro generador.
  • Conv2DTranspose: permite realizar convoluciones ‘hacia atrás’, es decir, agrandar (upscale) la imagen a medida que se convoluciona. Sería el equivalente de aplicar UpSampling2D seguido de Conv2D.
  • LeakyReLU: mejor que la función ReLU ya que LeakyRelu evita el gradient vanish. Paper explicativo sobre Cifar 100.
  • BatchNormalization: para normalizar las convoluciones y obtener resultados mejores y más rápidos. En mi caso lo he probado y lo descarté, ya que no mejoraba los resultados.
  • Reshape: para convertir un vector de una única dimensión en una imagen.

Además, hay que tener en cuenta que el shape del output de la red generativa dede ser igual que el shape de los datos reales. Para conseguirlo tenemos que ir agrandando el vector de ruido hasta conseguir una imagen del tamaño que la imagen de salida.

Para eso utilizamos la función Conv2DTranspose, pero… ¿Cómo sabeR qué tamaño vamos a obtener? Para eso usamos los strides.

La forma de entender Conv2DTranspose es como si quisiéramos hacer una convolución inversa, esto es, en vez de pasar de una imagen grande a una más pequeña, consiste en pasar de una imagen pequeña a una grande. Así pues, dados los parámetros, Conv2DTranspose calculará el tamaño de la imagen de «destino».

Así pues, los strides se refieren a cuánto se desplaza el kernel para hacer la convolución. Si, por ejemplo, tenemos una imagen de 18×18 y aplicamos una convolución (normal) con un kernel de 3 y con un stride de 3 el resultado final será una imagen de 6×6 (18/3 x 18/3).

Inversamente, para agrandar la imagen mediante Conv2DTranspose, si a un input de 6×6 pasamos un strides de 3, el resultado será una imagen de 18×18 (suponiendo un padding que mantenga proporciones).

Por otro lado, en la última capa usaremos la función tangente. De esta manera nos aseguramos que nuestros valores vayan de -1 a 1. ¿Por qué? Pues… prueba y error. Probé la función sigmoide pero la tangente funciona mejor.

Por último, cabe destacar que en este apartado no definiremos el método de aprendizaje del modelo (funciones de coste y optimización). Esto es debido a que el modelo generador no se entrena con el resultado de este modelo en sí, sino con el resultado de la red discriminadora.

import keras
from keras.layers import Dense, Conv2DTranspose, LeakyReLU, Reshape, BatchNormalization, Activation, Conv2D
from keras.models import Model, Sequential


def generador_de_imagenes():

    generador = Sequential()

    generador.add(Dense(256*4*4, input_shape = (100,)))
    #generador.add(BatchNormalization())
    generador.add(LeakyReLU())
    generador.add(Reshape((4,4,256)))

    generador.add(Conv2DTranspose(128,kernel_size=3, strides=2, padding = "same"))
    #generador.add(BatchNormalization())
    generador.add(LeakyReLU(alpha=0.2))


    generador.add(Conv2DTranspose(128,kernel_size=3, strides=2, padding = "same"))
    #generador.add(BatchNormalization())
    generador.add(LeakyReLU(alpha=0.2))

    generador.add(Conv2DTranspose(128,kernel_size=3, strides=2, padding = "same"))
    #generador.add(BatchNormalization())
    generador.add(LeakyReLU(alpha=0.2))

    generador.add(Conv2D(3,kernel_size=3, padding = "same", activation='tanh'))

    return(generador)

modelo_generador = generador_de_imagenes()

modelo_generador.summary()
Using TensorFlow backend.
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 4096)              413696    
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 4096)              0         
_________________________________________________________________
reshape_1 (Reshape)          (None, 4, 4, 256)         0         
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 8, 8, 128)         295040    
_________________________________________________________________
leaky_re_lu_2 (LeakyReLU)    (None, 8, 8, 128)         0         
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 16, 16, 128)       147584    
_________________________________________________________________
leaky_re_lu_3 (LeakyReLU)    (None, 16, 16, 128)       0         
_________________________________________________________________
conv2d_transpose_3 (Conv2DTr (None, 32, 32, 128)       147584    
_________________________________________________________________
leaky_re_lu_4 (LeakyReLU)    (None, 32, 32, 128)       0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 32, 32, 3)         3459      
=================================================================
Total params: 1,007,363
Trainable params: 1,007,363
Non-trainable params: 0
_________________________________________________________________

Con esto ya tendríamos nuestra red neuronal generativa terminada. Ahora, vamos a ver que está funcionando correctamente. Para ello vamos a definir unos datos de entrada aleatorios y ver qué nos devuelve la red (seguramente ruido).

import matplotlib.pyplot as plt
import numpy as np

# Definir datos de entrada
def generar_datos_entrada(n_muestras):
  X = np.random.randn(100 * n_muestras)
  X = X.reshape(n_muestras, 100)
  return X

def crear_datos_fake(modelo_generador, n_muestras):
  input = generar_datos_entrada(n_muestras)
  X = modelo_generador.predict(input)
  y = np.zeros((n_muestras, 1))
  return X,y

numero_muestras = 4
X,_ = crear_datos_fake(modelo_generador, numero_muestras)

# Visualizamos resultados
for i in range(numero_muestras):
    plt.subplot(2, 2, 1 + i)
    plt.axis('off')
    plt.imshow(X[i])
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Imágenes generadas mediante la red generadora de imágenes de una red generativa antagónica (GAN) sin entrenar

¡Funciona! Aunque, como ves, de momento lo único que genera es ruido… Pero bueno, ya iremos mejorando eso. En cualquier caso, primer paso para crear nuestra red generativa adversaria en Python hecho. Ahora vayamos con la segunda parte, ¡vamos a crear la red discriminadora!

Programando la red discriminadora

Estructura de la red discriminadora

Nuestra red discriminadora es una red convolucional normal, que como valor de entrada toma una imagen y devuelve un único valor.

Aunque suele ser recomendable incluir una capa de dropout después de cada convolución para evitar el overttifiting (paper), en nuestro caso únicamente lo hemos dejado en la última capa, ya que tras muchas pruebas vi que no mejora mucho el rendimiento del modelo.

Por otro lado, en la última en este caso aplicaremos la función de activación sigmoide. Como comenté en este post, al querer clasificar entre dos clases, podemos entender el resultado de la función sigmoide como la probabilidad de pertenencia a una clase.

En lo que a optimizador respecta, usaremos el optimizador Adam, ya que funciona especialmente bien en datasets grandes y suele dar mejor resultados que Gradient Descen paper.

Además, aunque generalmente el learning rate y betas por defecto de la función Adam suelen funcionar bastante bien, en este caso seguí las recomendaciones de este otro paper, donde se indicaba que los mejores parámetros (en su caso) eran un learning rate de 0,0002 y una beta de 0,5.

from keras.layers import Conv2D, Flatten, Dropout
from keras.optimizers import Adam

def discriminador_de_imagenes():

    discriminador = Sequential()
    discriminador.add(Conv2D(64, kernel_size=3, padding = "same", input_shape = (32,32,3)))
    discriminador.add(LeakyReLU(alpha=0.2))
    #discriminador.add(Dropout(0.2))

    discriminador.add(Conv2D(128, kernel_size=3,strides=(2,2), padding = "same"))
    discriminador.add(LeakyReLU(alpha=0.2))
    #discriminador.add(Dropout(0.2))

    discriminador.add(Conv2D(128, kernel_size=3,strides=(2,2), padding = "same"))
    discriminador.add(LeakyReLU(alpha=0.2))
    #discriminador.add(Dropout(0.2))

    discriminador.add(Conv2D(256, kernel_size=3, strides=(2,2), padding = "same"))
    discriminador.add(LeakyReLU(alpha=0.2))
    #discriminador.add(Dropout(0.2))

    discriminador.add(Flatten())
    discriminador.add(Dropout(0.4))
    discriminador.add(Dense(1, activation='sigmoid'))

    opt = Adam(lr=0.0002 ,beta_1=0.5)
    discriminador.compile(loss='binary_crossentropy', optimizer= opt , metrics = ['accuracy'])

    return(discriminador)

modelo_discriminador = discriminador_de_imagenes()
modelo_discriminador.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_2 (Conv2D)            (None, 32, 32, 64)        1792      
_________________________________________________________________
leaky_re_lu_5 (LeakyReLU)    (None, 32, 32, 64)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 16, 16, 128)       73856     
_________________________________________________________________
leaky_re_lu_6 (LeakyReLU)    (None, 16, 16, 128)       0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 8, 8, 128)         147584    
_________________________________________________________________
leaky_re_lu_7 (LeakyReLU)    (None, 8, 8, 128)         0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 4, 4, 256)         295168    
_________________________________________________________________
leaky_re_lu_8 (LeakyReLU)    (None, 4, 4, 256)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 4096)              0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 4096)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 4097      
=================================================================
Total params: 522,497
Trainable params: 522,497
Non-trainable params: 0
_________________________________________________________________

Con esto ya tendríamos la estructura de nuestra red. Sin embargo faltan aún varios pasos importantes:

  • Crear nuestra GAN: de momento tenemos las dos redes que la componen, pero son dos piezas inconexas. Esta función nos permitirá conectar ambas partes.
  • Definir la función de entrenamiento: para que nuestras redes generativas antagónicas vayan aprendiendo. Será en este paso donde implementaremos el siguiente punto.
  • Guardar los resultados de nuestra red, tanto los pesos del modelo como las imágenes que genera. Y es que, que nuestra red entrene está muy bien, pero… ¿qué sentido tiene si no podemos visualizar cómo ha ido mejorando o si perdemos todo su progreso? Con esta función nos encargaremos de que eso no ocurra.

Como ves, todavía hay tarea, así que, ¡vamos con ello!

Últimos pasos para crear nuestra red generativa Antagónica (GAN) en Python

Cargar Datos de Cifar10

Para entrenar nuestra GAN, primero debemos cargar los datos de Cifar10.

Además, para que sea más fácil entrenar el modelo, vamos a normalizar los datos, de tal forma que vayan de -1 a 1. Como los datos de una capa RGB van de 0 a 255, simplemente vamos a restar la mediana (127,5) y dividir sobre el mismo valor. Así, el valor máximo quedará como 1 y el mínimo como -1.

Como comentamos en el post de cómo crear una red neuronal desde 0 en Python, normalizar los datos facilitará el aprendizaje del modelo.

from keras.datasets import cifar10

def cargar_imagenes():
    (Xtrain, Ytrain), (_, _) = cifar10.load_data()

    # Nos quedamos con los perros
    indice = np.where(Ytrain == 0)
    indice = indice[0]
    Xtrain = Xtrain[indice, :,:,:]

    # Normalizamos los datos
    X = Xtrain.astype('float32')
    X = (X - 127.5) / 127.5

    return X

print(cargar_imagenes().shape)
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170500096/170498071 [==============================] - 6s 0us/step
(5000, 32, 32, 3)

Entrenando a la red discriminadora

Ahora que ya tenemos nuestras imágenes cargadas, vamos a entrenar el discriminador. Para ello, vamos a necesitar tanto imágenes reales como imágenes ficticias, por lo que crearemos un par de funciones que no generen eso mismo.

De cara a crear imágenes ficticias es importante tener en cuenta el la forma (shape) de nuestras imágenes. Como hemos visto en el punto anterior, las imágenes son de 32x32x3, por lo que tendremos que generar n imágenes con esas mismas dimensiones.

Además, vamos a aprovechar para que estas funciones nos devuelvan la etiqueta de nuestros datos. El discriminador va a predecir la probabilidad de que una imagen sea real.

Aunque encontré algunos sitios donde indicaban que es mejor dar el valor 0 a las imágenes falsas (post), tras probar vi que no generaba grandes avances.

Tras probar ambas formas, efectivamente comprobé como invertir las etiquetas funciona mejor a la hora de generar imágenes, por lo que lo dejé así.

import random

def cargar_datos_reales(dataset, n_muestras):
  ix = np.random.randint(0, dataset.shape[0], n_muestras)
  X = dataset[ix]
  y = np.ones((n_muestras, 1))
  return X,y

def cargar_datos_fake(n_muestras):
  X = np.random.rand(32 * 32 * 3 * n_muestras)
  X = -1 + X * 2
  X = X.reshape((n_muestras, 32,32,3))
  y = np.zeros((n_muestras, 1))
  return X,y

Ahora que ya tenemos nuestros generadores de imágenes reales y ficticias, vamos a usarlos para entrenar a nuestro discriminador. Es importante pre-entrenar el discriminador, ya que cuando implementemos nuestra GAN, el clasificador no lo vamos a entrenar. Pero claro, si queremos que funcione bien y luego no lo entrenamos… pues tendremos que entrenarlo ahora.

Para comprobar que se ha entrenado bien, vamos a pedir que nos devuelva el accuracy tanto para datos reales como fake. Con eso es suficiente, ya que saber que la red discriminadora ha entrenado es lo único que nos importa.

De cara al entrenamiento, pasaremos la mitad de los datos fake y la otra mitad de los datos reales. De ahí que calculamos el medio_batch. :

def entrenar_discriminador(modelo, dataset, n_iteraciones=20, batch = 128):
  medio_batch = int(batch/2)

  for i in range(n_iteraciones):
    X_real, y_real = cargar_datos_reales(dataset, medio_batch)
    _, acc_real = modelo.train_on_batch(X_real, y_real)

    X_fake, y_fake = cargar_datos_fake(medio_batch)
    _, acc_fake = modelo.train_on_batch(X_fake, y_fake)

    print(str(i+1) + ' Real:' + str(acc_real*100) + ', Fake:' + str(acc_fake*100))

Y, ahora que ya tenemos todas nuestras piezas, simplemente tenemos que encajarlas para entrenar a nuestra red discriminadora:

dataset = cargar_imagenes()
entrenar_discriminador(modelo_discriminador, dataset)
1 Real:78.125, Fake:1.5625
2 Real:96.875, Fake:1.5625
3 Real:95.3125, Fake:26.5625
4 Real:98.4375, Fake:56.25
5 Real:95.3125, Fake:96.875
6 Real:85.9375, Fake:100.0
7 Real:92.1875, Fake:100.0
8 Real:75.0, Fake:100.0
9 Real:78.125, Fake:100.0
10 Real:84.375, Fake:100.0
11 Real:93.75, Fake:100.0
12 Real:92.1875, Fake:100.0
13 Real:98.4375, Fake:100.0
14 Real:100.0, Fake:100.0
15 Real:98.4375, Fake:100.0
16 Real:95.3125, Fake:100.0
17 Real:100.0, Fake:100.0
18 Real:100.0, Fake:100.0
19 Real:100.0, Fake:100.0
20 Real:98.4375, Fake:100.0

¡Red discriminadora entrenada! ¿Fácil, verdad? Ahora que ya tenemos tanto la red generadora como la red discriminadora montadas y, en este segundo caso, entrenada, ¡vamos a crear nuestra red generativa antagónica (GAN) con Python!

Creando nuestra red generativa adversaria

Ya tenemos todas las piezas de nuestra red generativa adversaria. Por un lado, un clasificador de imágenes ya entrenado que distingue entre imágenes reales y fake y por el otro lado un generador de imágenes. Aunque este último de momento no aprende.

Ahora tenemos que juntar estas dos piezas de tal manera que dados unos datos de entrada el modelo nos devuelva la probabilidad de ser real y que la red generativa se vaya entrenando.

Para evitar que el modelo discriminador se entrene, fijaremos el parámetro trainable a FALSE.

Asimismo, la función de coste será binary_crossentropy, ya que vamos a clasificar las imágenes en dos tipos: fake (0) y reales (1).

def crear_gan(discriminador, generador):
    discriminador.trainable=False
    gan = Sequential()
    gan.add(generador)
    gan.add(discriminador)

    opt = Adam(lr=0.0002,beta_1=0.5) 
    gan.compile(loss = "binary_crossentropy", optimizer = opt)

    return gan

gan = crear_gan(modelo_discriminador,modelo_generador)
gan.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
sequential_1 (Sequential)    (None, 32, 32, 3)         1007363   
_________________________________________________________________
sequential_2 (Sequential)    (None, 1)                 522497    
=================================================================
Total params: 1,529,860
Trainable params: 1,007,363
Non-trainable params: 522,497
_________________________________________________________________

¡Ya tenemos nuestra red generativa antagónica (GAN) creada con Python! Ahora solo queda definir el loop de entrenamiento de la red y algunas cosas adicionales que queramos ir viendo durante el entrenamiento, tales como la mejora en el acurracy o las propias imágenes que se van generando.

Para esto, crearé un par de funciones: una para calcular el error del modelo e ir guardando el mismo y generad y guardar los resultados del modelo.

Funciones de evaluación del modelo y generación de imágenes

Empecemos por la función que muestra los resultados del modelo. Para ello, dado un array de datos predicho, voy a guardar 10 imágenes generadas. De esta forma podemos ir viendo cómo va funcionando y he podido también crear el GIF de portada de este post.

import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from datetime import datetime

def mostrar_imagenes_generadas(datos_fake, epoch):

  now = datetime.now()
  now = now.strftime("%Y%m%d_%H%M%S")

  # Hacemos que los datos vayan de 0 a 1
  datos_fake = (datos_fake + 1) / 2.0

  for i in range(10):
    plt.imshow(datos_fake[i])
    plt.axis('off')
    nombre = str(epoch) + '_imagen_generada_' + str(i) + '.png'
    plt.savefig(nombre, bbox_inches='tight')
    plt.close()

Es importante también ir guardando el modelo generador a medida que se va entrenando. Google Colab te puede terminar sesión, por lo que, si no vamos guardando checkpoints, podemos perder todo el progreso.

Por otro lado, vamos a evaluar el rendimiento del modelo clasificador. Para ello, generaremos nuevos datos con los que evaluar el modelo. ¿Por qué hacemos esto y no usar los datos que ya hemos creado en ese batch de entrenamiento? Pues… porque creo es mejor entrenar sobre datos nuevos que hacerlo sobre datos que el modelo justo acaba de ver.

Por último, ya que en esta función nuestra red genera nuevas imágenes, incluiremos la función de visualización de imágenes dentro de la misma.

def evaluar_y_guardar(modelo_generador, epoch, medio_dataset):

  # Guardamos el modelo
  now = datetime.now()
  now = now.strftime("%Y%m%d_%H%M%S")
  nombre = str(epoch) + '_' + str(now)+"_modelo_generador_" + '.h5'
  modelo_generador.save(nombre)

  # Generamos nuevos datos
  X_real,Y_real = cargar_datos_reales(dataset, medio_dataset)
  X_fake, Y_fake =  crear_datos_fake(modelo_generador,medio_dataset)

  # Evaluamos el modelo
  _, acc_real = modelo_discriminador.evaluate(X_real, Y_real)
  _, acc_fake = modelo_discriminador.evaluate(X_fake, Y_fake)

  print('Acc Real:' + str(acc_real*100) + '% Acc Fake:' + str(acc_fake*100)+'%')

Ahora que ya tenemos tanto la función que visualiza los resultados como el que los va generando y calculando el acurracy, vamos a definir la función de entranamiento:

def entrenamiento(datos, modelo_generador, modelo_discriminador, epochs, n_batch, inicio = 0):
  dimension_batch = int(datos.shape[0]/n_batch)
  medio_dataset = int(n_batch/2)

  # Iteramos para todos los epochs
  for epoch in range(inicio, inicio + epochs):
    # Iteramos para todos los batches
    for batch in range(n_batch):

      # Cargamos datos reales
      X_real,Y_real = cargar_datos_reales(dataset, medio_dataset)


      # Enrenamos discriminador con datos reales
      coste_discriminador_real, _ = modelo_discriminador.train_on_batch(X_real, Y_real)
      X_fake, Y_fake =  crear_datos_fake(modelo_generador,medio_dataset)

      coste_discriminador_fake, _ = modelo_discriminador.train_on_batch(X_fake, Y_fake)

      # Generamos datos de entadas de la GAN
      X_gan = generar_datos_entrada(medio_dataset)
      Y_gan = np.ones((medio_dataset, 1))

      # Entrenamos la GAN con datos falsos
      coste_gan = gan.train_on_batch(X_gan, Y_gan)

    # Cada 10 Epochs mostramos resultados y el coste
    if (epoch+1) % 10 == 0:
      evaluar_y_guardar(modelo_generador,epoch = epoch, medio_dataset= medio_dataset)
      mostrar_imagenes_generadas(X_fake, epoch = epoch)

Con esto, vamos a comprobar si nuestra GAN funciona. ¡Veamos!

entrenamiento(dataset, modelo_generador, modelo_discriminador, epochs = 300, n_batch=128, inicio = 0)
/usr/local/lib/python3.6/dist-packages/keras/engine/training.py:297: UserWarning: Discrepancy between trainable weights and collected trainable weights, did you set `model.trainable` without calling `model.compile` after ?
  'Discrepancy between trainable weights and collected trainable'
64/64 [==============================] - 0s 2ms/step
64/64 [==============================] - 0s 225us/step
Acc Real:79.6875% Acc Fake:84.375%
64/64 [==============================] - 0s 224us/step
64/64 [==============================] - 0s 199us/step
Acc Real:65.625% Acc Fake:95.3125%
64/64 [==============================] - 0s 247us/step
64/64 [==============================] - 0s 207us/step
Acc Real:67.1875% Acc Fake:87.5%
64/64 [==============================] - 0s 220us/step
64/64 [==============================] - 0s 200us/step
Acc Real:76.5625% Acc Fake:78.125%
64/64 [==============================] - 0s 239us/step
64/64 [==============================] - 0s 187us/step
Acc Real:75.0% Acc Fake:78.125%
64/64 [==============================] - 0s 230us/step
64/64 [==============================] - 0s 195us/step
Acc Real:70.3125% Acc Fake:78.125%
64/64 [==============================] - 0s 208us/step
64/64 [==============================] - 0s 191us/step
Acc Real:73.4375% Acc Fake:90.625%
64/64 [==============================] - 0s 216us/step
64/64 [==============================] - 0s 193us/step
Acc Real:71.875% Acc Fake:87.5%
64/64 [==============================] - 0s 247us/step
64/64 [==============================] - 0s 186us/step
Acc Real:76.5625% Acc Fake:89.0625%
64/64 [==============================] - 0s 236us/step
64/64 [==============================] - 0s 195us/step
Acc Real:78.125% Acc Fake:90.625%
64/64 [==============================] - 0s 256us/step
64/64 [==============================] - 0s 202us/step
Acc Real:84.375% Acc Fake:89.0625%
64/64 [==============================] - 0s 218us/step
64/64 [==============================] - 0s 199us/step
Acc Real:85.9375% Acc Fake:96.875%
64/64 [==============================] - 0s 233us/step
64/64 [==============================] - 0s 198us/step
Acc Real:90.625% Acc Fake:96.875%
64/64 [==============================] - 0s 258us/step
64/64 [==============================] - 0s 206us/step
Acc Real:90.625% Acc Fake:93.75%
64/64 [==============================] - 0s 277us/step
64/64 [==============================] - 0s 230us/step
Acc Real:95.3125% Acc Fake:98.4375%
64/64 [==============================] - 0s 251us/step
64/64 [==============================] - 0s 194us/step
Acc Real:98.4375% Acc Fake:96.875%
64/64 [==============================] - 0s 236us/step
64/64 [==============================] - 0s 202us/step
Acc Real:98.4375% Acc Fake:96.875%
64/64 [==============================] - 0s 225us/step
64/64 [==============================] - 0s 197us/step
Acc Real:96.875% Acc Fake:93.75%
64/64 [==============================] - 0s 246us/step
64/64 [==============================] - 0s 203us/step
Acc Real:96.875% Acc Fake:98.4375%
64/64 [==============================] - 0s 238us/step
64/64 [==============================] - 0s 233us/step
Acc Real:98.4375% Acc Fake:100.0%
64/64 [==============================] - 0s 262us/step
64/64 [==============================] - 0s 208us/step
Acc Real:95.3125% Acc Fake:100.0%
64/64 [==============================] - 0s 310us/step
64/64 [==============================] - 0s 259us/step
Acc Real:98.4375% Acc Fake:95.3125%
64/64 [==============================] - 0s 275us/step
64/64 [==============================] - 0s 205us/step
Acc Real:98.4375% Acc Fake:98.4375%
64/64 [==============================] - 0s 268us/step
64/64 [==============================] - 0s 228us/step
Acc Real:96.875% Acc Fake:98.4375%
64/64 [==============================] - 0s 227us/step
64/64 [==============================] - 0s 191us/step
Acc Real:100.0% Acc Fake:96.875%
64/64 [==============================] - 0s 276us/step
64/64 [==============================] - 0s 212us/step
Acc Real:100.0% Acc Fake:98.4375%
64/64 [==============================] - 0s 220us/step
64/64 [==============================] - 0s 200us/step
Acc Real:100.0% Acc Fake:100.0%
64/64 [==============================] - 0s 260us/step
64/64 [==============================] - 0s 217us/step
Acc Real:100.0% Acc Fake:95.3125%
64/64 [==============================] - 0s 207us/step
64/64 [==============================] - 0s 207us/step
Acc Real:100.0% Acc Fake:98.4375%
64/64 [==============================] - 0s 258us/step
64/64 [==============================] - 0s 207us/step
Acc Real:96.875% Acc Fake:98.4375%

¡Ya está! ¡Hemos aprendido a crear una GAN en Python y la hemos entrenado! Vamos a ver qué imágenes de aviones nos genera:

X_fake, _ = crear_datos_fake(n_muestras=49, modelo_generador=modelo_generador)
X_fake = (X_fake+1)/2

for i in range(49):
  plt.subplot(7,7,i+1)
  plt.axis('off')
  plt.imshow(X_fake[i])
Imágenes de aviones generadas mediante una red generativa antagónica (GAN) hecha en Python

¡Funciona! Aunque en baja resolución (no dejan de ser imágenes de 32×32 píxeles), sí que nuestra red neuronal ha conseguido generar imágenes que parezcan aviones. Sí, es cierto que no todas las imágenes parecen aviones. Seguramente si siguiéramos entrenando el modelo seguiríamos mejorando nuestras predicciones.

En cualquier caso, ya has aprendido a generar imágenes que no existen mediante redes neuronales antagónicas con Python.

Aunque en el post haya parecido sencillo, la verdad es que ha sido un proceso bastante largo, con mucha prueba/error con cambios en hiperparámetros, capas, etc.

Por si te gustaría probar a crear una GAN en Python más allá del ejemplo de este post, en la siguiente sección te doy unos consejos.

3 Consejos para crear una red generativa antagónica (GAN) en Python

1. Genera un único tipo de imagen

En un principio intenté crear una GAN que generara imágenes del dataset CIFAR10. Claro, para ello la red tenía que aprender a generar 10 tipos de imágenes diferentes… En definitiva, era algo mucho más complejo.

Por tanto, te recomendaría entrenar una GAN sobre una única cosa. Perros, aviones, pinturas, caras de famosos… sea lo que sea, que sea de una única «etiqueta».

2. Prueba y falla rápido

Al principio dejaba entrenar los modelos por 100 o 200 epochs antes de ver cómo funcionaban. Sin duda, esto enlenteció mucho el proceso de creación de la GAN, ya que entre modelo y modelo pasaban entre 10 y 20 minutos.

Por eso, para hacer las pruebas te recomendaría probar con unos solos epochs (20 o 30), para ver cómo funciona la red. Si ves que el cambio que has implementado no mejora… para el proceso de entrenamiento y prueba con otro cambio.

3. Identificar la métrica para evaluar tu modelo

Cuando entrenamos una red neuronal normal, tenemos claro el indicador que queremos «visualizar» para saber si nuestra red neuronal está mejorando o no. Pero… ¿y en un GAN?

En este post encontré los valores que debían tener los diferentes indicadores del aprendizaje de la red. En mi caso, me quedé con el accuracy, centrándome sobre todo en el accuracy para imágenes ficticias.

4. Si se acaba la sesión… carga tu modelo

Como he explicado antes, una de las claves (sobre todo si trabajas en Colab) es ir guardando tu modelo generador. De esta forma, si se te acaba la sesión, siempre podrás cargar el último modelo entrenado y así ahorrarte tener que volver a entrenarlo.

Cargar un modelo es muy sencillo. Para hacerlo simplemen hay que:

  1. Situarse en la carpeta de Drive correspondiente.
  2. Cargar el modelo mediante la función load_model.

Os pongo un ejemplo:

# 1. Nos situamos la carpeta de Drive 
from google.colab import drive
import os
drive.mount('/content/gdrive/')
%cd /content/gdrive/My\ Drive/Red \Neuronal \Generativa \Antagonica

# 2. Importamos el modelo
from keras.models import load_model

modelo_generador = load_model('299_20200712_104051_modelo_generador_.h5')
/usr/local/lib/python3.6/dist-packages/keras/engine/saving.py:341: UserWarning: No training configuration found in save file: the model was *not* compiled. Compile it manually.
  warnings.warn('No training configuration found in save file: '

¡Espero que te te haya gustado este tutorial de cómo crear una GAN en Python! Cualquier duda o sugerencia, escríbeme en el formulario de la página o contáctame por Linkedin. ¡Nos vemos en la siguiente!