Skip to main content

Dr. Crispy the Potato

dr-crispy

Prerequisites

This is a practical project that assumes you have already covered the following concepts:

Visualize Dataset

for dir in "$(pwd)"/*; do                      
label=$(basename "$dir")
count=$(find "$dir" -type f -name "*.JPG" | wc -l)
echo "\"$label\": $count"
done
"Potato___Early_blight": 1000
"Potato___Late_blight": 1000
"Potato___healthy": 152

Early Blight

| | | | | | | |

Late Blight

| | | | | | | |

Healthy

| | | | | | | |

Visualize Wrong predictions

Late Blight predected HealthyLate Blight predected HealthyLate Blight predected Early Blight
Late Blight predected HealthyLate Blight predected Healthy

Output

Confusion Matrix

confusion-matrix

Accuracy/Loss Graph

accuracy

Logs

[2023-10-19 21:16:49] Train size: 54, Validation size: 6, Test size: 8
[2023-10-19 21:16:49] Training model for 50 epochs.
[2023-10-19 21:48:43] Training took 1,914.36 seconds.
[2023-10-19 21:53:24] Loss: 0.0487, Accuracy: 98.05%
[2023-10-19 22:32:04] Number of wrong predictions: 5 / 256
[2023-10-19 22:32:04] Accuracy: 98.05%
[2023-10-26 11:12:11] Train size: 54, Validation size: 6, Test size: 8

Important Concepts

Data Augmentation

  • Data augmentation is a technique to artificially create new training data from existing training data.

Code

Import and Magic Numbers

import os
from typing import Final

import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf

import utils
EPOCHS: Final[int] = 50

utils.py

BASE_DIR: Final[Path] = Path(__file__).resolve().parent.parent.parent
DATA_DIR: Final[Path] = os.path.join(BASE_DIR, "data")
LOGGING_DIR: Final[Path] = os.path.join(BASE_DIR, "logs")
TEST_DIR: Final[Path] = os.path.join(DATA_DIR, "test")

IMG_HEIGHT: Final[int] = 256
IMG_WIDTH: Final[int] = 256
CHANNELS: Final[int] = 3

BATCH_SIZE: Final[int] = 32


def show_image(image: np.ndarray, title: str) -> None:
cv2.startWindowThread()
cv2.imshow(title, image)
while cv2.waitKey(1) & 0xFF != ord("q"):
pass
cv2.destroyWindow(title)
# cv2 on Mac M1 is still buggy, so we need to wait a bit
for _ in range(5):
cv2.waitKey(1)


def log_to_file(message: str, custom_file: str="tune.log") -> None:
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] {message}"
logs_file_path: Path = os.path.join(LOGGING_DIR, custom_file)
with open(logs_file_path, "a") as f:
f.write(f"{log_message}\n")

Load Dataset

dataset: tf.data.Dataset = tf.keras.preprocessing.image_dataset_from_directory(
os.path.join(utils.DATA_DIR, 'dataset'),
shuffle=True,
image_size=(utils.IMG_HEIGHT, utils.IMG_WIDTH),
batch_size=utils.BATCH_SIZE
)
Found 2152 files belonging to 3 classes.
class_names: list[str] = dataset.class_names
print(class_names)
['Potato___Early_blight', 'Potato___Late_blight', 'Potato___healthy']

Preprocessing

  • 80% "1721 Images or 54 Batches" of the data is used for training
  • 10% "0215 Images or 06 Batches" of the data is used for validation
  • 10% "0216 Images or 08 Batches" of the data is used for testing
    -- total: 2152 images
def get_dataset_partitions(
dataset: tf.data.Dataset,
train_split: float=0.8,
validation_split: float=0.1,
test_split: float=0.1,
shuffle: bool=True
) -> tuple[tf.data.Dataset, tf.data.Dataset, tf.data.Dataset]:
"""Split dataset into train, validation and test partitions."""
DATASET_SIZE: Final[int] = dataset.cardinality().numpy()

if shuffle:
import random
dataset = dataset.shuffle(
buffer_size=dataset.cardinality(),
seed=random.randint(0, 10_000)
)

train_size = int(train_split * DATASET_SIZE)
val_size = int(validation_split * DATASET_SIZE)


train_dataset = dataset.take(train_size)
validation_dataset = dataset.skip(train_size).take(val_size)
test_dataset = dataset.skip(train_size + val_size)

utils.log_to_file(
f"Train size: {len(train_dataset)}, Validation size: {len(validation_dataset)}, Test size: {len(test_dataset)}"
)

return train_dataset, validation_dataset, test_dataset
train_dataset, validation_dataset, test_dataset = get_dataset_partitions(dataset)

Shuffle and Cache

train_dataset = train_dataset.cache().shuffle(
buffer_size=train_dataset.cardinality()
).prefetch(
buffer_size=tf.data.AUTOTUNE
)

validation_dataset = validation_dataset.cache().shuffle(
buffer_size=validation_dataset.cardinality()
).prefetch(
buffer_size=tf.data.AUTOTUNE
)

test_dataset = test_dataset.cache().shuffle(
buffer_size=test_dataset.cardinality()
).prefetch(
buffer_size=tf.data.AUTOTUNE
)

Normalize and Data Augmentation

# Normalize the data
resize_rescale = tf.keras.Sequential([
tf.keras.layers.experimental.preprocessing.Resizing(
utils.IMG_HEIGHT,
utils.IMG_WIDTH
),
tf.keras.layers.experimental.preprocessing.Rescaling(1.0/255)
])

# Data augmentation
data_augmentation = tf.keras.Sequential([
tf.keras.layers.experimental.preprocessing.RandomFlip(
"horizontal_and_vertical"
),
tf.keras.layers.experimental.preprocessing.RandomRotation(0.2)
])

Training

input_shape = (
utils.BATCH_SIZE,
utils.IMG_HEIGHT,
utils.IMG_WIDTH,
utils.CHANNELS
)

model = tf.keras.Sequential([
resize_rescale,
data_augmentation,
tf.keras.layers.Conv2D(filters=32, kernel_size=(3, 3), activation='relu', input_shape=input_shape),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(units=64, activation='relu'),
tf.keras.layers.Dense(units=3, activation='softmax')
])

model.build(input_shape=input_shape)
model.compile(
optimizer=tf.keras.optimizers.Adam(),
loss=tf.keras.losses.SparseCategoricalCrossentropy(),
metrics=['accuracy']
)
utils.log_to_file(f"Training model for {EPOCHS} epochs.")
import time
start_time = time.time()
history = model.fit(
train_dataset,
epochs=EPOCHS,
validation_data=validation_dataset
)
utils.log_to_file(f"Training took {(time.time() - start_time):,.2f} seconds.")
accuracy_history = history.history['accuracy']
val_accuracy_history = history.history['val_accuracy']
loss_history = history.history['loss']
val_loss_history = history.history['val_loss']
plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(range(EPOCHS), accuracy_history, label='Training Accuracy')
plt.plot(range(EPOCHS), val_accuracy_history, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(range(EPOCHS), loss_history, label='Training Loss')
plt.plot(range(EPOCHS), val_loss_history, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')

plt.savefig(os.path.join(utils.DATA_DIR, "output", "accuracy.png"))

accuracy

loss, accuracy = model.evaluate(test_dataset)
utils.log_to_file(f"Loss: {loss:,.4f}, Accuracy: {accuracy*100:,.2f}%")
model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
sequential (Sequential) (None, 256, 256, 3) 0

sequential_1 (Sequential) (None, 256, 256, 3) 0

conv2d (Conv2D) (None, 254, 254, 32) 896

max_pooling2d (MaxPooling2 (None, 127, 127, 32) 0
D)

conv2d_1 (Conv2D) (None, 125, 125, 64) 18496

max_pooling2d_1 (MaxPoolin (None, 62, 62, 64) 0
g2D)

conv2d_2 (Conv2D) (None, 60, 60, 64) 36928

max_pooling2d_2 (MaxPoolin (None, 30, 30, 64) 0
g2D)

conv2d_3 (Conv2D) (None, 28, 28, 64) 36928

max_pooling2d_3 (MaxPoolin (None, 14, 14, 64) 0
g2D)

conv2d_4 (Conv2D) (None, 12, 12, 64) 36928

max_pooling2d_4 (MaxPoolin (None, 6, 6, 64) 0
g2D)

conv2d_5 (Conv2D) (None, 4, 4, 64) 36928

max_pooling2d_5 (MaxPoolin (None, 2, 2, 64) 0
g2D)

flatten (Flatten) (None, 256) 0

dense (Dense) (None, 64) 16448

dense_1 (Dense) (None, 3) 195

=================================================================
Total params: 183747 (717.76 KB)
Trainable params: 183747 (717.76 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
model.save(os.path.join(
utils.DATA_DIR,
'models',
f"potato_model_{EPOCHS}_{accuracy*100:.2f}.model"
))

Confusion Matrix

from sklearn.metrics import confusion_matrix
import seaborn as sns

y_pred = []
y_true = []

for images_batch, label_batch in test_dataset:
for image, label in zip(images_batch, label_batch):
y_pred.append(np.argmax(model.predict(image[np.newaxis, ...])))
y_true.append(label.numpy())

cm = confusion_matrix(y_true, y_pred)
utils.log_to_file(
f"Number of wrong predictions: {(len(y_true) - np.trace(cm)):,} / {len(y_true):,}"
)
utils.log_to_file(f"Accuracy: {(np.trace(cm) / len(y_true))*100:.2f}%")
class_names = ['Early Blight', 'Late Blight', 'Healthy']
plt.figure(figsize=(8, 8))
sns.heatmap(cm, annot=True, fmt='g', xticklabels=class_names, yticklabels=class_names)
plt.xlabel("Predicted")
plt.ylabel("True")

plt.savefig(os.path.join(
utils.DATA_DIR,
"output",
"confusion_matrix.png"
))

confusion-matrix

Wrong predictions

wrong_predictions = []
for images_batch, label_batch in test_dataset:
wrong_predictions.extend(
(image.numpy().astype("uint8"), label.numpy(), np.argmax(model.predict(image[np.newaxis, ...])))
for image, label in zip(images_batch, label_batch)
if np.argmax(model.predict(image[np.newaxis, ...])) != label.numpy()
)

print(len(wrong_predictions))
5
for i, (image, true_label, predicted_label) in enumerate(wrong_predictions):
image_path: Path = os.path.join(
utils.BASE_DIR,
"temp",
"wrong-predictions",
f"{i:02d}-{class_names[true_label]}-{class_names[predicted_label]}.png"
)
os.makedirs(os.path.dirname(image_path), exist_ok=True)
cv2.imwrite(str(image_path), image)
Late Blight predected HealthyLate Blight predected HealthyLate Blight predected Early Blight
Late Blight predected HealthyLate Blight predected Healthy

Logs

[2023-10-19 21:16:49] Train size: 54, Validation size: 6, Test size: 8
[2023-10-19 21:16:49] Training model for 50 epochs.
[2023-10-19 21:48:43] Training took 1,914.36 seconds.
[2023-10-19 21:53:24] Loss: 0.0487, Accuracy: 98.05%
[2023-10-19 22:32:04] Number of wrong predictions: 5 / 256
[2023-10-19 22:32:04] Accuracy: 98.05%
[2023-10-26 11:12:11] Train size: 54, Validation size: 6, Test size: 8

REFERENCES