Master GAN architecture and training dynamics. Learn generator and discriminator design, adversarial training, and build image generation models. Explore DCGAN, StyleGAN, and creative AI applications.
Generative Adversarial Networks (GANs) are one of the most exciting breakthroughs in AI—they can generate photorealistic faces, create art, transform images, and even synthesize data. Introduced by Ian Goodfellow in 2014, GANs revolutionized generative modeling through an ingenious idea: two neural networks competing against each other.
Imagine an art forger (generator) trying to create fake paintings, while an art detective (discriminator) tries to spot the fakes. As they compete, the forger gets better at creating realistic art, and the detective becomes better at spotting fakes. Eventually, the forgeries become indistinguishable from real art!
In this lesson, you'll understand:
1. GAN Architecture: Generator creates fake data, discriminator judges real vs fake
2. Adversarial Training: The minimax game that makes both networks improve
3. DCGAN: Deep convolutional GAN architecture for image generation
4. Training Challenges: Mode collapse, vanishing gradients, and stabilization techniques
5. Advanced GANs: StyleGAN (face generation), CycleGAN (style transfer), Pix2Pix (image-to-image)
You've learned neural networks and CNNs—now you'll harness them for creative AI. By the end, you'll build a GAN that generates handwritten digits from random noise, witnessing the magic of generative models firsthand.
Ready to create artificial intelligence that creates? Let's dive into the fascinating world of GANs! 🎨
GANs consist of two neural networks locked in competition—a generator and a discriminator. Understanding their roles and interaction is key to mastering generative modeling.
Two Networks, One Goal:
1import numpy as np
2import torch
3import torch.nn as nn
4import torch.optim as optim
5import matplotlib.pyplot as plt
6from torchvision import datasets, transforms
7from torch.utils.data import DataLoader
8
9print("=" * 80)
10print("GAN ARCHITECTURE: GENERATOR VS DISCRIMINATOR")
11print("=" * 80)
12print()
13
14# ============================================================================
15# PART 1: Understanding the Components
16# ============================================================================
17
18class Generator(nn.Module):
19 """
20 Generator Network: Transforms random noise into fake images
21
22 Input: Random noise vector (latent space) z ~ N(0,1)
23 Output: Fake image that looks real
24 """
25
26 def __init__(self, latent_dim=100, img_shape=(1, 28, 28)):
27 super(Generator, self).__init__()
28 self.img_shape = img_shape
29
30 # Calculate output dimension
31 self.output_dim = int(np.prod(img_shape))
32
33 # Generator network: noise → hidden layers → image
34 self.model = nn.Sequential(
35 # Input: latent_dim (100)
36 nn.Linear(latent_dim, 128),
37 nn.LeakyReLU(0.2, inplace=True),
38
39 nn.Linear(128, 256),
40 nn.BatchNorm1d(256),
41 nn.LeakyReLU(0.2, inplace=True),
42
43 nn.Linear(256, 512),
44 nn.BatchNorm1d(512),
45 nn.LeakyReLU(0.2, inplace=True),
46
47 nn.Linear(512, 1024),
48 nn.BatchNorm1d(1024),
49 nn.LeakyReLU(0.2, inplace=True),
50
51 # Output: image pixels
52 nn.Linear(1024, self.output_dim),
53 nn.Tanh() # Output in [-1, 1]
54 )
55
56 def forward(self, z):
57 """Generate fake image from noise"""
58 img = self.model(z)
59 img = img.view(img.size(0), *self.img_shape)
60 return img
61
62
63class Discriminator(nn.Module):
64 """
65 Discriminator Network: Judges if image is real or fake
66
67 Input: Image (real or fake)
68 Output: Probability that image is real [0, 1]
69 """
70
71 def __init__(self, img_shape=(1, 28, 28)):
72 super(Discriminator, self).__init__()
73 self.img_shape = img_shape
74
75 # Calculate input dimension
76 self.input_dim = int(np.prod(img_shape))
77
78 # Discriminator network: image → hidden layers → probability
79 self.model = nn.Sequential(
80 # Input: flattened image
81 nn.Linear(self.input_dim, 512),
82 nn.LeakyReLU(0.2, inplace=True),
83 nn.Dropout(0.3),
84
85 nn.Linear(512, 256),
86 nn.LeakyReLU(0.2, inplace=True),
87 nn.Dropout(0.3),
88
89 nn.Linear(256, 128),
90 nn.LeakyReLU(0.2, inplace=True),
91 nn.Dropout(0.3),
92
93 # Output: probability [0, 1]
94 nn.Linear(128, 1),
95 nn.Sigmoid()
96 )
97
98 def forward(self, img):
99 """Judge if image is real or fake"""
100 img_flat = img.view(img.size(0), -1)
101 validity = self.model(img_flat)
102 return validity
103
104
105# Initialize networks
106latent_dim = 100
107img_shape = (1, 28, 28)
108
109generator = Generator(latent_dim, img_shape)
110discriminator = Discriminator(img_shape)
111
112print("Generator Architecture:")
113print(generator)
114print(f"\nParameters: {sum(p.numel() for p in generator.parameters()):,}")
115print()
116
117print("Discriminator Architecture:")
118print(discriminator)
119print(f"\nParameters: {sum(p.numel() for p in discriminator.parameters()):,}")
120print()
121
122# Test the networks
123print("Testing GAN Components:")
124print("-" * 80)
125
126# Generate fake image from noise
127z = torch.randn(4, latent_dim) # 4 noise vectors
128fake_images = generator(z)
129print(f"Generator: noise {z.shape} → fake images {fake_images.shape}")
130
131# Discriminator judges real vs fake
132real_images = torch.randn(4, *img_shape) # 4 "real" images
133real_pred = discriminator(real_images)
134fake_pred = discriminator(fake_images)
135
136print(f"\nDiscriminator judgments:")
137print(f" Real images → {real_pred.squeeze().detach().numpy()}")
138print(f" (should be close to 1.0 = real)")
139print(f" Fake images → {fake_pred.squeeze().detach().numpy()}")
140print(f" (should be close to 0.0 = fake)")1. Generator's Goal: Fool the discriminator
1Generate fake images that discriminator classifies as real
2Maximize: D(G(z)) → 12. Discriminator's Goal: Distinguish real from fake
1Correctly classify real images as real: D(x_real) → 1
2Correctly classify fake images as fake: D(G(z)) → 03. The Minimax Game
The GAN objective is a minimax optimization:
1min_G max_D V(D, G) = E[log D(x)] + E[log(1 - D(G(z)))]
2
3Where:
4- E[log D(x)]: Discriminator correctly identifies real images
5- E[log(1 - D(G(z)))]: Discriminator correctly identifies fake images
6- Generator minimizes, Discriminator maximizesMathematical Intuition:
1# Discriminator Loss: Maximize log D(x) + log(1 - D(G(z)))
2def discriminator_loss(real_output, fake_output):
3 """
4 Discriminator wants:
5 - real_output → 1 (real images classified as real)
6 - fake_output → 0 (fake images classified as fake)
7 """
8 real_loss = -torch.log(real_output + 1e-8).mean() # Maximize log D(x)
9 fake_loss = -torch.log(1 - fake_output + 1e-8).mean() # Maximize log(1-D(G(z)))
10 return real_loss + fake_loss
11
12# Generator Loss: Minimize log(1 - D(G(z))) ≈ Maximize log D(G(z))
13def generator_loss(fake_output):
14 """
15 Generator wants:
16 - fake_output → 1 (fake images classified as real)
17 """
18 # Practical trick: maximize log D(G(z)) instead of minimize log(1-D(G(z)))
19 # (provides stronger gradients early in training)
20 return -torch.log(fake_output + 1e-8).mean()
21
22
23# Example: One training step
24print("\n" + "=" * 80)
25print("ADVERSARIAL TRAINING STEP")
26print("=" * 80)
27print()
28
29# Generate fake images
30z = torch.randn(32, latent_dim)
31fake_images = generator(z)
32
33# Real images (simulated)
34real_images = torch.randn(32, *img_shape)
35
36# Forward pass
37real_pred = discriminator(real_images)
38fake_pred = discriminator(fake_images.detach()) # Detach for D training
39
40# Compute losses
41d_loss = discriminator_loss(real_pred, fake_pred)
42g_loss = generator_loss(discriminator(fake_images)) # No detach for G training
43
44print(f"Discriminator Loss: {d_loss.item():.4f}")
45print(f" Real prediction: {real_pred.mean().item():.4f} (want 1.0)")
46print(f" Fake prediction: {fake_pred.mean().item():.4f} (want 0.0)")
47print()
48
49print(f"Generator Loss: {g_loss.item():.4f}")
50print(f" Fake prediction: {discriminator(fake_images).mean().item():.4f} (want 1.0)")Ideal Convergence:
At Nash equilibrium:
Reality:
Why GANs are Powerful:
Why GANs are Tricky:
Architectural Tricks:
Training GANs is an art—balancing two competing networks requires careful techniques and hyperparameter tuning. Let's master the adversarial training process.
Alternating Gradient Descent:
1import torch.nn.functional as F
2
3def train_gan(generator, discriminator, dataloader, epochs=50,
4 latent_dim=100, device='cpu'):
5 """Complete GAN training loop"""
6
7 # Move models to device
8 generator.to(device)
9 discriminator.to(device)
10
11 # Optimizers (separate for each network)
12 g_optimizer = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
13 d_optimizer = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))
14
15 # Loss function (Binary Cross Entropy)
16 criterion = nn.BCELoss()
17
18 # Training history
19 history = {
20 'g_loss': [],
21 'd_loss': [],
22 'd_real_acc': [],
23 'd_fake_acc': []
24 }
25
26 print("=" * 80)
27 print("GAN TRAINING: ADVERSARIAL LEARNING")
28 print("=" * 80)
29 print()
30
31 for epoch in range(epochs):
32 g_losses = []
33 d_losses = []
34 d_real_accs = []
35 d_fake_accs = []
36
37 for batch_idx, (real_images, _) in enumerate(dataloader):
38 batch_size = real_images.size(0)
39 real_images = real_images.to(device)
40
41 # Labels for real and fake
42 real_labels = torch.ones(batch_size, 1).to(device)
43 fake_labels = torch.zeros(batch_size, 1).to(device)
44
45 # ============================================================
46 # STEP 1: Train Discriminator (maximize log D(x) + log(1-D(G(z))))
47 # ============================================================
48
49 d_optimizer.zero_grad()
50
51 # Real images
52 real_pred = discriminator(real_images)
53 d_real_loss = criterion(real_pred, real_labels)
54
55 # Fake images
56 z = torch.randn(batch_size, latent_dim).to(device)
57 fake_images = generator(z)
58 fake_pred = discriminator(fake_images.detach()) # Detach to avoid backprop to G
59 d_fake_loss = criterion(fake_pred, fake_labels)
60
61 # Total discriminator loss
62 d_loss = d_real_loss + d_fake_loss
63 d_loss.backward()
64 d_optimizer.step()
65
66 # Discriminator accuracy
67 d_real_acc = (real_pred > 0.5).float().mean().item()
68 d_fake_acc = (fake_pred < 0.5).float().mean().item()
69
70 # ============================================================
71 # STEP 2: Train Generator (minimize log(1-D(G(z))) ≈ maximize log D(G(z)))
72 # ============================================================
73
74 g_optimizer.zero_grad()
75
76 # Generate new fake images
77 z = torch.randn(batch_size, latent_dim).to(device)
78 fake_images = generator(z)
79 fake_pred = discriminator(fake_images) # No detach - backprop through D to G
80
81 # Generator wants discriminator to think fakes are real
82 g_loss = criterion(fake_pred, real_labels) # Trick: use real_labels
83 g_loss.backward()
84 g_optimizer.step()
85
86 # Store metrics
87 g_losses.append(g_loss.item())
88 d_losses.append(d_loss.item())
89 d_real_accs.append(d_real_acc)
90 d_fake_accs.append(d_fake_acc)
91
92 # Epoch statistics
93 avg_g_loss = np.mean(g_losses)
94 avg_d_loss = np.mean(d_losses)
95 avg_d_real_acc = np.mean(d_real_accs)
96 avg_d_fake_acc = np.mean(d_fake_accs)
97
98 history['g_loss'].append(avg_g_loss)
99 history['d_loss'].append(avg_d_loss)
100 history['d_real_acc'].append(avg_d_real_acc)
101 history['d_fake_acc'].append(avg_d_fake_acc)
102
103 # Print progress
104 if (epoch + 1) % 5 == 0:
105 print(f"Epoch [{epoch+1}/{epochs}]")
106 print(f" G Loss: {avg_g_loss:.4f} | D Loss: {avg_d_loss:.4f}")
107 print(f" D Real Acc: {avg_d_real_acc:.4f} | D Fake Acc: {avg_d_fake_acc:.4f}")
108 print()
109
110 return history
111
112
113# Example: Training indicators
114print("Understanding Training Dynamics:")
115print("-" * 80)
116print()
117
118print("HEALTHY TRAINING:")
119print(" ✓ D Loss ≈ 0.6-0.7 (not too confident)")
120print(" ✓ G Loss ≈ 0.6-0.7 (making progress)")
121print(" ✓ D Real Acc ≈ 0.7-0.9 (detects real well)")
122print(" ✓ D Fake Acc ≈ 0.5-0.7 (G is improving)")
123print()
124
125print("PROBLEMS:")
126print(" ✗ D Loss → 0: Discriminator too strong (G can't learn)")
127print(" ✗ G Loss → ∞: Generator failing (mode collapse)")
128print(" ✗ D Fake Acc → 1.0: Generator producing obvious fakes")
129print(" ✗ Oscillating losses: Training instability")1. Mode Collapse
Generator produces limited variety—ignores parts of data distribution.
Symptoms:
1# All generated images look similar
2# Generator "collapses" to one or few modesSolutions:
1def feature_matching_loss(fake_features, real_features):
2 """
3 Instead of fooling discriminator, match feature statistics
4 Reduces mode collapse
5 """
6 return F.mse_loss(fake_features.mean(0), real_features.mean(0))2. Vanishing Gradients
If discriminator is perfect, generator receives no learning signal.
Problem:
1# When D(G(z)) → 0, gradient of log(1 - D(G(z))) → 0
2# Generator stops learningSolutions:
1# Standard GAN loss (can saturate)
2g_loss = -torch.log(1 - discriminator(fake_images) + 1e-8).mean()
3
4# Non-saturating loss (better gradients)
5g_loss = -torch.log(discriminator(fake_images) + 1e-8).mean()3. Training Instability
Losses oscillate, models don't converge.
Solutions:
1# Stable optimizer settings for GANs
2g_optimizer = optim.Adam(generator.parameters(),
3 lr=0.0002, betas=(0.5, 0.999))
4d_optimizer = optim.Adam(discriminator.parameters(),
5 lr=0.0002, betas=(0.5, 0.999))1. Normalize Inputs
1# Scale images to [-1, 1] to match Tanh output
2transform = transforms.Compose([
3 transforms.ToTensor(),
4 transforms.Normalize([0.5], [0.5]) # [-1, 1]
5])2. Label Smoothing
1# Smooth labels to prevent overconfidence
2real_labels = torch.ones(batch_size, 1) * 0.9 # Instead of 1.0
3fake_labels = torch.zeros(batch_size, 1) * 0.1 # Instead of 0.03. Noisy Labels (occasionally flip labels)
1# Add noise to prevent D from becoming too confident
2if np.random.rand() < 0.05: # 5% chance
3 real_labels, fake_labels = fake_labels, real_labels4. Balance Training
1# Train D more if it's too weak
2if d_loss > 1.0:
3 # Train discriminator k extra times
4 for _ in range(k):
5 train_discriminator_step()Key Metrics:
1def evaluate_gan(generator, discriminator, real_samples, latent_dim=100):
2 """Evaluate GAN training quality"""
3
4 with torch.no_grad():
5 # Generate fake samples
6 z = torch.randn(len(real_samples), latent_dim)
7 fake_samples = generator(z)
8
9 # Discriminator predictions
10 real_pred = discriminator(real_samples).mean().item()
11 fake_pred = discriminator(fake_samples).mean().item()
12
13 print(f"Discriminator Performance:")
14 print(f" Real samples: {real_pred:.4f} (want ≈ 0.7-0.9)")
15 print(f" Fake samples: {fake_pred:.4f} (want ≈ 0.3-0.5)")
16 print()
17
18 # Check for mode collapse
19 fake_std = fake_samples.std().item()
20 real_std = real_samples.std().item()
21 diversity_ratio = fake_std / real_std
22
23 print(f"Diversity Check:")
24 print(f" Fake std: {fake_std:.4f}")
25 print(f" Real std: {real_std:.4f}")
26 print(f" Ratio: {diversity_ratio:.4f} (want ≈ 0.8-1.2)")
27
28 if diversity_ratio < 0.5:
29 print(" ⚠️ Warning: Possible mode collapse!")Training is a Delicate Balance:
Success Indicators:
The Art of GAN Training:
Training GANs is more art than science. Requires experimentation with:
DCGAN (Deep Convolutional GAN) introduced architectural guidelines that made GAN training stable and scalable. It's the foundation for modern GAN architectures.
Key Design Choices:
Architecture: Noise → Deep Convolutional Layers → Image
1import torch.nn as nn
2
3class DCGANGenerator(nn.Module):
4 """
5 DCGAN Generator: Transform noise to image using transposed convolutions
6
7 Architecture progression:
8 100 → 7×7×512 → 14×14×256 → 28×28×128 → 28×28×1
9 """
10
11 def __init__(self, latent_dim=100, img_channels=1):
12 super(DCGANGenerator, self).__init__()
13
14 # Initial projection: noise → feature maps
15 self.init_size = 7 # Initial spatial size
16 self.fc = nn.Linear(latent_dim, 512 * self.init_size * self.init_size)
17
18 # Transposed convolutional layers (upsampling)
19 self.conv_blocks = nn.Sequential(
20 nn.BatchNorm2d(512),
21
22 # Upsample: 7×7×512 → 14×14×256
23 nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1),
24 nn.BatchNorm2d(256),
25 nn.ReLU(inplace=True),
26
27 # Upsample: 14×14×256 → 28×28×128
28 nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1),
29 nn.BatchNorm2d(128),
30 nn.ReLU(inplace=True),
31
32 # Final layer: 28×28×128 → 28×28×1
33 nn.Conv2d(128, img_channels, kernel_size=3, stride=1, padding=1),
34 nn.Tanh() # Output in [-1, 1]
35 )
36
37 def forward(self, z):
38 # Project and reshape
39 out = self.fc(z)
40 out = out.view(out.size(0), 512, self.init_size, self.init_size)
41
42 # Generate image
43 img = self.conv_blocks(out)
44 return img
45
46
47class DCGANDiscriminator(nn.Module):
48 """
49 DCGAN Discriminator: Judge image authenticity using convolutions
50
51 Architecture progression:
52 28×28×1 → 14×14×64 → 7×7×128 → 3×3×256 → 1 (probability)
53 """
54
55 def __init__(self, img_channels=1):
56 super(DCGANDiscriminator, self).__init__()
57
58 # Convolutional layers (downsampling)
59 self.conv_blocks = nn.Sequential(
60 # Input: 28×28×1
61 nn.Conv2d(img_channels, 64, kernel_size=4, stride=2, padding=1),
62 nn.LeakyReLU(0.2, inplace=True),
63 nn.Dropout2d(0.25),
64
65 # 14×14×64
66 nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
67 nn.BatchNorm2d(128),
68 nn.LeakyReLU(0.2, inplace=True),
69 nn.Dropout2d(0.25),
70
71 # 7×7×128
72 nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1),
73 nn.BatchNorm2d(256),
74 nn.LeakyReLU(0.2, inplace=True),
75 nn.Dropout2d(0.25),
76
77 # 3×3×256
78 )
79
80 # Output layer
81 self.fc = nn.Sequential(
82 nn.Flatten(),
83 nn.Linear(256 * 3 * 3, 1),
84 nn.Sigmoid()
85 )
86
87 def forward(self, img):
88 features = self.conv_blocks(img)
89 validity = self.fc(features)
90 return validity
91
92
93# Initialize DCGAN
94print("=" * 80)
95print("DCGAN ARCHITECTURE")
96print("=" * 80)
97print()
98
99latent_dim = 100
100img_channels = 1
101
102dcgan_generator = DCGANGenerator(latent_dim, img_channels)
103dcgan_discriminator = DCGANDiscriminator(img_channels)
104
105print("DCGAN Generator:")
106print("-" * 40)
107print(dcgan_generator)
108print(f"\nTotal parameters: {sum(p.numel() for p in dcgan_generator.parameters()):,}")
109print()
110
111print("DCGAN Discriminator:")
112print("-" * 40)
113print(dcgan_discriminator)
114print(f"\nTotal parameters: {sum(p.numel() for p in dcgan_discriminator.parameters()):,}")
115print()
116
117# Test forward pass
118z = torch.randn(8, latent_dim)
119generated_images = dcgan_generator(z)
120validity = dcgan_discriminator(generated_images)
121
122print("Forward Pass Test:")
123print(f" Noise shape: {z.shape}")
124print(f" Generated images: {generated_images.shape}")
125print(f" Discriminator output: {validity.shape}")
126print(f" Output range: [{generated_images.min():.2f}, {generated_images.max():.2f}]")1. Transposed Convolutions (Generator)
Learn to upsample intelligently:
1# Standard upsampling (fixed)
2upsampled = F.interpolate(x, scale_factor=2, mode='nearest')
3
4# Transposed convolution (learned)
5upsampled = nn.ConvTranspose2d(in_channels, out_channels,
6 kernel_size=4, stride=2, padding=1)(x)2. Strided Convolutions (Discriminator)
Replace max pooling with learned downsampling:
1# Max pooling (loses information)
2downsampled = nn.MaxPool2d(2)(x)
3
4# Strided convolution (learned, preserves info)
5downsampled = nn.Conv2d(in_channels, out_channels,
6 kernel_size=4, stride=2, padding=1)(x)3. Batch Normalization
Stabilizes training by normalizing layer inputs:
1# Prevents internal covariate shift
2# Allows higher learning rates
3# Acts as regularization4. Activation Functions
1# LeakyReLU prevents "dying ReLU" in discriminator
2# Small negative slope (0.2) allows gradient flow1# Prepare MNIST data
2transform = transforms.Compose([
3 transforms.Resize(28),
4 transforms.ToTensor(),
5 transforms.Normalize([0.5], [0.5]) # [-1, 1]
6])
7
8mnist_dataset = datasets.MNIST(
9 root='./data',
10 train=True,
11 transform=transform,
12 download=True
13)
14
15dataloader = DataLoader(
16 mnist_dataset,
17 batch_size=128,
18 shuffle=True,
19 num_workers=2
20)
21
22print("Training DCGAN on MNIST:")
23print("-" * 80)
24print(f"Dataset size: {len(mnist_dataset)}")
25print(f"Batch size: 128")
26print(f"Batches per epoch: {len(dataloader)}")
27print()
28
29# Train DCGAN
30device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
31print(f"Using device: {device}")
32print()
33
34# (Training code would go here using train_gan function from previous concept)
35# history = train_gan(dcgan_generator, dcgan_discriminator, dataloader,
36# epochs=50, latent_dim=latent_dim, device=device)1def visualize_generation(generator, latent_dim=100, n_samples=64, device='cpu'):
2 """Visualize generated images"""
3
4 generator.eval()
5
6 with torch.no_grad():
7 # Generate samples
8 z = torch.randn(n_samples, latent_dim).to(device)
9 generated = generator(z).cpu()
10
11 # Denormalize [-1, 1] → [0, 1]
12 generated = (generated + 1) / 2
13
14 # Plot grid
15 fig, axes = plt.subplots(8, 8, figsize=(12, 12))
16
17 for i, ax in enumerate(axes.flat):
18 if i < n_samples:
19 ax.imshow(generated[i].squeeze(), cmap='gray')
20 ax.axis('off')
21
22 plt.suptitle('DCGAN Generated Digits', fontsize=16, fontweight='bold')
23 plt.tight_layout()
24 plt.show()
25
26
27def visualize_interpolation(generator, latent_dim=100, n_steps=10, device='cpu'):
28 """Visualize smooth interpolation in latent space"""
29
30 generator.eval()
31
32 with torch.no_grad():
33 # Two random points in latent space
34 z1 = torch.randn(1, latent_dim).to(device)
35 z2 = torch.randn(1, latent_dim).to(device)
36
37 # Interpolate
38 alphas = torch.linspace(0, 1, n_steps)
39 interpolated = []
40
41 for alpha in alphas:
42 z = (1 - alpha) * z1 + alpha * z2
43 img = generator(z).cpu()
44 img = (img + 1) / 2 # Denormalize
45 interpolated.append(img.squeeze())
46
47 # Plot
48 fig, axes = plt.subplots(1, n_steps, figsize=(20, 2))
49
50 for i, (ax, img) in enumerate(zip(axes, interpolated)):
51 ax.imshow(img, cmap='gray')
52 ax.set_title(f'α={alphas[i]:.1f}', fontsize=10)
53 ax.axis('off')
54
55 plt.suptitle('Latent Space Interpolation', fontsize=14, fontweight='bold')
56 plt.tight_layout()
57 plt.show()DCGAN Advantages:
DCGAN vs Simple GAN:
Modern Impact:
DCGAN principles are used in:
Modern GAN variants push the boundaries of what's possible with generative models. Let's explore three revolutionary architectures that power today's creative AI.
Key Innovation: Style-based generator architecture with adaptive instance normalization.
Architecture Overview:
1class StyleGANGenerator(nn.Module):
2 """
3 StyleGAN Generator (simplified)
4
5 Key components:
6 1. Mapping Network: z → w (intermediate latent space)
7 2. Synthesis Network: w → image (with style injection at each layer)
8 3. Adaptive Instance Normalization (AdaIN): Apply style
9 """
10
11 def __init__(self, latent_dim=512, style_dim=512):
12 super(StyleGANGenerator, self).__init__()
13
14 # Mapping network: z → w
15 self.mapping = nn.Sequential(
16 nn.Linear(latent_dim, style_dim),
17 nn.LeakyReLU(0.2),
18 nn.Linear(style_dim, style_dim),
19 nn.LeakyReLU(0.2),
20 nn.Linear(style_dim, style_dim),
21 nn.LeakyReLU(0.2),
22 # ... 8 layers total
23 )
24
25 # Synthesis network with style modulation
26 # (Simplified - actual StyleGAN has progressive layers)
27 self.synthesis = nn.ModuleList([
28 StyleBlock(512, 512, style_dim), # 4×4
29 StyleBlock(512, 512, style_dim), # 8×8
30 StyleBlock(512, 256, style_dim), # 16×16
31 StyleBlock(256, 128, style_dim), # 32×32
32 StyleBlock(128, 64, style_dim), # 64×64
33 # ... up to 1024×1024
34 ])
35
36 def forward(self, z):
37 # Map to intermediate latent space
38 w = self.mapping(z)
39
40 # Generate image with style injection
41 x = self.const_input # Learnable constant input
42
43 for block in self.synthesis:
44 x = block(x, w)
45
46 return x
47
48
49class StyleBlock(nn.Module):
50 """Single StyleGAN block with AdaIN"""
51
52 def __init__(self, in_channels, out_channels, style_dim):
53 super(StyleBlock, self).__init__()
54
55 self.conv = nn.Conv2d(in_channels, out_channels, 3, padding=1)
56 self.upsample = nn.Upsample(scale_factor=2, mode='bilinear')
57
58 # Style modulation (AdaIN)
59 self.style_scale = nn.Linear(style_dim, out_channels)
60 self.style_shift = nn.Linear(style_dim, out_channels)
61
62 self.noise = nn.Parameter(torch.randn(1, out_channels, 1, 1))
63 self.activation = nn.LeakyReLU(0.2)
64
65 def forward(self, x, w):
66 # Upsample
67 x = self.upsample(x)
68
69 # Convolution
70 x = self.conv(x)
71
72 # Add noise
73 x = x + self.noise
74
75 # Adaptive Instance Normalization (AdaIN)
76 # Normalize, then apply learned style
77 mean = x.mean([2, 3], keepdim=True)
78 std = x.std([2, 3], keepdim=True)
79 x_normalized = (x - mean) / (std + 1e-8)
80
81 # Apply style
82 scale = self.style_scale(w).unsqueeze(2).unsqueeze(3)
83 shift = self.style_shift(w).unsqueeze(2).unsqueeze(3)
84 x = scale * x_normalized + shift
85
86 return self.activation(x)
87
88
89print("=" * 80)
90print("STYLEGAN ARCHITECTURE")
91print("=" * 80)
92print()
93
94print("Key Features:")
95print("-" * 80)
96print("1. Mapping Network")
97print(" • Transforms z → w (intermediate latent space)")
98print(" • W space is more disentangled than Z")
99print(" • Better control over generated features")
100print()
101
102print("2. Adaptive Instance Normalization (AdaIN)")
103print(" • Inject style at each resolution")
104print(" • Coarse levels: pose, face shape")
105print(" • Fine levels: hair, skin texture, eyes")
106print()
107
108print("3. Style Mixing")
109print(" • Use different w vectors at different layers")
110print(" • Mix coarse style from one image, fine style from another")
111print(" • Example: Face shape from A, hair from B")
112print()
113
114print("4. Progressive Growing")
115print(" • Train on low resolution first (4×4)")
116print(" • Gradually add higher resolution layers")
117print(" • Final: 1024×1024 photorealistic faces")Why StyleGAN Matters:
1# Style mixing example (conceptual)
2def style_mixing(generator, z1, z2):
3 """
4 Generate image with mixed styles
5
6 Coarse layers (pose, face shape): from z1
7 Fine layers (texture, hair): from z2
8 """
9 w1 = generator.mapping(z1)
10 w2 = generator.mapping(z2)
11
12 # Use w1 for coarse, w2 for fine
13 x = generator.const_input
14
15 for i, block in enumerate(generator.synthesis):
16 w = w1 if i < 4 else w2 # Switch at layer 4
17 x = block(x, w)
18
19 return xApplications:
---
Key Innovation: Learn mapping between two domains without paired examples.
Architecture: Two GANs with Cycle Consistency
1class CycleGAN:
2 """
3 CycleGAN: Translate between two domains X and Y
4
5 Components:
6 - G: X → Y (e.g., horse → zebra)
7 - F: Y → X (e.g., zebra → horse)
8 - D_X: Discriminator for domain X
9 - D_Y: Discriminator for domain Y
10 """
11
12 def __init__(self):
13 self.G = UNetGenerator() # X → Y
14 self.F = UNetGenerator() # Y → X
15 self.D_X = PatchDiscriminator() # Judge X images
16 self.D_Y = PatchDiscriminator() # Judge Y images
17
18 def cycle_consistency_loss(self, x, y):
19 """
20 Cycle Consistency: Ensure translation is reversible
21
22 Forward cycle: x → G(x) → F(G(x)) ≈ x
23 Backward cycle: y → F(y) → G(F(y)) ≈ y
24 """
25 # Forward cycle loss
26 x_reconstructed = self.F(self.G(x))
27 forward_loss = F.l1_loss(x_reconstructed, x)
28
29 # Backward cycle loss
30 y_reconstructed = self.G(self.F(y))
31 backward_loss = F.l1_loss(y_reconstructed, y)
32
33 return forward_loss + backward_loss
34
35 def total_loss(self, x, y):
36 """
37 Total CycleGAN loss:
38 1. GAN loss (fool discriminators)
39 2. Cycle consistency loss (preserve content)
40 3. Identity loss (preserve colors)
41 """
42 # GAN losses
43 fake_y = self.G(x)
44 fake_x = self.F(y)
45
46 gan_loss_G = F.mse_loss(self.D_Y(fake_y), torch.ones_like(self.D_Y(fake_y)))
47 gan_loss_F = F.mse_loss(self.D_X(fake_x), torch.ones_like(self.D_X(fake_x)))
48
49 # Cycle consistency
50 cycle_loss = self.cycle_consistency_loss(x, y)
51
52 # Identity loss (optional - preserve input if already in target domain)
53 identity_loss = F.l1_loss(self.G(y), y) + F.l1_loss(self.F(x), x)
54
55 # Weighted combination
56 total = gan_loss_G + gan_loss_F + 10.0 * cycle_loss + 5.0 * identity_loss
57
58 return total
59
60
61print("=" * 80)
62print("CYCLEGAN: UNPAIRED IMAGE TRANSLATION")
63print("=" * 80)
64print()
65
66print("Key Insight: Cycle Consistency")
67print("-" * 80)
68print("Without paired data, enforce:")
69print(" x → G(x) → F(G(x)) ≈ x (reconstruct original)")
70print(" y → F(y) → G(F(y)) ≈ y (reconstruct original)")
71print()
72print("This constraint prevents mode collapse and preserves content")
73print()
74
75print("Famous Applications:")
76print("-" * 80)
77print("• Horse ↔ Zebra")
78print("• Summer ↔ Winter")
79print("• Photo ↔ Monet painting")
80print("• Apple ↔ Orange")
81print("• Male ↔ Female faces")---
Key Innovation: Conditional GAN with U-Net generator and PatchGAN discriminator.
Architecture:
1class UNetGenerator(nn.Module):
2 """
3 U-Net Generator for Pix2Pix
4
5 Encoder-decoder with skip connections
6 Preserves spatial information for precise translation
7 """
8
9 def __init__(self, in_channels=3, out_channels=3):
10 super(UNetGenerator, self).__init__()
11
12 # Encoder (downsampling)
13 self.down1 = self.conv_block(in_channels, 64, normalize=False)
14 self.down2 = self.conv_block(64, 128)
15 self.down3 = self.conv_block(128, 256)
16 self.down4 = self.conv_block(256, 512)
17
18 # Bottleneck
19 self.bottleneck = self.conv_block(512, 512)
20
21 # Decoder (upsampling)
22 self.up1 = self.upconv_block(512, 512)
23 self.up2 = self.upconv_block(1024, 256) # 1024 = 512 + 512 (skip)
24 self.up3 = self.upconv_block(512, 128) # 512 = 256 + 256 (skip)
25 self.up4 = self.upconv_block(256, 64) # 256 = 128 + 128 (skip)
26
27 # Output
28 self.final = nn.Sequential(
29 nn.ConvTranspose2d(128, out_channels, 4, 2, 1), # 128 = 64 + 64 (skip)
30 nn.Tanh()
31 )
32
33 def conv_block(self, in_c, out_c, normalize=True):
34 layers = [nn.Conv2d(in_c, out_c, 4, 2, 1)]
35 if normalize:
36 layers.append(nn.BatchNorm2d(out_c))
37 layers.append(nn.LeakyReLU(0.2))
38 return nn.Sequential(*layers)
39
40 def upconv_block(self, in_c, out_c):
41 return nn.Sequential(
42 nn.ConvTranspose2d(in_c, out_c, 4, 2, 1),
43 nn.BatchNorm2d(out_c),
44 nn.ReLU()
45 )
46
47 def forward(self, x):
48 # Encoder with skip connections
49 d1 = self.down1(x)
50 d2 = self.down2(d1)
51 d3 = self.down3(d2)
52 d4 = self.down4(d3)
53
54 # Bottleneck
55 b = self.bottleneck(d4)
56
57 # Decoder with skip connections
58 u1 = self.up1(b)
59 u2 = self.up2(torch.cat([u1, d4], 1)) # Skip connection
60 u3 = self.up3(torch.cat([u2, d3], 1))
61 u4 = self.up4(torch.cat([u3, d2], 1))
62
63 return self.final(torch.cat([u4, d1], 1))
64
65
66class PatchGANDiscriminator(nn.Module):
67 """
68 PatchGAN Discriminator
69
70 Classifies 70×70 patches as real/fake
71 Encourages sharp, high-frequency details
72 """
73
74 def __init__(self, in_channels=6): # 6 = 3 (input) + 3 (output)
75 super(PatchGANDiscriminator, self).__init__()
76
77 self.model = nn.Sequential(
78 nn.Conv2d(in_channels, 64, 4, 2, 1),
79 nn.LeakyReLU(0.2),
80
81 nn.Conv2d(64, 128, 4, 2, 1),
82 nn.BatchNorm2d(128),
83 nn.LeakyReLU(0.2),
84
85 nn.Conv2d(128, 256, 4, 2, 1),
86 nn.BatchNorm2d(256),
87 nn.LeakyReLU(0.2),
88
89 nn.Conv2d(256, 512, 4, 1, 1),
90 nn.BatchNorm2d(512),
91 nn.LeakyReLU(0.2),
92
93 nn.Conv2d(512, 1, 4, 1, 1), # Output: patch validity map
94 nn.Sigmoid()
95 )
96
97 def forward(self, input_img, target_img):
98 # Concatenate input and target
99 x = torch.cat([input_img, target_img], 1)
100 return self.model(x)
101
102
103print("=" * 80)
104print("PIX2PIX: SUPERVISED IMAGE TRANSLATION")
105print("=" * 80)
106print()
107
108print("Loss Function:")
109print("-" * 80)
110print("L = L_GAN + λ × L_L1")
111print()
112print("Where:")
113print(" L_GAN: Adversarial loss (fool discriminator)")
114print(" L_L1: Pixel-wise L1 loss (preserve structure)")
115print(" λ = 100 (weight L1 heavily)")
116print()
117
118print("Applications:")
119print("-" * 80)
120print("• Edges → Photo")
121print("• Semantic labels → Street scene")
122print("• Day → Night")
123print("• Black & white → Color")
124print("• Sketch → Photo")
125print("• Satellite → Map")| Feature | Pix2Pix | CycleGAN |
|---------|---------|----------|
| Training Data | Paired | Unpaired |
| Networks | 1 Generator, 1 Discriminator | 2 Generators, 2 Discriminators |
| Loss | GAN + L1 | GAN + Cycle Consistency |
| Quality | Higher (has ground truth) | Lower (no ground truth) |
| Flexibility | Less (needs pairs) | More (no pairs needed) |
StyleGAN:
CycleGAN:
Pix2Pix:
Modern Landscape:
GANs have transformed creative industries and opened new possibilities in AI. Let's explore real-world applications and implement practical use cases.
1. Face Generation
1def generate_faces(generator, n_faces=16, latent_dim=512, device='cpu'):
2 """Generate random faces using StyleGAN-like model"""
3
4 generator.eval()
5
6 with torch.no_grad():
7 # Sample random latent vectors
8 z = torch.randn(n_faces, latent_dim).to(device)
9
10 # Generate faces
11 faces = generator(z).cpu()
12
13 # Denormalize
14 faces = (faces + 1) / 2
15
16 # Visualize
17 fig, axes = plt.subplots(4, 4, figsize=(12, 12))
18
19 for i, ax in enumerate(axes.flat):
20 ax.imshow(faces[i].permute(1, 2, 0))
21 ax.axis('off')
22
23 plt.suptitle('AI-Generated Faces (This Person Does Not Exist)',
24 fontsize=16, fontweight='bold')
25 plt.tight_layout()
26 plt.show()
27
28
29# Applications:
30print("Face Generation Applications:")
31print("-" * 80)
32print("✓ Stock photos (royalty-free faces)")
33print("✓ Game characters (procedural NPCs)")
34print("✓ Training data augmentation")
35print("✓ Privacy protection (synthetic datasets)")2. Image Super-Resolution
1class SRGANGenerator(nn.Module):
2 """Super-Resolution GAN Generator"""
3
4 def __init__(self, scale_factor=4):
5 super(SRGANGenerator, self).__init__()
6
7 # Initial convolution
8 self.initial = nn.Sequential(
9 nn.Conv2d(3, 64, 9, padding=4),
10 nn.PReLU()
11 )
12
13 # Residual blocks
14 self.residuals = nn.Sequential(*[
15 ResidualBlock(64) for _ in range(16)
16 ])
17
18 # Upsampling blocks
19 self.upsampling = nn.Sequential(
20 UpscaleBlock(64, scale_factor=2),
21 UpscaleBlock(64, scale_factor=2)
22 )
23
24 # Output convolution
25 self.output = nn.Conv2d(64, 3, 9, padding=4)
26
27 def forward(self, x):
28 initial = self.initial(x)
29 residual = self.residuals(initial)
30 upsampled = self.upsampling(residual + initial)
31 return torch.tanh(self.output(upsampled))
32
33
34print("Super-Resolution Applications:")
35print("-" * 80)
36print("✓ Enhance old photos/videos")
37print("✓ Improve medical imaging")
38print("✓ Upscale game graphics")
39print("✓ Satellite image enhancement")3. Style Transfer
1def neural_style_transfer(content_img, style_img, generator):
2 """
3 Apply artistic style to content image using GAN
4
5 Args:
6 content_img: Photo to transform
7 style_img: Artwork to extract style from
8 generator: Pre-trained StyleGAN or CycleGAN
9 """
10
11 with torch.no_grad():
12 # Extract content features
13 content_features = generator.encode(content_img)
14
15 # Extract style features
16 style_features = generator.encode(style_img)
17
18 # Combine: content structure + style appearance
19 stylized = generator.decode(content_features, style_features)
20
21 return stylized
22
23
24print("Style Transfer Applications:")
25print("-" * 80)
26print("✓ Artistic filters (Prisma, FaceApp)")
27print("✓ Video game art styles")
28print("✓ Interior design visualization")
29print("✓ Fashion design")Generate Synthetic Training Data:
1class DataAugmentationGAN:
2 """Use GAN to generate synthetic training samples"""
3
4 def __init__(self, generator, latent_dim=100):
5 self.generator = generator
6 self.latent_dim = latent_dim
7
8 def augment_dataset(self, dataset, n_synthetic=1000):
9 """
10 Add synthetic samples to existing dataset
11
12 Useful when:
13 - Limited training data
14 - Imbalanced classes
15 - Need diverse examples
16 """
17 synthetic_samples = []
18
19 self.generator.eval()
20 with torch.no_grad():
21 for _ in range(n_synthetic):
22 z = torch.randn(1, self.latent_dim)
23 fake_sample = self.generator(z)
24 synthetic_samples.append(fake_sample)
25
26 # Combine real + synthetic
27 augmented_dataset = dataset + synthetic_samples
28
29 return augmented_dataset
30
31
32print("Data Augmentation Use Cases:")
33print("-" * 80)
34print("✓ Medical imaging (rare diseases)")
35print("✓ Fraud detection (synthetic fraud patterns)")
36print("✓ Autonomous driving (rare scenarios)")
37print("✓ Minority class balancing")Use Discriminator for Outlier Detection:
1def gan_anomaly_detection(discriminator, test_samples, threshold=0.5):
2 """
3 Detect anomalies using trained GAN discriminator
4
5 Logic:
6 - Discriminator learns "normal" data distribution
7 - Anomalies are classified as "fake" (low D score)
8 """
9
10 discriminator.eval()
11
12 with torch.no_grad():
13 scores = discriminator(test_samples)
14
15 # Low scores indicate anomalies
16 is_anomaly = scores < threshold
17
18 return is_anomaly, scores
19
20
21print("Anomaly Detection Applications:")
22print("-" * 80)
23print("✓ Manufacturing defect detection")
24print("✓ Network intrusion detection")
25print("✓ Medical abnormality screening")
26print("✓ Credit card fraud detection")1. Semantic Image Editing
1def semantic_editing(generator, image, edits):
2 """
3 Edit image semantics using latent space manipulation
4
5 Examples:
6 - Add smile
7 - Change hair color
8 - Adjust age
9 - Modify pose
10 """
11
12 # Encode image to latent space
13 w = generator.encode(image)
14
15 # Apply edits (learned direction vectors)
16 w_edited = w + edits['smile'] * 2.0 # Increase smile
17 w_edited += edits['age'] * -1.5 # Make younger
18
19 # Generate edited image
20 edited_image = generator.decode(w_edited)
21
22 return edited_image
23
24
25print("Image Editing Applications:")
26print("-" * 80)
27print("✓ FaceApp (aging, gender swap)")
28print("✓ Virtual makeup")
29print("✓ Photo retouching")
30print("✓ Expression editing")2. Image Inpainting (Fill Missing Regions)
1class InpaintingGAN:
2 """Fill missing regions in images"""
3
4 def __init__(self, generator, discriminator):
5 self.generator = generator
6 self.discriminator = discriminator
7
8 def inpaint(self, image_with_hole, mask):
9 """
10 Fill hole in image
11
12 Args:
13 image_with_hole: Image with missing region (0s)
14 mask: Binary mask (1 = missing, 0 = present)
15 """
16
17 # Generator fills the hole
18 inpainted = self.generator(image_with_hole, mask)
19
20 # Blend: use original where present, generated where missing
21 result = image_with_hole * (1 - mask) + inpainted * mask
22
23 return result
24
25
26print("Inpainting Applications:")
27print("-" * 80)
28print("✓ Remove objects from photos")
29print("✓ Restore damaged artwork")
30print("✓ Fill occluded regions")
31print("✓ Content-aware fill (Photoshop)")Modern Frontier: Generate Images from Text Descriptions
1class TextToImageGAN:
2 """
3 Generate images from text descriptions
4
5 Architecture:
6 1. Text encoder (BERT, CLIP)
7 2. Conditional GAN (text → image)
8 3. Discriminator (real vs fake + text matching)
9 """
10
11 def __init__(self, text_encoder, generator, discriminator):
12 self.text_encoder = text_encoder
13 self.generator = generator
14 self.discriminator = discriminator
15
16 def generate(self, text_description):
17 """
18 Generate image from text
19
20 Example: "A red car on a mountain road at sunset"
21 """
22
23 # Encode text to embedding
24 text_embedding = self.text_encoder(text_description)
25
26 # Random noise
27 z = torch.randn(1, 100)
28
29 # Conditional generation
30 image = self.generator(z, text_embedding)
31
32 return image
33
34
35print("Text-to-Image Examples:")
36print("-" * 80)
37print("Input: 'A blue bird sitting on a tree branch'")
38print("Output: [Generated image of blue bird on branch]")
39print()
40print("Input: 'An astronaut riding a horse in space'")
41print("Output: [Generated surreal image]")
42print()
43print("Technologies: DALL-E, Stable Diffusion, Midjourney")Temporal GANs for Video Synthesis:
1class VideoGAN:
2 """
3 Generate videos using temporal GANs
4
5 Challenges:
6 - Temporal consistency (smooth motion)
7 - Higher computational cost
8 - Larger memory requirements
9 """
10
11 def __init__(self, generator, discriminator):
12 self.generator = generator # 3D ConvNet (time dimension)
13 self.discriminator = discriminator
14
15 def generate_video(self, n_frames=30, fps=30):
16 """
17 Generate video from noise
18
19 Output: Sequence of temporally consistent frames
20 """
21
22 z = torch.randn(1, 100) # Latent code
23
24 # Generate all frames at once (ensures consistency)
25 video = self.generator(z, n_frames=n_frames)
26
27 return video # Shape: [batch, channels, time, height, width]
28
29
30print("Video Generation Applications:")
31print("-" * 80)
32print("✓ Deepfakes (face swapping)")
33print("✓ Video prediction (forecast future frames)")
34print("✓ Slow-motion generation")
35print("✓ Video compression")1print("=" * 80)
2print("ETHICAL CONSIDERATIONS FOR GANS")
3print("=" * 80)
4print()
5
6print("⚠️ POTENTIAL MISUSES:")
7print("-" * 80)
8print("1. Deepfakes: Fake videos of real people")
9print(" • Spread misinformation")
10print(" • Damage reputations")
11print(" • Fake news")
12print()
13
14print("2. Synthetic Media at Scale:")
15print(" • Flood internet with AI-generated content")
16print(" • Manipulate public opinion")
17print(" • Impersonate individuals")
18print()
19
20print("3. Privacy Concerns:")
21print(" • Generate realistic fake identities")
22print(" • Bypass biometric security")
23print(" • Create synthetic training data from private images")
24print()
25
26print("✅ RESPONSIBLE AI PRACTICES:")
27print("-" * 80)
28print("1. Transparency: Label AI-generated content")
29print("2. Authentication: Develop deepfake detection tools")
30print("3. Regulation: Legal frameworks for synthetic media")
31print("4. Education: Teach public to identify fake content")
32print("5. Purpose: Use GANs for beneficial applications")GANs Revolutionized:
Real-World Impact:
The Future:
Remember: With great power comes great responsibility. Use GANs ethically!
Implement a complete Deep Convolutional GAN to generate handwritten digits. Train generator and discriminator adversarially, visualize training dynamics, and explore latent space interpolation.
1# Complete DCGAN Implementation for MNIST
2# Generate realistic handwritten digits from random noise
3
4import torch
5import torch.nn as nn
6import torch.optim as optim
7import torchvision
8import torchvision.transforms as transforms
9from torch.utils.data import DataLoader
10import matplotlib.pyplot as plt
11import numpy as np
12
13print("=" * 80)
14print("DCGAN: DEEP CONVOLUTIONAL GAN FOR MNIST DIGIT GENERATION")
15print("=" * 80)
16print()
17
18# Set device
19device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
20print(f"Using device: {device}")
21print()
22
23# ============================================================================
24# PART 1: DCGAN Architecture
25# ============================================================================
26
27class DCGANGenerator(nn.Module):
28 """
29 DCGAN Generator: Noise → Image
30
31 Architecture: 100 → 7×7×256 → 14×14×128 → 28×28×64 → 28×28×1
32 """
33
34 def __init__(self, latent_dim=100):
35 super(DCGANGenerator, self).__init__()
36
37 self.latent_dim = latent_dim
38 self.init_size = 7
39
40 # Project and reshape
41 self.fc = nn.Linear(latent_dim, 256 * self.init_size * self.init_size)
42
43 # Transposed convolutional layers
44 self.conv_blocks = nn.Sequential(
45 nn.BatchNorm2d(256),
46
47 # 7×7×256 → 14×14×128
48 nn.ConvTranspose2d(256, 128, 4, stride=2, padding=1),
49 nn.BatchNorm2d(128),
50 nn.ReLU(inplace=True),
51
52 # 14×14×128 → 28×28×64
53 nn.ConvTranspose2d(128, 64, 4, stride=2, padding=1),
54 nn.BatchNorm2d(64),
55 nn.ReLU(inplace=True),
56
57 # 28×28×64 → 28×28×1
58 nn.Conv2d(64, 1, 3, stride=1, padding=1),
59 nn.Tanh()
60 )
61
62 def forward(self, z):
63 # Project and reshape
64 out = self.fc(z)
65 out = out.view(out.size(0), 256, self.init_size, self.init_size)
66
67 # Generate image
68 img = self.conv_blocks(out)
69
70 return img
71
72
73class DCGANDiscriminator(nn.Module):
74 """
75 DCGAN Discriminator: Image → Probability
76
77 Architecture: 28×28×1 → 14×14×64 → 7×7×128 → 3×3×256 → 1
78 """
79
80 def __init__(self):
81 super(DCGANDiscriminator, self).__init__()
82
83 self.conv_blocks = nn.Sequential(
84 # 28×28×1 → 14×14×64
85 nn.Conv2d(1, 64, 4, stride=2, padding=1),
86 nn.LeakyReLU(0.2, inplace=True),
87 nn.Dropout2d(0.25),
88
89 # 14×14×64 → 7×7×128
90 nn.Conv2d(64, 128, 4, stride=2, padding=1),
91 nn.BatchNorm2d(128),
92 nn.LeakyReLU(0.2, inplace=True),
93 nn.Dropout2d(0.25),
94
95 # 7×7×128 → 3×3×256
96 nn.Conv2d(128, 256, 4, stride=2, padding=1),
97 nn.BatchNorm2d(256),
98 nn.LeakyReLU(0.2, inplace=True),
99 nn.Dropout2d(0.25),
100 )
101
102 # Output layer
103 self.fc = nn.Sequential(
104 nn.Flatten(),
105 nn.Linear(256 * 3 * 3, 1),
106 nn.Sigmoid()
107 )
108
109 def forward(self, img):
110 features = self.conv_blocks(img)
111 validity = self.fc(features)
112 return validity
113
114
115# Initialize models
116latent_dim = 100
117
118generator = DCGANGenerator(latent_dim).to(device)
119discriminator = DCGANDiscriminator().to(device)
120
121print("DCGAN Models Initialized:")
122print("-" * 80)
123print(f"Generator parameters: {sum(p.numel() for p in generator.parameters()):,}")
124print(f"Discriminator parameters: {sum(p.numel() for p in discriminator.parameters()):,}")
125print()
126
127# ============================================================================
128# PART 2: Prepare MNIST Dataset
129# ============================================================================
130
131# Data transforms
132transform = transforms.Compose([
133 transforms.Resize(28),
134 transforms.ToTensor(),
135 transforms.Normalize([0.5], [0.5]) # Scale to [-1, 1]
136])
137
138# Load MNIST
139mnist_dataset = torchvision.datasets.MNIST(
140 root='./data',
141 train=True,
142 transform=transform,
143 download=True
144)
145
146dataloader = DataLoader(
147 mnist_dataset,
148 batch_size=128,
149 shuffle=True,
150 num_workers=2
151)
152
153print("Dataset Loaded:")
154print("-" * 80)
155print(f"Training samples: {len(mnist_dataset)}")
156print(f"Batch size: 128")
157print(f"Batches per epoch: {len(dataloader)}")
158print()
159
160# ============================================================================
161# PART 3: Training Setup
162# ============================================================================
163
164# Loss function
165criterion = nn.BCELoss()
166
167# Optimizers (Adam with custom betas for GAN stability)
168g_optimizer = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
169d_optimizer = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))
170
171print("Training Configuration:")
172print("-" * 80)
173print("Loss: Binary Cross-Entropy")
174print("Optimizer: Adam (lr=0.0002, betas=(0.5, 0.999))")
175print("Epochs: 50")
176print()
177
178# ============================================================================
179# PART 4: Training Loop
180# ============================================================================
181
182def train_dcgan(n_epochs=50):
183 """Train DCGAN with adversarial learning"""
184
185 history = {
186 'g_loss': [],
187 'd_loss': [],
188 'd_real_acc': [],
189 'd_fake_acc': [],
190 'generated_samples': []
191 }
192
193 # Fixed noise for consistent visualization
194 fixed_z = torch.randn(64, latent_dim).to(device)
195
196 print("=" * 80)
197 print("TRAINING DCGAN")
198 print("=" * 80)
199 print()
200
201 for epoch in range(n_epochs):
202 g_losses = []
203 d_losses = []
204 d_real_accs = []
205 d_fake_accs = []
206
207 for batch_idx, (real_images, _) in enumerate(dataloader):
208 batch_size = real_images.size(0)
209 real_images = real_images.to(device)
210
211 # Labels
212 real_labels = torch.ones(batch_size, 1).to(device) * 0.9 # Label smoothing
213 fake_labels = torch.zeros(batch_size, 1).to(device) * 0.1
214
215 # ================================================================
216 # Train Discriminator
217 # ================================================================
218
219 d_optimizer.zero_grad()
220
221 # Real images
222 real_pred = discriminator(real_images)
223 d_real_loss = criterion(real_pred, real_labels)
224
225 # Fake images
226 z = torch.randn(batch_size, latent_dim).to(device)
227 fake_images = generator(z)
228 fake_pred = discriminator(fake_images.detach())
229 d_fake_loss = criterion(fake_pred, fake_labels)
230
231 # Total discriminator loss
232 d_loss = d_real_loss + d_fake_loss
233 d_loss.backward()
234 d_optimizer.step()
235
236 # Discriminator accuracy
237 d_real_acc = (real_pred > 0.5).float().mean().item()
238 d_fake_acc = (fake_pred < 0.5).float().mean().item()
239
240 # ================================================================
241 # Train Generator
242 # ================================================================
243
244 g_optimizer.zero_grad()
245
246 # Generate fake images
247 z = torch.randn(batch_size, latent_dim).to(device)
248 fake_images = generator(z)
249 fake_pred = discriminator(fake_images)
250
251 # Generator wants discriminator to think fakes are real
252 g_loss = criterion(fake_pred, torch.ones(batch_size, 1).to(device))
253 g_loss.backward()
254 g_optimizer.step()
255
256 # Store metrics
257 g_losses.append(g_loss.item())
258 d_losses.append(d_loss.item())
259 d_real_accs.append(d_real_acc)
260 d_fake_accs.append(d_fake_acc)
261
262 # Epoch statistics
263 avg_g_loss = np.mean(g_losses)
264 avg_d_loss = np.mean(d_losses)
265 avg_d_real_acc = np.mean(d_real_accs)
266 avg_d_fake_acc = np.mean(d_fake_accs)
267
268 history['g_loss'].append(avg_g_loss)
269 history['d_loss'].append(avg_d_loss)
270 history['d_real_acc'].append(avg_d_real_acc)
271 history['d_fake_acc'].append(avg_d_fake_acc)
272
273 # Generate samples for visualization
274 if (epoch + 1) % 10 == 0:
275 with torch.no_grad():
276 samples = generator(fixed_z).cpu()
277 history['generated_samples'].append(samples)
278
279 # Print progress
280 print(f"Epoch [{epoch+1}/{n_epochs}]")
281 print(f" G Loss: {avg_g_loss:.4f} | D Loss: {avg_d_loss:.4f}")
282 print(f" D Real Acc: {avg_d_real_acc:.4f} | D Fake Acc: {avg_d_fake_acc:.4f}")
283
284 # Training health check
285 if avg_d_loss < 0.1:
286 print(" ⚠️ Warning: Discriminator too strong!")
287 if avg_g_loss > 3.0:
288 print(" ⚠️ Warning: Generator struggling!")
289
290 print()
291
292 print("=" * 80)
293 print("TRAINING COMPLETE")
294 print("=" * 80)
295 print()
296
297 return history
298
299
300# Train the GAN
301history = train_dcgan(n_epochs=50)
302
303# ============================================================================
304# PART 5: Visualizations
305# ============================================================================
306
307print("Generating Visualizations...")
308print()
309
310# Plot 1: Training curves
311fig, axes = plt.subplots(2, 2, figsize=(15, 12))
312
313# Loss curves
314axes[0, 0].plot(history['g_loss'], label='Generator', linewidth=2, color='blue')
315axes[0, 0].plot(history['d_loss'], label='Discriminator', linewidth=2, color='red')
316axes[0, 0].set_xlabel('Epoch', fontweight='bold', fontsize=11)
317axes[0, 0].set_ylabel('Loss', fontweight='bold', fontsize=11)
318axes[0, 0].set_title('GAN Training Loss', fontweight='bold', fontsize=13)
319axes[0, 0].legend(fontsize=10)
320axes[0, 0].grid(True, alpha=0.3)
321
322# Discriminator accuracy
323axes[0, 1].plot(history['d_real_acc'], label='Real Images', linewidth=2, color='green')
324axes[0, 1].plot(history['d_fake_acc'], label='Fake Images', linewidth=2, color='orange')
325axes[0, 1].axhline(0.5, color='black', linestyle='--', alpha=0.5, label='Random Guess')
326axes[0, 1].set_xlabel('Epoch', fontweight='bold', fontsize=11)
327axes[0, 1].set_ylabel('Accuracy', fontweight='bold', fontsize=11)
328axes[0, 1].set_title('Discriminator Performance', fontweight='bold', fontsize=13)
329axes[0, 1].legend(fontsize=10)
330axes[0, 1].grid(True, alpha=0.3)
331
332# Generated samples progression
333if len(history['generated_samples']) > 0:
334 # Early training
335 early_samples = history['generated_samples'][0][:16]
336 early_grid = torchvision.utils.make_grid(early_samples, nrow=4, normalize=True)
337 axes[1, 0].imshow(early_grid.permute(1, 2, 0))
338 axes[1, 0].set_title('Generated Digits (Epoch 10)', fontweight='bold', fontsize=13)
339 axes[1, 0].axis('off')
340
341 # Final training
342 final_samples = history['generated_samples'][-1][:16]
343 final_grid = torchvision.utils.make_grid(final_samples, nrow=4, normalize=True)
344 axes[1, 1].imshow(final_grid.permute(1, 2, 0))
345 axes[1, 1].set_title('Generated Digits (Epoch 50)', fontweight='bold', fontsize=13)
346 axes[1, 1].axis('off')
347
348plt.tight_layout()
349plt.show()
350
351# Plot 2: Large grid of generated digits
352with torch.no_grad():
353 z = torch.randn(64, latent_dim).to(device)
354 generated = generator(z).cpu()
355
356 fig, axes = plt.subplots(8, 8, figsize=(14, 14))
357
358 for i, ax in enumerate(axes.flat):
359 img = generated[i].squeeze()
360 img = (img + 1) / 2 # Denormalize [-1, 1] → [0, 1]
361 ax.imshow(img, cmap='gray')
362 ax.axis('off')
363
364 plt.suptitle('DCGAN Generated MNIST Digits', fontsize=16, fontweight='bold')
365 plt.tight_layout()
366 plt.show()
367
368# Plot 3: Latent space interpolation
369print("Generating Latent Space Interpolation...")
370
371with torch.no_grad():
372 # Two random points
373 z1 = torch.randn(1, latent_dim).to(device)
374 z2 = torch.randn(1, latent_dim).to(device)
375
376 # Interpolate
377 n_steps = 10
378 alphas = torch.linspace(0, 1, n_steps)
379
380 fig, axes = plt.subplots(1, n_steps, figsize=(20, 2))
381
382 for i, alpha in enumerate(alphas):
383 z = (1 - alpha) * z1 + alpha * z2
384 img = generator(z).cpu().squeeze()
385 img = (img + 1) / 2 # Denormalize
386
387 axes[i].imshow(img, cmap='gray')
388 axes[i].set_title(f'α={alpha:.1f}', fontsize=9)
389 axes[i].axis('off')
390
391 plt.suptitle('Latent Space Interpolation (Smooth Transitions)',
392 fontsize=14, fontweight='bold')
393 plt.tight_layout()
394 plt.show()
395
396# ============================================================================
397# PART 6: Analysis and Insights
398# ============================================================================
399
400print("=" * 80)
401print("DCGAN TRAINING ANALYSIS")
402print("=" * 80)
403print()
404
405print("Final Training Metrics:")
406print("-" * 80)
407print(f"Generator Loss: {history['g_loss'][-1]:.4f}")
408print(f"Discriminator Loss: {history['d_loss'][-1]:.4f}")
409print(f"D Real Accuracy: {history['d_real_acc'][-1]:.4f}")
410print(f"D Fake Accuracy: {history['d_fake_acc'][-1]:.4f}")
411print()
412
413# Evaluate quality
414final_d_real = history['d_real_acc'][-1]
415final_d_fake = history['d_fake_acc'][-1]
416
417print("Training Quality Assessment:")
418print("-" * 80)
419
420if 0.7 <= final_d_real <= 0.9 and 0.5 <= final_d_fake <= 0.7:
421 print("✅ EXCELLENT: Well-balanced training")
422 print(" Generator produces realistic digits")
423 print(" Discriminator provides useful feedback")
424elif final_d_real > 0.95 or final_d_fake > 0.95:
425 print("⚠️ DISCRIMINATOR TOO STRONG")
426 print(" Generator may struggle to improve")
427 print(" Solution: Increase G learning rate or train G more frequently")
428elif final_d_real < 0.6 and final_d_fake < 0.6:
429 print("⚠️ GENERATOR TOO STRONG")
430 print(" Discriminator cannot distinguish real from fake")
431 print(" Solution: Increase D learning rate or complexity")
432else:
433 print("✓ GOOD: Training progressing normally")
434print()
435
436print("Key Observations:")
437print("-" * 80)
438print("1️⃣ Generated digits progress from noise to recognizable numbers")
439print("2️⃣ Latent space is smooth (interpolation shows gradual transitions)")
440print("3️⃣ Training is adversarial (losses oscillate, not monotonic)")
441print("4️⃣ Discriminator accuracy ≈ 50-70% indicates healthy competition")
442print()
443
444print("=" * 80)
445print("🎨 GANs Master the Art of Creation!")
446print("=" * 80)
447print()
448
449print("What you've accomplished:")
450print(" ✓ Built DCGAN from scratch with PyTorch")
451print(" ✓ Trained generator and discriminator adversarially")
452print(" ✓ Generated realistic handwritten digits from noise")
453print(" ✓ Explored smooth latent space interpolation")
454print(" ✓ Monitored training dynamics and stability")
455print()
456
457print("Next steps:")
458print(" → Apply to higher resolution images (64×64, 128×128)")
459print(" → Experiment with conditional GANs (control digit class)")
460print(" → Try StyleGAN, CycleGAN architectures")
461print(" → Explore creative applications (art, design, data augmentation)")
462print()
463
464print("Ready to explore Variational Autoencoders (VAEs)? 🚀")This comprehensive DCGAN implementation demonstrates the complete GAN training pipeline on MNIST.
1. Complete DCGAN Architecture
Generator (100 → 28×28×1):
Discriminator (28×28×1 → 1):
2. Adversarial Training Process
Alternating Optimization:
1for each batch:
2 # Train Discriminator
3 D_loss = BCE(D(real), 1) + BCE(D(G(z)), 0)
4
5 # Train Generator
6 G_loss = BCE(D(G(z)), 1)Training Dynamics:
3. Training Stabilization Techniques
Label Smoothing:
1real_labels = 0.9 # Instead of 1.0
2fake_labels = 0.1 # Instead of 0.0Adam with Custom Betas:
1betas=(0.5, 0.999) # Instead of default (0.9, 0.999)
2# Reduces momentum for stabilityArchitecture Choices:
4. Comprehensive Visualizations
Training Curves:
Generated Samples:
Latent Space Interpolation:
Success Indicators:
Training Progression:
Common Issues (and solutions):
Mode Collapse:
Vanishing Gradients:
Training Instability:
1. Foundation for Modern GANs
Understanding DCGAN is crucial for:
2. Real-World Applications
This same architecture (with modifications) powers:
3. Research Impact
GANs sparked an entire field:
Minimax Objective:
1min_G max_D V(D,G) = E[log D(x)] + E[log(1 - D(G(z)))]In Practice:
Nash Equilibrium:
This DCGAN implementation bridges theory and practice—showing how mathematical concepts translate to working code that creates artificial intelligence capable of creativity!
Incredible work! You've mastered Generative Adversarial Networks—one of the most exciting areas of modern AI. You've learned how two neural networks compete to create realistic data, navigated the challenges of adversarial training, and built a DCGAN that generates handwritten digits from pure noise.
Next: In Lesson 17: Variational Autoencoders (VAEs), we'll explore another powerful generative model that takes a probabilistic approach:
While GANs excel at sharp, realistic images, VAEs offer principled probabilistic modeling and stable training. Together, they form the foundation of modern generative AI!
Your Generative AI Journey Continues → Ready to explore the probabilistic side of generation? Let's dive into VAEs! 🎨
Your progress is saved in this browser, on this device only.