In this blog, we will build a multi-block 2D CNN in TensorFlow 2.0 with Conv2D, MaxPooling, and Dropout layers. We train it to classify 10 object categories from the CIFAR-10 dataset. Convolutional Neural Networks (CNNs) use learned filters to pull spatial features from images, which makes them the top choice for object recognition.
Download Data and Model Building
!pip install tensorflow
!pip install mlxtend
import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Flatten, Dense, Conv2D, MaxPool2D, Dropout
print(tf.__version__)
2.1.1
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from tensorflow.keras.datasets import cifar10
The CIFAR10 dataset has 60,000 color images in 10 classes, with 6,000 images in each class. It is split into 50,000 training images and 10,000 testing images. The classes do not overlap.
(X_train, y_train), (X_test, y_test) = cifar10.load_data()
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170500096/170498071 [==============================] - 50s 0us/step
classes_name = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
X_train.max()
255
X_train = X_train/255
X_test = X_test/255
X_train.shape, X_test.shape
((50000, 32, 32, 3), (10000, 32, 32, 3))
Verify the data
Plot the first test image to confirm the data loaded correctly:
plt.imshow(X_test[0])
y_test
array([[3],
[8],
[8],
...,
[5],
[1],
[7]], dtype=uint8)
Build CNN Model
The code below defines the convolutional base with a common pattern: a stack of Conv2D, MaxPooling2D, Dropout, Flatten, and Dense layers.
A Conv2D takes tensors of shape (image_height, image_width, color_channels), and ignores the batch size. Here we set it to process inputs of shape (32, 32, 3), the format of CIFAR images.
The Maxpool2D() layer downsamples the input by taking the largest value in each pool_size(2,2) window along the feature axis. The window moves by strides(2) each step. With the "valid" padding option, the output has a smaller shape.
Dropout() randomly sets some hidden-unit outputs to 0 at each training step. The value we pass sets the chance that an output is dropped.
Flatten() is used to convert the data into a 1-dimensional array for inputting it to the next layer.
Dense() is the regular fully connected layer with 128 neurons. The output layer is also a dense layer, with 10 neurons for the 10 classes.
The activation function is softmax. Softmax turns a real vector into a vector of class probabilities. The output values are between 0 and 1 and sum to 1. We often use it in the last layer of a classifier, since we can read the result as a probability spread.
model = Sequential()
model.add(Conv2D(filters=32, kernel_size=(3, 3), padding='same', activation='relu', input_shape = [32, 32, 3]))
model.add(Conv2D(filters=32, kernel_size=(3, 3), padding='same', activation='relu'))
model.add(MaxPool2D(pool_size=(2,2), strides=2, padding='valid'))
model.add(Dropout(0.5))
model.add(Flatten())
model.add(Dense(units = 128, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d (Conv2D) (None, 32, 32, 32) 896
_________________________________________________________________
conv2d_1 (Conv2D) (None, 32, 32, 32) 9248
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 16, 16, 32) 0
_________________________________________________________________
dropout (Dropout) (None, 16, 16, 32) 0
_________________________________________________________________
flatten (Flatten) (None, 8192) 0
_________________________________________________________________
dense (Dense) (None, 128) 1048704
_________________________________________________________________
dense_1 (Dense) (None, 10) 1290
=================================================================
Total params: 1,060,138
Trainable params: 1,060,138
Non-trainable params: 0
_________________________________________________________________
Compile and train the model
We compile the model and fit it to the training data for 10 epochs. An epoch is one pass over the whole data. validation_data is the data used to check the loss and metrics at the end of each epoch, and the model does not train on it. With metrics = ['sparse_categorical_accuracy'], we judge the model by accuracy.
model.compile(optimizer='adam', loss = 'sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])
history = model.fit(X_train, y_train, batch_size=10, epochs=10, verbose=1, validation_data=(X_test, y_test))
Train on 50000 samples, validate on 10000 samples
Epoch 1/10
50000/50000 [==============================] - 177s 4ms/sample - loss: 1.4127 - sparse_categorical_accuracy: 0.4918 - val_loss: 1.1079 - val_sparse_categorical_accuracy: 0.6095
Epoch 2/10
50000/50000 [==============================] - 159s 3ms/sample - loss: 1.1058 - sparse_categorical_accuracy: 0.6091 - val_loss: 1.0284 - val_sparse_categorical_accuracy: 0.6377
Epoch 3/10
50000/50000 [==============================] - 146s 3ms/sample - loss: 0.9946 - sparse_categorical_accuracy: 0.6477 - val_loss: 0.9682 - val_sparse_categorical_accuracy: 0.6564
The charts below show model accuracy and model loss. The accuracy chart plots training vs validation accuracy. The loss chart plots training vs validation loss.
# Plot training & validation accuracy values
epoch_range = range(1, 11)
plt.plot(epoch_range, history.history['sparse_categorical_accuracy'])
plt.plot(epoch_range, history.history['val_sparse_categorical_accuracy'])
plt.title('Model accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Val'], loc='upper left')
plt.show()
# Plot training & validation loss values
plt.plot(epoch_range, history.history['loss'])
plt.plot(epoch_range, history.history['val_loss'])
plt.title('Model loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Val'], loc='upper left')
plt.show()
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=2)
10000/10000 - 5s - loss: 0.9383 - sparse_categorical_accuracy: 0.6830
from mlxtend.plotting import plot_confusion_matrix
from sklearn.metrics import confusion_matrix
y_pred = model.predict_classes(X_test)
y_pred
array([3, 8, 8, ..., 5, 1, 7])
y_test
array([[3],
[8],
[8],
...,
[5],
[1],
[7]], dtype=uint8)
mat = confusion_matrix(y_test, y_pred)
mat
array([[737, 27, 22, 17, 14, 4, 12, 14, 106, 47],
[ 20, 821, 3, 12, 0, 7, 5, 4, 47, 81],
[ 95, 8, 476, 97, 83, 110, 67, 30, 21, 13],
[ 34, 14, 42, 520, 52, 203, 58, 43, 21, 13],
[ 22, 4, 74, 118, 570, 69, 54, 66, 21, 2],
[ 23, 5, 34, 213, 24, 610, 17, 47, 16, 11],
[ 10, 8, 34, 80, 42, 40, 760, 8, 13, 5],
[ 26, 5, 23, 45, 51, 76, 5, 743, 12, 14],
[ 56, 41, 9, 10, 3, 4, 3, 2, 843, 29],
[ 43, 116, 5, 18, 6, 4, 5, 21, 32, 750]])
plot_confusion_matrix(mat,figsize=(9,9), class_names=classes_name, show_normed=True)
Conclusion
In this blog, we built a 2D CNN in TensorFlow 2.0 to classify images from the CIFAR-10 dataset into 10 object categories. After 10 epochs the model reached about 68% test accuracy. The confusion matrix showed that visually similar classes, especially bird, cat, deer, and dog, are the hardest to tell apart, while airplane and ship scored the highest.
Key takeaways:
- A two-block Conv2D + MaxPool2D architecture with Dropout achieves solid baseline performance on CIFAR-10.
- Validation accuracy pulling away from training accuracy after epoch 3 is a clear sign of overfitting. Reduce the model size or add stronger regularization.
- The confusion matrix shows per-class weaknesses far better than a single accuracy number. Always inspect it when classes look similar.
Next steps:
- Try Dog vs Cat Classification with CNN to apply a deeper VGG16-inspired architecture with ImageDataGenerator augmentation.
- Explore Image Classification with Pre-trained VGG-16 to see how transfer learning dramatically boosts accuracy on image tasks.
- Add
BatchNormalizationafter each convolutional block and tuneDropoutrate to reduce the overfitting gap observed in this model.