CONVOLUTIONAL NEURAL NETWORKS + TRANSFER LEARNING

Convolutional Neural Networks (CNNs)

 

Convolutional Neural Networks (CNNs) represent a groundbreaking advancement in the field of deep learning, particularly for tasks related to computer vision. Unlike traditional neural networks that struggle with spatial relationships in image data, CNNs are specifically designed to efficiently handle grid-like data, making them highly effective for tasks such as image recognition, object detection, and image segmentation.

The key architectural innovation of CNNs lies in the use of convolutional layers. These layers employ convolutional operations to scan input data with learnable filters, capturing hierarchical features from local regions. By utilizing shared weights and biases across the entire input space, CNNs can efficiently recognize patterns, edges, and textures in images, enabling them to learn hierarchical representations of visual information automatically.

The typical CNN architecture consists of multiple convolutional layers, followed by pooling layers to reduce spatial dimensions and decrease computational load. Fully connected layers are often added towards the end to perform classification based on the extracted features.

 

CNNs have demonstrated remarkable success in various computer vision tasks, including image classification competitions like ImageNet. Their ability to automatically learn hierarchical features from raw pixel values has led to advancements in fields such as medical imaging, autonomous vehicles, and facial recognition. In short, the following table shows the most representative CNN layers.

 


 

CNN LAYER

Description

Convolutional Layer

Applies convolutional operations to input data using filters (kernels) to detect patterns and features. Each filter captures different aspects of the input.

Pooling Layer

Reduces spatial dimensions of the input by downsampling, commonly using max pooling or average pooling. This helps to decrease computational load and retain essential features.

Batch Normalization

Normalizes the input of a layer to have zero mean and unit variance, improving convergence and generalization. It is often applied before or after the activation function.

Activation Layer (ReLU)

Introduces non-linearity by applying the Rectified Linear Unit (ReLU) activation function to the output of a layer, allowing the network to learn complex representations.

Dropout Layer

Randomly drops a fraction of neurons during training, preventing overfitting and enhancing the network's robustness. It promotes generalization by avoiding reliance on specific neurons.

Flatten Layer

Converts the multi-dimensional output of the previous layer into a one-dimensional vector, preparing the data for input to fully connected layers.

Fully Connected Layer

Traditional neural network layer where each neuron is connected to every neuron in the previous and subsequent layers. Often used for classification tasks at the end of the network.

Global Average Pooling

Computes the average value of each feature map, producing a global representation of the input. It reduces the spatial dimensions while preserving important information. |

Transfer Learning

 

Transfer learning is a powerful technique in the realm of machine learning, where knowledge gained from solving one problem is applied to a different but related problem. In the context of deep learning and neural networks, transfer learning involves using pre-trained models on a source task and leveraging their learned representations for a target task.

For CNNs, transfer learning is particularly effective due to the hierarchical nature of learned features. Pre-trained models, often trained on massive datasets, have already acquired rich feature representations that can be valuable for new tasks with limited labeled data.

There are two primary approaches to transfer learning:

  • Feature Extraction: The pre-trained model is used as a fixed feature extractor, where the learned weights are frozen, and only the final layers are retrained for the specific task. This is beneficial when the source and target tasks share similar low-level features.

 

  • Fine-Tuning: The pre-trained model is further trained on the target task with a smaller learning rate. This allows the model to adapt its learned representations to the specific nuances of the target task.

 

Transfer learning is widely employed in various domains, from natural language processing to computer vision. It not only accelerates model training but also enhances performance, especially when dealing with limited labeled data for the target task.

In summary, Convolutional Neural Networks and Transfer Learning are pivotal concepts in contemporary machine learning, playing a crucial role in advancing the state-of-the-art in image-related tasks and beyond. Their combination has proven instrumental in achieving high performance even with limited data, making them indispensable tools in the machine learning practitioner's toolkit

ABOUT THE PROJECT:

 

The dataset was acquired from tomato crops within a greenhouse, specifically focusing on plants affected by various illnesses. To capture the characteristics of these illnesses, an autonomous robot was deployed to take images of the leaves while following a predefined route. Subsequently, domain experts meticulously annotated the dataset, categorizing the leaves into four distinct labels: Healthy leaves, dried leaves, leaves with stains, and leaves with yellow stains. The dataset, comprising these annotated images, is available for access and exploration at the following Kaggle link: Tomato Leaf Illness Detection Dataset.

Step 1: In the first step, access the dataset from the provided link in the preceding section. To facilitate the organization of your project, three Python functions have been created for unzipping the file into the target folder. You can refer to this tutorial (link) for guidance on structuring your project effectively. Once the dataset is successfully placed in the designated folder, the subsequent function is designed to inspect the contents, listing the folders and files present. Then, the function "get_labels" gets the name and number of samples of each label. Lastly, the final function generates visualizations, specifically plotting three images for each label.

import os
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import random
import numpy as np
from sklearn import manifold
from PIL import Image
import matplotlib.pyplot as plt

import zipfile

import tensorflow as tf

from tensorflow.keras.applications import EfficientNetB0

 

img_height = 224
img_width = 224

batch_size = 32

epochs = 20

callback = tf.keras.callbacks.EarlyStopping(monitor='val_accuracy', patience=5)

 

#################################################################

 

dataset_path = "dataset_path"

train_path = "trainset_path"

test_path = "test_path"

 

#################################################################

 

def check_data(dataset):
    """
    Check data in a dataset
    Args:
        dataset: The dataset directory to check
    Returns: 
        None: Does not return anything
    - Checks for any errors or invalid files in the dataset

    - Logs any errors or issues found with the files to a report
    - Returns nothing after completing the check"""
    for dirpath, dirnames, filenames in os.walk(dataset):
        print(f"There are {len(dirnames)} directories and {len(filenames)} images in '{dirpath}'.")

 

def get_labels(train_path):
    labels=[]
    tam = []
    for cont, i in enumerate((os.listdir(train_path))):
        labels.append(i)  
    for cont, i in enumerate (labels):
        num = len(os.listdir(train_path+"/"+i))
        tam.append(num)
        print("Label ", labels[cont], ": ", num)
        tam_labels = np.array(tam) 
    return labels, tam_labels  

 

def view_n_images(target_dir, target_class,n):
    target_path = target_dir+"/"+target_class
    file_names = os.listdir(target_path)
    target_images = random.sample(file_names, n)   
    # Plot images
    plt.figure(figsize=(15, 6))
    for i, img in enumerate(target_images):
        img_path = target_path + "/" + img
        plt.subplot(1, 3, i+1)
        plt.imshow(mpimg.imread(img_path))
        plt.title(target_class)
        plt.axis("off")

Step 2: Once the dataset is checked and the path is correctly set, the dataset must be split preferably into three parts: the training set, the validation set, and the test set. Furthermore, Transfer learning usually uses the data augmentation technique to provide better features to the model and avoid overfitting the model. Therefore, two methods were written in this section.  

 

 

def split_tratin_test_set(path_data_source,batch_size,img_height, img_width):
    """
    Splits training and test datasets
    Args: 
        train_dir: Path to training directory
        test_dir: Path to test directory
    Returns:
        train_data: Training dataset
        validation_data: Validation dataset 
        test_data: Test dataset
    Splits training images into train and validation sets. Imports test images separately. Converts all images to specified height and width.
    - Imports data from train and test directories 
    - Splits train data into train and validation sets
    - Converts images to specified height and width
    - Returns train, validation and test datasets
    """
    train_dir = path_data_source + "/" + "train"
    test_dir = path_data_source + "/" + "test"
    # Import data from directories and turn it into batches
    train_data = tf.keras.preprocessing.image_dataset_from_directory(train_dir,
                                                                     seed=123,
                                                                     label_mode="categorical",
                                                                     batch_size=batch_size,  # number of images to process                                                                            validation_split=0.2,
                                                                     subset="training",
                                                                    image_size=(img_height, img_width))  

                                                                    # convert all images to be 224 x 224

    validation_data = tf.keras.preprocessing.image_dataset_from_directory(train_dir,
                                                                          seed=123,
                                                                          label_mode="categorical",
                                                                        batch_size=batch_size,  # number of images to process me
                                                                          validation_split=0.2,
                                                                          subset="validation",
                                                                          image_size=(img_height, img_width))  

                                                                          # convert all images to be 224 x 224

    test_data = tf.keras.preprocessing.image_dataset_from_directory(test_dir,
                                                                    seed=123,
                                                                    label_mode="categorical",
                                                                    batch_size=batch_size,  # number of images to process 
                                                                    image_size=(img_height, img_width))  

                                                                     # convert all images to be 224 x 224
    return train_data, validation_data, test_data

.

 

img_augmentation = tf.keras.models.Sequential(
    [
        tf.keras.layers.experimental.preprocessing.RandomRotation(0.40),
        tf.keras.layers.experimental.preprocessing.RandomTranslation(
            height_factor=0.1,
            width_factor=0.1),
        tf.keras.layers.experimental.preprocessing.RandomFlip("horizontal_and_vertical"),
        tf.keras.layers.RandomContrast(
            factor=0.2),
    ],
    name="img_augmentation",
)

Step 3. In this tutorial, we used a well-known CNN architecture, which was already pre-trained with a large dataset (imagenet). This model was downloaded from the TensorFlow HUB, where ML models are stored for further applications. In this repository, the whole model or the feature extractor can be found. We recommend using the whole model and adding at the top of the architecture a Dense Layer with the number of labels of the target dataset, in this case is four labels.  

 

def build_model(num_classes, aprov_pre):
    """
    Builds a transfer learning model for image classification
    Args:
        num_classes: {Number of classes for the classification task} 
        aprov_pre: {Whether to use preprocessing augmentation}
    Returns: 
        model: {The Keras model}
    Processing Logic:
        - Builds an EfficientNetB0 model with pretrained weights
        - Freezes the pretrained weights
        - Adds global average pooling, dropout and prediction layers on top
        - Compiles the model with Adam optimizer and categorical crossentropy loss
    """
  
    inputs = tf.keras.layers.Input(shape=(img_height, img_width, 3))
    if aprov_pre:
        x = img_augmentation(inputs)
        model = EfficientNetB0(
            include_top=False,
            input_tensor=x,
            weights="imagenet")
        print("preprocessing:", aprov_pre)
    else:
        model = EfficientNetB0(
            include_top=False,
            input_tensor=inputs,
            weights="imagenet")
        print("preprocessing:", aprov_pre)
    # Freeze the pretrained weights
    model.trainable = False

    # Rebuild top
    x = tf.keras.layers.GlobalAveragePooling2D(name="avg_pool")(model.output)
    x = tf.keras.layers.BatchNormalization()(x)

    top_dropout_rate = 0.2
    x = tf.keras.layers.Dropout(top_dropout_rate, name="top_dropout")(x)
    outputs = tf.keras.layers.Dense(
        num_classes,
        activation="softmax",
        name="pred")(x)

    # Compile
    model = tf.keras.Model(inputs, outputs, name="EfficientNet")
    optimizer = tf.keras.optimizers.Adam(learning_rate=1e-2)
    model.compile(
        optimizer=optimizer,
        loss="categorical_crossentropy",
        metrics=["accuracy"])
    return model

 

def unfreeze_model(model, num):
    """Unfreeze top layers of a model
    Args:
        model: The model to unfreeze layers for
        num: The number of top layers to unfreeze
    Returns: 
        model: The model with top layers unfrozen
    Processing Logic:
        - Loop through layers from num to the end of the model
        - Set trainable to True for layers that are not BatchNormaliztion layers
        - Compile the model with an Adam optimizer with learning rate of 1e-4"""
    # We unfreeze the top num layers while leaving BatchNorm layers frozen
    for layer in model.layers[num:]:
        if not isinstance(layer, tf.keras.layers.BatchNormalization):
            layer.trainable = True
    optimizer = tf.keras.optimizers.Adam(learning_rate=1e-4)
    model.compile(
        optimizer=optimizer, loss="binary_crossentropy", metrics=["accuracy"]
    ).

 

Step 4. Since the model was built in the previous step, it is time to be trained with the new dataset. In this step, all code is merged, and the next step also was called here to test and evaluate the model. 

 

def train_model(model, train_data, validation_data, test_data, callback, path_model_destination,epochs, name):
    start = datetime.now()
    history = model.fit(train_data,
                        epochs=epochs,
                        steps_per_epoch=len(train_data),
                        validation_data=validation_data,
                        # Go through less of the validation data so epochs are
                        # faster (we want faster experiments!)
                        validation_steps=int(len(validation_data)),
                        callbacks=[callback],
                        verbose=1,
                        )
    end = datetime.now()
    # find difference loop start and end time and display
    td = (end - start)
    model.save(
                path_model_destination +
                name + 
                    ".h5")
    print("Exceuction time:", td)
    results(model, test_data, name, path_model_destination )
    plot_loss_curves(history, name, path_model_destination)
    return model, history

 

Step 5. Once the model was trained, it can be tested by using the test set. If the results are promising, the model also could be saved for production. Finally, some methods aim to print some figures about the model's performance. Remember that this step was embedded in the previous step.

 

def results(model, test_data, name,path_model_destination):
    """
    Calculates model performance metrics on test data
    Args:
        model: The trained model
        test_data: The test data
        name: The name of the model
    Returns: 
        None: Does not return anything, just prints metrics
    Processing Logic:
        - Predicts classes for test data using model
        - Extracts true and predicted classes
        - Calculates precision, recall, f1 score and mean absolute error
        - Prints classification report and confusion matrix
        - Plots and saves confusion matrix
    """
    y_pred = []
    results = model.predict(test_data)
    for i in results:
        y_pred.append(np.argmax(i))

    y_test = []
    for test_image, test_label in test_data:
        for t in test_label:
            print(np.array(t))
            y_test.append(np.argmax(t))
    print("")
    print(
        "Precision: {}%".format(
            100 *
            metrics.precision_score(
                y_test,
                y_pred,
                average="weighted")))
    print(
        "Recall: {}%".format(
            100 *
            metrics.recall_score(
                y_test,
                y_pred,
                average="weighted")))
    print(
        "f1_score: {}%".format(
            100 *
            metrics.f1_score(
                y_test,
                y_pred,
                average="weighted")))
    print("Error: {}%".format(metrics.mean_absolute_error(y_test, y_pred)))
    report = classification_report(y_test, y_pred)
    matrix = confusion_matrix(y_test, y_pred)
    print('\\Report\n')
    print(report)

    ax = plt.plot()
    ax = sns.heatmap(matrix, annot=True, cmap='Blues')
    ax.set_title('Confusion Matrix with labels\n\n')
    ax.set_xlabel('\nPredicted Values')
    ax.set_ylabel('Actual Values ')
    plt.savefig(
        path_model_destination +
        "cf_efficient_" +
        str(name) +
        ".png")
    # Display the visualization of the Confusion Matrix.
    plt.show()

 

def plot_loss_curves(history, name, path_model_destination):
    """
    Plots loss and accuracy curves from the Keras model history
    Args:
        history: Keras model history object
        name: Name of the model
    Returns: 
        None: Does not return anything, saves figures to file
    Processing Logic:
        - Extracts loss and accuracy values from history
        - Plots loss vs epochs and saves figure
        - Plots accuracy vs epochs and saves figure
    """
    loss = history.history['loss']
    val_loss = history.history['val_loss']

    accuracy = history.history['accuracy']
    val_accuracy = history.history['val_accuracy']

    epochs = range(len(history.history['loss']))

    # Plot loss
    plt.plot(epochs, loss, label='training_loss')
    plt.plot(epochs, val_loss, label='val_loss')
    plt.title('Loss')
    plt.xlabel('Epochs')
    plt.legend()
    plt.savefig(path_model_destination +
                "Loss_" +
                str(name) + ".png")

    # Plot accuracy
    plt.figure()
    plt.plot(epochs, accuracy, label='training_accuracy')
    plt.plot(epochs, val_accuracy, label='val_accuracy')
    plt.title('Accuracy')
    plt.xlabel('Epochs')
    plt.legend()
    plt.savefig(path_model_destination +
                "ACC_" +
                str(name) + ".png")

 

 

RESULTS

3These functions can run in different Python environments, and they can be called in this way:

 

check_data (dataset_path)

Out:

There are 2 directories and 0 images in 'your_path'.
There are 4 directories and 0 images in 'your_path/dataset/test'.
There are 0 directories and 24 images in 'your_path/dataset/test\dried_leaves'.
There are 0 directories and 27 images in 'your_path/dataset/test\healthy_leaves'.
There are 0 directories and 21 images in 'your_path/dataset/test\leaves_with_stains'.
There are 0 directories and 28 images in 'your_path/dataset/test\leaves_yellow_stains'.
There are 4 directories and 0 images in 'your_path/dataset/train'.
There are 0 directories and 99 images in 'your_path/dataset/train\dried_leaves'.
There are 0 directories and 108 images in 'your_path/dataset/train\healthy_leaves'.
There are 0 directories and 64 images in 'your_path/dataset/train\leaves_with_stains'.
There are 0 directories and 113 images in ''your_path/dataset/train\leaves_yellow_stains'.

 

labels, tam_labels = get_labels (train_path)

Out:

Label  dried_leaves :  99
Label  healthy_leaves :  108
Label  leaves_with_stains :  64
Label  leaves_yellow_stains :  113

 

for i in labels:

    view_n_images (train_path, i, 3)

Out:

train_data, validation_data, test_data = split_train_test_set(path_data_source,batch_size,img_height, img_width)

model = build_model(4, False) # we don't want to use data augmentation, otherwise set in True

 

  • Train the model without fine-tuning

train_model(model, train_data, validation_data, test_data, callback, 'your_model_destination_path',epochs, "Efficient_model")

 

  • Train a fine-tunned model with data augmentation.

 

model = build_model(4, True) 

fine_tunned_model = unfreeze_model(model, 20) # unfreeze the last 20 layers

train_model(fine_tunned_model,train_data,validation_data, test_data, callback, 'your_model_destination_path',epochs, "Efficient_model")

 

If you are interested in digging into this topic, you can follow this project: Transfer Learning Benchmarking with IoT data