Deep Convolutional GANs

In this notebook, you'll build a GAN using convolutional layers in the generator and discriminator. This is called a Deep Convolutional GAN, or DCGAN for short. The DCGAN architecture was first explored in 2016 and has seen impressive results in generating new images; you can read the original paper, here.

You'll be training DCGAN on the Street View House Numbers (SVHN) dataset. These are color images of house numbers collected from Google street view. SVHN images are in color and much more variable than MNIST.

So, our goal is to create a DCGAN that can generate new, realistic-looking images of house numbers. We'll go through the following steps to do this:

  • Load in and pre-process the house numbers dataset
  • Define discriminator and generator networks
  • Train these adversarial networks
  • Visualize the loss over time and some sample, generated images

Deeper Convolutional Networks

Since this dataset is more complex than our MNIST data, we'll need a deeper network to accurately identify patterns in these images and be able to generate new ones. Specifically, we'll use a series of convolutional or transpose convolutional layers in the discriminator and generator. It's also necessary to use batch normalization to get these convolutional networks to train.

Besides these changes in network structure, training the discriminator and generator networks should be the same as before. That is, the discriminator will alternate training on real and fake (generated) images, and the generator will aim to trick the discriminator into thinking that its generated images are real!


In [1]:
# import libraries
import matplotlib.pyplot as plt
import numpy as np
import pickle as pkl

%matplotlib inline

Getting the data

Here you can download the SVHN dataset. It's a dataset built-in to the PyTorch datasets library. We can load in training data, transform it into Tensor datatypes, then create dataloaders to batch our data into a desired size.


In [2]:
import torch
from torchvision import datasets
from torchvision import transforms

# Tensor transform
transform = transforms.ToTensor()

# SVHN training datasets
svhn_train = datasets.SVHN(root='data/', split='train', download=True, transform=transform)

batch_size = 128
num_workers = 0

# build DataLoaders for SVHN dataset
train_loader = torch.utils.data.DataLoader(dataset=svhn_train,
                                          batch_size=batch_size,
                                          shuffle=True,
                                          num_workers=num_workers)


Downloading http://ufldl.stanford.edu/housenumbers/train_32x32.mat to data/train_32x32.mat

Visualize the Data

Here I'm showing a small sample of the images. Each of these is 32x32 with 3 color channels (RGB). These are the real, training images that we'll pass to the discriminator. Notice that each image has one associated, numerical label.


In [3]:
# obtain one batch of training images
dataiter = iter(train_loader)
images, labels = dataiter.next()

# plot the images in the batch, along with the corresponding labels
fig = plt.figure(figsize=(25, 4))
plot_size=20
for idx in np.arange(plot_size):
    ax = fig.add_subplot(2, plot_size/2, idx+1, xticks=[], yticks=[])
    ax.imshow(np.transpose(images[idx], (1, 2, 0)))
    # print out the correct label for each image
    # .item() gets the value contained in a Tensor
    ax.set_title(str(labels[idx].item()))


Pre-processing: scaling from -1 to 1

We need to do a bit of pre-processing; we know that the output of our tanh activated generator will contain pixel values in a range from -1 to 1, and so, we need to rescale our training images to a range of -1 to 1. (Right now, they are in a range from 0-1.)


In [4]:
# current range
img = images[0]

print('Min: ', img.min())
print('Max: ', img.max())


Min:  tensor(0.1647)
Max:  tensor(0.6275)

In [5]:
# helper scale function
def scale(x, feature_range=(-1, 1)):
    ''' Scale takes in an image x and returns that image, scaled
       with a feature_range of pixel values from -1 to 1. 
       This function assumes that the input x is already scaled from 0-1.'''
    # assume x is scaled to (0, 1)
    # scale to feature_range and return scaled x
    min, max = feature_range
    x = x * (max - min) + min
    return x

In [6]:
# scaled range
scaled_img = scale(img)

print('Scaled min: ', scaled_img.min())
print('Scaled max: ', scaled_img.max())


Scaled min:  tensor(-0.6706)
Scaled max:  tensor(0.2549)

Define the Model

A GAN is comprised of two adversarial networks, a discriminator and a generator.

Discriminator

Here you'll build the discriminator. This is a convolutional classifier like you've built before, only without any maxpooling layers.

  • The inputs to the discriminator are 32x32x3 tensor images
  • You'll want a few convolutional, hidden layers
  • Then a fully connected layer for the output; as before, we want a sigmoid output, but we'll add that in the loss function, BCEWithLogitsLoss, later

For the depths of the convolutional layers I suggest starting with 32 filters in the first layer, then double that depth as you add layers (to 64, 128, etc.). Note that in the DCGAN paper, they did all the downsampling using only strided convolutional layers with no maxpooling layers.

You'll also want to use batch normalization with nn.BatchNorm2d on each layer except the first convolutional layer and final, linear output layer.

Helper conv function

In general, each layer should look something like convolution > batch norm > leaky ReLU, and so we'll define a function to put these layers together. This function will create a sequential series of a convolutional + an optional batch norm layer. We'll create these using PyTorch's Sequential container, which takes in a list of layers and creates layers according to the order that they are passed in to the Sequential constructor.

Note: It is also suggested that you use a kernel_size of 4 and a stride of 2 for strided convolutions.


In [7]:
import torch.nn as nn
import torch.nn.functional as F

# helper conv function
def conv(in_channels, out_channels, kernel_size, stride=2, padding=1, batch_norm=True):
    """Creates a convolutional layer, with optional batch normalization.
    """
    layers = []
    conv_layer = nn.Conv2d(in_channels, out_channels, 
                           kernel_size, stride, padding, bias=False)
    
    # append conv layer
    layers.append(conv_layer)

    if batch_norm:
        # append batchnorm layer
        layers.append(nn.BatchNorm2d(out_channels))
     
    # using Sequential container
    return nn.Sequential(*layers)

In [8]:
class Discriminator(nn.Module):

    def __init__(self, conv_dim=32):
        super(Discriminator, self).__init__()

        # complete init function
        self.conv_dim = conv_dim

        # 32x32 input
        self.conv1 = conv(3, conv_dim, 4, batch_norm=False) # first layer, no batch_norm
        # 16x16 out
        self.conv2 = conv(conv_dim, conv_dim*2, 4)
        # 8x8 out
        self.conv3 = conv(conv_dim*2, conv_dim*4, 4)
        # 4x4 out
        
        # final, fully-connected layer
        self.fc = nn.Linear(conv_dim*4*4*4, 1)

    def forward(self, x):
        # all hidden layers + leaky relu activation
        out = F.leaky_relu(self.conv1(x), 0.2)
        out = F.leaky_relu(self.conv2(out), 0.2)
        out = F.leaky_relu(self.conv3(out), 0.2)
        
        # flatten
        out = out.view(-1, self.conv_dim*4*4*4)
        
        # final output layer
        out = self.fc(out)        
        return out

Generator

Next, you'll build the generator network. The input will be our noise vector z, as before. And, the output will be a $tanh$ output, but this time with size 32x32 which is the size of our SVHN images.

What's new here is we'll use transpose convolutional layers to create our new images.

  • The first layer is a fully connected layer which is reshaped into a deep and narrow layer, something like 4x4x512.
  • Then, we use batch normalization and a leaky ReLU activation.
  • Next is a series of transpose convolutional layers, where you typically halve the depth and double the width and height of the previous layer.
  • And, we'll apply batch normalization and ReLU to all but the last of these hidden layers. Where we will just apply a tanh activation.

Helper deconv function

For each of these layers, the general scheme is transpose convolution > batch norm > ReLU, and so we'll define a function to put these layers together. This function will create a sequential series of a transpose convolutional + an optional batch norm layer. We'll create these using PyTorch's Sequential container, which takes in a list of layers and creates layers according to the order that they are passed in to the Sequential constructor.

Note: It is also suggested that you use a kernel_size of 4 and a stride of 2 for transpose convolutions.


In [9]:
# helper deconv function
def deconv(in_channels, out_channels, kernel_size, stride=2, padding=1, batch_norm=True):
    """Creates a transposed-convolutional layer, with optional batch normalization.
    """
    # create a sequence of transpose + optional batch norm layers
    layers = []
    transpose_conv_layer = nn.ConvTranspose2d(in_channels, out_channels, 
                                              kernel_size, stride, padding, bias=False)
    # append transpose convolutional layer
    layers.append(transpose_conv_layer)
    
    if batch_norm:
        # append batchnorm layer
        layers.append(nn.BatchNorm2d(out_channels))
        
    return nn.Sequential(*layers)

In [10]:
class Generator(nn.Module):
    
    def __init__(self, z_size, conv_dim=32):
        super(Generator, self).__init__()

        # complete init function
        
        self.conv_dim = conv_dim
        
        # first, fully-connected layer
        self.fc = nn.Linear(z_size, conv_dim*4*4*4)

        # transpose conv layers
        self.t_conv1 = deconv(conv_dim*4, conv_dim*2, 4)
        self.t_conv2 = deconv(conv_dim*2, conv_dim, 4)
        self.t_conv3 = deconv(conv_dim, 3, 4, batch_norm=False)
        

    def forward(self, x):
        # fully-connected + reshape 
        out = self.fc(x)
        out = out.view(-1, self.conv_dim*4, 4, 4) # (batch_size, depth, 4, 4)
        
        # hidden transpose conv layers + relu
        out = F.relu(self.t_conv1(out))
        out = F.relu(self.t_conv2(out))
        
        # last layer + tanh activation
        out = self.t_conv3(out)
        out = F.tanh(out)
        
        return out

Build complete network

Define your models' hyperparameters and instantiate the discriminator and generator from the classes defined above. Make sure you've passed in the correct input arguments.


In [11]:
# define hyperparams
conv_dim = 32
z_size = 100

# define discriminator and generator
D = Discriminator(conv_dim)
G = Generator(z_size=z_size, conv_dim=conv_dim)

print(D)
print()
print(G)


Discriminator(
  (conv1): Sequential(
    (0): Conv2d(3, 32, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
  )
  (conv2): Sequential(
    (0): Conv2d(32, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (conv3): Sequential(
    (0): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (fc): Linear(in_features=2048, out_features=1, bias=True)
)

Generator(
  (fc): Linear(in_features=100, out_features=2048, bias=True)
  (t_conv1): Sequential(
    (0): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (t_conv2): Sequential(
    (0): ConvTranspose2d(64, 32, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  )
  (t_conv3): Sequential(
    (0): ConvTranspose2d(32, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
  )
)

Training on GPU

Check if you can train on GPU. If you can, set this as a variable and move your models to GPU.

Later, we'll also move any inputs our models and loss functions see (real_images, z, and ground truth labels) to GPU as well.


In [12]:
train_on_gpu = torch.cuda.is_available()

if train_on_gpu:
    # move models to GPU
    G.cuda()
    D.cuda()
    print('GPU available for training. Models moved to GPU')
else:
    print('Training on CPU.')


GPU available for training. Models moved to GPU

Discriminator and Generator Losses

Now we need to calculate the losses. And this will be exactly the same as before.

Discriminator Losses

  • For the discriminator, the total loss is the sum of the losses for real and fake images, d_loss = d_real_loss + d_fake_loss.
  • Remember that we want the discriminator to output 1 for real images and 0 for fake images, so we need to set up the losses to reflect that.

The losses will by binary cross entropy loss with logits, which we can get with BCEWithLogitsLoss. This combines a sigmoid activation function and and binary cross entropy loss in one function.

For the real images, we want D(real_images) = 1. That is, we want the discriminator to classify the the real images with a label = 1, indicating that these are real. The discriminator loss for the fake data is similar. We want D(fake_images) = 0, where the fake images are the generator output, fake_images = G(z).

Generator Loss

The generator loss will look similar only with flipped labels. The generator's goal is to get D(fake_images) = 1. In this case, the labels are flipped to represent that the generator is trying to fool the discriminator into thinking that the images it generates (fakes) are real!


In [13]:
def real_loss(D_out, smooth=False):
    batch_size = D_out.size(0)
    # label smoothing
    if smooth:
        # smooth, real labels = 0.9
        labels = torch.ones(batch_size)*0.9
    else:
        labels = torch.ones(batch_size) # real labels = 1
    # move labels to GPU if available     
    if train_on_gpu:
        labels = labels.cuda()
    # binary cross entropy with logits loss
    criterion = nn.BCEWithLogitsLoss()
    # calculate loss
    loss = criterion(D_out.squeeze(), labels)
    return loss

def fake_loss(D_out):
    batch_size = D_out.size(0)
    labels = torch.zeros(batch_size) # fake labels = 0
    if train_on_gpu:
        labels = labels.cuda()
    criterion = nn.BCEWithLogitsLoss()
    # calculate loss
    loss = criterion(D_out.squeeze(), labels)
    return loss

Optimizers

Not much new here, but notice how I am using a small learning rate and custom parameters for the Adam optimizers, This is based on some research into DCGAN model convergence.

Hyperparameters

GANs are very sensitive to hyperparameters. A lot of experimentation goes into finding the best hyperparameters such that the generator and discriminator don't overpower each other. Try out your own hyperparameters or read the DCGAN paper to see what worked for them.


In [14]:
import torch.optim as optim

# params
lr = 0.0002
beta1=0.5
beta2=0.999 # default value

# Create optimizers for the discriminator and generator
d_optimizer = optim.Adam(D.parameters(), lr, [beta1, beta2])
g_optimizer = optim.Adam(G.parameters(), lr, [beta1, beta2])

Training

Training will involve alternating between training the discriminator and the generator. We'll use our functions real_loss and fake_loss to help us calculate the discriminator losses in all of the following cases.

Discriminator training

  1. Compute the discriminator loss on real, training images
  2. Generate fake images
  3. Compute the discriminator loss on fake, generated images
  4. Add up real and fake loss
  5. Perform backpropagation + an optimization step to update the discriminator's weights

Generator training

  1. Generate fake images
  2. Compute the discriminator loss on fake images, using flipped labels!
  3. Perform backpropagation + an optimization step to update the generator's weights

Saving Samples

As we train, we'll also print out some loss statistics and save some generated "fake" samples.

Evaluation mode

Notice that, when we call our generator to create the samples to display, we set our model to evaluation mode: G.eval(). That's so the batch normalization layers will use the population statistics rather than the batch statistics (as they do during training), and so dropout layers will operate in eval() mode; not turning off any nodes for generating samples.


In [15]:
import pickle as pkl

# training hyperparams
num_epochs = 50

# keep track of loss and generated, "fake" samples
samples = []
losses = []

print_every = 300

# Get some fixed data for sampling. These are images that are held
# constant throughout training, and allow us to inspect the model's performance
sample_size=16
fixed_z = np.random.uniform(-1, 1, size=(sample_size, z_size))
fixed_z = torch.from_numpy(fixed_z).float()

# train the network
for epoch in range(num_epochs):
    
    for batch_i, (real_images, _) in enumerate(train_loader):
                
        batch_size = real_images.size(0)
        
        # important rescaling step
        real_images = scale(real_images)
        
        # ============================================
        #            TRAIN THE DISCRIMINATOR
        # ============================================
        
        d_optimizer.zero_grad()
        
        # 1. Train with real images

        # Compute the discriminator losses on real images 
        if train_on_gpu:
            real_images = real_images.cuda()
        
        D_real = D(real_images)
        d_real_loss = real_loss(D_real)
        
        # 2. Train with fake images
        
        # Generate fake images
        z = np.random.uniform(-1, 1, size=(batch_size, z_size))
        z = torch.from_numpy(z).float()
        # move x to GPU, if available
        if train_on_gpu:
            z = z.cuda()
        fake_images = G(z)
        
        # Compute the discriminator losses on fake images            
        D_fake = D(fake_images)
        d_fake_loss = fake_loss(D_fake)
        
        # add up loss and perform backprop
        d_loss = d_real_loss + d_fake_loss
        d_loss.backward()
        d_optimizer.step()
        
        
        # =========================================
        #            TRAIN THE GENERATOR
        # =========================================
        g_optimizer.zero_grad()
        
        # 1. Train with fake images and flipped labels
        
        # Generate fake images
        z = np.random.uniform(-1, 1, size=(batch_size, z_size))
        z = torch.from_numpy(z).float()
        if train_on_gpu:
            z = z.cuda()
        fake_images = G(z)
        
        # Compute the discriminator losses on fake images 
        # using flipped labels!
        D_fake = D(fake_images)
        g_loss = real_loss(D_fake) # use real loss to flip labels
        
        # perform backprop
        g_loss.backward()
        g_optimizer.step()

        # Print some loss stats
        if batch_i % print_every == 0:
            # append discriminator loss and generator loss
            losses.append((d_loss.item(), g_loss.item()))
            # print discriminator and generator loss
            print('Epoch [{:5d}/{:5d}] | d_loss: {:6.4f} | g_loss: {:6.4f}'.format(
                    epoch+1, num_epochs, d_loss.item(), g_loss.item()))

    
    ## AFTER EACH EPOCH##    
    # generate and save sample, fake images
    G.eval() # for generating samples
    if train_on_gpu:
        fixed_z = fixed_z.cuda()
    samples_z = G(fixed_z)
    samples.append(samples_z)
    G.train() # back to training mode


# Save training generator samples
with open('train_samples.pkl', 'wb') as f:
    pkl.dump(samples, f)


Epoch [    1/   50] | d_loss: 1.3871 | g_loss: 0.7894
Epoch [    1/   50] | d_loss: 0.8700 | g_loss: 2.5015
Epoch [    2/   50] | d_loss: 1.0024 | g_loss: 1.4002
Epoch [    2/   50] | d_loss: 1.2057 | g_loss: 1.1445
Epoch [    3/   50] | d_loss: 0.9766 | g_loss: 1.0346
Epoch [    3/   50] | d_loss: 0.9508 | g_loss: 0.9849
Epoch [    4/   50] | d_loss: 1.0338 | g_loss: 1.2916
Epoch [    4/   50] | d_loss: 0.7476 | g_loss: 1.7354
Epoch [    5/   50] | d_loss: 0.8847 | g_loss: 1.9047
Epoch [    5/   50] | d_loss: 0.9131 | g_loss: 2.6848
Epoch [    6/   50] | d_loss: 0.3747 | g_loss: 2.0961
Epoch [    6/   50] | d_loss: 0.5761 | g_loss: 1.4796
Epoch [    7/   50] | d_loss: 1.0538 | g_loss: 2.5600
Epoch [    7/   50] | d_loss: 0.5655 | g_loss: 1.1675
Epoch [    8/   50] | d_loss: 1.1302 | g_loss: 0.6184
Epoch [    8/   50] | d_loss: 0.6106 | g_loss: 1.3510
Epoch [    9/   50] | d_loss: 0.9274 | g_loss: 2.9446
Epoch [    9/   50] | d_loss: 0.4790 | g_loss: 2.1970
Epoch [   10/   50] | d_loss: 0.5220 | g_loss: 2.5080
Epoch [   10/   50] | d_loss: 0.4743 | g_loss: 1.6137
Epoch [   11/   50] | d_loss: 0.7634 | g_loss: 2.0439
Epoch [   11/   50] | d_loss: 0.4358 | g_loss: 2.0743
Epoch [   12/   50] | d_loss: 0.9056 | g_loss: 2.8160
Epoch [   12/   50] | d_loss: 0.3225 | g_loss: 2.2658
Epoch [   13/   50] | d_loss: 0.5460 | g_loss: 2.8996
Epoch [   13/   50] | d_loss: 0.2596 | g_loss: 2.5365
Epoch [   14/   50] | d_loss: 0.5203 | g_loss: 2.7283
Epoch [   14/   50] | d_loss: 0.4269 | g_loss: 2.0782
Epoch [   15/   50] | d_loss: 0.7738 | g_loss: 1.0159
Epoch [   15/   50] | d_loss: 0.4269 | g_loss: 1.9207
Epoch [   16/   50] | d_loss: 0.4112 | g_loss: 2.8402
Epoch [   16/   50] | d_loss: 0.3925 | g_loss: 1.5451
Epoch [   17/   50] | d_loss: 0.3715 | g_loss: 1.7045
Epoch [   17/   50] | d_loss: 0.7009 | g_loss: 3.0560
Epoch [   18/   50] | d_loss: 0.3727 | g_loss: 2.4927
Epoch [   18/   50] | d_loss: 0.3543 | g_loss: 2.8200
Epoch [   19/   50] | d_loss: 0.3737 | g_loss: 2.0694
Epoch [   19/   50] | d_loss: 0.3062 | g_loss: 2.7253
Epoch [   20/   50] | d_loss: 0.4124 | g_loss: 1.9771
Epoch [   20/   50] | d_loss: 0.6703 | g_loss: 2.5589
Epoch [   21/   50] | d_loss: 0.2947 | g_loss: 1.9481
Epoch [   21/   50] | d_loss: 0.4341 | g_loss: 2.1836
Epoch [   22/   50] | d_loss: 0.2596 | g_loss: 2.4994
Epoch [   22/   50] | d_loss: 0.2450 | g_loss: 2.8164
Epoch [   23/   50] | d_loss: 0.3782 | g_loss: 3.0394
Epoch [   23/   50] | d_loss: 0.3612 | g_loss: 2.5715
Epoch [   24/   50] | d_loss: 0.1769 | g_loss: 3.6052
Epoch [   24/   50] | d_loss: 0.2952 | g_loss: 3.0570
Epoch [   25/   50] | d_loss: 0.4618 | g_loss: 4.1390
Epoch [   25/   50] | d_loss: 0.2637 | g_loss: 3.1937
Epoch [   26/   50] | d_loss: 0.2144 | g_loss: 2.7835
Epoch [   26/   50] | d_loss: 0.2960 | g_loss: 3.2470
Epoch [   27/   50] | d_loss: 0.4798 | g_loss: 2.2351
Epoch [   27/   50] | d_loss: 0.2642 | g_loss: 3.3904
Epoch [   28/   50] | d_loss: 0.3362 | g_loss: 3.4251
Epoch [   28/   50] | d_loss: 0.8281 | g_loss: 4.0355
Epoch [   29/   50] | d_loss: 0.3406 | g_loss: 2.9678
Epoch [   29/   50] | d_loss: 0.1694 | g_loss: 3.6185
Epoch [   30/   50] | d_loss: 0.1856 | g_loss: 2.3010
Epoch [   30/   50] | d_loss: 0.8694 | g_loss: 3.1096
Epoch [   31/   50] | d_loss: 0.4050 | g_loss: 1.9086
Epoch [   31/   50] | d_loss: 0.2215 | g_loss: 2.7986
Epoch [   32/   50] | d_loss: 1.9408 | g_loss: 9.9110
Epoch [   32/   50] | d_loss: 0.2688 | g_loss: 3.3067
Epoch [   33/   50] | d_loss: 0.2684 | g_loss: 3.6979
Epoch [   33/   50] | d_loss: 0.3910 | g_loss: 2.7411
Epoch [   34/   50] | d_loss: 0.3097 | g_loss: 2.4075
Epoch [   34/   50] | d_loss: 0.1618 | g_loss: 3.0312
Epoch [   35/   50] | d_loss: 0.3315 | g_loss: 2.3083
Epoch [   35/   50] | d_loss: 0.2600 | g_loss: 4.0688
Epoch [   36/   50] | d_loss: 0.2050 | g_loss: 2.5516
Epoch [   36/   50] | d_loss: 0.1700 | g_loss: 3.3212
Epoch [   37/   50] | d_loss: 0.3518 | g_loss: 3.2154
Epoch [   37/   50] | d_loss: 0.2002 | g_loss: 2.3394
Epoch [   38/   50] | d_loss: 0.2736 | g_loss: 3.9040
Epoch [   38/   50] | d_loss: 0.2298 | g_loss: 2.2667
Epoch [   39/   50] | d_loss: 0.7857 | g_loss: 2.6393
Epoch [   39/   50] | d_loss: 0.1813 | g_loss: 3.2973
Epoch [   40/   50] | d_loss: 0.1523 | g_loss: 3.3429
Epoch [   40/   50] | d_loss: 0.2379 | g_loss: 3.4967
Epoch [   41/   50] | d_loss: 0.1664 | g_loss: 4.0548
Epoch [   41/   50] | d_loss: 0.7907 | g_loss: 2.7059
Epoch [   42/   50] | d_loss: 0.2806 | g_loss: 3.2769
Epoch [   42/   50] | d_loss: 0.3690 | g_loss: 4.7627
Epoch [   43/   50] | d_loss: 0.1582 | g_loss: 3.7862
Epoch [   43/   50] | d_loss: 0.2007 | g_loss: 1.8766
Epoch [   44/   50] | d_loss: 0.6256 | g_loss: 4.1040
Epoch [   44/   50] | d_loss: 0.6256 | g_loss: 4.5599
Epoch [   45/   50] | d_loss: 0.1694 | g_loss: 3.1243
Epoch [   45/   50] | d_loss: 0.2005 | g_loss: 2.0720
Epoch [   46/   50] | d_loss: 0.3385 | g_loss: 3.6107
Epoch [   46/   50] | d_loss: 0.1430 | g_loss: 3.3863
Epoch [   47/   50] | d_loss: 2.3355 | g_loss: 4.3694
Epoch [   47/   50] | d_loss: 0.2589 | g_loss: 3.8996
Epoch [   48/   50] | d_loss: 0.4828 | g_loss: 2.7014
Epoch [   48/   50] | d_loss: 0.3688 | g_loss: 3.0661
Epoch [   49/   50] | d_loss: 3.1969 | g_loss: 7.7299
Epoch [   49/   50] | d_loss: 0.3317 | g_loss: 4.3268
Epoch [   50/   50] | d_loss: 0.2904 | g_loss: 2.4885
Epoch [   50/   50] | d_loss: 0.2320 | g_loss: 2.1839

Training loss

Here we'll plot the training losses for the generator and discriminator, recorded after each epoch.


In [16]:
fig, ax = plt.subplots()
losses = np.array(losses)
plt.plot(losses.T[0], label='Discriminator', alpha=0.5)
plt.plot(losses.T[1], label='Generator', alpha=0.5)
plt.title("Training Losses")
plt.legend()


Out[16]:
<matplotlib.legend.Legend at 0x7f17062edc18>

Generator samples from training

Here we can view samples of images from the generator. We'll look at the images we saved during training.


In [19]:
# helper function for viewing a list of passed in sample images
def view_samples(epoch, samples):
    fig, axes = plt.subplots(figsize=(16,4), nrows=2, ncols=8, sharey=True, sharex=True)
    for ax, img in zip(axes.flatten(), samples[epoch]):
        img = img.detach().cpu().numpy()
        img = np.transpose(img, (1, 2, 0))
        img = ((img +1)*255 / (2)).astype(np.uint8) # rescale to pixel range (0-255)
        ax.xaxis.set_visible(False)
        ax.yaxis.set_visible(False)
        im = ax.imshow(img.reshape((32,32,3)))

In [20]:
_ = view_samples(-1, samples)



In [ ]: