Convolutional Neural Networks


Credit: Udacity's Deep Learning Nanodegree program

In this notebook, we train a CNN to classify images from the CIFAR-10 database.

The images in this database are small color images that fall into one of ten classes; some example images are pictured below.

Test for CUDA

Since these are larger (32x32x3) images, it may prove useful to speed up your training time by using a GPU. CUDA is a parallel computing platform and CUDA Tensors are the same as typical Tensors, only they utilize GPU's for computation.

In [1]:
import torch
import numpy as np

# check if CUDA is available
train_on_gpu = torch.cuda.is_available()

if not train_on_gpu:
    print('CUDA is not available.  Training on CPU ...')
else:
    print('CUDA is available!  Training on GPU ...')
CUDA is not available.  Training on CPU ...

Load the Data

Downloading may take a minute. We load in the training and test data, split the training data into a training and validation set, then create DataLoaders for each of these sets of data.

In [2]:
from torchvision import datasets
import torchvision.transforms as transforms
from torch.utils.data.sampler import SubsetRandomSampler

num_workers = 0   # number of subprocesses to use for data loading
batch_size = 20   # how many samples per batch to load
valid_size = 0.2  # percentage of training set to use as validation

# convert data to a normalized torch.FloatTensor
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])

# choose the training and test datasets
train_data = datasets.CIFAR10('data', train=True,
                              download=True, transform=transform)
test_data = datasets.CIFAR10('data', train=False,
                             download=True, transform=transform)

# obtain training indices that will be used for validation
num_train = len(train_data)
indices = list(range(num_train))
np.random.shuffle(indices)
split = int(np.floor(valid_size * num_train))
train_idx, valid_idx = indices[split:], indices[:split]

# define samplers for obtaining training and validation batches
train_sampler = SubsetRandomSampler(train_idx)
valid_sampler = SubsetRandomSampler(valid_idx)

# prepare data loaders (combine dataset and sampler)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size,
    sampler=train_sampler, num_workers=num_workers)
valid_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, 
    sampler=valid_sampler, num_workers=num_workers)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, 
    num_workers=num_workers)

# specify the image classes
classes = ['airplane', 'automobile', 'bird', 'cat', 'deer',
           'dog', 'frog', 'horse', 'ship', 'truck']
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to data/cifar-10-python.tar.gz
Extracting data/cifar-10-python.tar.gz to data
Files already downloaded and verified

Visualize a Batch of Training Data

In [3]:
import matplotlib.pyplot as plt
%matplotlib inline

# helper function to un-normalize and display an image
def imshow(img):
    img = img / 2 + 0.5  # unnormalize
    plt.imshow(np.transpose(img, (1, 2, 0)))  # convert from Tensor image
In [4]:
# obtain one batch of training images
dataiter = iter(train_loader)
images, labels = dataiter.next()
images = images.numpy() # convert images to numpy for display

# plot the images in the batch, along with the corresponding labels
fig = plt.figure(figsize=(25, 4))
# display 20 images
for idx in np.arange(20):
    ax = fig.add_subplot(2, 20/2, idx+1, xticks=[], yticks=[])
    imshow(images[idx])
    ax.set_title(classes[labels[idx]])

View an Image in More Detail

Here, we look at the normalized red, green, and blue (RGB) color channels as three separate, grayscale intensity images.

In [5]:
rgb_img = np.squeeze(images[3])
channels = ['red channel', 'green channel', 'blue channel']

fig = plt.figure(figsize = (36, 36)) 
for idx in np.arange(rgb_img.shape[0]):
    ax = fig.add_subplot(1, 3, idx + 1)
    img = rgb_img[idx]
    ax.imshow(img, cmap='gray')
    ax.set_title(channels[idx])
    width, height = img.shape
    thresh = img.max()/2.5
    for x in range(width):
        for y in range(height):
            val = round(img[x][y],2) if img[x][y] !=0 else 0
            ax.annotate(str(val), xy=(y,x),
                    horizontalalignment='center',
                    verticalalignment='center', size=8,
                    color='white' if img[x][y]<thresh else 'black')

Define the Network Architecture

This time, we'll define a CNN architecture. Instead of an MLP, which used linear, fully-connected layers, we'll use the following:

  • Convolutional layers, which can be thought of as stack of filtered images.
  • Maxpooling layers, which reduce the x-y size of an input, keeping only the most active pixels from the previous layer.
  • The usual Linear + Dropout layers to avoid overfitting and produce a 10-dim output.

A network with 2 convolutional layers is shown in the image below. And the network in the code is with 3 convolutional layers.

Define a model with multiple convolutional layers, and define the feedforward metwork behavior.

The more convolutional layers you include, the more complex patterns in color and shape a model can detect. It's suggested that your final model include 2 or 3 convolutional layers as well as linear layers + dropout in between to avoid overfitting.

It's good practice to look at existing research and implementations of related models as a starting point for defining your own models. You may find it useful to look at this PyTorch classification example or this, more complex Keras example to help decide on a final structure.

Output volume for a convolutional layer

To compute the output size of a given convolutional layer we can perform the following calculation (taken from Stanford's cs231n course):

We can compute the spatial size of the output volume as a function of the input volume size (W), the kernel/filter size (F), the stride with which they are applied (S), and the amount of zero padding used (P) on the border. The correct formula for calculating how many neurons define the output_W is given by (W−F+2P)/S+1.

For example for a 7x7 input and a 3x3 filter with stride 1 and pad 0 we would get a 5x5 output. With stride 2 we would get a 3x3 output.

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

# define the CNN architecture
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)  # convolutional layer (sees 32x32x3 image tensor)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1) # convolutional layer (sees 16x16x16 tensor)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1) # convolutional layer (sees 8x8x32 tensor)
        
        self.pool = nn.MaxPool2d(2, 2)        # max pooling layer
        self.fc1 = nn.Linear(64 * 4 * 4, 500) # linear layer (64 * 4 * 4 -> 500)
        self.fc2 = nn.Linear(500, 10)         # linear layer (500 -> 10)

        self.dropout = nn.Dropout(0.25)       # dropout layer (p=0.25)

    def forward(self, x):
        # add sequence of convolutional and max pooling layers
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = self.pool(F.relu(self.conv3(x)))

        x = x.view(-1, 64 * 4 * 4)  # flatten image input
        
        x = self.dropout(x)     # add dropout layer
        x = F.relu(self.fc1(x)) # add 1st hidden layer, with relu activation function
        
        x = self.dropout(x)     # add dropout layer
        x = self.fc2(x)         # add 2nd hidden layer, with relu activation function
        return x

# create a complete CNN
model = Net()
print(model)

# move tensors to GPU if CUDA is available
if train_on_gpu:
    model.cuda()
Net(
  (conv1): Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv2): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (conv3): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (fc1): Linear(in_features=1024, out_features=500, bias=True)
  (fc2): Linear(in_features=500, out_features=10, bias=True)
  (dropout): Dropout(p=0.25, inplace=False)
)

Specify Loss Function and Optimizer

Decide on a loss and optimization function that is best suited for this classification task. The linked code examples from above, may be a good starting point; this PyTorch classification example or this, more complex Keras example. Pay close attention to the value for learning rate as this value determines how our model converges to a small error.

Define the loss and optimizer and see how these choices change the loss over time.

In [7]:
import torch.optim as optim

# specify loss function (categorical cross-entropy)
criterion = nn.CrossEntropyLoss()

# specify optimizer
optimizer = optim.SGD(model.parameters(), lr=0.01)

Train the Network

Remember to look at how the training and validation loss decreases over time; if the validation loss ever increases it indicates possible overfitting. (In fact, in the below example, we could have stopped around epoch 33 or so!)

In [8]:
# number of epochs to train the model
n_epochs = 30

valid_loss_min = np.Inf # track change in validation loss

for epoch in range(1, n_epochs+1):

    # keep track of training and validation loss
    train_loss = 0.0
    valid_loss = 0.0
    
    ###################
    # train the model #
    ###################
    model.train()
    for data, target in train_loader:
        # move tensors to GPU if CUDA is available
        if train_on_gpu:
            data, target = data.cuda(), target.cuda()
        # clear the gradients of all optimized variables
        optimizer.zero_grad()
        
        output = model(data)             # forward pass: compute predicted outputs by passing inputs to the model
        loss = criterion(output, target) # calculate the batch loss

        loss.backward()   # backward pass: compute gradient of the loss with respect to model parameters
        optimizer.step()  # perform a single optimization step (parameter update)
        
        train_loss += loss.item()*data.size(0) # update training loss
        
    ######################    
    # validate the model #
    ######################
    model.eval()
    for data, target in valid_loader:
        # move tensors to GPU if CUDA is available
        if train_on_gpu:
            data, target = data.cuda(), target.cuda()
        output = model(data)                   # forward pass: compute predicted outputs by passing inputs to the model
        loss = criterion(output, target)       # calculate the batch loss
        valid_loss += loss.item()*data.size(0) # update average validation loss 
    
    # calculate average losses
    train_loss = train_loss/ len(train_loader.sampler)
    valid_loss = valid_loss/ len(valid_loader.sampler)
        
    # print training/validation statistics 
    print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f}'.format(
        epoch, train_loss, valid_loss))
    
    # save model if validation loss has decreased
    if valid_loss <= valid_loss_min:
        print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(
        valid_loss_min,
        valid_loss))
        torch.save(model.state_dict(), 'model_cifar.pt')
        valid_loss_min = valid_loss
Epoch: 1 	Training Loss: 2.129741 	Validation Loss: 1.844137
Validation loss decreased (inf --> 1.844137).  Saving model ...
Epoch: 2 	Training Loss: 1.692067 	Validation Loss: 1.512130
Validation loss decreased (1.844137 --> 1.512130).  Saving model ...
Epoch: 3 	Training Loss: 1.497202 	Validation Loss: 1.381921
Validation loss decreased (1.512130 --> 1.381921).  Saving model ...
Epoch: 4 	Training Loss: 1.379254 	Validation Loss: 1.282604
Validation loss decreased (1.381921 --> 1.282604).  Saving model ...
Epoch: 5 	Training Loss: 1.287359 	Validation Loss: 1.185551
Validation loss decreased (1.282604 --> 1.185551).  Saving model ...
Epoch: 6 	Training Loss: 1.210061 	Validation Loss: 1.126468
Validation loss decreased (1.185551 --> 1.126468).  Saving model ...
Epoch: 7 	Training Loss: 1.140791 	Validation Loss: 1.068255
Validation loss decreased (1.126468 --> 1.068255).  Saving model ...
Epoch: 8 	Training Loss: 1.080110 	Validation Loss: 1.016508
Validation loss decreased (1.068255 --> 1.016508).  Saving model ...
Epoch: 9 	Training Loss: 1.022645 	Validation Loss: 0.983536
Validation loss decreased (1.016508 --> 0.983536).  Saving model ...
Epoch: 10 	Training Loss: 0.972870 	Validation Loss: 0.937517
Validation loss decreased (0.983536 --> 0.937517).  Saving model ...
Epoch: 11 	Training Loss: 0.927451 	Validation Loss: 0.899967
Validation loss decreased (0.937517 --> 0.899967).  Saving model ...
Epoch: 12 	Training Loss: 0.882385 	Validation Loss: 0.855450
Validation loss decreased (0.899967 --> 0.855450).  Saving model ...
Epoch: 13 	Training Loss: 0.849731 	Validation Loss: 0.840973
Validation loss decreased (0.855450 --> 0.840973).  Saving model ...
Epoch: 14 	Training Loss: 0.802934 	Validation Loss: 0.839010
Validation loss decreased (0.840973 --> 0.839010).  Saving model ...
Epoch: 15 	Training Loss: 0.771120 	Validation Loss: 0.804474
Validation loss decreased (0.839010 --> 0.804474).  Saving model ...
Epoch: 16 	Training Loss: 0.741963 	Validation Loss: 0.801660
Validation loss decreased (0.804474 --> 0.801660).  Saving model ...
Epoch: 17 	Training Loss: 0.714152 	Validation Loss: 0.769414
Validation loss decreased (0.801660 --> 0.769414).  Saving model ...
Epoch: 18 	Training Loss: 0.679098 	Validation Loss: 0.776053
Epoch: 19 	Training Loss: 0.650363 	Validation Loss: 0.768203
Validation loss decreased (0.769414 --> 0.768203).  Saving model ...
Epoch: 20 	Training Loss: 0.625073 	Validation Loss: 0.741022
Validation loss decreased (0.768203 --> 0.741022).  Saving model ...
Epoch: 21 	Training Loss: 0.593329 	Validation Loss: 0.739588
Validation loss decreased (0.741022 --> 0.739588).  Saving model ...
Epoch: 22 	Training Loss: 0.571188 	Validation Loss: 0.719633
Validation loss decreased (0.739588 --> 0.719633).  Saving model ...
Epoch: 23 	Training Loss: 0.549154 	Validation Loss: 0.722312
Epoch: 24 	Training Loss: 0.524275 	Validation Loss: 0.737365
Epoch: 25 	Training Loss: 0.502138 	Validation Loss: 0.743853
Epoch: 26 	Training Loss: 0.483472 	Validation Loss: 0.745420
Epoch: 27 	Training Loss: 0.462984 	Validation Loss: 0.737420
Epoch: 28 	Training Loss: 0.443932 	Validation Loss: 0.745055
Epoch: 29 	Training Loss: 0.421292 	Validation Loss: 0.726055
Epoch: 30 	Training Loss: 0.406065 	Validation Loss: 0.720140

Load the Model with the Lowest Validation Loss

In [9]:
model.load_state_dict(torch.load('model_cifar.pt'))
Out[9]:
<All keys matched successfully>

Test the Trained Network

Test your trained model on previously unseen data.

In [10]:
# track test loss
test_loss = 0.0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

model.eval()
# iterate over test data
for data, target in test_loader:
    # move tensors to GPU if CUDA is available
    if train_on_gpu:
        data, target = data.cuda(), target.cuda()
        
    output = model(data)                   # forward pass: compute predicted outputs by passing inputs to the model
    loss = criterion(output, target)       # calculate the batch loss
    test_loss += loss.item()*data.size(0)  # update test loss 
    
    _, pred = torch.max(output, 1)                      # convert output probabilities to predicted class
    correct_tensor = pred.eq(target.data.view_as(pred)) # compare predictions to true label
    correct = np.squeeze(correct_tensor.numpy()) if not train_on_gpu else np.squeeze(correct_tensor.cpu().numpy())
    
    # calculate test accuracy for each object class
    for i in range(batch_size):
        label = target.data[i]
        class_correct[label] += correct[i].item()
        class_total[label] += 1

# average test loss
test_loss = test_loss/len(test_loader.dataset)
print('Test Loss: {:.6f}\n'.format(test_loss))

for i in range(10):
    if class_total[i] > 0:
        print('Test Accuracy of %5s: %2d%% (%2d/%2d)' % (
            classes[i], 100 * class_correct[i] / class_total[i],
            np.sum(class_correct[i]), np.sum(class_total[i])))
    else:
        print('Test Accuracy of %5s: N/A (no training examples)' % (classes[i]))

print('\nTest Accuracy (Overall): %2d%% (%2d/%2d)' % (
    100. * np.sum(class_correct) / np.sum(class_total),
    np.sum(class_correct), np.sum(class_total)))
Test Loss: 0.742244

Test Accuracy of airplane: 79% (795/1000)
Test Accuracy of automobile: 85% (859/1000)
Test Accuracy of  bird: 63% (633/1000)
Test Accuracy of   cat: 57% (575/1000)
Test Accuracy of  deer: 70% (702/1000)
Test Accuracy of   dog: 60% (601/1000)
Test Accuracy of  frog: 84% (841/1000)
Test Accuracy of horse: 76% (763/1000)
Test Accuracy of  ship: 87% (878/1000)
Test Accuracy of truck: 78% (780/1000)

Test Accuracy (Overall): 74% (7427/10000)

Visualize Sample Test Results

In [11]:
# obtain one batch of test images
dataiter = iter(test_loader)
images, labels = dataiter.next()
images.numpy()

# move model inputs to cuda, if GPU available
if train_on_gpu:
    images = images.cuda()

# get sample outputs
output = model(images)
# convert output probabilities to predicted class
_, preds_tensor = torch.max(output, 1)
preds = np.squeeze(preds_tensor.numpy()) if not train_on_gpu else np.squeeze(preds_tensor.cpu().numpy())

# plot the images in the batch, along with predicted and true labels
fig = plt.figure(figsize=(25, 4))
for idx in np.arange(20):
    ax = fig.add_subplot(2, 20/2, idx+1, xticks=[], yticks=[])
    imshow(images.cpu()[idx])
    ax.set_title("{} ({})".format(classes[preds[idx]], classes[labels[idx]]),
                 color=("green" if preds[idx]==labels[idx].item() else "red"))
In [ ]: