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

# 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)

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

# Train all the images with binary label
NUM_CLASSES = 1 # number of classes = 5 if multilabel is used
In [2]:
# List the gpu
devices = tf.config.list_physical_devices("GPU")
print(devices)
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
In [3]:
# path of the dataset saved from previous step
import os
# 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_binary
In [4]:
# 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 [5]:
print_dataset_info(ds)
Image arrays: (224, 224, 3), <dtype: 'float32'>
Label arrays: (), <dtype: 'int32'>
cardinality: 2028

Prepare dataset¶

In [6]:
# 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 [7]:
print(f"train set: {ds_train.cardinality()}, \nvalidation set: {ds_val.cardinality()}")
train set: 1825, 
validation set: 203
In [8]:
# 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) 
    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 [9]:
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 [10]:
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 [11]:
ds_t = ds_train.take(1)
ds_v = ds_val.take(1)
In [12]:
ds_t
Out[12]:
<TakeDataset element_spec=(TensorSpec(shape=(16, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(16,), dtype=tf.int32, name=None))>
In [13]:
ds_v
Out[13]:
<TakeDataset element_spec=(TensorSpec(shape=(16, 224, 224, 3), dtype=tf.float32, name=None), TensorSpec(shape=(16,), dtype=tf.int32, name=None))>
In [14]:
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 [15]:
# 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 = "logdir"
checkpoint_dir = "checkpoint"

# 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\best_binary_10_02_1124.keras

Learning rate scheduluer¶

In [16]:
# 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[16]:
<keras.optimizers.schedules.learning_rate_schedule.ExponentialDecay at 0x21ab1367340>
In [ ]:
 

Efficient net¶

In [17]:
# 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 [18]:
# 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 [19]:
# 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')(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 searche_summary()¶

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

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 [21]:
model = build_model(keras_tuner.HyperParameters())
#model = compile_model(model) # this will be called in the build_model

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

Best val_binary_accuracy So Far: 0.6770833730697632
Total elapsed time: 18h 22m 19s
In [23]:
# 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_faces
Showing 1 best trials
Objective(name="val_binary_accuracy", direction="max")

Trial 02 summary
Hyperparameters:
pooling: global
norm: False
units: 512
dropout: 0.4
Score: 0.6770833730697632
In [35]:
# Save the best model
best_model = model_list[0] # the best model from the tuner
In [34]:
# print a summary of a few moretrials
tuner.results_summary(num_trials=5)
Results summary
Results in hp_search\all_faces
Showing 5 best trials
Objective(name="val_binary_accuracy", direction="max")

Trial 02 summary
Hyperparameters:
pooling: global
norm: False
units: 512
dropout: 0.4
Score: 0.6770833730697632

Trial 08 summary
Hyperparameters:
pooling: global
norm: True
units: 64
dropout: 0.4
Score: 0.6588541865348816

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

Trial 05 summary
Hyperparameters:
pooling: average
norm: False
units: 480
dropout: 0.4
Score: 0.6536458432674408

Trial 01 summary
Hyperparameters:
pooling: average
norm: False
units: 256
dropout: 0.1
Score: 0.6484375298023224
In [25]:
# 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 {model.name}: {len(model.layers)}")
Number of layers in base model efficientnetv2-b0: 270
Number of layers in feature extraction model MyEfficient-V2B0: 274

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 [37]:
# Save in tf SavedModel format
model_filepath = os.path.join("model", "all_binary_tf_6771")
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_binary_tf_6771\assets
INFO:tensorflow:Assets written to: model\all_binary_tf_6771\assets
In [38]:
# save in keras format
model_filepath = os.path.join("model", "all_binary_6771.keras")
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 [24]:
# 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 [==============================] - 21s 152ms/step - loss: 1.7248 - binary_accuracy: 0.5132 - precision: 0.4819 - recall: 0.4337 - val_loss: 0.7009 - val_binary_accuracy: 0.5833 - val_precision: 0.5263 - val_recall: 0.3614
In [30]:
history.history["val_binary_accuracy"][0]
Out[30]:
0.5833333730697632
In [31]:
# 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.5833333730697632
Precision: 0.5263158082962036
Recall: 0.3614457845687866
In [32]:
plot_history(history)
No description has been provided for this image
In [ ]:
 
In [ ]: