Train with efficient net¶

Train all the images with binary label¶

In [1]:
# import libraries
import numpy as np
import tensorflow as tf
import keras_tuner
import PIL
import matplotlib.pyplot as plt
import pandas as pd
import os

# Seed the tf, numpy and python with a fix random number
seed = 93
import random # from the python library
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)

# Various directories
CHECKPOINT_DIR = os.path.join("checkpoint", "all_multi_tuner")
LOG_DIR = os.path.join("logdir", "all_multi_tuner")
HP_DIR = os.path.join("hp_search", "all_multi_tuner")
MODEL_TF_DIR = os.path.join("model", "all_multi_tuner")
MODEL_KERAS_DIR = os.path.join("model", "all_multi_tuner", "multi_bin_acc.keras")

# Set the difficulty level to train
DIFF_LEVEL = "multi" # binary, multi, easy, mid, hard

# Train all the images with binary label
NUM_CLASSES = 5 # number of classes = 5 if multilabel is used
In [3]:
# The various directories
print(f"Checkpoint: {CHECKPOINT_DIR}")
print(f"Log: {LOG_DIR}")
print(f"Hyperparamters: {HP_DIR}")
print(f"TF model: {MODEL_TF_DIR}")
print(f"Keras model: {MODEL_KERAS_DIR}")
Checkpoint: checkpoint\all_multi_tuner
Log: logdir\all_multi_tuner
Hyperparamters: hp_search\all_multi_tuner
TF model: model\all_multi_tuner
Keras model: model\all_multi_tuner\multi_bin_acc.keras
In [4]:
# List the gpu
devices = tf.config.list_physical_devices("GPU")
print(devices)
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
In [5]:
# path of the dataset saved from previous step

# path: dataset\train\ds_binary.
# path: dataset\train\ds_multi.
diff_level = f"ds_{DIFF_LEVEL}"
dataset_file = os.path.join("dataset", "train", diff_level)
print(f"Dataset file: {dataset_file}")
if os.path.exists(dataset_file) is False:
    print(f"File not found: {dataset_file}")
    exit(0)
# Load the dataset
ds = tf.data.Dataset.load(dataset_file)
Dataset file: dataset\train\ds_multi
In [6]:
# print the loaded dataset info
def print_dataset_info(dataset):
    print(f"Image arrays: {dataset.element_spec[0].shape}, {dataset.element_spec[0].dtype}")
    print(f"Label arrays: {dataset.element_spec[1].shape}, {dataset.element_spec[1].dtype}")
    print(f"cardinality: {dataset.cardinality()}")
In [7]:
print_dataset_info(ds)
Image arrays: (224, 224, 3), <dtype: 'float32'>
Label arrays: (5,), <dtype: 'int32'>
cardinality: 2028

Prepare dataset¶

In [8]:
# split the dataset into train and validation set
ds_train, ds_val = tf.keras.utils.split_dataset(
    ds, left_size=0.9, right_size=0.1, shuffle=True, seed=seed
)
In [9]:
print(f"train set: {ds_train.cardinality()}, \nvalidation set: {ds_val.cardinality()}")
train set: 1825, 
validation set: 203
In [10]:
# preprocess input
# data augmentation and the pretrained model's preprocess function
img_augmentation_layers = [
    tf.keras.layers.RandomRotation(factor=0.15),
    tf.keras.layers.RandomTranslation(height_factor=0.1, width_factor=0.1),
]

def img_augmentation(images):
    for layer in img_augmentation_layers:
        images = layer(images)
    return images

# the preprocess function of the pretrained model
pretrained_model_preprocess_fn = tf.keras.applications.efficientnet_v2.preprocess_input

def input_preprocess_train(image, label):
    #image = img_augmentation(image) # not used
    image = pretrained_model_preprocess_fn(image)
    return image, label

# do not augment the data in the validation or test set
def input_preprocess_test(image, label):
    image = pretrained_model_preprocess_fn(image)
    return image, label
In [11]:
BATCH_SIZE = 16

ds_train = ds_train.shuffle(buffer_size=1000, seed=seed)
ds_train = ds_train.map(lambda x, y: input_preprocess_train(x, y), 
                        num_parallel_calls=tf.data.AUTOTUNE,
                       deterministic=True)
ds_train = ds_train.cache()
ds_train = ds_train.batch(batch_size=BATCH_SIZE,
                          num_parallel_calls=tf.data.AUTOTUNE,
                          deterministic=True,
                          drop_remainder=True)
ds_train = ds_train.prefetch(tf.data.AUTOTUNE)
In [12]:
ds_val = ds_val.shuffle(buffer_size=1000, seed=seed)
ds_val = ds_val.map(lambda x, y: input_preprocess_test(x, y),
                        num_parallel_calls=tf.data.AUTOTUNE,
                        deterministic=True)
ds_val = ds_val.cache()
ds_val = ds_val.batch(batch_size=BATCH_SIZE,
                          num_parallel_calls=tf.data.AUTOTUNE,
                          deterministic=True,
                          drop_remainder=True)
ds_val = ds_val.prefetch(tf.data.AUTOTUNE)
In [13]:
ds_t = ds_train.take(1)
ds_v = ds_val.take(1)
In [14]:
ds_t
Out[14]:
<TakeDataset element_spec=(TensorSpec(shape=(16, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(16, 5), dtype=tf.int32, name=None))>
In [15]:
ds_v
Out[15]:
<TakeDataset element_spec=(TensorSpec(shape=(16, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(16, 5), dtype=tf.int32, name=None))>
In [16]:
print(f"train set: {ds_t.cardinality()}, \nvalidation set: {ds_v.cardinality()}")
train set: 1, 
validation set: 1

Callbacks layers and optimizer learning rate scheduler¶

In [17]:
# The ModelCheckpoint callback can be used to implement fault-tolerance: the 
# ability to restart training from the last saved state of the model in case 
# training gets randomly interrupted.
# setup the callbacks
# tensorboard log directory
logdir = LOG_DIR
checkpoint_dir = CHECKPOINT_DIR

# check for the directory
if not os.path.exists(logdir):
    os.mkdir(logdir)
if not os.path.exists(checkpoint_dir):
    os.mkdir(checkpoint_dir)    

import time # to format time
model_name = f"best_{DIFF_LEVEL}_{time.strftime('%d_%m_%H%M')}.keras"
checkpoint_filepath = os.path.join(checkpoint_dir, model_name)
print(f"Checkpoint filepath: {checkpoint_filepath}")
callbacks = [
    # early stopping
    tf.keras.callbacks.EarlyStopping(patience=150),
    # Save the best model
    tf.keras.callbacks.ModelCheckpoint(
        filepath=checkpoint_filepath,
        save_weights_only=True,
        monitor='val_binary_accuracy',
        mode='max',
        save_best_only=True
    ),
    # tensorboard
    tf.keras.callbacks.TensorBoard(
        log_dir=logdir,
        update_freq="epoch"
    )
]
Checkpoint filepath: checkpoint\all_multi_tuner\best_multi_11_02_1312.keras

Learning rate scheduluer¶

In [18]:
# Not using
# Gradually reduce learning rate
# Use default value

initial_learning_rate = 1e-3
lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
    initial_learning_rate,
    decay_steps=100000,
    decay_rate=0.96,
    staircase=True
)

lr_schedule
Out[18]:
<keras.optimizers.schedules.learning_rate_schedule.ExponentialDecay at 0x1dcaa87b1f0>
In [ ]:
 

Efficient net¶

In [19]:
# prepare the dataset
def plot_history(history):
    plt.plot(history.history["binary_accuracy"])
    plt.plot(history.history["val_binary_accuracy"])
    plt.title("Model accuracy")
    plt.ylabel("accuracy")
    plt.xlabel("epoch")
    plt.grid(visible=True, axis="both")
    plt.legend(["train", "validation"], loc="upper left")
    plt.show()
In [20]:
# EfficientNetB7, image resolution: 600 x 600

# Get the image size
IMG_SIZE = ds.element_spec[0].shape[1]
print(f"Image size: {IMG_SIZE}")
Image size: 224

Keras tuner¶

Build model and compile model¶

In [21]:
# EfficientNetV2 models expect their inputs to be float tensors of pixels with values in the [0-255] range.
# Use EfficientNetB7

# build_model: Set the range of hyperparameters.
# Then, it will call the exisiting model building code.
def build_model(hp):
    pooling = hp.Choice("pooling", ["average", "global"])
    norm = hp.Boolean("norm")
    units = hp.Int("units", min_value=64, max_value=512, step=32)
    dropout = hp.Float("dropout", min_value=0.1, max_value=0.4, step=0.1)
    # call the actual model building code
    model = call_existing_code(pooling, norm, units, dropout)
    return model
    
# This is called by the keras tuner.
# This is the existing model building code that is working.
def call_existing_code(pooling, norm, units, dropout):
    inputs = tf.keras.layers.Input(shape=(IMG_SIZE, IMG_SIZE, 3))
    # drop_connect_rate which controls the dropout rate responsible for stochastic depth
    # use this for stronger regularization
    global base_model
    base_model = tf.keras.applications.EfficientNetV2B0(
        include_top=False, 
        input_tensor=inputs, 
        weights="imagenet",
    )

    # Freeze the pretrained weights
    base_model.trainable = False
 
    # Rebuild top
    # pooling=None means that the output of the model will be the 
    # 4D tensor output of the last convolutional layer.
    if pooling == "average":
        x = tf.keras.layers.GlobalAveragePooling2D(name="avg_pool")(base_model.output)
    elif pooling == "global":
        x = tf.keras.layers.GlobalMaxPooling2D(name="max_pool")(base_model.output)

    # if normalization is used
    if norm is True:
        x = tf.keras.layers.BatchNormalization()(x)
        
    # Add a dense layer
    # The units are chosen from the hyperparameters
    x = tf.keras.layers.Dense(
        units, 
        activation='relu',
        kernel_regularizer="l2",
        bias_regularizer="l2",
        activity_regularizer="l2",
    )(x)
    # Drop out
    # dropout rate from the hyperparameters
    top_dropout_rate = dropout
    x = tf.keras.layers.Dropout(
        top_dropout_rate, 
        name="top_dropout",
        seed=seed
    )(x)
    # set the number of classes
    num_classes = NUM_CLASSES
    outputs = tf.keras.layers.Dense(
        num_classes, 
        activation="sigmoid", 
        name="predict",
        kernel_regularizer="l2",
        bias_regularizer="l2",
        activity_regularizer="l2",
    )(x)

    model = tf.keras.Model(inputs, outputs, name="MyEfficient-V2B0")
    model = compile_model(model)
    return model

def compile_model(model):
    # Compile
    optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
    metrics = [
        tf.keras.metrics.BinaryAccuracy(),
        tf.keras.metrics.Precision(name="precision"),
        tf.keras.metrics.Recall(name="recall"),
    ]
    model.compile(
        optimizer=optimizer, 
        loss="binary_crossentropy", 
        metrics=metrics,
    )

    return model

Keras tuner search summary¶

In [22]:
tuner = keras_tuner.RandomSearch(
    hypermodel=build_model,
    objective="val_binary_accuracy",
    max_trials=10,
    executions_per_trial=2,
    seed=seed,
    overwrite=True,
    directory=HP_DIR,
    project_name="all_multi_tuner",
)

tuner.search_space_summary()
Search space summary
Default search space size: 4
pooling (Choice)
{'default': 'average', 'conditions': [], 'values': ['average', 'global'], 'ordered': False}
norm (Boolean)
{'default': False, 'conditions': []}
units (Int)
{'default': None, 'conditions': [], 'min_value': 64, 'max_value': 512, 'step': 32, 'sampling': 'linear'}
dropout (Float)
{'default': 0.1, 'conditions': [], 'min_value': 0.1, 'max_value': 0.4, 'step': 0.1, 'sampling': 'linear'}
In [23]:
model = build_model(keras_tuner.HyperParameters())
#model = compile_model(model) # this will be called in the build_model

epochs = 1000
In [24]:
# This will be called by the tuner search
tuner.search(ds_train, epochs=epochs, validation_data=ds_val, callbacks=callbacks)
Trial 10 Complete [01h 37m 07s]
val_binary_accuracy: 0.700520783662796

Best val_binary_accuracy So Far: 0.7036457657814026
Total elapsed time: 18h 25m 42s
In [25]:
# The model weights (that are considered the best) can be loaded as -
#model.load_weights(checkpoint_filepath)
model_list = tuner.get_best_models(num_models=1)
model = model_list[0] # the best model from the tuner
# print a summary of the best trials
tuner.results_summary(num_trials=1)
Results summary
Results in hp_search\all_multi_tuner\all_multi_tuner
Showing 1 best trials
Objective(name="val_binary_accuracy", direction="max")

Trial 07 summary
Hyperparameters:
pooling: average
norm: True
units: 64
dropout: 0.4
Score: 0.7036457657814026
In [26]:
# Save the best model
best_model = model_list[0] # the best model from the tuner
In [27]:
# print a summary of a few moretrials
tuner.results_summary(num_trials=5)
Results summary
Results in hp_search\all_multi_tuner\all_multi_tuner
Showing 5 best trials
Objective(name="val_binary_accuracy", direction="max")

Trial 07 summary
Hyperparameters:
pooling: average
norm: True
units: 64
dropout: 0.4
Score: 0.7036457657814026

Trial 09 summary
Hyperparameters:
pooling: global
norm: True
units: 192
dropout: 0.2
Score: 0.700520783662796

Trial 01 summary
Hyperparameters:
pooling: average
norm: False
units: 256
dropout: 0.1
Score: 0.6979166269302368

Trial 00 summary
Hyperparameters:
pooling: global
norm: True
units: 416
dropout: 0.2
Score: 0.6963541209697723

Trial 06 summary
Hyperparameters:
pooling: average
norm: True
units: 480
dropout: 0.30000000000000004
Score: 0.6963541209697723
In [28]:
# Reference: https://keras.io/api/applications/

# Layers in the base models and models
print(f"Number of layers in base model {base_model.name}: {len(base_model.layers)}")
print(f"Number of layers in feature extraction model {best_model.name}: {len(best_model.layers)}")
Number of layers in base model efficientnetv2-b0: 270
Number of layers in feature extraction model MyEfficient-V2B0: 275

Further layer unfreezing is not workable on my device.

In [26]:
# we chose to train the top 1 we will freeze
# the first 249 layers and unfreeze the rest:
# 
"""
for layer in model.layers[:268]:
   layer.trainable = False
for layer in model.layers[268:]:
   layer.trainable = True
"""
Out[26]:
'\nfor layer in model.layers[:268]:\n   layer.trainable = False\nfor layer in model.layers[268:]:\n   layer.trainable = True\n'
In [27]:
"""
model = compile_model(model)
model.fit(ds_train,
         epochs=epochs, 
         validation_data=ds_val,
         callbacks=callbacks)
"""
Out[27]:
'\nmodel = compile_model(model)\nmodel.fit(ds_train,\n         epochs=epochs, \n         validation_data=ds_val,\n         callbacks=callbacks)\n'

Save the model¶

In [29]:
# Save in tf SavedModel format
model_filepath = MODEL_TF_DIR
tf.saved_model.save(best_model, model_filepath)
WARNING:absl:Found untraced functions such as _jit_compiled_convolution_op, _jit_compiled_convolution_op, _jit_compiled_convolution_op, _jit_compiled_convolution_op, _jit_compiled_convolution_op while saving (showing 5 of 91). These functions will not be directly callable after loading.
INFO:tensorflow:Assets written to: model\all_multi_tuner\assets
INFO:tensorflow:Assets written to: model\all_multi_tuner\assets
In [30]:
# save in keras format
model_filepath = MODEL_KERAS_DIR
tf.keras.models.save_model(best_model, model_filepath, overwrite=True)

What would happened if the best hyperparameters are used to retrained the model?¶

Retrained the model with the best hyperparameters. However the val_binary_accuracy was not the same as the one from the best model. The reason could be that one epoch is not enough. The retraining may need many epochs to reach the val_binary_accuracy like the best model.

In [31]:
# Get the top hyperparameters.
best_hps = tuner.get_best_hyperparameters(1)
# Build the model with the best hp.
model = build_model(best_hps[0])
# Fit with the entire dataset.
# Only need to train 1 time with the best hyperparameters.
history = model.fit(ds_train, epochs=1, validation_data=ds_val, callbacks=callbacks)
114/114 [==============================] - 22s 147ms/step - loss: 1.9913 - binary_accuracy: 0.5382 - precision: 0.3684 - recall: 0.4266 - val_loss: 1.3897 - val_binary_accuracy: 0.6594 - val_precision: 0.3750 - val_recall: 0.0473
In [32]:
history.history["val_binary_accuracy"][0]
Out[32]:
0.6593749523162842
In [33]:
# Quick summary of the best metrics according to binary_accuracy
val = np.argmax(np.array(history.history["val_binary_accuracy"]))
print(f"Binary accuracy: {history.history['val_binary_accuracy'][val]}")
print(f"Precision: {history.history['val_precision'][val]}")
print(f"Recall: {history.history['val_recall'][val]}")
Binary accuracy: 0.6593749523162842
Precision: 0.375
Recall: 0.04731861129403114
In [32]:
plot_history(history)
No description has been provided for this image
In [ ]:
 
In [ ]: