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()
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()
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()
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')]
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.