Skip to content

Training a GAN

Training a Generative Adversarial Network (GAN)

Section titled “Training a Generative Adversarial Network (GAN)”

Having explored the architecture and working principles of Generative Adversarial Networks, this chapter provides a practical example of implementing and training a GAN. We will build a GAN to generate handwritten digits similar to those in the well-known MNIST dataset, using Python with TensorFlow and its Keras API.

The Process of Training a Generative Adversarial Network

Section titled “The Process of Training a Generative Adversarial Network”

Training GANs involves an iterative optimization of two neural networks: the Generator and the Discriminator. Here’s a breakdown of the typical training process:

  • The system comprises two neural networks: a Generator Network (G) and a Discriminator Network (D). Their weights are typically initialized randomly.
  • The Generator (G) takes a random noise vector (latent vector) as input and aims to produce synthetic data samples.
  • The Discriminator (D) takes data samples (either real from the dataset or synthetic from G) as input and tries to classify them as ‘real’ or ‘fake’.
  • A random noise vector (e.g., from a Gaussian distribution) is fed into the Generator network.
  • The Generator processes this noise through its layers to output synthetic data samples designed to resemble the real data.
  • A batch of real data samples is drawn from the training dataset.
  • A batch of fake data samples is generated by the Generator using random noise.
  • The Discriminator is trained on both these real and fake samples. Its goal is to correctly identify real samples as real and fake samples as fake. Its weights are updated to minimize its classification error (e.g., using binary cross-entropy loss).
  • A new batch of fake data samples is generated by the Generator.
  • These fake samples are passed through the Discriminator (whose weights are kept frozen during this phase).
  • The Generator’s loss is calculated based on the Discriminator’s output. The Generator aims to ‘fool’ the Discriminator, i.e., to make the Discriminator classify its fake samples as real. Its weights are updated to minimize this loss (e.g., by minimizing the negative log-probability of the Discriminator being correct, or maximizing the log-probability of the Discriminator being fooled).
  • Steps 2 (Generating Fake Data), 3 (Discriminator Training), and 4 (Generator Training) are repeated for many iterations or epochs.
  • During each iteration, both the Generator and Discriminator are trained alternately (or sometimes with different frequencies), constantly trying to outperform each other.
  • This adversarial process ideally leads to an equilibrium where the Generator produces highly realistic data, and the Discriminator can no longer reliably distinguish between real and fake samples (its accuracy hovers around 50%).

Building and Training a GAN for MNIST Digits

Section titled “Building and Training a GAN for MNIST Digits”

We will now walk through the step-by-step procedure of building and training a GAN using Python, TensorFlow, and the MNIST dataset of handwritten digits.

First, ensure your Python environment has the necessary libraries. You’ll primarily need TensorFlow (which includes Keras) and Matplotlib. You can install them using pip if you haven’t already:

pip install tensorflow matplotlib numpy

Begin by importing the required modules in your Python script:

import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses
from tensorflow.keras.datasets import mnist
import matplotlib.pyplot as plt

Step 3: Load and Preprocess the MNIST Dataset

Section titled “Step 3: Load and Preprocess the MNIST Dataset”

The MNIST dataset contains 60,000 training images and 10,000 testing images of handwritten digits (0-9), each of size 28x28 pixels. We will normalize the pixel values to the range [-1, 1], which is often beneficial for GAN training, especially when using tanh activation in the generator’s output layer.

# Load the dataset (we only need the training images for GANs)
(x_train, _), (_, _) = mnist.load_data()
# Normalize the images to the range [-1, 1]
x_train = (x_train.astype('float32') - 127.5) / 127.5
# Add a channel dimension (for grayscale)
x_train = np.expand_dims(x_train, axis=-1)
# Define constants
BUFFER_SIZE = 60000 # Equal to the number of training examples
BATCH_SIZE = 256
NOISE_DIM = 100 # Dimensionality of the random noise vector

Step 4: Create the Generator and Discriminator Models

Section titled “Step 4: Create the Generator and Discriminator Models”

The Generator will create fake digit images from random noise, and the Discriminator will attempt to distinguish between real MNIST digits and these fakes.

The generator takes a random noise vector (latent vector) as input and transforms it through several layers to produce a 28x28x1 image. We use Dense layers followed by BatchNormalization and LeakyReLU activation, and a final tanh activation to output values in [-1, 1].

def build_generator():
model = models.Sequential(name='Generator')
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(NOISE_DIM,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((7, 7, 256)))
# assert model.output_shape == (None, 7, 7, 256) # Note: None is the batch size
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
# assert model.output_shape == (None, 7, 7, 128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
# assert model.output_shape == (None, 14, 14, 64)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
# assert model.output_shape == (None, 28, 28, 1)
return model
generator = build_generator()

The discriminator is a CNN-based classifier. It takes a 28x28x1 image as input (either real or generated) and outputs a single scalar value representing the probability (or logit) that the image is real. We use Conv2D layers, LeakyReLU activation, Dropout for regularization, and a final Dense layer without activation (to output logits).

def build_discriminator():
model = models.Sequential(name='Discriminator')
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Flatten())
model.add(layers.Dense(1)) # Output logits (no sigmoid activation here)
return model
discriminator = build_discriminator()

Step 5: Define Loss Functions and Optimizers

Section titled “Step 5: Define Loss Functions and Optimizers”

We’ll use binary cross-entropy loss. Since our discriminator outputs logits, we set from_logits=True. The generator aims to make the discriminator output ‘real’ (label 1) for its fake images. The discriminator aims to correctly classify real images as ‘real’ (label 1) and fake images as ‘fake’ (label 0).

# Binary cross-entropy loss function (expects logits)
cross_entropy = losses.BinaryCrossentropy(from_logits=True)
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss
def generator_loss(fake_output):
# Generator wants discriminator to classify fake images as real (label 1)
return cross_entropy(tf.ones_like(fake_output), fake_output)
# Optimizers (Adam is commonly used for GANs)
generator_optimizer = optimizers.Adam(learning_rate=1e-4)
discriminator_optimizer = optimizers.Adam(learning_rate=1e-4)

Step 6: Define the Training Loop (Training Step)

Section titled “Step 6: Define the Training Loop (Training Step)”

The training step involves generating fake images, calculating losses for both networks, and updating their weights using backpropagation and the defined optimizers. We use tf.GradientTape to record operations for automatic differentiation.

@tf.function
def train_step(images):
noise = tf.random.normal([BATCH_SIZE, NOISE_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)
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
# Main training function
def train(dataset, epochs):
for epoch in range(epochs):
epoch_gen_loss_avg = tf.keras.metrics.Mean()
epoch_disc_loss_avg = tf.keras.metrics.Mean()
for image_batch in dataset:
gen_loss, disc_loss = train_step(image_batch)
epoch_gen_loss_avg.update_state(gen_loss)
epoch_disc_loss_avg.update_state(disc_loss)
print(f'Epoch {epoch + 1}, Gen Loss: {epoch_gen_loss_avg.result():.4f}, Disc Loss: {epoch_disc_loss_avg.result():.4f}')
# Optionally, generate and save some images periodically
if (epoch + 1) % 10 == 0:
generate_and_save_images(generator, epoch + 1, seed_for_generating_images)

Step 7: Prepare the Dataset and Train the GAN

Section titled “Step 7: Prepare the Dataset and Train the GAN”

Next, prepare the MNIST dataset by shuffling and batching the images, then start the training process. We also define a fixed seed for generating sample images to observe progress.

# Prepare the dataset for training
train_dataset = tf.data.Dataset.from_tensor_slices(x_train).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
# A fixed noise seed for generating sample images during training to visualize progress
NUM_EXAMPLES_TO_GENERATE = 16
seed_for_generating_images = tf.random.normal([NUM_EXAMPLES_TO_GENERATE, NOISE_DIM])
# Define training epochs
EPOCHS = 50 # Adjust as needed; GANs often require many epochs
# Start training
# train(train_dataset, EPOCHS) # Uncomment to run training

After training (or periodically during training), you can generate new images using the trained generator and display them. This involves creating random noise, feeding it to the generator, and plotting the resulting images.

def generate_and_save_images(model, epoch, test_input):
# `training=False` so all layers run in inference mode (e.g., batchnorm).
predictions = model(test_input, training=False)
fig = plt.figure(figsize=(4, 4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i + 1)
# Denormalize pixel values from [-1, 1] to [0, 1] for imshow if using grayscale
# or [0, 255] if needed. Here, * 0.5 + 0.5 maps to [0,1]
plt.imshow(predictions[i, :, :, 0] * 0.5 + 0.5, cmap='gray')
plt.axis('off')
# plt.savefig(f'image_at_epoch_{epoch:04d}.png') # Optional: save images
plt.show()
# Example of generating images after training (assuming 'generator' is trained):
# generate_and_save_images(generator, EPOCHS, seed_for_generating_images)
# Or if you load a pre-trained generator:
# loaded_generator = tf.keras.models.load_model('path_to_your_saved_generator_model')
# generate_and_save_images(loaded_generator, 0, seed_for_generating_images)

When you run this code (after uncommenting the train call and potentially the final generate_and_save_images call), you will observe the generator and discriminator losses during training. If training is successful, the images generated by the generate_and_save_images function will gradually improve from noisy patterns to recognizable handwritten digits. The output would typically be a grid of 16 generated digit images, looking increasingly like those from the MNIST dataset over epochs.

Training a GAN involves several key steps: setting up the environment, defining robust generator and discriminator models, choosing appropriate loss functions and optimizers, and carefully implementing the adversarial training loop. By following these steps, you can train your own GAN to generate novel data, as demonstrated with the MNIST dataset.

This chapter provided a detailed guide to building and training a GAN using Python, TensorFlow, and Keras. GAN training can be sensitive to hyperparameters and model architecture, so experimentation is often key to achieving good results. This example serves as a foundational starting point for exploring the fascinating world of generative adversarial networks and their applications. For further learning, consider exploring more advanced GAN architectures and techniques for stabilizing training and improving sample quality, such as Wasserstein GANs (WGANs) or Spectral Normalization.

Common challenges in GAN training include mode collapse (generator produces limited varieties of samples) and training instability. Resources like the TensorFlow tutorials (tensorflow.org/tutorials/generative) and research papers on GAN improvements can provide deeper insights and solutions.