Menu iconMenu iconGenerative Deep Learning with Python
Generative Deep Learning with Python

Chapter 4: Project: Face Generation with GANs

4.7 Example of Full Code for the Project

In this section, you will get a high-level template demonstrating the key components of the GAN and how they fit together. However, it lacks some specific implementation details. For example, the forward functions of the Generator and Discriminator, the training steps within the training loop, and the specifics of the data preprocessing step are not included.

These parts are highly dependent on the specific architecture of your GAN and the dataset you're working with. They're also the parts of the code where you'd do the majority of your experimenting and fine-tuning, so they're less amenable to being included in a template. These are the details that were covered in the different sections of this chapter. 

import torch
from torch import nn
from torchvision import transforms, datasets

# Load and preprocess the data
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = datasets.CelebA(root='./data', download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True)

# Define the Generator
class Generator(nn.Module):
    def __init__(self, z_size, conv_dim):
        super(Generator, self).__init__()
        self.conv_dim = conv_dim
        self.fc = nn.Linear(z_size, conv_dim*8*4*4)
        self.deconv1 = nn.ConvTranspose2d(conv_dim*8, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.deconv2 = nn.ConvTranspose2d(conv_dim*4, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.deconv3 = nn.ConvTranspose2d(conv_dim*2, conv_dim, kernel_size=4, stride=2, padding=1)
        self.deconv4 = nn.ConvTranspose2d(conv_dim, 3, kernel_size=4, stride=2, padding=1)
        self.dropout = nn.Dropout(0.5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.fc(x)
        x = x.view(-1, self.conv_dim*8, 4, 4)
        x = self.relu(self.deconv1(x))
        x = self.dropout(x)
        x = self.relu(self.deconv2(x))
        x = self.dropout(x)
        x = self.relu(self.deconv3(x))
        x = self.dropout(x)
        x = torch.tanh(self.deconv4(x))
        return x

# Define the Discriminator
class Discriminator(nn.Module):
    def __init__(self, conv_dim):
        super(Discriminator, self).__init__()
        self.conv_dim = conv_dim
        self.conv1 = nn.Conv2d(3, conv_dim, kernel_size=4, stride=2, padding=1)
        self.conv2 = nn.Conv2d(conv_dim, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.conv3 = nn.Conv2d(conv_dim*2, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.conv4 = nn.Conv2d(conv_dim*4, conv_dim*8, kernel_size=4, stride=2, padding=1)
        self.fc = nn.Linear(conv_dim*8*4*4, 1)
        self.dropout = nn.Dropout(0.5)
        self.leaky_relu = nn.LeakyReLU(0.2)

    def forward(self, x):
        x = self.leaky_relu(self.conv1(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv2(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv3(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv4(x))
        x = self.dropout(x)
        x = x.view(-1, self.conv_dim*8*4*4)
        x = self.fc(x)
        return x

# Training parameters
z_size = 100
conv_dim = 64

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

G = Generator(z_size=z_size, conv_dim=conv_dim).to(device)
D = Discriminator(conv_dim=conv_dim).to(device)

# Define loss
criterion = nn.BCEWithLogitsLoss()

# Optimizers
lr = 0.0002
beta1 = 0.5
beta2 = 0.999

g_optimizer = torch.optim.Adam(G.parameters(), lr, [beta1, beta2])
d_optimizer = torch.optim.Adam(D.parameters(), lr, [beta1, beta2])

# Training
num_epochs = 30
sample_interval = 200

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)

        # Train Discriminator
        d_optimizer.zero_grad()

        # Real images
        real_outputs = D(real_images)
        d_real_loss = criterion(real_outputs, torch.ones_like(real_outputs))

        # Fake images
        z = torch.randn(batch_size, z_size).to(device)
        fake_images = G(z)
        fake_outputs = D(fake_images.detach())
        d_fake_loss = criterion(fake_outputs,

Note: Remember, this code is not complete. It only serves as a template to indicate how the code for the entire project could look. The complete code would contain full definitions of the Generator and Discriminator classes, the training code within the for loops, data preprocessing steps, as well as other necessary components for the project. For brevity, those sections are not included here. The full detailed code should follow the step-by-step guidelines discussed in each section of the chapter.

Chapter 4 Conclusion

Congratulations on completing your first project using Generative Adversarial Networks (GANs)! In this chapter, we've covered every step of creating a GAN from start to finish. We've gone through the process of collecting and preprocessing data, constructing the Generator and Discriminator components of the GAN, training the GAN, and using it to generate new faces.

We've also dived deeper into some advanced topics like improving the quality of generated images, making the training process more stable, and extending our GAN with conditional inputs. We have seen how GANs, despite their complexity, can be understood, implemented, and modified when broken down into their individual components.

The project we undertook was a challenging one: generating realistic human faces. This task is a testament to the power of GANs and their impact on the field of generative deep learning. Our project demonstrated how this complex task can be accomplished with a well-designed and well-trained GAN.

Remember that the process doesn't stop here. GANs are a deep and complex subject, and there is always more to learn and experiment with. This project should serve as a solid foundation, but don't be afraid to venture into more complex architectures, different types of data, and new ideas. The field is rapidly evolving, and there's always something new and exciting to discover. 

In the next chapter, we'll explore another important category of generative models: Variational Autoencoders (VAEs). Like GANs, they provide a powerful way to generate new data, but they approach the problem from a different angle and offer their own unique advantages.

Onward to the next step in your generative deep learning journey!


4.7 Example of Full Code for the Project

In this section, you will get a high-level template demonstrating the key components of the GAN and how they fit together. However, it lacks some specific implementation details. For example, the forward functions of the Generator and Discriminator, the training steps within the training loop, and the specifics of the data preprocessing step are not included.

These parts are highly dependent on the specific architecture of your GAN and the dataset you're working with. They're also the parts of the code where you'd do the majority of your experimenting and fine-tuning, so they're less amenable to being included in a template. These are the details that were covered in the different sections of this chapter. 

import torch
from torch import nn
from torchvision import transforms, datasets

# Load and preprocess the data
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = datasets.CelebA(root='./data', download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True)

# Define the Generator
class Generator(nn.Module):
    def __init__(self, z_size, conv_dim):
        super(Generator, self).__init__()
        self.conv_dim = conv_dim
        self.fc = nn.Linear(z_size, conv_dim*8*4*4)
        self.deconv1 = nn.ConvTranspose2d(conv_dim*8, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.deconv2 = nn.ConvTranspose2d(conv_dim*4, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.deconv3 = nn.ConvTranspose2d(conv_dim*2, conv_dim, kernel_size=4, stride=2, padding=1)
        self.deconv4 = nn.ConvTranspose2d(conv_dim, 3, kernel_size=4, stride=2, padding=1)
        self.dropout = nn.Dropout(0.5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.fc(x)
        x = x.view(-1, self.conv_dim*8, 4, 4)
        x = self.relu(self.deconv1(x))
        x = self.dropout(x)
        x = self.relu(self.deconv2(x))
        x = self.dropout(x)
        x = self.relu(self.deconv3(x))
        x = self.dropout(x)
        x = torch.tanh(self.deconv4(x))
        return x

# Define the Discriminator
class Discriminator(nn.Module):
    def __init__(self, conv_dim):
        super(Discriminator, self).__init__()
        self.conv_dim = conv_dim
        self.conv1 = nn.Conv2d(3, conv_dim, kernel_size=4, stride=2, padding=1)
        self.conv2 = nn.Conv2d(conv_dim, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.conv3 = nn.Conv2d(conv_dim*2, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.conv4 = nn.Conv2d(conv_dim*4, conv_dim*8, kernel_size=4, stride=2, padding=1)
        self.fc = nn.Linear(conv_dim*8*4*4, 1)
        self.dropout = nn.Dropout(0.5)
        self.leaky_relu = nn.LeakyReLU(0.2)

    def forward(self, x):
        x = self.leaky_relu(self.conv1(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv2(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv3(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv4(x))
        x = self.dropout(x)
        x = x.view(-1, self.conv_dim*8*4*4)
        x = self.fc(x)
        return x

# Training parameters
z_size = 100
conv_dim = 64

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

G = Generator(z_size=z_size, conv_dim=conv_dim).to(device)
D = Discriminator(conv_dim=conv_dim).to(device)

# Define loss
criterion = nn.BCEWithLogitsLoss()

# Optimizers
lr = 0.0002
beta1 = 0.5
beta2 = 0.999

g_optimizer = torch.optim.Adam(G.parameters(), lr, [beta1, beta2])
d_optimizer = torch.optim.Adam(D.parameters(), lr, [beta1, beta2])

# Training
num_epochs = 30
sample_interval = 200

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)

        # Train Discriminator
        d_optimizer.zero_grad()

        # Real images
        real_outputs = D(real_images)
        d_real_loss = criterion(real_outputs, torch.ones_like(real_outputs))

        # Fake images
        z = torch.randn(batch_size, z_size).to(device)
        fake_images = G(z)
        fake_outputs = D(fake_images.detach())
        d_fake_loss = criterion(fake_outputs,

Note: Remember, this code is not complete. It only serves as a template to indicate how the code for the entire project could look. The complete code would contain full definitions of the Generator and Discriminator classes, the training code within the for loops, data preprocessing steps, as well as other necessary components for the project. For brevity, those sections are not included here. The full detailed code should follow the step-by-step guidelines discussed in each section of the chapter.

Chapter 4 Conclusion

Congratulations on completing your first project using Generative Adversarial Networks (GANs)! In this chapter, we've covered every step of creating a GAN from start to finish. We've gone through the process of collecting and preprocessing data, constructing the Generator and Discriminator components of the GAN, training the GAN, and using it to generate new faces.

We've also dived deeper into some advanced topics like improving the quality of generated images, making the training process more stable, and extending our GAN with conditional inputs. We have seen how GANs, despite their complexity, can be understood, implemented, and modified when broken down into their individual components.

The project we undertook was a challenging one: generating realistic human faces. This task is a testament to the power of GANs and their impact on the field of generative deep learning. Our project demonstrated how this complex task can be accomplished with a well-designed and well-trained GAN.

Remember that the process doesn't stop here. GANs are a deep and complex subject, and there is always more to learn and experiment with. This project should serve as a solid foundation, but don't be afraid to venture into more complex architectures, different types of data, and new ideas. The field is rapidly evolving, and there's always something new and exciting to discover. 

In the next chapter, we'll explore another important category of generative models: Variational Autoencoders (VAEs). Like GANs, they provide a powerful way to generate new data, but they approach the problem from a different angle and offer their own unique advantages.

Onward to the next step in your generative deep learning journey!


4.7 Example of Full Code for the Project

In this section, you will get a high-level template demonstrating the key components of the GAN and how they fit together. However, it lacks some specific implementation details. For example, the forward functions of the Generator and Discriminator, the training steps within the training loop, and the specifics of the data preprocessing step are not included.

These parts are highly dependent on the specific architecture of your GAN and the dataset you're working with. They're also the parts of the code where you'd do the majority of your experimenting and fine-tuning, so they're less amenable to being included in a template. These are the details that were covered in the different sections of this chapter. 

import torch
from torch import nn
from torchvision import transforms, datasets

# Load and preprocess the data
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = datasets.CelebA(root='./data', download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True)

# Define the Generator
class Generator(nn.Module):
    def __init__(self, z_size, conv_dim):
        super(Generator, self).__init__()
        self.conv_dim = conv_dim
        self.fc = nn.Linear(z_size, conv_dim*8*4*4)
        self.deconv1 = nn.ConvTranspose2d(conv_dim*8, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.deconv2 = nn.ConvTranspose2d(conv_dim*4, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.deconv3 = nn.ConvTranspose2d(conv_dim*2, conv_dim, kernel_size=4, stride=2, padding=1)
        self.deconv4 = nn.ConvTranspose2d(conv_dim, 3, kernel_size=4, stride=2, padding=1)
        self.dropout = nn.Dropout(0.5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.fc(x)
        x = x.view(-1, self.conv_dim*8, 4, 4)
        x = self.relu(self.deconv1(x))
        x = self.dropout(x)
        x = self.relu(self.deconv2(x))
        x = self.dropout(x)
        x = self.relu(self.deconv3(x))
        x = self.dropout(x)
        x = torch.tanh(self.deconv4(x))
        return x

# Define the Discriminator
class Discriminator(nn.Module):
    def __init__(self, conv_dim):
        super(Discriminator, self).__init__()
        self.conv_dim = conv_dim
        self.conv1 = nn.Conv2d(3, conv_dim, kernel_size=4, stride=2, padding=1)
        self.conv2 = nn.Conv2d(conv_dim, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.conv3 = nn.Conv2d(conv_dim*2, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.conv4 = nn.Conv2d(conv_dim*4, conv_dim*8, kernel_size=4, stride=2, padding=1)
        self.fc = nn.Linear(conv_dim*8*4*4, 1)
        self.dropout = nn.Dropout(0.5)
        self.leaky_relu = nn.LeakyReLU(0.2)

    def forward(self, x):
        x = self.leaky_relu(self.conv1(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv2(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv3(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv4(x))
        x = self.dropout(x)
        x = x.view(-1, self.conv_dim*8*4*4)
        x = self.fc(x)
        return x

# Training parameters
z_size = 100
conv_dim = 64

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

G = Generator(z_size=z_size, conv_dim=conv_dim).to(device)
D = Discriminator(conv_dim=conv_dim).to(device)

# Define loss
criterion = nn.BCEWithLogitsLoss()

# Optimizers
lr = 0.0002
beta1 = 0.5
beta2 = 0.999

g_optimizer = torch.optim.Adam(G.parameters(), lr, [beta1, beta2])
d_optimizer = torch.optim.Adam(D.parameters(), lr, [beta1, beta2])

# Training
num_epochs = 30
sample_interval = 200

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)

        # Train Discriminator
        d_optimizer.zero_grad()

        # Real images
        real_outputs = D(real_images)
        d_real_loss = criterion(real_outputs, torch.ones_like(real_outputs))

        # Fake images
        z = torch.randn(batch_size, z_size).to(device)
        fake_images = G(z)
        fake_outputs = D(fake_images.detach())
        d_fake_loss = criterion(fake_outputs,

Note: Remember, this code is not complete. It only serves as a template to indicate how the code for the entire project could look. The complete code would contain full definitions of the Generator and Discriminator classes, the training code within the for loops, data preprocessing steps, as well as other necessary components for the project. For brevity, those sections are not included here. The full detailed code should follow the step-by-step guidelines discussed in each section of the chapter.

Chapter 4 Conclusion

Congratulations on completing your first project using Generative Adversarial Networks (GANs)! In this chapter, we've covered every step of creating a GAN from start to finish. We've gone through the process of collecting and preprocessing data, constructing the Generator and Discriminator components of the GAN, training the GAN, and using it to generate new faces.

We've also dived deeper into some advanced topics like improving the quality of generated images, making the training process more stable, and extending our GAN with conditional inputs. We have seen how GANs, despite their complexity, can be understood, implemented, and modified when broken down into their individual components.

The project we undertook was a challenging one: generating realistic human faces. This task is a testament to the power of GANs and their impact on the field of generative deep learning. Our project demonstrated how this complex task can be accomplished with a well-designed and well-trained GAN.

Remember that the process doesn't stop here. GANs are a deep and complex subject, and there is always more to learn and experiment with. This project should serve as a solid foundation, but don't be afraid to venture into more complex architectures, different types of data, and new ideas. The field is rapidly evolving, and there's always something new and exciting to discover. 

In the next chapter, we'll explore another important category of generative models: Variational Autoencoders (VAEs). Like GANs, they provide a powerful way to generate new data, but they approach the problem from a different angle and offer their own unique advantages.

Onward to the next step in your generative deep learning journey!


4.7 Example of Full Code for the Project

In this section, you will get a high-level template demonstrating the key components of the GAN and how they fit together. However, it lacks some specific implementation details. For example, the forward functions of the Generator and Discriminator, the training steps within the training loop, and the specifics of the data preprocessing step are not included.

These parts are highly dependent on the specific architecture of your GAN and the dataset you're working with. They're also the parts of the code where you'd do the majority of your experimenting and fine-tuning, so they're less amenable to being included in a template. These are the details that were covered in the different sections of this chapter. 

import torch
from torch import nn
from torchvision import transforms, datasets

# Load and preprocess the data
transform = transforms.Compose([
    transforms.Resize(64),
    transforms.CenterCrop(64),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = datasets.CelebA(root='./data', download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True)

# Define the Generator
class Generator(nn.Module):
    def __init__(self, z_size, conv_dim):
        super(Generator, self).__init__()
        self.conv_dim = conv_dim
        self.fc = nn.Linear(z_size, conv_dim*8*4*4)
        self.deconv1 = nn.ConvTranspose2d(conv_dim*8, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.deconv2 = nn.ConvTranspose2d(conv_dim*4, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.deconv3 = nn.ConvTranspose2d(conv_dim*2, conv_dim, kernel_size=4, stride=2, padding=1)
        self.deconv4 = nn.ConvTranspose2d(conv_dim, 3, kernel_size=4, stride=2, padding=1)
        self.dropout = nn.Dropout(0.5)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.fc(x)
        x = x.view(-1, self.conv_dim*8, 4, 4)
        x = self.relu(self.deconv1(x))
        x = self.dropout(x)
        x = self.relu(self.deconv2(x))
        x = self.dropout(x)
        x = self.relu(self.deconv3(x))
        x = self.dropout(x)
        x = torch.tanh(self.deconv4(x))
        return x

# Define the Discriminator
class Discriminator(nn.Module):
    def __init__(self, conv_dim):
        super(Discriminator, self).__init__()
        self.conv_dim = conv_dim
        self.conv1 = nn.Conv2d(3, conv_dim, kernel_size=4, stride=2, padding=1)
        self.conv2 = nn.Conv2d(conv_dim, conv_dim*2, kernel_size=4, stride=2, padding=1)
        self.conv3 = nn.Conv2d(conv_dim*2, conv_dim*4, kernel_size=4, stride=2, padding=1)
        self.conv4 = nn.Conv2d(conv_dim*4, conv_dim*8, kernel_size=4, stride=2, padding=1)
        self.fc = nn.Linear(conv_dim*8*4*4, 1)
        self.dropout = nn.Dropout(0.5)
        self.leaky_relu = nn.LeakyReLU(0.2)

    def forward(self, x):
        x = self.leaky_relu(self.conv1(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv2(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv3(x))
        x = self.dropout(x)
        x = self.leaky_relu(self.conv4(x))
        x = self.dropout(x)
        x = x.view(-1, self.conv_dim*8*4*4)
        x = self.fc(x)
        return x

# Training parameters
z_size = 100
conv_dim = 64

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

G = Generator(z_size=z_size, conv_dim=conv_dim).to(device)
D = Discriminator(conv_dim=conv_dim).to(device)

# Define loss
criterion = nn.BCEWithLogitsLoss()

# Optimizers
lr = 0.0002
beta1 = 0.5
beta2 = 0.999

g_optimizer = torch.optim.Adam(G.parameters(), lr, [beta1, beta2])
d_optimizer = torch.optim.Adam(D.parameters(), lr, [beta1, beta2])

# Training
num_epochs = 30
sample_interval = 200

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)

        # Train Discriminator
        d_optimizer.zero_grad()

        # Real images
        real_outputs = D(real_images)
        d_real_loss = criterion(real_outputs, torch.ones_like(real_outputs))

        # Fake images
        z = torch.randn(batch_size, z_size).to(device)
        fake_images = G(z)
        fake_outputs = D(fake_images.detach())
        d_fake_loss = criterion(fake_outputs,

Note: Remember, this code is not complete. It only serves as a template to indicate how the code for the entire project could look. The complete code would contain full definitions of the Generator and Discriminator classes, the training code within the for loops, data preprocessing steps, as well as other necessary components for the project. For brevity, those sections are not included here. The full detailed code should follow the step-by-step guidelines discussed in each section of the chapter.

Chapter 4 Conclusion

Congratulations on completing your first project using Generative Adversarial Networks (GANs)! In this chapter, we've covered every step of creating a GAN from start to finish. We've gone through the process of collecting and preprocessing data, constructing the Generator and Discriminator components of the GAN, training the GAN, and using it to generate new faces.

We've also dived deeper into some advanced topics like improving the quality of generated images, making the training process more stable, and extending our GAN with conditional inputs. We have seen how GANs, despite their complexity, can be understood, implemented, and modified when broken down into their individual components.

The project we undertook was a challenging one: generating realistic human faces. This task is a testament to the power of GANs and their impact on the field of generative deep learning. Our project demonstrated how this complex task can be accomplished with a well-designed and well-trained GAN.

Remember that the process doesn't stop here. GANs are a deep and complex subject, and there is always more to learn and experiment with. This project should serve as a solid foundation, but don't be afraid to venture into more complex architectures, different types of data, and new ideas. The field is rapidly evolving, and there's always something new and exciting to discover. 

In the next chapter, we'll explore another important category of generative models: Variational Autoencoders (VAEs). Like GANs, they provide a powerful way to generate new data, but they approach the problem from a different angle and offer their own unique advantages.

Onward to the next step in your generative deep learning journey!