Imports¶

In [ ]:
# Keras/TensorFlow imports
import tensorflow as tf 
from keras.models import Sequential 
from tensorflow.keras.layers import Dense, Flatten, Conv2D, MaxPooling2D, Dropout, BatchNormalization, Resizing, Rescaling
from tensorflow.keras import regularizers
from keras.callbacks import EarlyStopping, ReduceLROnPlateau

# Other imports
import seaborn as sns
import numpy as np
import pandas as pd
import os
import matplotlib.pyplot as plt 
%matplotlib inline

Training constansts¶

In [ ]:
batch_size = 32
img_height = 224
img_width = 224
seed = 42

Load data¶

In [ ]:
# Define working directory
try:
    working_directory = os.path.dirname(__file__)
except:
    working_directory = str(os.getcwd())
In [ ]:
# Load and resize train data
train_data = tf.keras.utils.image_dataset_from_directory(
    working_directory + "/train",
    seed=seed,
    image_size=(img_height, img_width),
    batch_size=batch_size,
    shuffle=True
)
Found 6000 files belonging to 6 classes.
In [ ]:
# Load and resize validation data
val_data = tf.keras.utils.image_dataset_from_directory(
  working_directory + "/validation",
  seed=seed,
  image_size=(img_height, img_width),
  batch_size=batch_size,
  shuffle=True
)
Found 1200 files belonging to 6 classes.
In [ ]:
# Load and resize test data
test_data = tf.keras.utils.image_dataset_from_directory(
  working_directory + "/test",
  seed=seed,
  image_size=(img_height, img_width),
  batch_size=batch_size,
  shuffle=False
)
Found 1200 files belonging to 6 classes.

Display data¶

In [ ]:
# Display three random images from the train dataset
image_list = []
for images, labels in train_data:
    image_list.extend(images.numpy())

random_indices = np.random.choice(len(image_list), 3, replace=False)

plt.figure(figsize=(10, 5))
for i, index in enumerate(random_indices):
    plt.subplot(1, 3, i + 1)
    plt.imshow(image_list[index].astype("uint8"))
    plt.axis("off")
    plt.title(f"Randomly selected image: {index}", fontsize=12)
plt.show()
No description has been provided for this image

Examine class names and numbers¶

In [ ]:
# Display number and names of classes
class_names = train_data.class_names
num_classes = len(class_names)

print("There are " + (str(num_classes)) + " classes in the training dataset, they are: " + ', '.join(class_names))
There are 6 classes in the training dataset, they are: Bean, Broccoli, Cabbage, Carrot, Potato, Tomato
In [ ]:
# Define function to create distributions dataframe
def plot_label_distribution(dataset, dataset_label):
    labels = np.concatenate([batch[1] for batch in dataset], axis=0)
    df = pd.DataFrame(labels, columns = ['Labels'])
    df['Labels'] = df['Labels'].map({i:class_names[i] for i in range(num_classes)})
    df = df['Labels'].value_counts().reset_index(name='Count')
    df = df.rename(columns={'Count': 'Count ' + dataset_label})
    return df
In [ ]:
# Use function and see all class distributions
df_dist_training = plot_label_distribution(train_data, "Training")
df_dist_testing = plot_label_distribution(test_data, "Testing")
df_dist_validation = plot_label_distribution(val_data, "Validation")
df_df_all_dists = df_dist_training.merge(df_dist_testing, how='left', on='Labels')\
                .merge(df_dist_validation, how='left', on='Labels')
df_df_all_dists.head(len(df_df_all_dists))
Out[ ]:
Labels Count Training Count Testing Count Validation
0 Bean 1000 200 200
1 Tomato 1000 200 200
2 Carrot 1000 200 200
3 Cabbage 1000 200 200
4 Potato 1000 200 200
5 Broccoli 1000 200 200

Prepare dataset for training and evaluation¶

In [ ]:
val_data = val_data.unbatch().cache().shuffle(1000).batch(batch_size, drop_remainder=True)
train_data = train_data.unbatch().cache().shuffle(2000).batch(batch_size, drop_remainder=True)
test_data = test_data.unbatch().batch(batch_size, drop_remainder=True)

Build model¶

In [ ]:
# Design model
starting_num_filters = 32

model = Sequential()
model.add(tf.keras.layers.Resizing(img_width, img_height))
model.add(tf.keras.layers.Rescaling(1./255))
model.add(tf.keras.layers.Conv2D(filters=starting_num_filters, kernel_size=(5,5), padding='Same', activation='relu', kernel_regularizer=regularizers.l2(0.001)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2,2), strides=(2,2)))
          
model.add(tf.keras.layers.Conv2D(filters=starting_num_filters*2, kernel_size=(5,5), padding='Same', activation='relu', kernel_regularizer=regularizers.l2(0.001)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2,2), strides=(2,2)))
model.add(tf.keras.layers.BatchNormalization())
model.add(tf.keras.layers.Dropout(0.5))

model.add(tf.keras.layers.Conv2D(filters=starting_num_filters*3, kernel_size=(5,5), padding='Same', activation='relu', kernel_regularizer=regularizers.l2(0.001)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2,2), strides=(2,2)))
model.add(tf.keras.layers.BatchNormalization())
model.add(tf.keras.layers.Dropout(0.5))   

model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(units=512, activation='relu'))
model.add(tf.keras.layers.Dense(units=num_classes, activation='softmax'))

# Compile model
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['accuracy'])
In [ ]:
model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 resizing (Resizing)         (32, 224, 224, 3)         0         
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 resizing (Resizing)         (32, 224, 224, 3)         0         
                                                                 
 rescaling (Rescaling)       (32, 224, 224, 3)         0         
                                                                 
 conv2d (Conv2D)             (32, 224, 224, 32)        2432      
                                                                 
 max_pooling2d (MaxPooling2  (32, 112, 112, 32)        0         
 D)                                                              
                                                                 
 conv2d_1 (Conv2D)           (32, 112, 112, 64)        51264     
                                                                 
 max_pooling2d_1 (MaxPoolin  (32, 56, 56, 64)          0         
 g2D)                                                            
                                                                 
 batch_normalization (Batch  (32, 56, 56, 64)          256       
 Normalization)                                                  
                                                                 
 dropout (Dropout)           (32, 56, 56, 64)          0         
                                                                 
 conv2d_2 (Conv2D)           (32, 56, 56, 96)          153696    
                                                                 
 max_pooling2d_2 (MaxPoolin  (32, 28, 28, 96)          0         
 g2D)                                                            
                                                                 
 batch_normalization_1 (Bat  (32, 28, 28, 96)          384       
 chNormalization)                                                
                                                                 
 dropout_1 (Dropout)         (32, 28, 28, 96)          0         
                                                                 
 flatten (Flatten)           (32, 75264)               0         
                                                                 
 dense (Dense)               (32, 512)                 38535680  
                                                                 
 dense_1 (Dense)             (32, 6)                   3078      
                                                                 
=================================================================
Total params: 38746790 (147.81 MB)
Trainable params: 38746470 (147.81 MB)
Non-trainable params: 320 (1.25 KB)
_________________________________________________________________

Define callbacks and train (fit) model¶

In [ ]:
# Callbacks
early_stopping=EarlyStopping(monitor='val_loss',
                             patience=10, 
                             start_from_epoch=3, 
                             restore_best_weights=True <
                             )

reduce_lr=ReduceLROnPlateau(monitor='val_loss',
                            factor=0.15, 
                            patience=2,
                            min_lr=1e-10,
                            min_delta=0.0004,
                            mode='min')

# Fit model
history = model.fit(
       train_data,
       epochs = 50,
       validation_data = val_data,
       batch_size = batch_size,
       callbacks = [early_stopping, reduce_lr],
       verbose = 1)
Epoch 1/50
187/187 [==============================] - 399s 2s/step - loss: 0.7895 - accuracy: 0.8336 - val_loss: 8.3138 - val_accuracy: 0.1655 - lr: 1.0000e-04
Epoch 2/50
187/187 [==============================] - 451s 2s/step - loss: 0.2554 - accuracy: 0.9597 - val_loss: 7.3579 - val_accuracy: 0.3184 - lr: 1.0000e-04
Epoch 3/50
187/187 [==============================] - 414s 2s/step - loss: 0.2028 - accuracy: 0.9733 - val_loss: 3.2689 - val_accuracy: 0.4983 - lr: 1.0000e-04
Epoch 4/50
187/187 [==============================] - 404s 2s/step - loss: 0.1893 - accuracy: 0.9808 - val_loss: 1.7649 - val_accuracy: 0.6993 - lr: 1.0000e-04
Epoch 5/50
187/187 [==============================] - 410s 2s/step - loss: 0.1593 - accuracy: 0.9858 - val_loss: 2.4070 - val_accuracy: 0.6774 - lr: 1.0000e-04
Epoch 6/50
187/187 [==============================] - 410s 2s/step - loss: 0.1730 - accuracy: 0.9853 - val_loss: 1.5810 - val_accuracy: 0.7449 - lr: 1.0000e-04
Epoch 7/50
187/187 [==============================] - 411s 2s/step - loss: 0.1473 - accuracy: 0.9905 - val_loss: 1.0227 - val_accuracy: 0.8100 - lr: 1.0000e-04
Epoch 8/50
187/187 [==============================] - 418s 2s/step - loss: 0.1346 - accuracy: 0.9921 - val_loss: 7.6148 - val_accuracy: 0.4113 - lr: 1.0000e-04
Epoch 9/50
187/187 [==============================] - 415s 2s/step - loss: 0.1625 - accuracy: 0.9868 - val_loss: 1.9015 - val_accuracy: 0.7399 - lr: 1.0000e-04
Epoch 10/50
187/187 [==============================] - 413s 2s/step - loss: 0.1199 - accuracy: 0.9962 - val_loss: 0.8005 - val_accuracy: 0.8632 - lr: 1.5000e-05
Epoch 11/50
187/187 [==============================] - 417s 2s/step - loss: 0.1097 - accuracy: 0.9997 - val_loss: 0.7377 - val_accuracy: 0.8708 - lr: 1.5000e-05
Epoch 12/50
187/187 [==============================] - 408s 2s/step - loss: 0.1098 - accuracy: 0.9995 - val_loss: 0.8246 - val_accuracy: 0.8547 - lr: 1.5000e-05
Epoch 13/50
187/187 [==============================] - 411s 2s/step - loss: 0.1085 - accuracy: 0.9998 - val_loss: 0.7918 - val_accuracy: 0.8581 - lr: 1.5000e-05
Epoch 14/50
187/187 [==============================] - 432s 2s/step - loss: 0.1087 - accuracy: 0.9997 - val_loss: 0.7854 - val_accuracy: 0.8640 - lr: 2.2500e-06
Epoch 15/50
187/187 [==============================] - 415s 2s/step - loss: 0.1078 - accuracy: 0.9998 - val_loss: 0.7829 - val_accuracy: 0.8657 - lr: 2.2500e-06
Epoch 16/50
187/187 [==============================] - 420s 2s/step - loss: 0.1093 - accuracy: 0.9992 - val_loss: 0.7899 - val_accuracy: 0.8640 - lr: 3.3750e-07
Epoch 17/50
187/187 [==============================] - 424s 2s/step - loss: 0.1079 - accuracy: 0.9997 - val_loss: 0.7719 - val_accuracy: 0.8657 - lr: 3.3750e-07
Epoch 18/50
187/187 [==============================] - 416s 2s/step - loss: 0.1082 - accuracy: 0.9997 - val_loss: 0.7980 - val_accuracy: 0.8632 - lr: 5.0625e-08
Epoch 19/50
187/187 [==============================] - 417s 2s/step - loss: 0.1080 - accuracy: 0.9997 - val_loss: 0.7933 - val_accuracy: 0.8657 - lr: 5.0625e-08
Epoch 20/50
187/187 [==============================] - 415s 2s/step - loss: 0.1077 - accuracy: 1.0000 - val_loss: 0.7863 - val_accuracy: 0.8632 - lr: 7.5937e-09
Epoch 21/50
187/187 [==============================] - 416s 2s/step - loss: 0.1081 - accuracy: 0.9997 - val_loss: 0.7896 - val_accuracy: 0.8649 - lr: 7.5937e-09

Visualise training results¶

In [ ]:
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']
In [ ]:
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
No description has been provided for this image

Evaluate model on test (unseen) data¶

In [ ]:
loss, accuracy = model.evaluate(test_data)
37/37 [==============================] - 18s 493ms/step - loss: 0.7549 - accuracy: 0.8590
In [ ]:
plt.figure(figsize=(5, 5))
plt.pie([accuracy, 1-accuracy], labels=['Correct', 'Incorrect'], colors=['green', 'red'], autopct='%1.1f%%', startangle=90)
plt.title('Correct vs incorrect predictions on unseen images')
plt.show()
No description has been provided for this image
In [ ]:
# Get actual labels and make predictions
actual = [labels for _, labels in test_data.unbatch()]
predicted = model.predict(test_data)
actual = tf.stack(actual, axis=0)
predicted = tf.concat(predicted, axis=0)
predicted = tf.argmax(predicted, axis=1)
In [ ]:
# Create plot
confusion_matrix = tf.math.confusion_matrix(actual, predicted, num_classes=num_classes)
ax = sns.heatmap(confusion_matrix, annot=True, fmt='g', cmap='Blues')
sns.set(rc={'figure.figsize':(5, 5)})
sns.set(font_scale=1.4)
ax.set_title('Confusion matrix of vegetable recognition on test dataset')
ax.set_xlabel('Predicted vegetable')
ax.set_ylabel('Actual vegetable')
plt.xticks(rotation=90)
plt.yticks(rotation=0)
ax.xaxis.set_ticklabels(class_names)
ax.yaxis.set_ticklabels(class_names)
Out[ ]:
[Text(0, 0.5, 'Bean'),
 Text(0, 1.5, 'Broccoli'),
 Text(0, 2.5, 'Cabbage'),
 Text(0, 3.5, 'Carrot'),
 Text(0, 4.5, 'Potato'),
 Text(0, 5.5, 'Tomato')]
No description has been provided for this image

Save model locally¶

In [ ]:
model_name = 'vegetable_cnn'
model_name_with_ext = model_name + '.keras'
model.save(working_directory + model_name_with_ext)

model_size_mb = round(os.stat(working_directory + model_name_with_ext).st_size/2**20,2)
print(str(model_size_mb) + 'mb model saved.')
443.48mb model saved.