Menu iconMenu icon
Superhéroe de Aprendizaje Profundo e IA

Capítulo 9: Proyectos Prácticos

9.5 Proyecto 5: Generación de imágenes basada en GAN

Las Redes Generativas Antagónicas (GAN) han marcado una nueva era en el ámbito de la generación de imágenes, revolucionando el campo con su enfoque innovador. Este ambicioso proyecto busca elevar la implementación original de GAN, específicamente adaptada para generar dígitos manuscritos a partir del ampliamente utilizado conjunto de datos MNIST.

Nuestro objetivo principal es incorporar una serie de mejoras de vanguardia diseñadas para aumentar significativamente el rendimiento general, mejorar la estabilidad del entrenamiento y elevar la calidad de las imágenes generadas a niveles sin precedentes.

Al aprovechar técnicas de última generación y mejoras arquitectónicas, buscamos superar los límites de lo que es posible con GAN. Estas mejoras no solo abordarán los desafíos comunes asociados con el entrenamiento de GAN, como el colapso de modo y los problemas de convergencia, sino que también introducirán características novedosas que prometen generar resultados más realistas y diversos.

A través de este proyecto, anticipamos demostrar todo el potencial de las GAN en la creación de imágenes de dígitos manuscritos de alta fidelidad que son prácticamente indistinguibles de sus contrapartes reales.

9.5.1 Arquitectura GAN mejorada

Para mejorar el rendimiento general y la capacidad de nuestra GAN, implementaremos una arquitectura más compleja y escalonada tanto para los componentes del generador como del discriminador. Esta estructura avanzada incorporará capas convolucionales adicionales, conexiones de salto y técnicas de normalización para mejorar la capacidad de la red para aprender características complejas y generar imágenes de alta calidad. Al aumentar la profundidad y la sofisticación de nuestros modelos, esperamos capturar patrones más detallados en los datos y producir imágenes de dígitos manuscritos más realistas y detalladas.

import tensorflow as tf
from tensorflow.keras import layers, models

def build_generator(latent_dim):
    model = models.Sequential([
        layers.Dense(7*7*256, use_bias=False, input_shape=(latent_dim,)),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        layers.Reshape((7, 7, 256)),
        
        layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')
    ])
    return model

def build_discriminator():
    model = models.Sequential([
        layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Flatten(),
        layers.Dense(1)
    ])
    return model

generator = build_generator(latent_dim=100)
discriminator = build_discriminator()

Desglose:

  1. Generador:
    • Toma un vector latente (ruido) como entrada.
    • Utiliza convoluciones transpuestas para aumentar la resolución de la entrada a una imagen de 28x28.
    • Incorpora normalización por lotes (batch normalization) y activaciones LeakyReLU para estabilidad y no linealidad.
    • La capa final utiliza activación tanh para producir una salida similar a una imagen.
  2. Discriminador:
    • Toma una imagen de 28x28 como entrada.
    • Utiliza capas convolucionales para reducir la resolución de la entrada.
    • Incorpora activaciones LeakyReLU y dropout para regularización.
    • La capa densa final genera un solo valor, que representa la probabilidad de que la entrada sea real.

La arquitectura está diseñada para generar y discriminar imágenes en escala de grises de 28x28, lo que se alinea con el formato del conjunto de datos MNIST. El uso de normalización por lotes, LeakyReLU y dropout ayuda a estabilizar el proceso de entrenamiento y a prevenir problemas como el colapso de modo.

9.5.2 Pérdida Wasserstein con penalización de gradiente

Para mejorar la estabilidad del entrenamiento y mitigar el colapso de modo, implementaremos la función de pérdida Wasserstein con penalización de gradiente. Esta técnica avanzada, conocida como WGAN-GP (Wasserstein GAN con penalización de gradiente), ofrece varias ventajas sobre las funciones de pérdida tradicionales de GAN.

Al utilizar la distancia Wasserstein como medida de disimilitud entre las distribuciones de datos reales y generados, podemos lograr una dinámica de entrenamiento más estable y generar imágenes de mayor calidad.

El término de penalización de gradiente refuerza además la restricción de Lipschitz en la función crítica (discriminador), ayudando a prevenir problemas como los gradientes que desaparecen y asegurando un proceso de entrenamiento más fluido. Esta implementación contribuirá significativamente a la robustez y rendimiento general de nuestro modelo GAN.

import tensorflow as tf

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = tf.reduce_mean(real_output)
    fake_loss = tf.reduce_mean(fake_output)
    return fake_loss - real_loss

def generator_loss(fake_output):
    return -tf.reduce_mean(fake_output)

def gradient_penalty(discriminator, real_images, fake_images):
    alpha = tf.random.uniform([real_images.shape[0], 1, 1, 1], 0.0, 1.0)
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    
    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = discriminator(interpolated, training=True)
    
    grads = gp_tape.gradient(pred, interpolated)
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3]))
    gp = tf.reduce_mean((norm - 1.0) ** 2)
    return gp

@tf.function
def train_step(images, batch_size, latent_dim):
    noise = tf.random.normal([batch_size, latent_dim])
    
    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)
        
        real_output = discriminator(images, training=True)
        fake_output = discriminator(generated_images, training=True)
        
        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)
        
        gp = gradient_penalty(discriminator, images, generated_images)
        disc_loss += 10 * gp
    
    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
    
    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
    
    return gen_loss, disc_loss

 

Desglose:

  1. Funciones de pérdida:
    • La función discriminator_loss calcula la pérdida Wasserstein para el discriminador.
    • La función generator_loss calcula la pérdida Wasserstein para el generador.
  2. Penalización de gradiente:
    • La función gradient_penalty implementa la penalización de gradiente, que ayuda a reforzar la restricción de Lipschitz en el discriminador.
  3. Paso de entrenamiento:
    • La función train_step define una única iteración de entrenamiento para el generador y el discriminador.
    • Genera imágenes falsas, calcula las pérdidas, aplica la penalización de gradiente y actualiza ambas redes.

Esta implementación tiene como objetivo mejorar la estabilidad del entrenamiento y mitigar problemas como el colapso de modo, que son desafíos comunes en el entrenamiento de GAN.

9.5.3 Crecimiento progresivo

Implementa el crecimiento progresivo como una técnica avanzada para aumentar gradualmente la resolución y complejidad de las imágenes generadas durante el proceso de entrenamiento. Este enfoque comienza con imágenes de baja resolución y va agregando capas progresivamente tanto al generador como al discriminador, lo que permite que el modelo aprenda características generales primero antes de enfocarse en detalles más finos.

Al hacerlo, podemos lograr una dinámica de entrenamiento más estable y potencialmente generar imágenes de mayor calidad a resoluciones más grandes. Este método ha demostrado un notable éxito en la producción de imágenes altamente realistas y puede mejorar significativamente el rendimiento general de nuestro modelo GAN para la generación de dígitos manuscritos.

def build_progressive_generator(latent_dim, target_resolution=28):
    model = models.Sequential()
    model.add(layers.Dense(4*4*256, use_bias=False, input_shape=(latent_dim,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU(alpha=0.2))
    model.add(layers.Reshape((4, 4, 256)))
    
    current_resolution = 4
    while current_resolution < target_resolution:
        model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False))
        model.add(layers.BatchNormalization())
        model.add(layers.LeakyReLU(alpha=0.2))
        current_resolution *= 2
    
    model.add(layers.Conv2D(1, (5, 5), padding='same', use_bias=False, activation='tanh'))
    return model

progressive_generator = build_progressive_generator(latent_dim=100)

Desglose del código:

  • La función toma dos parámetros: latent_dim (el tamaño del vector de ruido de entrada) y target_resolution (por defecto es 28, que coincide con el tamaño de las imágenes de MNIST).
  • Comienza creando un modelo base con una capa densa que se remodela en un tensor de 4x4x256, seguido de normalización por lotes (batch normalization) y activación LeakyReLU.
  • El núcleo de la técnica de crecimiento progresivo se implementa en el bucle while:
    • Sigue agregando capas de convolución transpuesta (upsampling) hasta que la resolución actual alcanza la resolución objetivo.
    • Cada iteración duplica la resolución (por ejemplo, de 4x4 → 8x8 → 16x16 → 28x28).
  • Cada paso de upsampling incluye una capa Conv2DTranspose, normalización por lotes y activación LeakyReLU.
  • La capa final es una capa Conv2D con activación tanh, que produce la imagen de salida.
  • Después de definir la función, se utiliza para crear un progressive_generator con una dimensión latente de 100.

Este enfoque de crecimiento progresivo permite que el modelo aprenda primero características gruesas antes de centrarse en detalles más finos, lo que potencialmente conduce a un entrenamiento más estable y a la generación de imágenes de mayor calidad.

9.5.4 Normalización Espectral

Implementar la normalización espectral en el discriminador para mejorar la estabilidad del entrenamiento y prevenir la aparición de gradientes explosivos. Esta técnica limita la constante de Lipschitz de la función del discriminador, restringiendo efectivamente el impacto de las perturbaciones individuales en la entrada sobre la salida.

Al aplicar normalización espectral a los pesos de las capas del discriminador, garantizamos que el valor singular más grande de las matrices de pesos esté limitado, lo que conduce a dinámicas de entrenamiento más consistentes y confiables. Se ha demostrado que este enfoque es particularmente efectivo para estabilizar el entrenamiento de GANs, especialmente al trabajar con arquitecturas complejas o conjuntos de datos desafiantes.

La implementación de la normalización espectral contribuye significativamente a la robustez general de nuestro modelo GAN, lo que podría resultar en imágenes generadas de mayor calidad y características de convergencia mejoradas.

from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.constraints import max_norm

class SpectralNormalization(tf.keras.constraints.Constraint):
    def __init__(self, iterations=1):
        self.iterations = iterations
    
    def __call__(self, w):
        w_shape = w.shape.as_list()
        w = tf.reshape(w, [-1, w_shape[-1]])
        u = tf.random.normal([1, w_shape[-1]])
        
        for _ in range(self.iterations):
            v = tf.matmul(u, tf.transpose(w))
            v = v / tf.norm(v)
            u = tf.matmul(v, w)
            u = u / tf.norm(u)
        
        sigma = tf.matmul(tf.matmul(v, w), tf.transpose(u))[0, 0]
        return w / sigma

def SpectralConv2D(filters, kernel_size, **kwargs):
    return Conv2D(filters, kernel_size, kernel_constraint=SpectralNormalization(), **kwargs)

def SpectralDense(units, **kwargs):
    return Dense(units, kernel_constraint=SpectralNormalization(), **kwargs)

Desglose del código:

  • Clase SpectralNormalization: Esta es una clase de restricción personalizada que aplica normalización espectral a los pesos de una capa. Funciona estimando la norma espectral de la matriz de pesos y utilizándola para normalizar los pesos.
  • Método call: Este método implementa el algoritmo principal de la normalización espectral. Usa la iteración de potencia para estimar el valor singular más grande (norma espectral) de la matriz de pesos y luego utiliza esto para normalizar los pesos.
  • Funciones SpectralConv2D y SpectralDense: Estas son funciones que envuelven la creación de capas Conv2D y Dense con la normalización espectral aplicada a sus kernels. Facilitan la adición de la normalización espectral a un modelo.

El propósito de la normalización espectral es limitar la constante de Lipschitz de la función del discriminador en un GAN. Esto ayuda a prevenir gradientes explosivos y estabiliza el proceso de entrenamiento, lo que potencialmente conduce a la generación de imágenes de mayor calidad y una mejor convergencia.

9.5.5 Mecanismo de Autoatención

Incorporar un mecanismo de autoatención para mejorar la capacidad del modelo de capturar dependencias globales en las imágenes generadas. Esta técnica avanzada permite que la red se enfoque en características relevantes en diferentes ubicaciones espaciales, lo que lleva a una mayor coherencia y detalle en la salida.

Al implementar capas de autoatención tanto en el generador como en el discriminador, habilitamos al modelo para aprender dependencias de largo alcance de manera más efectiva, lo que resulta en imágenes de dígitos manuscritos de mayor calidad y más realistas. Este enfoque ha mostrado un éxito notable en varias tareas de generación de imágenes y promete mejorar significativamente el rendimiento de nuestro modelo GAN.

import tensorflow as tf
from tensorflow.keras import layers

class SelfAttention(layers.Layer):
    def __init__(self, channels):
        super(SelfAttention, self).__init__()
        self.channels = channels
        
        # Conv layers for self-attention
        self.f = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.g = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.h = layers.Conv2D(channels, 1, kernel_initializer='he_normal')

        # Trainable scalar weight gamma
        self.gamma = self.add_weight(name='gamma', shape=(1,), initializer='zeros', trainable=True)

    def call(self, x):
        batch_size, height, width, channels = tf.unstack(tf.shape(x))

        # Compute f, g, h transformations
        f = self.f(x)  # Query
        g = self.g(x)  # Key
        h = self.h(x)  # Value

        # Reshape tensors for self-attention calculation
        f_flatten = tf.reshape(f, [batch_size, height * width, -1])  # (B, H*W, C//8)
        g_flatten = tf.reshape(g, [batch_size, height * width, -1])  # (B, H*W, C//8)
        h_flatten = tf.reshape(h, [batch_size, height * width, channels])  # (B, H*W, C)

        # Compute attention scores
        s = tf.matmul(g_flatten, f_flatten, transpose_b=True)  # (B, H*W, H*W)
        beta = tf.nn.softmax(s)  # Attention map (B, H*W, H*W)

        # Apply attention weights to h
        o = tf.matmul(beta, h_flatten)  # (B, H*W, C)
        o = tf.reshape(o, [batch_size, height, width, channels])  # Reshape back

        # Apply self-attention mechanism
        return self.gamma * o + x  # Weighted residual connection

Analicémoslo:

  1. La clase SelfAttention es una capa personalizada que hereda de layers.Layer
    • Esta capa implementa la autoatención, permitiendo que el modelo aprenda dependencias de largo alcance en una imagen.
    • Comúnmente utilizada en GANs, modelos de segmentación de imágenes y transformers.
  2. En el método __init__:
    • Se definen tres capas convolucionales (fg, y h), cada una con un kernel 1x1.
      • f: Aprende características de consulta (reduce la dimensionalidad).
      • g: Aprende características clave (reduce la dimensionalidad).
      • h: Aprende características de valor (mantiene la dimensionalidad original).
    • Se añade un parámetro entrenable gamma, inicializado en cero, para controlar la contribución del mecanismo de atención.
  3. El método call define el paso hacia adelante:
    • Extrae dimensiones espaciales dinámicamente (batch_size, height, width, channels) para asegurar la compatibilidad con la ejecución de TensorFlow.
    • Calcula transformaciones de características usando convoluciones Conv2D(1x1):
      • f(x): Genera la representación de consulta.
      • g(x): Genera la representación clave.
      • h(x): Genera la representación de valor.
    • Calcula el mapa de atención:
      • Multiplica g y f (similitud por producto punto).
      • Aplica softmax para normalizar los puntajes de atención.
    • Aplica el mapa de atención a h (suma ponderada de características atendidas).
    • Utiliza una conexión residual (gamma * o + x) para combinar la entrada original con la salida de atención.
  4. ¿Por qué es importante?
    • Este mecanismo de autoatención permite que el modelo se enfoque en características relevantes a través de diferentes ubicaciones espaciales.
    • Particularmente útil en tareas de generación de imágenes (GANs) para mejorar la calidad y coherencia de las imágenes generadas.
    • Ayuda a capturar dependencias de largo alcance, a diferencia de las capas convolucionales, que tienen campos receptivos locales.

9.5.6 Bucle de Entrenamiento Mejorado

Mejora el proceso de entrenamiento mediante la implementación de un bucle de entrenamiento avanzado que incorpora ajustes dinámicos de la tasa de aprendizaje y mecanismos inteligentes de parada temprana. Este enfoque sofisticado adapta la tasa de aprendizaje a lo largo del tiempo para optimizar la convergencia y termina automáticamente el entrenamiento cuando el rendimiento se estabiliza, asegurando un uso eficiente de los recursos computacionales y previniendo el sobreajuste.

Las características principales de este bucle de entrenamiento mejorado incluyen:

  • Programación de la tasa de aprendizaje: Utiliza técnicas adaptativas de tasa de aprendizaje como la degradación exponencial o el recocido coseno para reducir gradualmente la tasa de aprendizaje conforme avanza el entrenamiento, permitiendo el ajuste fino de los parámetros del modelo.
  • Parada temprana: Implementa un criterio de parada temprana basado en paciencia que monitorea una métrica de rendimiento relevante (por ejemplo, la puntuación FID) y detiene el entrenamiento si no se observa mejora durante un número específico de épocas.
  • Guardado de puntos de control: Guarda regularmente puntos de control del modelo durante el entrenamiento, preservando las iteraciones del modelo con mejor rendimiento para uso o evaluación posterior.
  • Monitoreo del progreso: Integra herramientas completas de registro y visualización para seguir métricas clave, permitiendo la evaluación en tiempo real del rendimiento del modelo y la dinámica del entrenamiento.
from tensorflow.keras.optimizers.schedules import ExponentialDecay
from tensorflow.keras.optimizers import Adam
import tensorflow as tf

# Learning rate schedule
initial_learning_rate = 0.0002
lr_schedule = ExponentialDecay(initial_learning_rate, decay_steps=10000, decay_rate=0.96, staircase=True)

# Optimizers
generator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)
discriminator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)

# Number of samples for visualization
num_samples = 16  # Adjust based on needs
LATENT_DIM = 100  # Ensure consistency

# Generate a fixed noise seed for consistent visualization
seed = tf.random.normal([num_samples, LATENT_DIM])

def train(dataset, epochs, batch_size, latent_dim):
    best_fid = float('inf')
    patience = 10
    no_improvement = 0
    
    for epoch in range(epochs):
        for batch in dataset:
            gen_loss, disc_loss = train_step(batch, batch_size, latent_dim)
        
        print(f"Epoch {epoch + 1}, Gen Loss: {gen_loss:.4f}, Disc Loss: {disc_loss:.4f}")
        
        if (epoch + 1) % 10 == 0:
            generate_and_save_images(generator, epoch + 1, seed)
            
            # Generate fake images
            generated_images = generator(seed, training=False)
            
            # Select a batch of real images for FID calculation
            real_images = next(iter(dataset))[:num_samples]

            current_fid = calculate_fid(real_images, generated_images)
            
            if current_fid < best_fid:
                best_fid = current_fid
                no_improvement = 0
                
                # Save model properly
                generator.save(f"generator_epoch_{epoch + 1}.h5")
            else:
                no_improvement += 1
            
            if no_improvement >= patience:
                print(f"Early stopping at epoch {epoch + 1}")
                break

# Ensure dataset is properly defined
train(train_dataset, EPOCHS, BATCH_SIZE, LATENT_DIM)

Aquí está el desglose del código:

  1. Programación de la Tasa de Aprendizaje:
    • Utiliza una programación de DecaimientoExponencial para reducir gradualmente la tasa de aprendizaje, ayudando a ajustar los parámetros del modelo.
    • Esto previene la inestabilidad en el entrenamiento de GAN al reducir actualizaciones grandes y repentinas en los pesos.
  2. Optimizadores:
    • Utiliza optimizadores Adam tanto para el generador como para el discriminador, con:
      • Una tasa de aprendizaje decreciente (lr_schedule).
      • beta_1=0.5, que es común en el entrenamiento de GAN para estabilizar las actualizaciones.
  3. Bucle de Entrenamiento:
    • Itera a través de épocas y lotes, llamando a train_step() (no mostrado) para actualizar los pesos del generador y el discriminador.
    • Cada actualización de lote mejora la capacidad del generador para crear muestras más realistas y la capacidad del discriminador para distinguir entre imágenes reales y falsas.
  4. Evaluación Periódica (cada 10 épocas):
    • Genera y guarda imágenes usando un ruido aleatorio fijo seed para seguir el progreso.
    • Calcula la puntuación de Distancia Inception de Fréchet (FID), una métrica ampliamente utilizada para evaluar la calidad y diversidad de las imágenes generadas.
  5. Guardado del Modelo:
    • Guarda el modelo generador (generator.save()) cuando se logra una nueva mejor puntuación FID.
    • Ayuda a preservar el generador con mejor rendimiento en lugar de solo la época final.
  6. Parada Temprana:
    • Si no hay mejora en FID durante un número establecido de épocas de paciencia (por ejemplo, 10 épocas), el entrenamiento se detiene anticipadamente.
    • Previene el sobreajuste, ahorra computación y detiene el colapso modal (fallo de GAN donde el generador produce solo algunas imágenes similares).

9.5.7 Métricas de Evaluación

Implementa y utiliza métricas avanzadas de evaluación para valorar la calidad y diversidad de las imágenes generadas. Las dos métricas clave en las que nos enfocaremos son:

  1. Fréchet Inception Distance (FID): Esta métrica mide la similitud entre las imágenes reales y las generadas comparando sus representaciones de características extraídas de una red Inception preentrenada. Un puntaje FID más bajo indica imágenes generadas de mayor calidad y más realistas.
  2. Inception Score (IS): Esta métrica evalúa tanto la calidad como la diversidad de las imágenes generadas. Utiliza una red Inception preentrenada para medir qué tan bien las imágenes generadas pueden clasificarse en categorías distintas. Un puntaje de Inception más alto sugiere imágenes generadas de mejor calidad y más diversas.

Al incorporar estas métricas en nuestro proceso de evaluación, podemos valorar cuantitativamente el rendimiento de nuestro modelo GAN y rastrear mejoras a lo largo del tiempo. Esto proporcionará valiosas ideas sobre la efectividad de nuestras mejoras arquitectónicas y de entrenamiento.

import tensorflow as tf
from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
import numpy as np
from scipy.linalg import sqrtm

def calculate_fid(real_images, generated_images, batch_size=32):
    """
    Calculates the Fréchet Inception Distance (FID) between real and generated images.
    """
    inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

    def get_features(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        features = inception_model.predict(images, batch_size=batch_size)
        return features

    # Extract features
    real_features = get_features(real_images)
    generated_features = get_features(generated_images)

    # Compute mean and covariance of features
    mu1, sigma1 = np.mean(real_features, axis=0), np.cov(real_features, rowvar=False)
    mu2, sigma2 = np.mean(generated_features, axis=0), np.cov(generated_features, rowvar=False)

    # Compute squared mean difference
    ssdiff = np.sum((mu1 - mu2) ** 2.0)

    # Compute sqrt of covariance product (for numerical stability)
    covmean = sqrtm(sigma1.dot(sigma2))

    # Ensure the matrix is real-valued
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    # Compute final FID score
    fid = ssdiff + np.trace(sigma1 + sigma2 - 2.0 * covmean)
    return fid

def calculate_inception_score(images, batch_size=32, splits=10):
    """
    Computes the Inception Score (IS) for generated images.
    """
    inception_model = InceptionV3(include_top=True, weights="imagenet")  # Use full model

    def get_preds(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        preds = inception_model.predict(images, batch_size=batch_size)  # Get logits
        preds = tf.nn.softmax(preds).numpy()  # Convert logits to probabilities
        return preds

    # Get model predictions
    preds = get_preds(images)

    scores = []
    for i in range(splits):
        part = preds[i * (len(preds) // splits): (i + 1) * (len(preds) // splits), :]
        kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, 0), 0)))
        kl = np.mean(np.sum(kl, 1))
        scores.append(np.exp(kl))

    return np.mean(scores), np.std(scores)

Analicemos cada función:

1. FID (Distancia de Inception de Fréchet)

Compara imágenes reales vs. generadas para verificar la calidad.

Utiliza InceptionV3 para extraer características de las imágenes.

Mide la diferencia en las distribuciones de características (media y covarianza).

FID más bajo = Imágenes más realistas.

2. IS (Puntuación de Inception)

Verifica la calidad y diversidad de las imágenes generadas.

Utiliza InceptionV3 para clasificar imágenes.

Mide la nitidez (predicciones confiables) y la variación (distribución entre clases).

IS más alto = Mejor calidad y diversidad.

9.5.8 Conclusión

Este proyecto GAN incorpora varias técnicas avanzadas para mejorar la calidad de las imágenes generadas y la estabilidad del entrenamiento. Las mejoras principales incluyen:

  1. Una arquitectura más profunda y sofisticada tanto para el generador como para el discriminador.
  2. Pérdida de Wasserstein con penalización de gradiente para mejorar la estabilidad del entrenamiento.
  3. Crecimiento progresivo para generar imágenes de mayor resolución.
  4. Normalización espectral en el discriminador para prevenir gradientes explosivos.
  5. Mecanismo de auto-atención para capturar dependencias globales en las imágenes generadas.
  6. Un bucle de entrenamiento mejorado con programación de tasa de aprendizaje y parada temprana.
  7. Métricas de evaluación avanzadas (FID y Puntuación de Inception) para una mejor evaluación de la calidad de las imágenes generadas.

Estas mejoras deberían resultar en imágenes generadas de mayor calidad, un entrenamiento más estable y un mejor rendimiento general del GAN. Recuerda experimentar con hiperparámetros y arquitecturas para encontrar la configuración óptima para tu caso de uso específico.

9.5 Proyecto 5: Generación de imágenes basada en GAN

Las Redes Generativas Antagónicas (GAN) han marcado una nueva era en el ámbito de la generación de imágenes, revolucionando el campo con su enfoque innovador. Este ambicioso proyecto busca elevar la implementación original de GAN, específicamente adaptada para generar dígitos manuscritos a partir del ampliamente utilizado conjunto de datos MNIST.

Nuestro objetivo principal es incorporar una serie de mejoras de vanguardia diseñadas para aumentar significativamente el rendimiento general, mejorar la estabilidad del entrenamiento y elevar la calidad de las imágenes generadas a niveles sin precedentes.

Al aprovechar técnicas de última generación y mejoras arquitectónicas, buscamos superar los límites de lo que es posible con GAN. Estas mejoras no solo abordarán los desafíos comunes asociados con el entrenamiento de GAN, como el colapso de modo y los problemas de convergencia, sino que también introducirán características novedosas que prometen generar resultados más realistas y diversos.

A través de este proyecto, anticipamos demostrar todo el potencial de las GAN en la creación de imágenes de dígitos manuscritos de alta fidelidad que son prácticamente indistinguibles de sus contrapartes reales.

9.5.1 Arquitectura GAN mejorada

Para mejorar el rendimiento general y la capacidad de nuestra GAN, implementaremos una arquitectura más compleja y escalonada tanto para los componentes del generador como del discriminador. Esta estructura avanzada incorporará capas convolucionales adicionales, conexiones de salto y técnicas de normalización para mejorar la capacidad de la red para aprender características complejas y generar imágenes de alta calidad. Al aumentar la profundidad y la sofisticación de nuestros modelos, esperamos capturar patrones más detallados en los datos y producir imágenes de dígitos manuscritos más realistas y detalladas.

import tensorflow as tf
from tensorflow.keras import layers, models

def build_generator(latent_dim):
    model = models.Sequential([
        layers.Dense(7*7*256, use_bias=False, input_shape=(latent_dim,)),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        layers.Reshape((7, 7, 256)),
        
        layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')
    ])
    return model

def build_discriminator():
    model = models.Sequential([
        layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Flatten(),
        layers.Dense(1)
    ])
    return model

generator = build_generator(latent_dim=100)
discriminator = build_discriminator()

Desglose:

  1. Generador:
    • Toma un vector latente (ruido) como entrada.
    • Utiliza convoluciones transpuestas para aumentar la resolución de la entrada a una imagen de 28x28.
    • Incorpora normalización por lotes (batch normalization) y activaciones LeakyReLU para estabilidad y no linealidad.
    • La capa final utiliza activación tanh para producir una salida similar a una imagen.
  2. Discriminador:
    • Toma una imagen de 28x28 como entrada.
    • Utiliza capas convolucionales para reducir la resolución de la entrada.
    • Incorpora activaciones LeakyReLU y dropout para regularización.
    • La capa densa final genera un solo valor, que representa la probabilidad de que la entrada sea real.

La arquitectura está diseñada para generar y discriminar imágenes en escala de grises de 28x28, lo que se alinea con el formato del conjunto de datos MNIST. El uso de normalización por lotes, LeakyReLU y dropout ayuda a estabilizar el proceso de entrenamiento y a prevenir problemas como el colapso de modo.

9.5.2 Pérdida Wasserstein con penalización de gradiente

Para mejorar la estabilidad del entrenamiento y mitigar el colapso de modo, implementaremos la función de pérdida Wasserstein con penalización de gradiente. Esta técnica avanzada, conocida como WGAN-GP (Wasserstein GAN con penalización de gradiente), ofrece varias ventajas sobre las funciones de pérdida tradicionales de GAN.

Al utilizar la distancia Wasserstein como medida de disimilitud entre las distribuciones de datos reales y generados, podemos lograr una dinámica de entrenamiento más estable y generar imágenes de mayor calidad.

El término de penalización de gradiente refuerza además la restricción de Lipschitz en la función crítica (discriminador), ayudando a prevenir problemas como los gradientes que desaparecen y asegurando un proceso de entrenamiento más fluido. Esta implementación contribuirá significativamente a la robustez y rendimiento general de nuestro modelo GAN.

import tensorflow as tf

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = tf.reduce_mean(real_output)
    fake_loss = tf.reduce_mean(fake_output)
    return fake_loss - real_loss

def generator_loss(fake_output):
    return -tf.reduce_mean(fake_output)

def gradient_penalty(discriminator, real_images, fake_images):
    alpha = tf.random.uniform([real_images.shape[0], 1, 1, 1], 0.0, 1.0)
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    
    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = discriminator(interpolated, training=True)
    
    grads = gp_tape.gradient(pred, interpolated)
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3]))
    gp = tf.reduce_mean((norm - 1.0) ** 2)
    return gp

@tf.function
def train_step(images, batch_size, latent_dim):
    noise = tf.random.normal([batch_size, latent_dim])
    
    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)
        
        real_output = discriminator(images, training=True)
        fake_output = discriminator(generated_images, training=True)
        
        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)
        
        gp = gradient_penalty(discriminator, images, generated_images)
        disc_loss += 10 * gp
    
    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
    
    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
    
    return gen_loss, disc_loss

 

Desglose:

  1. Funciones de pérdida:
    • La función discriminator_loss calcula la pérdida Wasserstein para el discriminador.
    • La función generator_loss calcula la pérdida Wasserstein para el generador.
  2. Penalización de gradiente:
    • La función gradient_penalty implementa la penalización de gradiente, que ayuda a reforzar la restricción de Lipschitz en el discriminador.
  3. Paso de entrenamiento:
    • La función train_step define una única iteración de entrenamiento para el generador y el discriminador.
    • Genera imágenes falsas, calcula las pérdidas, aplica la penalización de gradiente y actualiza ambas redes.

Esta implementación tiene como objetivo mejorar la estabilidad del entrenamiento y mitigar problemas como el colapso de modo, que son desafíos comunes en el entrenamiento de GAN.

9.5.3 Crecimiento progresivo

Implementa el crecimiento progresivo como una técnica avanzada para aumentar gradualmente la resolución y complejidad de las imágenes generadas durante el proceso de entrenamiento. Este enfoque comienza con imágenes de baja resolución y va agregando capas progresivamente tanto al generador como al discriminador, lo que permite que el modelo aprenda características generales primero antes de enfocarse en detalles más finos.

Al hacerlo, podemos lograr una dinámica de entrenamiento más estable y potencialmente generar imágenes de mayor calidad a resoluciones más grandes. Este método ha demostrado un notable éxito en la producción de imágenes altamente realistas y puede mejorar significativamente el rendimiento general de nuestro modelo GAN para la generación de dígitos manuscritos.

def build_progressive_generator(latent_dim, target_resolution=28):
    model = models.Sequential()
    model.add(layers.Dense(4*4*256, use_bias=False, input_shape=(latent_dim,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU(alpha=0.2))
    model.add(layers.Reshape((4, 4, 256)))
    
    current_resolution = 4
    while current_resolution < target_resolution:
        model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False))
        model.add(layers.BatchNormalization())
        model.add(layers.LeakyReLU(alpha=0.2))
        current_resolution *= 2
    
    model.add(layers.Conv2D(1, (5, 5), padding='same', use_bias=False, activation='tanh'))
    return model

progressive_generator = build_progressive_generator(latent_dim=100)

Desglose del código:

  • La función toma dos parámetros: latent_dim (el tamaño del vector de ruido de entrada) y target_resolution (por defecto es 28, que coincide con el tamaño de las imágenes de MNIST).
  • Comienza creando un modelo base con una capa densa que se remodela en un tensor de 4x4x256, seguido de normalización por lotes (batch normalization) y activación LeakyReLU.
  • El núcleo de la técnica de crecimiento progresivo se implementa en el bucle while:
    • Sigue agregando capas de convolución transpuesta (upsampling) hasta que la resolución actual alcanza la resolución objetivo.
    • Cada iteración duplica la resolución (por ejemplo, de 4x4 → 8x8 → 16x16 → 28x28).
  • Cada paso de upsampling incluye una capa Conv2DTranspose, normalización por lotes y activación LeakyReLU.
  • La capa final es una capa Conv2D con activación tanh, que produce la imagen de salida.
  • Después de definir la función, se utiliza para crear un progressive_generator con una dimensión latente de 100.

Este enfoque de crecimiento progresivo permite que el modelo aprenda primero características gruesas antes de centrarse en detalles más finos, lo que potencialmente conduce a un entrenamiento más estable y a la generación de imágenes de mayor calidad.

9.5.4 Normalización Espectral

Implementar la normalización espectral en el discriminador para mejorar la estabilidad del entrenamiento y prevenir la aparición de gradientes explosivos. Esta técnica limita la constante de Lipschitz de la función del discriminador, restringiendo efectivamente el impacto de las perturbaciones individuales en la entrada sobre la salida.

Al aplicar normalización espectral a los pesos de las capas del discriminador, garantizamos que el valor singular más grande de las matrices de pesos esté limitado, lo que conduce a dinámicas de entrenamiento más consistentes y confiables. Se ha demostrado que este enfoque es particularmente efectivo para estabilizar el entrenamiento de GANs, especialmente al trabajar con arquitecturas complejas o conjuntos de datos desafiantes.

La implementación de la normalización espectral contribuye significativamente a la robustez general de nuestro modelo GAN, lo que podría resultar en imágenes generadas de mayor calidad y características de convergencia mejoradas.

from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.constraints import max_norm

class SpectralNormalization(tf.keras.constraints.Constraint):
    def __init__(self, iterations=1):
        self.iterations = iterations
    
    def __call__(self, w):
        w_shape = w.shape.as_list()
        w = tf.reshape(w, [-1, w_shape[-1]])
        u = tf.random.normal([1, w_shape[-1]])
        
        for _ in range(self.iterations):
            v = tf.matmul(u, tf.transpose(w))
            v = v / tf.norm(v)
            u = tf.matmul(v, w)
            u = u / tf.norm(u)
        
        sigma = tf.matmul(tf.matmul(v, w), tf.transpose(u))[0, 0]
        return w / sigma

def SpectralConv2D(filters, kernel_size, **kwargs):
    return Conv2D(filters, kernel_size, kernel_constraint=SpectralNormalization(), **kwargs)

def SpectralDense(units, **kwargs):
    return Dense(units, kernel_constraint=SpectralNormalization(), **kwargs)

Desglose del código:

  • Clase SpectralNormalization: Esta es una clase de restricción personalizada que aplica normalización espectral a los pesos de una capa. Funciona estimando la norma espectral de la matriz de pesos y utilizándola para normalizar los pesos.
  • Método call: Este método implementa el algoritmo principal de la normalización espectral. Usa la iteración de potencia para estimar el valor singular más grande (norma espectral) de la matriz de pesos y luego utiliza esto para normalizar los pesos.
  • Funciones SpectralConv2D y SpectralDense: Estas son funciones que envuelven la creación de capas Conv2D y Dense con la normalización espectral aplicada a sus kernels. Facilitan la adición de la normalización espectral a un modelo.

El propósito de la normalización espectral es limitar la constante de Lipschitz de la función del discriminador en un GAN. Esto ayuda a prevenir gradientes explosivos y estabiliza el proceso de entrenamiento, lo que potencialmente conduce a la generación de imágenes de mayor calidad y una mejor convergencia.

9.5.5 Mecanismo de Autoatención

Incorporar un mecanismo de autoatención para mejorar la capacidad del modelo de capturar dependencias globales en las imágenes generadas. Esta técnica avanzada permite que la red se enfoque en características relevantes en diferentes ubicaciones espaciales, lo que lleva a una mayor coherencia y detalle en la salida.

Al implementar capas de autoatención tanto en el generador como en el discriminador, habilitamos al modelo para aprender dependencias de largo alcance de manera más efectiva, lo que resulta en imágenes de dígitos manuscritos de mayor calidad y más realistas. Este enfoque ha mostrado un éxito notable en varias tareas de generación de imágenes y promete mejorar significativamente el rendimiento de nuestro modelo GAN.

import tensorflow as tf
from tensorflow.keras import layers

class SelfAttention(layers.Layer):
    def __init__(self, channels):
        super(SelfAttention, self).__init__()
        self.channels = channels
        
        # Conv layers for self-attention
        self.f = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.g = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.h = layers.Conv2D(channels, 1, kernel_initializer='he_normal')

        # Trainable scalar weight gamma
        self.gamma = self.add_weight(name='gamma', shape=(1,), initializer='zeros', trainable=True)

    def call(self, x):
        batch_size, height, width, channels = tf.unstack(tf.shape(x))

        # Compute f, g, h transformations
        f = self.f(x)  # Query
        g = self.g(x)  # Key
        h = self.h(x)  # Value

        # Reshape tensors for self-attention calculation
        f_flatten = tf.reshape(f, [batch_size, height * width, -1])  # (B, H*W, C//8)
        g_flatten = tf.reshape(g, [batch_size, height * width, -1])  # (B, H*W, C//8)
        h_flatten = tf.reshape(h, [batch_size, height * width, channels])  # (B, H*W, C)

        # Compute attention scores
        s = tf.matmul(g_flatten, f_flatten, transpose_b=True)  # (B, H*W, H*W)
        beta = tf.nn.softmax(s)  # Attention map (B, H*W, H*W)

        # Apply attention weights to h
        o = tf.matmul(beta, h_flatten)  # (B, H*W, C)
        o = tf.reshape(o, [batch_size, height, width, channels])  # Reshape back

        # Apply self-attention mechanism
        return self.gamma * o + x  # Weighted residual connection

Analicémoslo:

  1. La clase SelfAttention es una capa personalizada que hereda de layers.Layer
    • Esta capa implementa la autoatención, permitiendo que el modelo aprenda dependencias de largo alcance en una imagen.
    • Comúnmente utilizada en GANs, modelos de segmentación de imágenes y transformers.
  2. En el método __init__:
    • Se definen tres capas convolucionales (fg, y h), cada una con un kernel 1x1.
      • f: Aprende características de consulta (reduce la dimensionalidad).
      • g: Aprende características clave (reduce la dimensionalidad).
      • h: Aprende características de valor (mantiene la dimensionalidad original).
    • Se añade un parámetro entrenable gamma, inicializado en cero, para controlar la contribución del mecanismo de atención.
  3. El método call define el paso hacia adelante:
    • Extrae dimensiones espaciales dinámicamente (batch_size, height, width, channels) para asegurar la compatibilidad con la ejecución de TensorFlow.
    • Calcula transformaciones de características usando convoluciones Conv2D(1x1):
      • f(x): Genera la representación de consulta.
      • g(x): Genera la representación clave.
      • h(x): Genera la representación de valor.
    • Calcula el mapa de atención:
      • Multiplica g y f (similitud por producto punto).
      • Aplica softmax para normalizar los puntajes de atención.
    • Aplica el mapa de atención a h (suma ponderada de características atendidas).
    • Utiliza una conexión residual (gamma * o + x) para combinar la entrada original con la salida de atención.
  4. ¿Por qué es importante?
    • Este mecanismo de autoatención permite que el modelo se enfoque en características relevantes a través de diferentes ubicaciones espaciales.
    • Particularmente útil en tareas de generación de imágenes (GANs) para mejorar la calidad y coherencia de las imágenes generadas.
    • Ayuda a capturar dependencias de largo alcance, a diferencia de las capas convolucionales, que tienen campos receptivos locales.

9.5.6 Bucle de Entrenamiento Mejorado

Mejora el proceso de entrenamiento mediante la implementación de un bucle de entrenamiento avanzado que incorpora ajustes dinámicos de la tasa de aprendizaje y mecanismos inteligentes de parada temprana. Este enfoque sofisticado adapta la tasa de aprendizaje a lo largo del tiempo para optimizar la convergencia y termina automáticamente el entrenamiento cuando el rendimiento se estabiliza, asegurando un uso eficiente de los recursos computacionales y previniendo el sobreajuste.

Las características principales de este bucle de entrenamiento mejorado incluyen:

  • Programación de la tasa de aprendizaje: Utiliza técnicas adaptativas de tasa de aprendizaje como la degradación exponencial o el recocido coseno para reducir gradualmente la tasa de aprendizaje conforme avanza el entrenamiento, permitiendo el ajuste fino de los parámetros del modelo.
  • Parada temprana: Implementa un criterio de parada temprana basado en paciencia que monitorea una métrica de rendimiento relevante (por ejemplo, la puntuación FID) y detiene el entrenamiento si no se observa mejora durante un número específico de épocas.
  • Guardado de puntos de control: Guarda regularmente puntos de control del modelo durante el entrenamiento, preservando las iteraciones del modelo con mejor rendimiento para uso o evaluación posterior.
  • Monitoreo del progreso: Integra herramientas completas de registro y visualización para seguir métricas clave, permitiendo la evaluación en tiempo real del rendimiento del modelo y la dinámica del entrenamiento.
from tensorflow.keras.optimizers.schedules import ExponentialDecay
from tensorflow.keras.optimizers import Adam
import tensorflow as tf

# Learning rate schedule
initial_learning_rate = 0.0002
lr_schedule = ExponentialDecay(initial_learning_rate, decay_steps=10000, decay_rate=0.96, staircase=True)

# Optimizers
generator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)
discriminator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)

# Number of samples for visualization
num_samples = 16  # Adjust based on needs
LATENT_DIM = 100  # Ensure consistency

# Generate a fixed noise seed for consistent visualization
seed = tf.random.normal([num_samples, LATENT_DIM])

def train(dataset, epochs, batch_size, latent_dim):
    best_fid = float('inf')
    patience = 10
    no_improvement = 0
    
    for epoch in range(epochs):
        for batch in dataset:
            gen_loss, disc_loss = train_step(batch, batch_size, latent_dim)
        
        print(f"Epoch {epoch + 1}, Gen Loss: {gen_loss:.4f}, Disc Loss: {disc_loss:.4f}")
        
        if (epoch + 1) % 10 == 0:
            generate_and_save_images(generator, epoch + 1, seed)
            
            # Generate fake images
            generated_images = generator(seed, training=False)
            
            # Select a batch of real images for FID calculation
            real_images = next(iter(dataset))[:num_samples]

            current_fid = calculate_fid(real_images, generated_images)
            
            if current_fid < best_fid:
                best_fid = current_fid
                no_improvement = 0
                
                # Save model properly
                generator.save(f"generator_epoch_{epoch + 1}.h5")
            else:
                no_improvement += 1
            
            if no_improvement >= patience:
                print(f"Early stopping at epoch {epoch + 1}")
                break

# Ensure dataset is properly defined
train(train_dataset, EPOCHS, BATCH_SIZE, LATENT_DIM)

Aquí está el desglose del código:

  1. Programación de la Tasa de Aprendizaje:
    • Utiliza una programación de DecaimientoExponencial para reducir gradualmente la tasa de aprendizaje, ayudando a ajustar los parámetros del modelo.
    • Esto previene la inestabilidad en el entrenamiento de GAN al reducir actualizaciones grandes y repentinas en los pesos.
  2. Optimizadores:
    • Utiliza optimizadores Adam tanto para el generador como para el discriminador, con:
      • Una tasa de aprendizaje decreciente (lr_schedule).
      • beta_1=0.5, que es común en el entrenamiento de GAN para estabilizar las actualizaciones.
  3. Bucle de Entrenamiento:
    • Itera a través de épocas y lotes, llamando a train_step() (no mostrado) para actualizar los pesos del generador y el discriminador.
    • Cada actualización de lote mejora la capacidad del generador para crear muestras más realistas y la capacidad del discriminador para distinguir entre imágenes reales y falsas.
  4. Evaluación Periódica (cada 10 épocas):
    • Genera y guarda imágenes usando un ruido aleatorio fijo seed para seguir el progreso.
    • Calcula la puntuación de Distancia Inception de Fréchet (FID), una métrica ampliamente utilizada para evaluar la calidad y diversidad de las imágenes generadas.
  5. Guardado del Modelo:
    • Guarda el modelo generador (generator.save()) cuando se logra una nueva mejor puntuación FID.
    • Ayuda a preservar el generador con mejor rendimiento en lugar de solo la época final.
  6. Parada Temprana:
    • Si no hay mejora en FID durante un número establecido de épocas de paciencia (por ejemplo, 10 épocas), el entrenamiento se detiene anticipadamente.
    • Previene el sobreajuste, ahorra computación y detiene el colapso modal (fallo de GAN donde el generador produce solo algunas imágenes similares).

9.5.7 Métricas de Evaluación

Implementa y utiliza métricas avanzadas de evaluación para valorar la calidad y diversidad de las imágenes generadas. Las dos métricas clave en las que nos enfocaremos son:

  1. Fréchet Inception Distance (FID): Esta métrica mide la similitud entre las imágenes reales y las generadas comparando sus representaciones de características extraídas de una red Inception preentrenada. Un puntaje FID más bajo indica imágenes generadas de mayor calidad y más realistas.
  2. Inception Score (IS): Esta métrica evalúa tanto la calidad como la diversidad de las imágenes generadas. Utiliza una red Inception preentrenada para medir qué tan bien las imágenes generadas pueden clasificarse en categorías distintas. Un puntaje de Inception más alto sugiere imágenes generadas de mejor calidad y más diversas.

Al incorporar estas métricas en nuestro proceso de evaluación, podemos valorar cuantitativamente el rendimiento de nuestro modelo GAN y rastrear mejoras a lo largo del tiempo. Esto proporcionará valiosas ideas sobre la efectividad de nuestras mejoras arquitectónicas y de entrenamiento.

import tensorflow as tf
from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
import numpy as np
from scipy.linalg import sqrtm

def calculate_fid(real_images, generated_images, batch_size=32):
    """
    Calculates the Fréchet Inception Distance (FID) between real and generated images.
    """
    inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

    def get_features(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        features = inception_model.predict(images, batch_size=batch_size)
        return features

    # Extract features
    real_features = get_features(real_images)
    generated_features = get_features(generated_images)

    # Compute mean and covariance of features
    mu1, sigma1 = np.mean(real_features, axis=0), np.cov(real_features, rowvar=False)
    mu2, sigma2 = np.mean(generated_features, axis=0), np.cov(generated_features, rowvar=False)

    # Compute squared mean difference
    ssdiff = np.sum((mu1 - mu2) ** 2.0)

    # Compute sqrt of covariance product (for numerical stability)
    covmean = sqrtm(sigma1.dot(sigma2))

    # Ensure the matrix is real-valued
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    # Compute final FID score
    fid = ssdiff + np.trace(sigma1 + sigma2 - 2.0 * covmean)
    return fid

def calculate_inception_score(images, batch_size=32, splits=10):
    """
    Computes the Inception Score (IS) for generated images.
    """
    inception_model = InceptionV3(include_top=True, weights="imagenet")  # Use full model

    def get_preds(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        preds = inception_model.predict(images, batch_size=batch_size)  # Get logits
        preds = tf.nn.softmax(preds).numpy()  # Convert logits to probabilities
        return preds

    # Get model predictions
    preds = get_preds(images)

    scores = []
    for i in range(splits):
        part = preds[i * (len(preds) // splits): (i + 1) * (len(preds) // splits), :]
        kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, 0), 0)))
        kl = np.mean(np.sum(kl, 1))
        scores.append(np.exp(kl))

    return np.mean(scores), np.std(scores)

Analicemos cada función:

1. FID (Distancia de Inception de Fréchet)

Compara imágenes reales vs. generadas para verificar la calidad.

Utiliza InceptionV3 para extraer características de las imágenes.

Mide la diferencia en las distribuciones de características (media y covarianza).

FID más bajo = Imágenes más realistas.

2. IS (Puntuación de Inception)

Verifica la calidad y diversidad de las imágenes generadas.

Utiliza InceptionV3 para clasificar imágenes.

Mide la nitidez (predicciones confiables) y la variación (distribución entre clases).

IS más alto = Mejor calidad y diversidad.

9.5.8 Conclusión

Este proyecto GAN incorpora varias técnicas avanzadas para mejorar la calidad de las imágenes generadas y la estabilidad del entrenamiento. Las mejoras principales incluyen:

  1. Una arquitectura más profunda y sofisticada tanto para el generador como para el discriminador.
  2. Pérdida de Wasserstein con penalización de gradiente para mejorar la estabilidad del entrenamiento.
  3. Crecimiento progresivo para generar imágenes de mayor resolución.
  4. Normalización espectral en el discriminador para prevenir gradientes explosivos.
  5. Mecanismo de auto-atención para capturar dependencias globales en las imágenes generadas.
  6. Un bucle de entrenamiento mejorado con programación de tasa de aprendizaje y parada temprana.
  7. Métricas de evaluación avanzadas (FID y Puntuación de Inception) para una mejor evaluación de la calidad de las imágenes generadas.

Estas mejoras deberían resultar en imágenes generadas de mayor calidad, un entrenamiento más estable y un mejor rendimiento general del GAN. Recuerda experimentar con hiperparámetros y arquitecturas para encontrar la configuración óptima para tu caso de uso específico.

9.5 Proyecto 5: Generación de imágenes basada en GAN

Las Redes Generativas Antagónicas (GAN) han marcado una nueva era en el ámbito de la generación de imágenes, revolucionando el campo con su enfoque innovador. Este ambicioso proyecto busca elevar la implementación original de GAN, específicamente adaptada para generar dígitos manuscritos a partir del ampliamente utilizado conjunto de datos MNIST.

Nuestro objetivo principal es incorporar una serie de mejoras de vanguardia diseñadas para aumentar significativamente el rendimiento general, mejorar la estabilidad del entrenamiento y elevar la calidad de las imágenes generadas a niveles sin precedentes.

Al aprovechar técnicas de última generación y mejoras arquitectónicas, buscamos superar los límites de lo que es posible con GAN. Estas mejoras no solo abordarán los desafíos comunes asociados con el entrenamiento de GAN, como el colapso de modo y los problemas de convergencia, sino que también introducirán características novedosas que prometen generar resultados más realistas y diversos.

A través de este proyecto, anticipamos demostrar todo el potencial de las GAN en la creación de imágenes de dígitos manuscritos de alta fidelidad que son prácticamente indistinguibles de sus contrapartes reales.

9.5.1 Arquitectura GAN mejorada

Para mejorar el rendimiento general y la capacidad de nuestra GAN, implementaremos una arquitectura más compleja y escalonada tanto para los componentes del generador como del discriminador. Esta estructura avanzada incorporará capas convolucionales adicionales, conexiones de salto y técnicas de normalización para mejorar la capacidad de la red para aprender características complejas y generar imágenes de alta calidad. Al aumentar la profundidad y la sofisticación de nuestros modelos, esperamos capturar patrones más detallados en los datos y producir imágenes de dígitos manuscritos más realistas y detalladas.

import tensorflow as tf
from tensorflow.keras import layers, models

def build_generator(latent_dim):
    model = models.Sequential([
        layers.Dense(7*7*256, use_bias=False, input_shape=(latent_dim,)),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        layers.Reshape((7, 7, 256)),
        
        layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')
    ])
    return model

def build_discriminator():
    model = models.Sequential([
        layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Flatten(),
        layers.Dense(1)
    ])
    return model

generator = build_generator(latent_dim=100)
discriminator = build_discriminator()

Desglose:

  1. Generador:
    • Toma un vector latente (ruido) como entrada.
    • Utiliza convoluciones transpuestas para aumentar la resolución de la entrada a una imagen de 28x28.
    • Incorpora normalización por lotes (batch normalization) y activaciones LeakyReLU para estabilidad y no linealidad.
    • La capa final utiliza activación tanh para producir una salida similar a una imagen.
  2. Discriminador:
    • Toma una imagen de 28x28 como entrada.
    • Utiliza capas convolucionales para reducir la resolución de la entrada.
    • Incorpora activaciones LeakyReLU y dropout para regularización.
    • La capa densa final genera un solo valor, que representa la probabilidad de que la entrada sea real.

La arquitectura está diseñada para generar y discriminar imágenes en escala de grises de 28x28, lo que se alinea con el formato del conjunto de datos MNIST. El uso de normalización por lotes, LeakyReLU y dropout ayuda a estabilizar el proceso de entrenamiento y a prevenir problemas como el colapso de modo.

9.5.2 Pérdida Wasserstein con penalización de gradiente

Para mejorar la estabilidad del entrenamiento y mitigar el colapso de modo, implementaremos la función de pérdida Wasserstein con penalización de gradiente. Esta técnica avanzada, conocida como WGAN-GP (Wasserstein GAN con penalización de gradiente), ofrece varias ventajas sobre las funciones de pérdida tradicionales de GAN.

Al utilizar la distancia Wasserstein como medida de disimilitud entre las distribuciones de datos reales y generados, podemos lograr una dinámica de entrenamiento más estable y generar imágenes de mayor calidad.

El término de penalización de gradiente refuerza además la restricción de Lipschitz en la función crítica (discriminador), ayudando a prevenir problemas como los gradientes que desaparecen y asegurando un proceso de entrenamiento más fluido. Esta implementación contribuirá significativamente a la robustez y rendimiento general de nuestro modelo GAN.

import tensorflow as tf

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = tf.reduce_mean(real_output)
    fake_loss = tf.reduce_mean(fake_output)
    return fake_loss - real_loss

def generator_loss(fake_output):
    return -tf.reduce_mean(fake_output)

def gradient_penalty(discriminator, real_images, fake_images):
    alpha = tf.random.uniform([real_images.shape[0], 1, 1, 1], 0.0, 1.0)
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    
    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = discriminator(interpolated, training=True)
    
    grads = gp_tape.gradient(pred, interpolated)
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3]))
    gp = tf.reduce_mean((norm - 1.0) ** 2)
    return gp

@tf.function
def train_step(images, batch_size, latent_dim):
    noise = tf.random.normal([batch_size, latent_dim])
    
    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)
        
        real_output = discriminator(images, training=True)
        fake_output = discriminator(generated_images, training=True)
        
        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)
        
        gp = gradient_penalty(discriminator, images, generated_images)
        disc_loss += 10 * gp
    
    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
    
    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
    
    return gen_loss, disc_loss

 

Desglose:

  1. Funciones de pérdida:
    • La función discriminator_loss calcula la pérdida Wasserstein para el discriminador.
    • La función generator_loss calcula la pérdida Wasserstein para el generador.
  2. Penalización de gradiente:
    • La función gradient_penalty implementa la penalización de gradiente, que ayuda a reforzar la restricción de Lipschitz en el discriminador.
  3. Paso de entrenamiento:
    • La función train_step define una única iteración de entrenamiento para el generador y el discriminador.
    • Genera imágenes falsas, calcula las pérdidas, aplica la penalización de gradiente y actualiza ambas redes.

Esta implementación tiene como objetivo mejorar la estabilidad del entrenamiento y mitigar problemas como el colapso de modo, que son desafíos comunes en el entrenamiento de GAN.

9.5.3 Crecimiento progresivo

Implementa el crecimiento progresivo como una técnica avanzada para aumentar gradualmente la resolución y complejidad de las imágenes generadas durante el proceso de entrenamiento. Este enfoque comienza con imágenes de baja resolución y va agregando capas progresivamente tanto al generador como al discriminador, lo que permite que el modelo aprenda características generales primero antes de enfocarse en detalles más finos.

Al hacerlo, podemos lograr una dinámica de entrenamiento más estable y potencialmente generar imágenes de mayor calidad a resoluciones más grandes. Este método ha demostrado un notable éxito en la producción de imágenes altamente realistas y puede mejorar significativamente el rendimiento general de nuestro modelo GAN para la generación de dígitos manuscritos.

def build_progressive_generator(latent_dim, target_resolution=28):
    model = models.Sequential()
    model.add(layers.Dense(4*4*256, use_bias=False, input_shape=(latent_dim,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU(alpha=0.2))
    model.add(layers.Reshape((4, 4, 256)))
    
    current_resolution = 4
    while current_resolution < target_resolution:
        model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False))
        model.add(layers.BatchNormalization())
        model.add(layers.LeakyReLU(alpha=0.2))
        current_resolution *= 2
    
    model.add(layers.Conv2D(1, (5, 5), padding='same', use_bias=False, activation='tanh'))
    return model

progressive_generator = build_progressive_generator(latent_dim=100)

Desglose del código:

  • La función toma dos parámetros: latent_dim (el tamaño del vector de ruido de entrada) y target_resolution (por defecto es 28, que coincide con el tamaño de las imágenes de MNIST).
  • Comienza creando un modelo base con una capa densa que se remodela en un tensor de 4x4x256, seguido de normalización por lotes (batch normalization) y activación LeakyReLU.
  • El núcleo de la técnica de crecimiento progresivo se implementa en el bucle while:
    • Sigue agregando capas de convolución transpuesta (upsampling) hasta que la resolución actual alcanza la resolución objetivo.
    • Cada iteración duplica la resolución (por ejemplo, de 4x4 → 8x8 → 16x16 → 28x28).
  • Cada paso de upsampling incluye una capa Conv2DTranspose, normalización por lotes y activación LeakyReLU.
  • La capa final es una capa Conv2D con activación tanh, que produce la imagen de salida.
  • Después de definir la función, se utiliza para crear un progressive_generator con una dimensión latente de 100.

Este enfoque de crecimiento progresivo permite que el modelo aprenda primero características gruesas antes de centrarse en detalles más finos, lo que potencialmente conduce a un entrenamiento más estable y a la generación de imágenes de mayor calidad.

9.5.4 Normalización Espectral

Implementar la normalización espectral en el discriminador para mejorar la estabilidad del entrenamiento y prevenir la aparición de gradientes explosivos. Esta técnica limita la constante de Lipschitz de la función del discriminador, restringiendo efectivamente el impacto de las perturbaciones individuales en la entrada sobre la salida.

Al aplicar normalización espectral a los pesos de las capas del discriminador, garantizamos que el valor singular más grande de las matrices de pesos esté limitado, lo que conduce a dinámicas de entrenamiento más consistentes y confiables. Se ha demostrado que este enfoque es particularmente efectivo para estabilizar el entrenamiento de GANs, especialmente al trabajar con arquitecturas complejas o conjuntos de datos desafiantes.

La implementación de la normalización espectral contribuye significativamente a la robustez general de nuestro modelo GAN, lo que podría resultar en imágenes generadas de mayor calidad y características de convergencia mejoradas.

from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.constraints import max_norm

class SpectralNormalization(tf.keras.constraints.Constraint):
    def __init__(self, iterations=1):
        self.iterations = iterations
    
    def __call__(self, w):
        w_shape = w.shape.as_list()
        w = tf.reshape(w, [-1, w_shape[-1]])
        u = tf.random.normal([1, w_shape[-1]])
        
        for _ in range(self.iterations):
            v = tf.matmul(u, tf.transpose(w))
            v = v / tf.norm(v)
            u = tf.matmul(v, w)
            u = u / tf.norm(u)
        
        sigma = tf.matmul(tf.matmul(v, w), tf.transpose(u))[0, 0]
        return w / sigma

def SpectralConv2D(filters, kernel_size, **kwargs):
    return Conv2D(filters, kernel_size, kernel_constraint=SpectralNormalization(), **kwargs)

def SpectralDense(units, **kwargs):
    return Dense(units, kernel_constraint=SpectralNormalization(), **kwargs)

Desglose del código:

  • Clase SpectralNormalization: Esta es una clase de restricción personalizada que aplica normalización espectral a los pesos de una capa. Funciona estimando la norma espectral de la matriz de pesos y utilizándola para normalizar los pesos.
  • Método call: Este método implementa el algoritmo principal de la normalización espectral. Usa la iteración de potencia para estimar el valor singular más grande (norma espectral) de la matriz de pesos y luego utiliza esto para normalizar los pesos.
  • Funciones SpectralConv2D y SpectralDense: Estas son funciones que envuelven la creación de capas Conv2D y Dense con la normalización espectral aplicada a sus kernels. Facilitan la adición de la normalización espectral a un modelo.

El propósito de la normalización espectral es limitar la constante de Lipschitz de la función del discriminador en un GAN. Esto ayuda a prevenir gradientes explosivos y estabiliza el proceso de entrenamiento, lo que potencialmente conduce a la generación de imágenes de mayor calidad y una mejor convergencia.

9.5.5 Mecanismo de Autoatención

Incorporar un mecanismo de autoatención para mejorar la capacidad del modelo de capturar dependencias globales en las imágenes generadas. Esta técnica avanzada permite que la red se enfoque en características relevantes en diferentes ubicaciones espaciales, lo que lleva a una mayor coherencia y detalle en la salida.

Al implementar capas de autoatención tanto en el generador como en el discriminador, habilitamos al modelo para aprender dependencias de largo alcance de manera más efectiva, lo que resulta en imágenes de dígitos manuscritos de mayor calidad y más realistas. Este enfoque ha mostrado un éxito notable en varias tareas de generación de imágenes y promete mejorar significativamente el rendimiento de nuestro modelo GAN.

import tensorflow as tf
from tensorflow.keras import layers

class SelfAttention(layers.Layer):
    def __init__(self, channels):
        super(SelfAttention, self).__init__()
        self.channels = channels
        
        # Conv layers for self-attention
        self.f = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.g = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.h = layers.Conv2D(channels, 1, kernel_initializer='he_normal')

        # Trainable scalar weight gamma
        self.gamma = self.add_weight(name='gamma', shape=(1,), initializer='zeros', trainable=True)

    def call(self, x):
        batch_size, height, width, channels = tf.unstack(tf.shape(x))

        # Compute f, g, h transformations
        f = self.f(x)  # Query
        g = self.g(x)  # Key
        h = self.h(x)  # Value

        # Reshape tensors for self-attention calculation
        f_flatten = tf.reshape(f, [batch_size, height * width, -1])  # (B, H*W, C//8)
        g_flatten = tf.reshape(g, [batch_size, height * width, -1])  # (B, H*W, C//8)
        h_flatten = tf.reshape(h, [batch_size, height * width, channels])  # (B, H*W, C)

        # Compute attention scores
        s = tf.matmul(g_flatten, f_flatten, transpose_b=True)  # (B, H*W, H*W)
        beta = tf.nn.softmax(s)  # Attention map (B, H*W, H*W)

        # Apply attention weights to h
        o = tf.matmul(beta, h_flatten)  # (B, H*W, C)
        o = tf.reshape(o, [batch_size, height, width, channels])  # Reshape back

        # Apply self-attention mechanism
        return self.gamma * o + x  # Weighted residual connection

Analicémoslo:

  1. La clase SelfAttention es una capa personalizada que hereda de layers.Layer
    • Esta capa implementa la autoatención, permitiendo que el modelo aprenda dependencias de largo alcance en una imagen.
    • Comúnmente utilizada en GANs, modelos de segmentación de imágenes y transformers.
  2. En el método __init__:
    • Se definen tres capas convolucionales (fg, y h), cada una con un kernel 1x1.
      • f: Aprende características de consulta (reduce la dimensionalidad).
      • g: Aprende características clave (reduce la dimensionalidad).
      • h: Aprende características de valor (mantiene la dimensionalidad original).
    • Se añade un parámetro entrenable gamma, inicializado en cero, para controlar la contribución del mecanismo de atención.
  3. El método call define el paso hacia adelante:
    • Extrae dimensiones espaciales dinámicamente (batch_size, height, width, channels) para asegurar la compatibilidad con la ejecución de TensorFlow.
    • Calcula transformaciones de características usando convoluciones Conv2D(1x1):
      • f(x): Genera la representación de consulta.
      • g(x): Genera la representación clave.
      • h(x): Genera la representación de valor.
    • Calcula el mapa de atención:
      • Multiplica g y f (similitud por producto punto).
      • Aplica softmax para normalizar los puntajes de atención.
    • Aplica el mapa de atención a h (suma ponderada de características atendidas).
    • Utiliza una conexión residual (gamma * o + x) para combinar la entrada original con la salida de atención.
  4. ¿Por qué es importante?
    • Este mecanismo de autoatención permite que el modelo se enfoque en características relevantes a través de diferentes ubicaciones espaciales.
    • Particularmente útil en tareas de generación de imágenes (GANs) para mejorar la calidad y coherencia de las imágenes generadas.
    • Ayuda a capturar dependencias de largo alcance, a diferencia de las capas convolucionales, que tienen campos receptivos locales.

9.5.6 Bucle de Entrenamiento Mejorado

Mejora el proceso de entrenamiento mediante la implementación de un bucle de entrenamiento avanzado que incorpora ajustes dinámicos de la tasa de aprendizaje y mecanismos inteligentes de parada temprana. Este enfoque sofisticado adapta la tasa de aprendizaje a lo largo del tiempo para optimizar la convergencia y termina automáticamente el entrenamiento cuando el rendimiento se estabiliza, asegurando un uso eficiente de los recursos computacionales y previniendo el sobreajuste.

Las características principales de este bucle de entrenamiento mejorado incluyen:

  • Programación de la tasa de aprendizaje: Utiliza técnicas adaptativas de tasa de aprendizaje como la degradación exponencial o el recocido coseno para reducir gradualmente la tasa de aprendizaje conforme avanza el entrenamiento, permitiendo el ajuste fino de los parámetros del modelo.
  • Parada temprana: Implementa un criterio de parada temprana basado en paciencia que monitorea una métrica de rendimiento relevante (por ejemplo, la puntuación FID) y detiene el entrenamiento si no se observa mejora durante un número específico de épocas.
  • Guardado de puntos de control: Guarda regularmente puntos de control del modelo durante el entrenamiento, preservando las iteraciones del modelo con mejor rendimiento para uso o evaluación posterior.
  • Monitoreo del progreso: Integra herramientas completas de registro y visualización para seguir métricas clave, permitiendo la evaluación en tiempo real del rendimiento del modelo y la dinámica del entrenamiento.
from tensorflow.keras.optimizers.schedules import ExponentialDecay
from tensorflow.keras.optimizers import Adam
import tensorflow as tf

# Learning rate schedule
initial_learning_rate = 0.0002
lr_schedule = ExponentialDecay(initial_learning_rate, decay_steps=10000, decay_rate=0.96, staircase=True)

# Optimizers
generator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)
discriminator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)

# Number of samples for visualization
num_samples = 16  # Adjust based on needs
LATENT_DIM = 100  # Ensure consistency

# Generate a fixed noise seed for consistent visualization
seed = tf.random.normal([num_samples, LATENT_DIM])

def train(dataset, epochs, batch_size, latent_dim):
    best_fid = float('inf')
    patience = 10
    no_improvement = 0
    
    for epoch in range(epochs):
        for batch in dataset:
            gen_loss, disc_loss = train_step(batch, batch_size, latent_dim)
        
        print(f"Epoch {epoch + 1}, Gen Loss: {gen_loss:.4f}, Disc Loss: {disc_loss:.4f}")
        
        if (epoch + 1) % 10 == 0:
            generate_and_save_images(generator, epoch + 1, seed)
            
            # Generate fake images
            generated_images = generator(seed, training=False)
            
            # Select a batch of real images for FID calculation
            real_images = next(iter(dataset))[:num_samples]

            current_fid = calculate_fid(real_images, generated_images)
            
            if current_fid < best_fid:
                best_fid = current_fid
                no_improvement = 0
                
                # Save model properly
                generator.save(f"generator_epoch_{epoch + 1}.h5")
            else:
                no_improvement += 1
            
            if no_improvement >= patience:
                print(f"Early stopping at epoch {epoch + 1}")
                break

# Ensure dataset is properly defined
train(train_dataset, EPOCHS, BATCH_SIZE, LATENT_DIM)

Aquí está el desglose del código:

  1. Programación de la Tasa de Aprendizaje:
    • Utiliza una programación de DecaimientoExponencial para reducir gradualmente la tasa de aprendizaje, ayudando a ajustar los parámetros del modelo.
    • Esto previene la inestabilidad en el entrenamiento de GAN al reducir actualizaciones grandes y repentinas en los pesos.
  2. Optimizadores:
    • Utiliza optimizadores Adam tanto para el generador como para el discriminador, con:
      • Una tasa de aprendizaje decreciente (lr_schedule).
      • beta_1=0.5, que es común en el entrenamiento de GAN para estabilizar las actualizaciones.
  3. Bucle de Entrenamiento:
    • Itera a través de épocas y lotes, llamando a train_step() (no mostrado) para actualizar los pesos del generador y el discriminador.
    • Cada actualización de lote mejora la capacidad del generador para crear muestras más realistas y la capacidad del discriminador para distinguir entre imágenes reales y falsas.
  4. Evaluación Periódica (cada 10 épocas):
    • Genera y guarda imágenes usando un ruido aleatorio fijo seed para seguir el progreso.
    • Calcula la puntuación de Distancia Inception de Fréchet (FID), una métrica ampliamente utilizada para evaluar la calidad y diversidad de las imágenes generadas.
  5. Guardado del Modelo:
    • Guarda el modelo generador (generator.save()) cuando se logra una nueva mejor puntuación FID.
    • Ayuda a preservar el generador con mejor rendimiento en lugar de solo la época final.
  6. Parada Temprana:
    • Si no hay mejora en FID durante un número establecido de épocas de paciencia (por ejemplo, 10 épocas), el entrenamiento se detiene anticipadamente.
    • Previene el sobreajuste, ahorra computación y detiene el colapso modal (fallo de GAN donde el generador produce solo algunas imágenes similares).

9.5.7 Métricas de Evaluación

Implementa y utiliza métricas avanzadas de evaluación para valorar la calidad y diversidad de las imágenes generadas. Las dos métricas clave en las que nos enfocaremos son:

  1. Fréchet Inception Distance (FID): Esta métrica mide la similitud entre las imágenes reales y las generadas comparando sus representaciones de características extraídas de una red Inception preentrenada. Un puntaje FID más bajo indica imágenes generadas de mayor calidad y más realistas.
  2. Inception Score (IS): Esta métrica evalúa tanto la calidad como la diversidad de las imágenes generadas. Utiliza una red Inception preentrenada para medir qué tan bien las imágenes generadas pueden clasificarse en categorías distintas. Un puntaje de Inception más alto sugiere imágenes generadas de mejor calidad y más diversas.

Al incorporar estas métricas en nuestro proceso de evaluación, podemos valorar cuantitativamente el rendimiento de nuestro modelo GAN y rastrear mejoras a lo largo del tiempo. Esto proporcionará valiosas ideas sobre la efectividad de nuestras mejoras arquitectónicas y de entrenamiento.

import tensorflow as tf
from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
import numpy as np
from scipy.linalg import sqrtm

def calculate_fid(real_images, generated_images, batch_size=32):
    """
    Calculates the Fréchet Inception Distance (FID) between real and generated images.
    """
    inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

    def get_features(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        features = inception_model.predict(images, batch_size=batch_size)
        return features

    # Extract features
    real_features = get_features(real_images)
    generated_features = get_features(generated_images)

    # Compute mean and covariance of features
    mu1, sigma1 = np.mean(real_features, axis=0), np.cov(real_features, rowvar=False)
    mu2, sigma2 = np.mean(generated_features, axis=0), np.cov(generated_features, rowvar=False)

    # Compute squared mean difference
    ssdiff = np.sum((mu1 - mu2) ** 2.0)

    # Compute sqrt of covariance product (for numerical stability)
    covmean = sqrtm(sigma1.dot(sigma2))

    # Ensure the matrix is real-valued
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    # Compute final FID score
    fid = ssdiff + np.trace(sigma1 + sigma2 - 2.0 * covmean)
    return fid

def calculate_inception_score(images, batch_size=32, splits=10):
    """
    Computes the Inception Score (IS) for generated images.
    """
    inception_model = InceptionV3(include_top=True, weights="imagenet")  # Use full model

    def get_preds(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        preds = inception_model.predict(images, batch_size=batch_size)  # Get logits
        preds = tf.nn.softmax(preds).numpy()  # Convert logits to probabilities
        return preds

    # Get model predictions
    preds = get_preds(images)

    scores = []
    for i in range(splits):
        part = preds[i * (len(preds) // splits): (i + 1) * (len(preds) // splits), :]
        kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, 0), 0)))
        kl = np.mean(np.sum(kl, 1))
        scores.append(np.exp(kl))

    return np.mean(scores), np.std(scores)

Analicemos cada función:

1. FID (Distancia de Inception de Fréchet)

Compara imágenes reales vs. generadas para verificar la calidad.

Utiliza InceptionV3 para extraer características de las imágenes.

Mide la diferencia en las distribuciones de características (media y covarianza).

FID más bajo = Imágenes más realistas.

2. IS (Puntuación de Inception)

Verifica la calidad y diversidad de las imágenes generadas.

Utiliza InceptionV3 para clasificar imágenes.

Mide la nitidez (predicciones confiables) y la variación (distribución entre clases).

IS más alto = Mejor calidad y diversidad.

9.5.8 Conclusión

Este proyecto GAN incorpora varias técnicas avanzadas para mejorar la calidad de las imágenes generadas y la estabilidad del entrenamiento. Las mejoras principales incluyen:

  1. Una arquitectura más profunda y sofisticada tanto para el generador como para el discriminador.
  2. Pérdida de Wasserstein con penalización de gradiente para mejorar la estabilidad del entrenamiento.
  3. Crecimiento progresivo para generar imágenes de mayor resolución.
  4. Normalización espectral en el discriminador para prevenir gradientes explosivos.
  5. Mecanismo de auto-atención para capturar dependencias globales en las imágenes generadas.
  6. Un bucle de entrenamiento mejorado con programación de tasa de aprendizaje y parada temprana.
  7. Métricas de evaluación avanzadas (FID y Puntuación de Inception) para una mejor evaluación de la calidad de las imágenes generadas.

Estas mejoras deberían resultar en imágenes generadas de mayor calidad, un entrenamiento más estable y un mejor rendimiento general del GAN. Recuerda experimentar con hiperparámetros y arquitecturas para encontrar la configuración óptima para tu caso de uso específico.

9.5 Proyecto 5: Generación de imágenes basada en GAN

Las Redes Generativas Antagónicas (GAN) han marcado una nueva era en el ámbito de la generación de imágenes, revolucionando el campo con su enfoque innovador. Este ambicioso proyecto busca elevar la implementación original de GAN, específicamente adaptada para generar dígitos manuscritos a partir del ampliamente utilizado conjunto de datos MNIST.

Nuestro objetivo principal es incorporar una serie de mejoras de vanguardia diseñadas para aumentar significativamente el rendimiento general, mejorar la estabilidad del entrenamiento y elevar la calidad de las imágenes generadas a niveles sin precedentes.

Al aprovechar técnicas de última generación y mejoras arquitectónicas, buscamos superar los límites de lo que es posible con GAN. Estas mejoras no solo abordarán los desafíos comunes asociados con el entrenamiento de GAN, como el colapso de modo y los problemas de convergencia, sino que también introducirán características novedosas que prometen generar resultados más realistas y diversos.

A través de este proyecto, anticipamos demostrar todo el potencial de las GAN en la creación de imágenes de dígitos manuscritos de alta fidelidad que son prácticamente indistinguibles de sus contrapartes reales.

9.5.1 Arquitectura GAN mejorada

Para mejorar el rendimiento general y la capacidad de nuestra GAN, implementaremos una arquitectura más compleja y escalonada tanto para los componentes del generador como del discriminador. Esta estructura avanzada incorporará capas convolucionales adicionales, conexiones de salto y técnicas de normalización para mejorar la capacidad de la red para aprender características complejas y generar imágenes de alta calidad. Al aumentar la profundidad y la sofisticación de nuestros modelos, esperamos capturar patrones más detallados en los datos y producir imágenes de dígitos manuscritos más realistas y detalladas.

import tensorflow as tf
from tensorflow.keras import layers, models

def build_generator(latent_dim):
    model = models.Sequential([
        layers.Dense(7*7*256, use_bias=False, input_shape=(latent_dim,)),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        layers.Reshape((7, 7, 256)),
        
        layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),
        layers.BatchNormalization(),
        layers.LeakyReLU(alpha=0.2),
        
        layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')
    ])
    return model

def build_discriminator():
    model = models.Sequential([
        layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),
        layers.LeakyReLU(alpha=0.2),
        layers.Dropout(0.3),
        
        layers.Flatten(),
        layers.Dense(1)
    ])
    return model

generator = build_generator(latent_dim=100)
discriminator = build_discriminator()

Desglose:

  1. Generador:
    • Toma un vector latente (ruido) como entrada.
    • Utiliza convoluciones transpuestas para aumentar la resolución de la entrada a una imagen de 28x28.
    • Incorpora normalización por lotes (batch normalization) y activaciones LeakyReLU para estabilidad y no linealidad.
    • La capa final utiliza activación tanh para producir una salida similar a una imagen.
  2. Discriminador:
    • Toma una imagen de 28x28 como entrada.
    • Utiliza capas convolucionales para reducir la resolución de la entrada.
    • Incorpora activaciones LeakyReLU y dropout para regularización.
    • La capa densa final genera un solo valor, que representa la probabilidad de que la entrada sea real.

La arquitectura está diseñada para generar y discriminar imágenes en escala de grises de 28x28, lo que se alinea con el formato del conjunto de datos MNIST. El uso de normalización por lotes, LeakyReLU y dropout ayuda a estabilizar el proceso de entrenamiento y a prevenir problemas como el colapso de modo.

9.5.2 Pérdida Wasserstein con penalización de gradiente

Para mejorar la estabilidad del entrenamiento y mitigar el colapso de modo, implementaremos la función de pérdida Wasserstein con penalización de gradiente. Esta técnica avanzada, conocida como WGAN-GP (Wasserstein GAN con penalización de gradiente), ofrece varias ventajas sobre las funciones de pérdida tradicionales de GAN.

Al utilizar la distancia Wasserstein como medida de disimilitud entre las distribuciones de datos reales y generados, podemos lograr una dinámica de entrenamiento más estable y generar imágenes de mayor calidad.

El término de penalización de gradiente refuerza además la restricción de Lipschitz en la función crítica (discriminador), ayudando a prevenir problemas como los gradientes que desaparecen y asegurando un proceso de entrenamiento más fluido. Esta implementación contribuirá significativamente a la robustez y rendimiento general de nuestro modelo GAN.

import tensorflow as tf

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

def discriminator_loss(real_output, fake_output):
    real_loss = tf.reduce_mean(real_output)
    fake_loss = tf.reduce_mean(fake_output)
    return fake_loss - real_loss

def generator_loss(fake_output):
    return -tf.reduce_mean(fake_output)

def gradient_penalty(discriminator, real_images, fake_images):
    alpha = tf.random.uniform([real_images.shape[0], 1, 1, 1], 0.0, 1.0)
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    
    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = discriminator(interpolated, training=True)
    
    grads = gp_tape.gradient(pred, interpolated)
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3]))
    gp = tf.reduce_mean((norm - 1.0) ** 2)
    return gp

@tf.function
def train_step(images, batch_size, latent_dim):
    noise = tf.random.normal([batch_size, latent_dim])
    
    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)
        
        real_output = discriminator(images, training=True)
        fake_output = discriminator(generated_images, training=True)
        
        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)
        
        gp = gradient_penalty(discriminator, images, generated_images)
        disc_loss += 10 * gp
    
    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
    
    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
    
    return gen_loss, disc_loss

 

Desglose:

  1. Funciones de pérdida:
    • La función discriminator_loss calcula la pérdida Wasserstein para el discriminador.
    • La función generator_loss calcula la pérdida Wasserstein para el generador.
  2. Penalización de gradiente:
    • La función gradient_penalty implementa la penalización de gradiente, que ayuda a reforzar la restricción de Lipschitz en el discriminador.
  3. Paso de entrenamiento:
    • La función train_step define una única iteración de entrenamiento para el generador y el discriminador.
    • Genera imágenes falsas, calcula las pérdidas, aplica la penalización de gradiente y actualiza ambas redes.

Esta implementación tiene como objetivo mejorar la estabilidad del entrenamiento y mitigar problemas como el colapso de modo, que son desafíos comunes en el entrenamiento de GAN.

9.5.3 Crecimiento progresivo

Implementa el crecimiento progresivo como una técnica avanzada para aumentar gradualmente la resolución y complejidad de las imágenes generadas durante el proceso de entrenamiento. Este enfoque comienza con imágenes de baja resolución y va agregando capas progresivamente tanto al generador como al discriminador, lo que permite que el modelo aprenda características generales primero antes de enfocarse en detalles más finos.

Al hacerlo, podemos lograr una dinámica de entrenamiento más estable y potencialmente generar imágenes de mayor calidad a resoluciones más grandes. Este método ha demostrado un notable éxito en la producción de imágenes altamente realistas y puede mejorar significativamente el rendimiento general de nuestro modelo GAN para la generación de dígitos manuscritos.

def build_progressive_generator(latent_dim, target_resolution=28):
    model = models.Sequential()
    model.add(layers.Dense(4*4*256, use_bias=False, input_shape=(latent_dim,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU(alpha=0.2))
    model.add(layers.Reshape((4, 4, 256)))
    
    current_resolution = 4
    while current_resolution < target_resolution:
        model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False))
        model.add(layers.BatchNormalization())
        model.add(layers.LeakyReLU(alpha=0.2))
        current_resolution *= 2
    
    model.add(layers.Conv2D(1, (5, 5), padding='same', use_bias=False, activation='tanh'))
    return model

progressive_generator = build_progressive_generator(latent_dim=100)

Desglose del código:

  • La función toma dos parámetros: latent_dim (el tamaño del vector de ruido de entrada) y target_resolution (por defecto es 28, que coincide con el tamaño de las imágenes de MNIST).
  • Comienza creando un modelo base con una capa densa que se remodela en un tensor de 4x4x256, seguido de normalización por lotes (batch normalization) y activación LeakyReLU.
  • El núcleo de la técnica de crecimiento progresivo se implementa en el bucle while:
    • Sigue agregando capas de convolución transpuesta (upsampling) hasta que la resolución actual alcanza la resolución objetivo.
    • Cada iteración duplica la resolución (por ejemplo, de 4x4 → 8x8 → 16x16 → 28x28).
  • Cada paso de upsampling incluye una capa Conv2DTranspose, normalización por lotes y activación LeakyReLU.
  • La capa final es una capa Conv2D con activación tanh, que produce la imagen de salida.
  • Después de definir la función, se utiliza para crear un progressive_generator con una dimensión latente de 100.

Este enfoque de crecimiento progresivo permite que el modelo aprenda primero características gruesas antes de centrarse en detalles más finos, lo que potencialmente conduce a un entrenamiento más estable y a la generación de imágenes de mayor calidad.

9.5.4 Normalización Espectral

Implementar la normalización espectral en el discriminador para mejorar la estabilidad del entrenamiento y prevenir la aparición de gradientes explosivos. Esta técnica limita la constante de Lipschitz de la función del discriminador, restringiendo efectivamente el impacto de las perturbaciones individuales en la entrada sobre la salida.

Al aplicar normalización espectral a los pesos de las capas del discriminador, garantizamos que el valor singular más grande de las matrices de pesos esté limitado, lo que conduce a dinámicas de entrenamiento más consistentes y confiables. Se ha demostrado que este enfoque es particularmente efectivo para estabilizar el entrenamiento de GANs, especialmente al trabajar con arquitecturas complejas o conjuntos de datos desafiantes.

La implementación de la normalización espectral contribuye significativamente a la robustez general de nuestro modelo GAN, lo que podría resultar en imágenes generadas de mayor calidad y características de convergencia mejoradas.

from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.constraints import max_norm

class SpectralNormalization(tf.keras.constraints.Constraint):
    def __init__(self, iterations=1):
        self.iterations = iterations
    
    def __call__(self, w):
        w_shape = w.shape.as_list()
        w = tf.reshape(w, [-1, w_shape[-1]])
        u = tf.random.normal([1, w_shape[-1]])
        
        for _ in range(self.iterations):
            v = tf.matmul(u, tf.transpose(w))
            v = v / tf.norm(v)
            u = tf.matmul(v, w)
            u = u / tf.norm(u)
        
        sigma = tf.matmul(tf.matmul(v, w), tf.transpose(u))[0, 0]
        return w / sigma

def SpectralConv2D(filters, kernel_size, **kwargs):
    return Conv2D(filters, kernel_size, kernel_constraint=SpectralNormalization(), **kwargs)

def SpectralDense(units, **kwargs):
    return Dense(units, kernel_constraint=SpectralNormalization(), **kwargs)

Desglose del código:

  • Clase SpectralNormalization: Esta es una clase de restricción personalizada que aplica normalización espectral a los pesos de una capa. Funciona estimando la norma espectral de la matriz de pesos y utilizándola para normalizar los pesos.
  • Método call: Este método implementa el algoritmo principal de la normalización espectral. Usa la iteración de potencia para estimar el valor singular más grande (norma espectral) de la matriz de pesos y luego utiliza esto para normalizar los pesos.
  • Funciones SpectralConv2D y SpectralDense: Estas son funciones que envuelven la creación de capas Conv2D y Dense con la normalización espectral aplicada a sus kernels. Facilitan la adición de la normalización espectral a un modelo.

El propósito de la normalización espectral es limitar la constante de Lipschitz de la función del discriminador en un GAN. Esto ayuda a prevenir gradientes explosivos y estabiliza el proceso de entrenamiento, lo que potencialmente conduce a la generación de imágenes de mayor calidad y una mejor convergencia.

9.5.5 Mecanismo de Autoatención

Incorporar un mecanismo de autoatención para mejorar la capacidad del modelo de capturar dependencias globales en las imágenes generadas. Esta técnica avanzada permite que la red se enfoque en características relevantes en diferentes ubicaciones espaciales, lo que lleva a una mayor coherencia y detalle en la salida.

Al implementar capas de autoatención tanto en el generador como en el discriminador, habilitamos al modelo para aprender dependencias de largo alcance de manera más efectiva, lo que resulta en imágenes de dígitos manuscritos de mayor calidad y más realistas. Este enfoque ha mostrado un éxito notable en varias tareas de generación de imágenes y promete mejorar significativamente el rendimiento de nuestro modelo GAN.

import tensorflow as tf
from tensorflow.keras import layers

class SelfAttention(layers.Layer):
    def __init__(self, channels):
        super(SelfAttention, self).__init__()
        self.channels = channels
        
        # Conv layers for self-attention
        self.f = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.g = layers.Conv2D(channels // 8, 1, kernel_initializer='he_normal')
        self.h = layers.Conv2D(channels, 1, kernel_initializer='he_normal')

        # Trainable scalar weight gamma
        self.gamma = self.add_weight(name='gamma', shape=(1,), initializer='zeros', trainable=True)

    def call(self, x):
        batch_size, height, width, channels = tf.unstack(tf.shape(x))

        # Compute f, g, h transformations
        f = self.f(x)  # Query
        g = self.g(x)  # Key
        h = self.h(x)  # Value

        # Reshape tensors for self-attention calculation
        f_flatten = tf.reshape(f, [batch_size, height * width, -1])  # (B, H*W, C//8)
        g_flatten = tf.reshape(g, [batch_size, height * width, -1])  # (B, H*W, C//8)
        h_flatten = tf.reshape(h, [batch_size, height * width, channels])  # (B, H*W, C)

        # Compute attention scores
        s = tf.matmul(g_flatten, f_flatten, transpose_b=True)  # (B, H*W, H*W)
        beta = tf.nn.softmax(s)  # Attention map (B, H*W, H*W)

        # Apply attention weights to h
        o = tf.matmul(beta, h_flatten)  # (B, H*W, C)
        o = tf.reshape(o, [batch_size, height, width, channels])  # Reshape back

        # Apply self-attention mechanism
        return self.gamma * o + x  # Weighted residual connection

Analicémoslo:

  1. La clase SelfAttention es una capa personalizada que hereda de layers.Layer
    • Esta capa implementa la autoatención, permitiendo que el modelo aprenda dependencias de largo alcance en una imagen.
    • Comúnmente utilizada en GANs, modelos de segmentación de imágenes y transformers.
  2. En el método __init__:
    • Se definen tres capas convolucionales (fg, y h), cada una con un kernel 1x1.
      • f: Aprende características de consulta (reduce la dimensionalidad).
      • g: Aprende características clave (reduce la dimensionalidad).
      • h: Aprende características de valor (mantiene la dimensionalidad original).
    • Se añade un parámetro entrenable gamma, inicializado en cero, para controlar la contribución del mecanismo de atención.
  3. El método call define el paso hacia adelante:
    • Extrae dimensiones espaciales dinámicamente (batch_size, height, width, channels) para asegurar la compatibilidad con la ejecución de TensorFlow.
    • Calcula transformaciones de características usando convoluciones Conv2D(1x1):
      • f(x): Genera la representación de consulta.
      • g(x): Genera la representación clave.
      • h(x): Genera la representación de valor.
    • Calcula el mapa de atención:
      • Multiplica g y f (similitud por producto punto).
      • Aplica softmax para normalizar los puntajes de atención.
    • Aplica el mapa de atención a h (suma ponderada de características atendidas).
    • Utiliza una conexión residual (gamma * o + x) para combinar la entrada original con la salida de atención.
  4. ¿Por qué es importante?
    • Este mecanismo de autoatención permite que el modelo se enfoque en características relevantes a través de diferentes ubicaciones espaciales.
    • Particularmente útil en tareas de generación de imágenes (GANs) para mejorar la calidad y coherencia de las imágenes generadas.
    • Ayuda a capturar dependencias de largo alcance, a diferencia de las capas convolucionales, que tienen campos receptivos locales.

9.5.6 Bucle de Entrenamiento Mejorado

Mejora el proceso de entrenamiento mediante la implementación de un bucle de entrenamiento avanzado que incorpora ajustes dinámicos de la tasa de aprendizaje y mecanismos inteligentes de parada temprana. Este enfoque sofisticado adapta la tasa de aprendizaje a lo largo del tiempo para optimizar la convergencia y termina automáticamente el entrenamiento cuando el rendimiento se estabiliza, asegurando un uso eficiente de los recursos computacionales y previniendo el sobreajuste.

Las características principales de este bucle de entrenamiento mejorado incluyen:

  • Programación de la tasa de aprendizaje: Utiliza técnicas adaptativas de tasa de aprendizaje como la degradación exponencial o el recocido coseno para reducir gradualmente la tasa de aprendizaje conforme avanza el entrenamiento, permitiendo el ajuste fino de los parámetros del modelo.
  • Parada temprana: Implementa un criterio de parada temprana basado en paciencia que monitorea una métrica de rendimiento relevante (por ejemplo, la puntuación FID) y detiene el entrenamiento si no se observa mejora durante un número específico de épocas.
  • Guardado de puntos de control: Guarda regularmente puntos de control del modelo durante el entrenamiento, preservando las iteraciones del modelo con mejor rendimiento para uso o evaluación posterior.
  • Monitoreo del progreso: Integra herramientas completas de registro y visualización para seguir métricas clave, permitiendo la evaluación en tiempo real del rendimiento del modelo y la dinámica del entrenamiento.
from tensorflow.keras.optimizers.schedules import ExponentialDecay
from tensorflow.keras.optimizers import Adam
import tensorflow as tf

# Learning rate schedule
initial_learning_rate = 0.0002
lr_schedule = ExponentialDecay(initial_learning_rate, decay_steps=10000, decay_rate=0.96, staircase=True)

# Optimizers
generator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)
discriminator_optimizer = Adam(learning_rate=lr_schedule, beta_1=0.5)

# Number of samples for visualization
num_samples = 16  # Adjust based on needs
LATENT_DIM = 100  # Ensure consistency

# Generate a fixed noise seed for consistent visualization
seed = tf.random.normal([num_samples, LATENT_DIM])

def train(dataset, epochs, batch_size, latent_dim):
    best_fid = float('inf')
    patience = 10
    no_improvement = 0
    
    for epoch in range(epochs):
        for batch in dataset:
            gen_loss, disc_loss = train_step(batch, batch_size, latent_dim)
        
        print(f"Epoch {epoch + 1}, Gen Loss: {gen_loss:.4f}, Disc Loss: {disc_loss:.4f}")
        
        if (epoch + 1) % 10 == 0:
            generate_and_save_images(generator, epoch + 1, seed)
            
            # Generate fake images
            generated_images = generator(seed, training=False)
            
            # Select a batch of real images for FID calculation
            real_images = next(iter(dataset))[:num_samples]

            current_fid = calculate_fid(real_images, generated_images)
            
            if current_fid < best_fid:
                best_fid = current_fid
                no_improvement = 0
                
                # Save model properly
                generator.save(f"generator_epoch_{epoch + 1}.h5")
            else:
                no_improvement += 1
            
            if no_improvement >= patience:
                print(f"Early stopping at epoch {epoch + 1}")
                break

# Ensure dataset is properly defined
train(train_dataset, EPOCHS, BATCH_SIZE, LATENT_DIM)

Aquí está el desglose del código:

  1. Programación de la Tasa de Aprendizaje:
    • Utiliza una programación de DecaimientoExponencial para reducir gradualmente la tasa de aprendizaje, ayudando a ajustar los parámetros del modelo.
    • Esto previene la inestabilidad en el entrenamiento de GAN al reducir actualizaciones grandes y repentinas en los pesos.
  2. Optimizadores:
    • Utiliza optimizadores Adam tanto para el generador como para el discriminador, con:
      • Una tasa de aprendizaje decreciente (lr_schedule).
      • beta_1=0.5, que es común en el entrenamiento de GAN para estabilizar las actualizaciones.
  3. Bucle de Entrenamiento:
    • Itera a través de épocas y lotes, llamando a train_step() (no mostrado) para actualizar los pesos del generador y el discriminador.
    • Cada actualización de lote mejora la capacidad del generador para crear muestras más realistas y la capacidad del discriminador para distinguir entre imágenes reales y falsas.
  4. Evaluación Periódica (cada 10 épocas):
    • Genera y guarda imágenes usando un ruido aleatorio fijo seed para seguir el progreso.
    • Calcula la puntuación de Distancia Inception de Fréchet (FID), una métrica ampliamente utilizada para evaluar la calidad y diversidad de las imágenes generadas.
  5. Guardado del Modelo:
    • Guarda el modelo generador (generator.save()) cuando se logra una nueva mejor puntuación FID.
    • Ayuda a preservar el generador con mejor rendimiento en lugar de solo la época final.
  6. Parada Temprana:
    • Si no hay mejora en FID durante un número establecido de épocas de paciencia (por ejemplo, 10 épocas), el entrenamiento se detiene anticipadamente.
    • Previene el sobreajuste, ahorra computación y detiene el colapso modal (fallo de GAN donde el generador produce solo algunas imágenes similares).

9.5.7 Métricas de Evaluación

Implementa y utiliza métricas avanzadas de evaluación para valorar la calidad y diversidad de las imágenes generadas. Las dos métricas clave en las que nos enfocaremos son:

  1. Fréchet Inception Distance (FID): Esta métrica mide la similitud entre las imágenes reales y las generadas comparando sus representaciones de características extraídas de una red Inception preentrenada. Un puntaje FID más bajo indica imágenes generadas de mayor calidad y más realistas.
  2. Inception Score (IS): Esta métrica evalúa tanto la calidad como la diversidad de las imágenes generadas. Utiliza una red Inception preentrenada para medir qué tan bien las imágenes generadas pueden clasificarse en categorías distintas. Un puntaje de Inception más alto sugiere imágenes generadas de mejor calidad y más diversas.

Al incorporar estas métricas en nuestro proceso de evaluación, podemos valorar cuantitativamente el rendimiento de nuestro modelo GAN y rastrear mejoras a lo largo del tiempo. Esto proporcionará valiosas ideas sobre la efectividad de nuestras mejoras arquitectónicas y de entrenamiento.

import tensorflow as tf
from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
import numpy as np
from scipy.linalg import sqrtm

def calculate_fid(real_images, generated_images, batch_size=32):
    """
    Calculates the Fréchet Inception Distance (FID) between real and generated images.
    """
    inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

    def get_features(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        features = inception_model.predict(images, batch_size=batch_size)
        return features

    # Extract features
    real_features = get_features(real_images)
    generated_features = get_features(generated_images)

    # Compute mean and covariance of features
    mu1, sigma1 = np.mean(real_features, axis=0), np.cov(real_features, rowvar=False)
    mu2, sigma2 = np.mean(generated_features, axis=0), np.cov(generated_features, rowvar=False)

    # Compute squared mean difference
    ssdiff = np.sum((mu1 - mu2) ** 2.0)

    # Compute sqrt of covariance product (for numerical stability)
    covmean = sqrtm(sigma1.dot(sigma2))

    # Ensure the matrix is real-valued
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    # Compute final FID score
    fid = ssdiff + np.trace(sigma1 + sigma2 - 2.0 * covmean)
    return fid

def calculate_inception_score(images, batch_size=32, splits=10):
    """
    Computes the Inception Score (IS) for generated images.
    """
    inception_model = InceptionV3(include_top=True, weights="imagenet")  # Use full model

    def get_preds(images):
        images = tf.image.resize(images, (299, 299))  # Resize images
        images = preprocess_input(images)  # Normalize to [-1, 1]
        preds = inception_model.predict(images, batch_size=batch_size)  # Get logits
        preds = tf.nn.softmax(preds).numpy()  # Convert logits to probabilities
        return preds

    # Get model predictions
    preds = get_preds(images)

    scores = []
    for i in range(splits):
        part = preds[i * (len(preds) // splits): (i + 1) * (len(preds) // splits), :]
        kl = part * (np.log(part) - np.log(np.expand_dims(np.mean(part, 0), 0)))
        kl = np.mean(np.sum(kl, 1))
        scores.append(np.exp(kl))

    return np.mean(scores), np.std(scores)

Analicemos cada función:

1. FID (Distancia de Inception de Fréchet)

Compara imágenes reales vs. generadas para verificar la calidad.

Utiliza InceptionV3 para extraer características de las imágenes.

Mide la diferencia en las distribuciones de características (media y covarianza).

FID más bajo = Imágenes más realistas.

2. IS (Puntuación de Inception)

Verifica la calidad y diversidad de las imágenes generadas.

Utiliza InceptionV3 para clasificar imágenes.

Mide la nitidez (predicciones confiables) y la variación (distribución entre clases).

IS más alto = Mejor calidad y diversidad.

9.5.8 Conclusión

Este proyecto GAN incorpora varias técnicas avanzadas para mejorar la calidad de las imágenes generadas y la estabilidad del entrenamiento. Las mejoras principales incluyen:

  1. Una arquitectura más profunda y sofisticada tanto para el generador como para el discriminador.
  2. Pérdida de Wasserstein con penalización de gradiente para mejorar la estabilidad del entrenamiento.
  3. Crecimiento progresivo para generar imágenes de mayor resolución.
  4. Normalización espectral en el discriminador para prevenir gradientes explosivos.
  5. Mecanismo de auto-atención para capturar dependencias globales en las imágenes generadas.
  6. Un bucle de entrenamiento mejorado con programación de tasa de aprendizaje y parada temprana.
  7. Métricas de evaluación avanzadas (FID y Puntuación de Inception) para una mejor evaluación de la calidad de las imágenes generadas.

Estas mejoras deberían resultar en imágenes generadas de mayor calidad, un entrenamiento más estable y un mejor rendimiento general del GAN. Recuerda experimentar con hiperparámetros y arquitecturas para encontrar la configuración óptima para tu caso de uso específico.