Importing all the necessary libraries

In [1]:
import os
import pickle
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# Mixed precision reduces memory usage and speeds up training
from tensorflow.keras import mixed_precision
mixed_precision.set_global_policy('mixed_float16')
In [26]:
# I am using my GPU
gpus = tf.config.experimental.list_physical_devices('GPU')

if gpus:
    try:
        for gpu in gpus:
            tf.config.experimental.set_memory_growth(gpu, True)
            print('Using GPU - ', gpu)
    except RuntimeError as e:
        print(e)
Physical devices cannot be modified after being initialized

Loading the data

In [3]:
# Directory that has data
DATA_DIR = "cats_and_dogs_filtered/train"

# Subdirectories for each class
data_dir_dogs = os.path.join(DATA_DIR, 'dogs')
data_dir_cats = os.path.join(DATA_DIR, 'cats')

# os.listdir returns a list containing all files under the given dir
print(f"There are {len(os.listdir(data_dir_dogs))} images of dogs.")
print(f"There are {len(os.listdir(data_dir_cats))} images of cats.")
There are 1500 images of dogs.
There are 1500 images of cats.
In [4]:
# Get the filenames for cats and dogs images
cats_filenames = [os.path.join(data_dir_cats, filename) for filename in os.listdir(data_dir_cats)]
dogs_filenames = [os.path.join(data_dir_dogs, filename) for filename in os.listdir(data_dir_dogs)]

fig, axes = plt.subplots(2, 4, figsize=(14, 7))
fig.suptitle('Cat and Dog Images', fontsize=16)

# Plot the first 4 images of each class
for i, cat_image in enumerate(cats_filenames[:4]):
    img = tf.keras.utils.load_img(cat_image)
    axes[0, i].imshow(img)
    axes[0, i].set_title(f'Example Cat {i}')

for i, dog_image in enumerate(dogs_filenames[:4]):
    img = tf.keras.utils.load_img(dog_image)
    axes[1, i].imshow(img)
    axes[1, i].set_title(f'Example Dog {i}')

plt.show()
No description has been provided for this image

Getting training and validation dataset -

In [28]:
def train_val_datasets():
    """Creates datasets for training and validation.

    Returns:
        (tf.data.Dataset, tf.data.Dataset): Training and validation datasets.
    """

    training_dataset, validation_dataset = tf.keras.utils.image_dataset_from_directory( 
        directory=DATA_DIR,
        image_size=(120,120),
        batch_size=20,
        label_mode='binary',
        validation_split=0.15,
        subset='both',
        seed=42 
    )

    return training_dataset, validation_dataset
In [29]:
# Create the datasets
training_dataset, validation_dataset = train_val_datasets()
Found 3000 files belonging to 2 classes.
Using 2550 files for training.
Using 450 files for validation.
In [30]:
# Get the first batch of images and labels
for images, labels in training_dataset.take(1):
    example_batch_images = images
    example_batch_labels = labels

print(f"Maximum pixel value of images: {np.max(example_batch_images)}\n")
print(f"Shape of batch of images: {example_batch_images.shape}")
print(f"Shape of batch of labels: {example_batch_labels.shape}")
Maximum pixel value of images: 255.0

Shape of batch of images: (20, 120, 120, 3)
Shape of batch of labels: (20, 1)

Creating Augmentation Model

In [31]:
def create_augmentation_model():
    """Creates a model (layers stacked on top of each other) for augmenting images of cats and dogs.

    Returns:
        tf.keras.Model: The model made up of the layers that will be used to augment the images of cats and dogs.
    """
    
    augmentation_model = tf.keras.Sequential([ 
        tf.keras.Input(shape=(120, 120, 3)),
        tf.keras.layers.RandomFlip('horizontal'),
        tf.keras.layers.RandomRotation(0.2, fill_mode='nearest'),
        tf.keras.layers.RandomTranslation(0.2, 0.2, fill_mode='nearest'),
        tf.keras.layers.RandomZoom(0.2, fill_mode='nearest')
    ]) 

    return augmentation_model
In [32]:
# Load your model for augmentation
data_augmentor = create_augmentation_model()

# Take a sample image
sample_image = tf.convert_to_tensor(np.array(sample_image), dtype=tf.int32)

images = [sample_image]

# Apply random augmentation 3 times
for _ in range(3):
    image_aug = data_augmentor(tf.expand_dims(sample_image, axis=0))
    image_aug = tf.keras.utils.array_to_img(tf.squeeze(image_aug))
    images.append(image_aug)


fig, axs = plt.subplots(1, 4, figsize=(14, 7))
for ax, image, title in zip(axs, images, ['Original Image', 'Augmented 1', 'Augmented 2', 'Augmented 3']):
    ax.imshow(image, cmap='gray')
    ax.set_title(title)
    ax.axis('off')

plt.show()
No description has been provided for this image

Creating the model -

In [33]:
def create_model():
    """Creates the untrained model for classifying cats and dogs.

    Returns:
        tf.keras.Model: The model that will be trained to classify cats and dogs.
    """

	# Get the augmentation layers (or model) from your earlier function
    augmentation_layers = create_augmentation_model()

	# Don't forget to add the augmentation layers! These usually go right after the Input!
    model = tf.keras.models.Sequential([ 
		tf.keras.Input(shape=(120,120,3)),
        augmentation_layers,
        tf.keras.layers.Rescaling(1./255),
        
        # Improved convolutional base with increased filters
        tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.MaxPooling2D(2, 2),
        
        tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.MaxPooling2D(2, 2),
        
        tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.MaxPooling2D(2, 2),
        
        tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.MaxPooling2D(2, 2),
        
        tf.keras.layers.Conv2D(256, (3, 3), activation='relu'),
        tf.keras.layers.BatchNormalization(),
        tf.keras.layers.MaxPooling2D(2, 2),
        
        # Flatten and add fully connected layers with dropout
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(512, activation='relu'),
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(256, activation='relu'),
        tf.keras.layers.Dropout(0.5),
        tf.keras.layers.Dense(1, activation='sigmoid')
    ]) 

  
    model.compile( 
        optimizer=tf.keras.optimizers.RMSprop(learning_rate=0.0001),
        loss='binary_crossentropy',
        metrics=['accuracy'] 
    ) 
    

    return model
In [34]:
# Create the compiled but untrained model
model = create_model()

Check that the architecture you used is compatible with the dataset (you can ignore the warnings prompted by using the GPU):

In [35]:
try:
    model.evaluate(example_batch_images, example_batch_labels, verbose=False)
except:
    print("Your model is not compatible with the dataset you defined earlier. Check that the loss function, last layer and label_mode are compatible with one another.")
else:
    predictions = model.predict(example_batch_images, verbose=False)
    print(f"predictions have shape: {predictions.shape}")
predictions have shape: (20, 1)

Creating EarlyStoppingCallback -

In [36]:
class EarlyStoppingCallback(tf.keras.callbacks.Callback):

    # Define the correct function signature for on_epoch_end method
    def on_epoch_end(self, epoch, logs):
        # Check if the accuracy is greater or equal to 0.8 and validation accuracy is greater or equal to 0.8
        if logs['accuracy'] >= 0.80 and logs['val_accuracy'] >= 0.80:
            self.model.stop_training = True

            print("\nReached 80% train accuracy and 80% validation accuracy, so cancelling training!")

Training the Model -

In [37]:
history = model.fit(
    training_dataset,
    epochs=35,
    validation_data=validation_dataset,
    callbacks = [EarlyStoppingCallback()]
)
Epoch 1/35
128/128 [==============================] - 59s 410ms/step - loss: 0.8867 - accuracy: 0.5294 - val_loss: 0.7866 - val_accuracy: 0.4911
Epoch 2/35
128/128 [==============================] - 47s 362ms/step - loss: 0.8065 - accuracy: 0.5573 - val_loss: 0.9798 - val_accuracy: 0.4911
Epoch 3/35
128/128 [==============================] - 47s 365ms/step - loss: 0.7473 - accuracy: 0.5663 - val_loss: 0.7946 - val_accuracy: 0.4956
Epoch 4/35
128/128 [==============================] - 48s 372ms/step - loss: 0.7400 - accuracy: 0.5808 - val_loss: 0.6592 - val_accuracy: 0.5889
Epoch 5/35
128/128 [==============================] - 46s 360ms/step - loss: 0.7047 - accuracy: 0.5941 - val_loss: 0.6387 - val_accuracy: 0.6356
Epoch 6/35
128/128 [==============================] - 46s 356ms/step - loss: 0.6911 - accuracy: 0.6004 - val_loss: 0.6898 - val_accuracy: 0.6178
Epoch 7/35
128/128 [==============================] - 45s 354ms/step - loss: 0.6741 - accuracy: 0.6231 - val_loss: 0.8166 - val_accuracy: 0.5444
Epoch 8/35
128/128 [==============================] - 45s 354ms/step - loss: 0.6686 - accuracy: 0.6212 - val_loss: 0.7677 - val_accuracy: 0.5467
Epoch 9/35
128/128 [==============================] - 45s 355ms/step - loss: 0.6562 - accuracy: 0.6322 - val_loss: 0.6416 - val_accuracy: 0.6778
Epoch 10/35
128/128 [==============================] - 46s 355ms/step - loss: 0.6483 - accuracy: 0.6384 - val_loss: 0.6720 - val_accuracy: 0.6489
Epoch 11/35
128/128 [==============================] - 45s 354ms/step - loss: 0.6388 - accuracy: 0.6447 - val_loss: 0.6432 - val_accuracy: 0.6422
Epoch 12/35
128/128 [==============================] - 45s 348ms/step - loss: 0.6427 - accuracy: 0.6404 - val_loss: 0.6293 - val_accuracy: 0.6822
Epoch 13/35
128/128 [==============================] - 45s 353ms/step - loss: 0.6429 - accuracy: 0.6506 - val_loss: 0.6205 - val_accuracy: 0.6933
Epoch 14/35
128/128 [==============================] - 46s 356ms/step - loss: 0.6161 - accuracy: 0.6682 - val_loss: 0.6310 - val_accuracy: 0.6489
Epoch 15/35
128/128 [==============================] - 46s 356ms/step - loss: 0.6278 - accuracy: 0.6600 - val_loss: 0.5882 - val_accuracy: 0.7089
Epoch 16/35
128/128 [==============================] - 46s 355ms/step - loss: 0.6324 - accuracy: 0.6549 - val_loss: 0.6302 - val_accuracy: 0.6489
Epoch 17/35
128/128 [==============================] - 45s 354ms/step - loss: 0.6038 - accuracy: 0.6773 - val_loss: 0.6234 - val_accuracy: 0.6733
Epoch 18/35
128/128 [==============================] - 46s 355ms/step - loss: 0.6020 - accuracy: 0.6812 - val_loss: 0.5564 - val_accuracy: 0.7244
Epoch 19/35
128/128 [==============================] - 46s 356ms/step - loss: 0.6151 - accuracy: 0.6804 - val_loss: 0.5713 - val_accuracy: 0.7244
Epoch 20/35
128/128 [==============================] - 46s 356ms/step - loss: 0.6024 - accuracy: 0.6839 - val_loss: 0.5928 - val_accuracy: 0.7067
Epoch 21/35
128/128 [==============================] - 46s 360ms/step - loss: 0.5903 - accuracy: 0.6800 - val_loss: 0.5868 - val_accuracy: 0.7200
Epoch 22/35
128/128 [==============================] - 45s 350ms/step - loss: 0.5993 - accuracy: 0.6808 - val_loss: 0.6682 - val_accuracy: 0.6089
Epoch 23/35
128/128 [==============================] - 46s 356ms/step - loss: 0.5794 - accuracy: 0.6918 - val_loss: 0.5354 - val_accuracy: 0.7267
Epoch 24/35
128/128 [==============================] - 45s 351ms/step - loss: 0.5848 - accuracy: 0.6992 - val_loss: 0.5405 - val_accuracy: 0.7556
Epoch 25/35
128/128 [==============================] - 45s 347ms/step - loss: 0.5793 - accuracy: 0.7008 - val_loss: 0.5484 - val_accuracy: 0.7156
Epoch 26/35
128/128 [==============================] - 46s 356ms/step - loss: 0.5667 - accuracy: 0.7047 - val_loss: 0.5128 - val_accuracy: 0.7689
Epoch 27/35
128/128 [==============================] - 45s 354ms/step - loss: 0.5734 - accuracy: 0.7071 - val_loss: 0.5520 - val_accuracy: 0.7089
Epoch 28/35
128/128 [==============================] - 2377s 19s/step - loss: 0.5773 - accuracy: 0.6957 - val_loss: 0.5123 - val_accuracy: 0.7667
Epoch 29/35
128/128 [==============================] - 23s 177ms/step - loss: 0.5751 - accuracy: 0.7063 - val_loss: 0.5178 - val_accuracy: 0.7578
Epoch 30/35
128/128 [==============================] - 22s 174ms/step - loss: 0.5579 - accuracy: 0.7224 - val_loss: 0.5333 - val_accuracy: 0.7200
Epoch 31/35
128/128 [==============================] - 23s 181ms/step - loss: 0.5526 - accuracy: 0.7208 - val_loss: 0.5062 - val_accuracy: 0.7689
Epoch 32/35
128/128 [==============================] - 25s 197ms/step - loss: 0.5575 - accuracy: 0.7243 - val_loss: 0.6160 - val_accuracy: 0.6689
Epoch 33/35
128/128 [==============================] - 25s 196ms/step - loss: 0.5589 - accuracy: 0.7184 - val_loss: 0.5557 - val_accuracy: 0.7200
Epoch 34/35
128/128 [==============================] - 25s 198ms/step - loss: 0.5564 - accuracy: 0.7286 - val_loss: 0.5011 - val_accuracy: 0.7667
Epoch 35/35
128/128 [==============================] - 25s 199ms/step - loss: 0.5463 - accuracy: 0.7302 - val_loss: 0.5754 - val_accuracy: 0.7044

In the above case, as you can see the accuracy has not reached to 80% since I have used a very small batch size of 20 due to GPU memory resource limitations. If you use batch size of 128 (as suggested by the course), you will attain very high training and validation accuracy

Accuracy and Loss check -

In [38]:
# Get training and validation accuracies
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

# Get number of epochs
epochs = range(len(acc))

fig, ax = plt.subplots(1, 2, figsize=(10, 5))
fig.suptitle('Training and validation accuracy')

for i, (data, label) in enumerate(zip([(acc, val_acc), (loss, val_loss)], ["Accuracy", "Loss"])):
    ax[i].plot(epochs, data[0], 'r', label="Training " + label)
    ax[i].plot(epochs, data[1], 'b', label="Validation " + label)
    ax[i].legend()
    ax[i].set_xlabel('epochs')

plt.show()
No description has been provided for this image

Unlike the previous lab, this time the model should not be overfitting, which means that it is doing a great job at classifying the images in the training and validation sets.

In [ ]: