Cainvas
Model Files
hummingbird.h5
keras
Model
deepSea Compiled Models
hummingbird.exe
deepSea
Ubuntu

Identify hummingbirds

Credit: AITS Cainvas Community

Photo by Simon D'silva on Dribbble

Differentiate between different species of birds is a task that requires experience and expertise.

Here we will train neural networks to identify 3 species of hummingbirds, if they are in the image.

Deep learning models trained to identify birds and their species can be used to monitor wildlife over prolonged periods of time. Human intervention, support and input is necessary at times but this will drastically reduce the time and effort required for the task.

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tensorflow.keras import layers, optimizers, models, preprocessing, losses, callbacks
import os
import random
from PIL import Image
import tensorflow as tf
import tensorflow.keras

The dataset

The images in the datatset were all gathered from various hummingbird feeders in the towns of Morrison and Bailey, Colorado. Kaggle

The dataset has 3 folders, train, test and valid, each with 100, 20 and 20 images of each class label.

The four labels in the dataset are -

  • No_bird
  • Rufous_female
  • Bradtailed_female
  • Broadtailed_male
In [2]:
!wget -N "https://cainvas-static.s3.amazonaws.com/media/user_data/cainvas-admin/hummingbirds.zip"
!unzip -qo hummingbirds.zip
!rm hummingbirds.zip
--2021-09-07 09:14:44--  https://cainvas-static.s3.amazonaws.com/media/user_data/cainvas-admin/hummingbirds.zip
Resolving cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)... 52.219.62.72
Connecting to cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)|52.219.62.72|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7363689 (7.0M) [application/zip]
Saving to: ‘hummingbirds.zip’

hummingbirds.zip    100%[===================>]   7.02M  --.-KB/s    in 0.03s   

2021-09-07 09:14:44 (207 MB/s) - ‘hummingbirds.zip’ saved [7363689/7363689]

In [3]:
# Loading the dataset

path = 'hummingbirds/'

batch = 32

# The train and test datasets
print("Train dataset")
train_ds = preprocessing.image_dataset_from_directory(path+'train', batch_size=batch)

print("Test dataset")
test_ds = preprocessing.image_dataset_from_directory(path+'test', batch_size=batch)

print("Validation dataset")
val_ds = preprocessing.image_dataset_from_directory(path+'valid', batch_size=batch)
Train dataset
Found 400 files belonging to 4 classes.
Test dataset
Found 80 files belonging to 4 classes.
Validation dataset
Found 80 files belonging to 4 classes.
In [4]:
# How many samples in each class

for t in ['train', 'test', 'valid']:
    print('\n', t.upper())
    for x in os.listdir(path + t):
        print(x, ' - ', len(os.listdir(path + t + '/' + x)))
 TRAIN
Broadtailed_female  -  100
No_bird  -  100
Broadtailed_male  -  100
Rufous_female  -  100

 TEST
Broadtailed_female  -  20
No_bird  -  20
Broadtailed_male  -  20
Rufous_female  -  20

 VALID
Broadtailed_female  -  20
No_bird  -  20
Broadtailed_male  -  20
Rufous_female  -  20
In [5]:
# Looking into the class labels

class_names = train_ds.class_names

print("Train class names: ", train_ds.class_names)
print("Test class names: ", test_ds.class_names)
print("Validation class names: ", val_ds.class_names)
Train class names:  ['Broadtailed_female', 'Broadtailed_male', 'No_bird', 'Rufous_female']
Test class names:  ['Broadtailed_female', 'Broadtailed_male', 'No_bird', 'Rufous_female']
Validation class names:  ['Broadtailed_female', 'Broadtailed_male', 'No_bird', 'Rufous_female']
In [6]:
# Looking into the shape of the batches and individual samples
# Set the input shape

print("Looking into the shape of images and labels in one batch\n")  

for image_batch, labels_batch in train_ds:
    input_shape = image_batch[0].shape
    print("Shape of images input for one batch: ", image_batch.shape)
    print("Shape of images labels for one batch: ", labels_batch.shape)
    break
Looking into the shape of images and labels in one batch

Shape of images input for one batch:  (32, 256, 256, 3)
Shape of images labels for one batch:  (32,)

Visualization

In [7]:
num_samples = 4    # the number of samples to be displayed in each class

for x in class_names:
    plt.figure(figsize=(10, 10))

    filenames = os.listdir(path + 'train/' + x)

    for i in range(num_samples):
        ax = plt.subplot(1, num_samples, i + 1)
        img = Image.open(path +'train/' + x + '/' + filenames[i])
        plt.imshow(img)
        plt.title(x)
        plt.axis("off")

Preprocessing

Normalization

In [8]:
# Normalizing the pixel values for faster convergence

normalization_layer = layers.experimental.preprocessing.Rescaling(1./255)

train_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
test_ds = test_ds.map(lambda x, y: (normalization_layer(x), y))
val_ds = val_ds.map(lambda x, y: (normalization_layer(x), y))

Augmentation

The training data has only 100 images. Image augmentation can aid in increasing the number of samples in the training dataset.

In [9]:
# Augmenting images in the train set to increase dataset size

data_augmentation = tf.keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),    # Flip using vertical axis
        layers.experimental.preprocessing.RandomZoom(0.1),    # Randomly zoom images in dataset
    ])


print("Train size (number of batches) before augmentation: ", len(train_ds))

# Apply only to train set    
aug_ds = train_ds.map(lambda x, y: (data_augmentation(x, training=True), y))

print("Size (number of batches) of augmented dataset: ", len(aug_ds))

#Adding to train_ds
train_ds = train_ds.concatenate(aug_ds)

print("Train size (number of batches) after augmentation: ", len(train_ds))
Train size (number of batches) before augmentation:  13
Size (number of batches) of augmented dataset:  13
Train size (number of batches) after augmentation:  26

Model

Transfer learning gives a better result.

In [10]:
base_model = tensorflow.keras.applications.Xception(weights='imagenet', input_shape=input_shape, include_top=False)    # False, do not include the classification layer of the model

base_model.trainable = False

inputs = tf.keras.Input(shape=input_shape)

x = base_model(inputs, training=False)
x = tensorflow.keras.layers.GlobalAveragePooling2D()(x)
outputs = tensorflow.keras.layers.Dense(len(class_names), activation = 'softmax')(x)    # Add own classififcation layer

model = tensorflow.keras.Model(inputs, outputs)

cb = [callbacks.EarlyStopping(monitor = 'val_loss', patience = 5, restore_best_weights = True)]
model.summary()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/xception/xception_weights_tf_dim_ordering_tf_kernels_notop.h5
83689472/83683744 [==============================] - 5s 0us/step
Model: "functional_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_2 (InputLayer)         [(None, 256, 256, 3)]     0         
_________________________________________________________________
xception (Functional)        (None, 8, 8, 2048)        20861480  
_________________________________________________________________
global_average_pooling2d (Gl (None, 2048)              0         
_________________________________________________________________
dense (Dense)                (None, 4)                 8196      
=================================================================
Total params: 20,869,676
Trainable params: 8,196
Non-trainable params: 20,861,480
_________________________________________________________________
In [11]:
model.compile(loss='sparse_categorical_crossentropy', optimizer=optimizers.Adam(0.01), metrics=['accuracy'])

history = model.fit(train_ds, validation_data =  val_ds, epochs=32, callbacks = cb)
Epoch 1/32
26/26 [==============================] - 19s 737ms/step - loss: 0.6679 - accuracy: 0.7262 - val_loss: 0.3403 - val_accuracy: 0.8375
Epoch 2/32
26/26 [==============================] - 8s 297ms/step - loss: 0.2136 - accuracy: 0.9287 - val_loss: 0.2819 - val_accuracy: 0.9000
Epoch 3/32
26/26 [==============================] - 8s 291ms/step - loss: 0.2476 - accuracy: 0.9062 - val_loss: 0.2968 - val_accuracy: 0.9000
Epoch 4/32
26/26 [==============================] - 8s 299ms/step - loss: 0.1595 - accuracy: 0.9362 - val_loss: 0.2403 - val_accuracy: 0.9250
Epoch 5/32
26/26 [==============================] - 8s 296ms/step - loss: 0.0714 - accuracy: 0.9762 - val_loss: 0.2322 - val_accuracy: 0.9375
Epoch 6/32
26/26 [==============================] - 8s 299ms/step - loss: 0.0728 - accuracy: 0.9787 - val_loss: 0.2845 - val_accuracy: 0.9000
Epoch 7/32
26/26 [==============================] - 8s 296ms/step - loss: 0.0525 - accuracy: 0.9887 - val_loss: 0.2640 - val_accuracy: 0.8750
Epoch 8/32
26/26 [==============================] - 8s 296ms/step - loss: 0.0436 - accuracy: 0.9900 - val_loss: 0.2550 - val_accuracy: 0.9250
Epoch 9/32
26/26 [==============================] - 8s 297ms/step - loss: 0.0441 - accuracy: 0.9950 - val_loss: 0.2962 - val_accuracy: 0.9000
Epoch 10/32
26/26 [==============================] - 8s 294ms/step - loss: 0.0500 - accuracy: 0.9850 - val_loss: 0.2961 - val_accuracy: 0.9125
In [12]:
model.evaluate(test_ds)
3/3 [==============================] - 0s 120ms/step - loss: 0.2504 - accuracy: 0.9125
Out[12]:
[0.2503660321235657, 0.9125000238418579]
In [13]:
model.compile(loss='sparse_categorical_crossentropy', optimizer=optimizers.Adam(0.001), metrics=['accuracy'])

history1 = model.fit(train_ds, validation_data =  test_ds, epochs=32, callbacks = cb)
Epoch 1/32
26/26 [==============================] - 9s 332ms/step - loss: 0.0700 - accuracy: 0.9775 - val_loss: 0.2849 - val_accuracy: 0.9250
Epoch 2/32
26/26 [==============================] - 8s 310ms/step - loss: 0.0596 - accuracy: 0.9850 - val_loss: 0.2691 - val_accuracy: 0.9250
Epoch 3/32
26/26 [==============================] - 8s 300ms/step - loss: 0.0589 - accuracy: 0.9837 - val_loss: 0.2576 - val_accuracy: 0.9125
Epoch 4/32
26/26 [==============================] - 8s 304ms/step - loss: 0.0507 - accuracy: 0.9912 - val_loss: 0.2899 - val_accuracy: 0.9250
Epoch 5/32
26/26 [==============================] - 8s 301ms/step - loss: 0.0524 - accuracy: 0.9887 - val_loss: 0.2749 - val_accuracy: 0.9250
Epoch 6/32
26/26 [==============================] - 8s 312ms/step - loss: 0.0551 - accuracy: 0.9912 - val_loss: 0.2554 - val_accuracy: 0.9250
Epoch 7/32
26/26 [==============================] - 8s 302ms/step - loss: 0.0544 - accuracy: 0.9887 - val_loss: 0.2753 - val_accuracy: 0.9250
Epoch 8/32
26/26 [==============================] - 8s 306ms/step - loss: 0.0460 - accuracy: 0.9912 - val_loss: 0.2561 - val_accuracy: 0.9250
Epoch 9/32
26/26 [==============================] - 8s 304ms/step - loss: 0.0405 - accuracy: 0.9962 - val_loss: 0.2543 - val_accuracy: 0.9250
Epoch 10/32
26/26 [==============================] - 8s 308ms/step - loss: 0.0417 - accuracy: 0.9912 - val_loss: 0.2374 - val_accuracy: 0.9250
Epoch 11/32
26/26 [==============================] - 8s 308ms/step - loss: 0.0418 - accuracy: 0.9925 - val_loss: 0.2393 - val_accuracy: 0.9250
Epoch 12/32
26/26 [==============================] - 8s 316ms/step - loss: 0.0507 - accuracy: 0.9900 - val_loss: 0.2692 - val_accuracy: 0.9125
Epoch 13/32
26/26 [==============================] - 8s 309ms/step - loss: 0.0410 - accuracy: 0.9875 - val_loss: 0.2432 - val_accuracy: 0.9250
Epoch 14/32
26/26 [==============================] - 8s 311ms/step - loss: 0.0358 - accuracy: 0.9937 - val_loss: 0.2454 - val_accuracy: 0.9125
Epoch 15/32
26/26 [==============================] - 8s 311ms/step - loss: 0.0435 - accuracy: 0.9887 - val_loss: 0.2408 - val_accuracy: 0.9250
In [14]:
model.evaluate(test_ds)
3/3 [==============================] - 0s 124ms/step - loss: 0.2374 - accuracy: 0.9250
Out[14]:
[0.23741117119789124, 0.925000011920929]

Plotting the metrics

In [15]:
def plot(history1, history2, variable1, variable2):
    # combining metrics from both trainings    
    var1_history = history1[variable1]
    var1_history.extend(history2[variable1])
    
    var2_history = history1[variable2]
    var2_history.extend(history2[variable2])
    
    # plotting them
    plt.plot(range(len(var1_history)), var1_history)
    plt.plot(range(len(var2_history)), var2_history)
    plt.legend([variable1, variable2])
    plt.title(variable1)
In [16]:
plot(history.history, history1.history, "accuracy", 'val_accuracy')
In [17]:
plot(history.history, history1.history, "loss", 'val_loss')

Prediction

In [18]:
# pick random test data sample from one batch
x = random.randint(0, batch - 1)

for i in test_ds.as_numpy_iterator():
    img, label = i    
    plt.axis('off')   # remove axes
    plt.imshow(img[x])    # shape from (32, 256, 256, 3) --> (256, 256, 3)
    output = model.predict(np.expand_dims(img[x],0))    # getting output; input shape (256, 256, 3) --> (1, 256, 256, 3)
    pred = np.argmax(output[0])    # finding max
    print("Prdicted: ", class_names[pred])    # Picking the label from class_names base don the model output
    print("True: ", class_names[label[x]])
    print("Probability: ", output[0][pred])
    break
Prdicted:  Broadtailed_female
True:  Broadtailed_female
Probability:  0.6010286

deepC

In [19]:
model.save('hummingbird.h5')

!deepCC hummingbird.h5
[INFO]
Reading [keras model] 'hummingbird.h5'
[SUCCESS]
Saved 'hummingbird_deepC/hummingbird.onnx'
[ERROR]
ONNX Model size exceeds threshold of 30MB.          
Current ONNX Model size is 79MB.          
Override with '--mem_override'

usage: deepCC [-h] [--output] [--format] [--verbose] [--profile ]
              [--app_tensors FILE] [--archive] [--bundle] [--debug]
              [--mem_override] [--optimize_peak_mem] [--init_net_model]
              [--input_data_type] [--input_shape] [--cc] [--cc_flags  [...]]
              [--board]
              input