ConvNetsTF

artificial-intelligencernnganmicrosoft-for-beginnerslessonsAImicrosoft-AI-For-Beginnersmachine-learning07-ConvNetsdeep-learning4-ComputerVisioncomputer-visioncnnNLP

Convolutional neural networks

We have seen before that neural networks are quite good at dealing with images, and even one-layer perceptron is able to recognize handwritten digits from MNIST dataset with reasonable accuracy. However, MNIST dataset is very special, and all digits are centered inside the image, which makes the task simpler.

In real life, we want to be able to recognize objects on the picture regardless of their exact location in the image. Computer vision is different from generic classification, because when we are trying to find a certain object in the picture, we are scanning the image looking for some specific patterns and their combinations. For example, when looking for a cat, we first may look for horizontal lines, which can form whiskers, and then certain combination of whiskers can tell us that it is actually a picture of a cat. Relative position and presence of certain patterns is important, and not their exact position on the image.

To extract patterns, we will use the notion of convolutional filters. But first, let us load all dependencies and functions that we have defined in the previous units. We will also import tfcv helper library that contain some useful functions that we do not want to define inside this notebook to keep the code short and clean.

[1]

In this example, we will focus on the MNIST dataset that we have seen before, and on image classification. We will start by loading the dataset using Keras built-in functions.

[2]

Convolutional filters

Convolutional filters are small windows that run over each pixel of the image and compute weighted average of the neighboring pixels.

They are defined by matrices of weight coefficients. Let's see the examples of applying two different convolutional filters over our MNIST handwritten digits:

[3]
OutputOutput

First filter is called a vertical edge filter, and it is defined by the following matrix:

(−101−101−101)\left( \begin{matrix} -1 & 0 & 1 \cr -1 & 0 & 1 \cr -1 & 0 & 1 \cr \end{matrix} \right)

When this filter goes over relatively uniform pixel field, all values add up to 0. However, when it encounters a vertical edge in the image, high spike value is generated. That's why in the images above you can see vertical edges represented by high and low values, while horizontal edges are averaged out.

An opposite thing happens when we apply horizontal edge filter - horizontal lines are amplified, and vertical are averaged out.

In classical computer vision, multiple filters were applied to the image to generate features, which then were used by machine learning algorithm to build a classifier. Those filters are in fact similar to neural structures that are available in the vision system of some animals.

However, in deep learning we construct networks that learn best convolutional filters to solve classification problem. To do that, we introduce convolutional layers.

Covolutional layers

To make the weights of convolutional layer trainable, we need somehow to reduce the process of applying convolutional filter window to the image to the matrix operations, which can then be subject to backward propagation training. To do this, we use a clever matrix transformation, which we call im2col.

Suppose we have a small image x\mathbf{x}, with the following pixels:

x=(abcdefghijklmnopqrstuvwxy)\mathbf{x} = \left( \begin{array}{ccccc} a & b & c & d & e \\ f & g & h & i & j \\ k & l & m & n & o \\ p & q & r & s & t \\ u & v & w & x & y \\ \end{array} \right)

And we want to apply two conv filters, with the following weights:

W(i)=(w00(i)w01(i)w02(i)w10(i)w11(i)w12(i)w20(i)w21(i)w22(i))W^{(i)} = \left(\begin{array}{ccc} w^{(i)}_{00} & w^{(i)}_{01} & w^{(i)}_{02} \\ w^{(i)}_{10} & w^{(i)}_{11} & w^{(i)}_{12} \\ w^{(i)}_{20} & w^{(i)}_{21} & w^{(i)}_{22} \\ \end{array}\right)

When applying the convolution, the first pixel of the result would be obtained by element-wise multiplication of (abcfghklm)\left(\begin{array}{ccc} a & b & c \\ f & g & h \\ k & l & m \\ \end{array}\right) and W(i)W^{(i)}, the second element - by multiplying by (bcdghilmn)\left(\begin{array}{ccc} b & c & d \\ g & h & i \\ l & m & n \\ \end{array}\right) by W(i)W^{(i)}, and so on.

To formalize this process, let's extract all 3×33\times3 fragments of the original image xx into the following matrix:

im2col(x)=[ab…g…mbc…h…ncd…i…ofg…l…rgh…m…shi…n…tkl…q…wlm…r…xmn…s…y]\mathrm{im2col}(x) = \left[ \begin{array}{cccccc} a & b & \ldots & g & \ldots & m \\ b & c & \ldots & h & \ldots & n \\ c & d & \ldots & i & \ldots & o \\ f & g & \ldots & l & \ldots & r \\ g & h & \ldots & m & \ldots & s \\ h & i & \ldots & n & \ldots & t \\ k & l & \ldots & q & \ldots & w \\ l & m & \ldots & r & \ldots & x \\ m & n & \ldots & s & \ldots & y \\ \end{array} \right]

Each column of this matrix corresponds to each 3×33\times3 subregion of the original image. Now, to get the result of the convolution, we just need to multiply this matrix by the matrix or weights

W=[w00(0)w01(0)w02(0)w10(0)w11(0)…w21(0)w22(0)w00(1)w01(1)w02(1)w10(1)w11(1)…w21(1)w22(1)]\mathbf{W} = \left[ \begin{array}{cccccccc} w^{(0)}_{00} & w^{(0)}_{01} & w^{(0)}_{02} & w^{(0)}_{10} & w^{(0)}_{11} & \ldots & w^{(0)}_{21} & w^{(0)}_{22} \\ w^{(1)}_{00} & w^{(1)}_{01} & w^{(1)}_{02} & w^{(1)}_{10} & w^{(1)}_{11} & \ldots & w^{(1)}_{21} & w^{(1)}_{22} \\ \end{array} \right]

(each row of this matrix contains weights of ii-th filter, flattened into one row)

So the application of a convolution filter to the original image can be replaced by matrix multiplication, which we already know how to handle using back prop:

C(x)=W×im2col(x)C(x) = W\times\mathbf{im2col}(x)

Convolutional layers are defined using Conv2d class. We need to specify the following:

  • filters - number of filters to use. We will use 9 different filters, which will give the network plenty of opportunities to explore which filters work best for our scenario.
  • kernel_size is the size of the sliding window. Usually 3x3 or 5x5 filters are used.

Simplest CNN will contain one convolutional layer. Given the input size 28x28, after applying nine 5x5 filters we will end up with a tensor of 24x24x9. The spatial dimension is smaller, because there are only 24 positions where a sliding interval of length 5 can fit into 28 pixels).

After convolution, we flatten 24x24x9 tensor into one vector of size 5184, and then add linear layer, to produce 10 classes. We also use relu activation function in between layers.

[4]
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 24, 24, 9)         234       
                                                                 
 flatten (Flatten)           (None, 5184)              0         
                                                                 
 dense (Dense)               (None, 10)                51850     
                                                                 
=================================================================
Total params: 52,084
Trainable params: 52,084
Non-trainable params: 0
_________________________________________________________________

You can see that this network contains around 50k trainable parameters, compared to around 80k in fully-connected multi-layered networks. This allows us to achieve good results even on smaller datasets, because convolutional networks generalize much better.

Note: In most of the practical cases, we want to apply convolutional layers to color images. Thus, Conv2D layer expects the input to be of the shape W×H×CW\times H\times C, where WW and HH are width and height of the image, and CC is the number of color channels. For grayscale images, we need the same shape with C=1C=1.

We need to reshape our data before starting training:

[5]
Epoch 1/5
1875/1875 [==============================] - 15s 7ms/step - loss: 0.2099 - acc: 0.9410 - val_loss: 0.0879 - val_acc: 0.9735
Epoch 2/5
1875/1875 [==============================] - 13s 7ms/step - loss: 0.0858 - acc: 0.9753 - val_loss: 0.0682 - val_acc: 0.9791
Epoch 3/5
1875/1875 [==============================] - 13s 7ms/step - loss: 0.0665 - acc: 0.9808 - val_loss: 0.0553 - val_acc: 0.9829
Epoch 4/5
1875/1875 [==============================] - 15s 8ms/step - loss: 0.0582 - acc: 0.9835 - val_loss: 0.0513 - val_acc: 0.9835
Epoch 5/5
1875/1875 [==============================] - 14s 8ms/step - loss: 0.0527 - acc: 0.9847 - val_loss: 0.0503 - val_acc: 0.9833
[ ]
Output

As you can see, we are able to achieve higher accuracy, and much faster (in terms of number of epochs), compared to the fully-connected networks from previous unit. However, the training itself requires more resources, and may be slower on non-GPU computers.

Visualizing Convolutional Layers

We can also visualize the weights of our trained convolutional layers, to try and make some more sense of what is going on:

[ ]
Output

You can see that some of those filters look like they can recognize some oblique strokes, while others look pretty random.

Task: Train the same network with 3x3 filters and visualize them. Do you see more familiar patterns?

Multi-layered CNNs and pooling layers

First convolutional layers looks for primitive patterns, such as horizontal or vertical lines, but we can apply further convolutional layers on top of them to look for higher-level patterns, such as primitive shapes. Then more convolutional layers can combine those shapes into some parts of the picture, up to the final object that we are trying to classify.

When doing so, we may also apply one trick: reducing the spatial size of the image. Once we have detected there is a horizontal stoke within sliding 3x3 window, it is not so important at which exact pixel it occurred. Thus we can "scale down" the size of the image, which is done using one of the pooling layers:

  • Average Pooling takes a sliding window (for example, 2x2 pixels) and computes an average of values within the window
  • Max Pooling replaces the window with the maximum value. The idea behind max pooling is to detect a presence of a certain pattern within the sliding window.

Thus, in a typical CNN there would be several convolutional layers, with pooling layers in between them to decrease dimensions of the image. We would also increase the number of filters, because as patterns become more advanced - there are more possible interesting combinations that we need to be looking for.

An image showing several convolutional layers with pooling layers.

Because of decreasing spatial dimensions and increasing feature/filters dimensions, this architecture is also called pyramid architecture.

[ ]
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 24, 24, 10)        260       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 12, 12, 10)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 8, 8, 20)          5020      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 4, 4, 20)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 320)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 10)                3210      
=================================================================
Total params: 8,490
Trainable params: 8,490
Non-trainable params: 0
_________________________________________________________________

Notice that the number of trainable parameters (~8.5K) is dramatically smaller than in previous cases. This happens because convolutional layers in general have few parameters, and dimensionality of the image before applying final dense layer is significantly reduced. Small number of parameters have positive impact on our models, because it helps to prevent overfitting even on smaller dataset sizes.

[ ]
Epoch 1/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0723 - acc: 0.9780 - val_loss: 0.0423 - val_acc: 0.9861
Epoch 2/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0523 - acc: 0.9842 - val_loss: 0.0425 - val_acc: 0.9866
Epoch 3/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0448 - acc: 0.9868 - val_loss: 0.0403 - val_acc: 0.9865
Epoch 4/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0383 - acc: 0.9886 - val_loss: 0.0323 - val_acc: 0.9888
Epoch 5/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0338 - acc: 0.9895 - val_loss: 0.0331 - val_acc: 0.9896
[ ]
Output

What you should probably observe is that we are able to achieve higher accuracy than with just one layer, and much faster in terms of number of epochs - just with 1 or 2 epochs. It means that sophisticated network architecture needs much fewer data to figure out what is going on, and to extract generic patterns from our images. However, training also takes longer, and requires a GPU.

Playing with real images from the CIFAR-10 dataset

While our handwritten digit recognition problem may seem like a toy problem, we are now ready to do something more serious. Let's explore more advanced dataset of pictures of different objects, called CIFAR-10. It contains 60k 32x32 images, divided into 10 classes.

[ ]
[ ]
Output

A well-known architecture for CIFAR-10 is called LeNet, and has been proposed by Yann LeCun. It follows the same principles as we have outlined above, the main difference being 3 input color channels instead of 1.

[ ]
Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_8 (Conv2D)            (None, 28, 28, 6)         456       
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 14, 14, 6)         0         
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 10, 10, 16)        2416      
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 5, 5, 16)          0         
_________________________________________________________________
flatten_3 (Flatten)          (None, 400)               0         
_________________________________________________________________
dense_9 (Dense)              (None, 120)               48120     
_________________________________________________________________
dense_10 (Dense)             (None, 84)                10164     
_________________________________________________________________
dense_11 (Dense)             (None, 10)                850       
=================================================================
Total params: 62,006
Trainable params: 62,006
Non-trainable params: 0
_________________________________________________________________

Training this network properly will take significant amount of time, and should preferably be done on GPU-enabled compute.

[ ]
Epoch 1/10
1563/1563 [==============================] - 6s 4ms/step - loss: 1.6205 - acc: 0.4033 - val_loss: 1.4287 - val_acc: 0.4743
Epoch 2/10
1563/1563 [==============================] - 6s 4ms/step - loss: 1.3435 - acc: 0.5154 - val_loss: 1.2804 - val_acc: 0.5412
Epoch 3/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.2225 - acc: 0.5645 - val_loss: 1.2164 - val_acc: 0.5600
Epoch 4/10
1563/1563 [==============================] - 5s 4ms/step - loss: 1.1360 - acc: 0.5957 - val_loss: 1.1918 - val_acc: 0.5768
Epoch 5/10
1563/1563 [==============================] - 5s 3ms/step - loss: 1.0776 - acc: 0.6178 - val_loss: 1.1451 - val_acc: 0.5906
Epoch 6/10
1563/1563 [==============================] - 5s 4ms/step - loss: 1.0228 - acc: 0.6370 - val_loss: 1.1178 - val_acc: 0.6098
Epoch 7/10
1563/1563 [==============================] - 5s 4ms/step - loss: 0.9769 - acc: 0.6544 - val_loss: 1.0793 - val_acc: 0.6202
Epoch 8/10
1563/1563 [==============================] - 5s 3ms/step - loss: 0.9340 - acc: 0.6711 - val_loss: 1.0783 - val_acc: 0.6271
Epoch 9/10
1563/1563 [==============================] - 5s 4ms/step - loss: 0.8983 - acc: 0.6824 - val_loss: 1.0952 - val_acc: 0.6203
Epoch 10/10
1563/1563 [==============================] - 6s 4ms/step - loss: 0.8648 - acc: 0.6939 - val_loss: 1.1103 - val_acc: 0.6217
[ ]
Output

The accuracy that we have been able to achieve with few epochs of training does not seem too great. However, remember that bling guessing would only give us 10% accuracy, and that our problem is actually significantly more difficult than MNIST digit classification. Getting above 50% accuracy in such a short training time seems like a good accomplishment.

Takeaways

In this unit, we have learned the main concept behind computer vision neural networks - convolutional networks. Real-life architectures that power image classification, object detection, and even image generation networks are all based on CNNs, just with more layers and some additional training tricks.