Tomato Leaf Disease Classifier using TensorFlow 2.0

In this tutorial, we will learn how to classify images of tomato leaves that are infected or not by using transfer learning from a pre-trained network with the help of TensorFlow in Python.

A pre-trained model is a saved network that was previously trained on a large dataset, typically on a large-scale image classification task. You either use the pre-trained model as it is or use transfer learning to customize this model to a given task. In our case, it is tomato image classification.

You will follow the general machine learning workflow.

  • First of all, we examine and understand the data.
  • Second Build an input pipeline, in this case, we are using Keras ImageDataGenerator.
  • The next step is to Compose the model
  1. Load in the pre-trained base model (and pretrained weights)
  2. Stack the classification layers on top
  • Train the model
  • The final step is to Evaluate model

Loading All Required Python Library

Import all the libraries required for Image Classification.

import tensorflow as tf
from tensorflow.keras.layers import Input, Lambda, Dense, Flatten,GlobalAveragePooling2D,MaxPooling2D
from tensorflow.keras.models import Model
from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.applications.inception_v3 import preprocess_input
from tensorflow.keras.preprocessing import image
from tensorflow.keras.preprocessing.image import ImageDataGenerator,load_img
from tensorflow.keras.models import Sequential
import numpy as np
from glob import glob
import matplotlib.pyplot as plt

Reading Data

Let’s read the train and test data.

train='/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train'
test='/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/valid'

Check the number of folders in Train

Now we check the number of folders that are available on the train using glob library.

# useful for getting number of output classes
folders = glob('/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/*')
folders

Output

['/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Early_blight',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Bacterial_spot',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Late_blight',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___healthy',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Target_Spot',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Spider_mites Two-spotted_spider_mite',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Tomato_mosaic_virus',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Leaf_Mold',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Tomato_Yellow_Leaf_Curl_Virus',
 '/content/drive/MyDrive/Data/New Plant Diseases Dataset(Augmented)/train/Tomato___Septoria_leaf_spot']

As you can see that there are 10 folders inside the train folder.

Importing Data into TensorFlow Object

Here we are taking the batch size of 32 and image size off 224*224 i.e height, width.

  • Count number of images in train folder with tf.keras.utils.image_dataset_from_directory().
BATCH_SIZE = 32
IMG_SIZE = (224, 224)
train_t = tf.keras.utils.image_dataset_from_directory(train,
                                                            shuffle=True,
                                                            batch_size=BATCH_SIZE,
                                                            image_size=IMG_SIZE)

Output

As you can see there are 18365 images of tomatoes in the training folder.

Found 18365 files belonging to 10 classes.
  • Count number of images in test folder with tf.keras.utils.image_dataset_from_directory().
test_t = tf.keras.utils.image_dataset_from_directory(test, shuffle=True, batch_size=BATCH_SIZE, image_size=IMG_SIZE)

Output

Found 4585 files belonging to 10 classes.

As you can see there are 4585 images are available in the test.

Check Train Classes

Here we are checking the class names that belong to the train.

names1=train_t.class_names
names1

Output

['Tomato___Bacterial_spot',
 'Tomato___Early_blight',
 'Tomato___Late_blight',
 'Tomato___Leaf_Mold',
 'Tomato___Septoria_leaf_spot',
 'Tomato___Spider_mites Two-spotted_spider_mite',
 'Tomato___Target_Spot',
 'Tomato___Tomato_Yellow_Leaf_Curl_Virus',
 'Tomato___Tomato_mosaic_virus',
 'Tomato___healthy']

 

 Now Check Batch of Image

Let’s check the batch size of a single image.

for image_batch, labels_batch in train_t.take(1):
    print(image_batch.shape)
    print(labels_batch.numpy())

Output

(32, 224, 224, 3)
[8 1 6 3 9 6 6 2 5 4 5 0 9 3 2 6 2 1 9 9 7 8 7 7 1 3 3 2 0 8 1 8]

The image_batch is a tensor of the shape (32,224,224,3). This is a batch of 32 images of shape 224*224*3 (the last dimension refers to color channels RGB). The label_batch is a tensor of the shape (32,), these are corresponding labels to the 32 images.

You can call .numpy() on the image_batch and labels_batch tensors to convert them to a NumPy.ndarray.

Visualize some Random Image

Show the first nine images and labels from the training set:

fig=plt.figure(figsize=(10,10))
for img,label in train_t.take(1):
  for i in range(9):
    fig.add_subplot(3,3,i+1),plt.imshow(img[i].numpy().astype("uint8"))
    plt.title(names1[label[i]])
    plt.axis("off")

 

Since the original database does not have a test set, we will create it. To do so, find out how many data collections are available in the verification set using tf.data.experimental.cardinality, and submit 20% of them to the test set.

val_batches = tf.data.experimental.cardinality(test_t)
test_dataset = test_t.take(val_batches // 5)
validation_dataset = test_t.skip(val_batches // 5)
print('Number of validation batches: %d' % tf.data.experimental.cardinality(validation_dataset))
print('Number of test batches: %d' % tf.data.experimental.cardinality(test_dataset))

Output

Number of validation batches: 116
Number of test batches: 28

Configuring Dataset for Performance

Use buffered prefetching to upload images to disk without I / O being blocked.

cache: – read the image to disk and next time you need the image will keep the image in memory to improve performance.

prefetch: – if the GPU is busy downloading it will upload images using the CPU which leads to increased performance.

AUTOTUNE = tf.data.AUTOTUNE
train_dataset = train_t.prefetch(buffer_size=AUTOTUNE)
validation_dataset = test_t.prefetch(buffer_size=AUTOTUNE)
test_dataset = test_dataset.prefetch(buffer_size=AUTOTUNE)

Apply Data Augmentation

Used to create some more random sample of training data which include rotation and horizontal flip of images

data_augmentation = tf.keras.Sequential([
  tf.keras.layers.RandomFlip('horizontal'),
  tf.keras.layers.RandomRotation(0.2),
])

Model Building

Stages Include
  • Train the model
  • Exclude the last layer in Inception V3 with include_top=Falsd
  • Freezing layer in the network with trainable =False
  • Compile & Fit the model.

First, instantiate an Inception V3 model pre-loaded with weights trained on ImageNet. By specifying the include_top=False argument, you load a network that doesn’t include the classification layers at the top, which is ideal for feature extraction.

IMG_SHAPE = IMG_SIZE + (3,)
base_model = InceptionV3(input_shape=IMG_SHAPE, weights='imagenet', include_top=False)
base_model.summary()

Output

Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/inception_v3/inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5
87916544/87910968 [==============================] - 1s 0us/step
87924736/87910968 [==============================] - 1s 0us/step
Model: "inception_v3"

It is important to establish a convolutional base before assembling and training the model. Freezing (by setting layer. trainable = False) prevents weight loss in a given layer from being repeated during training. Inception V3 has many layers, so setting the entire model’s trainable flag to False will freeze all of them.

When you set it, the BatchNormalization layer will run in inference mode, and will not update its mean and variance statistics.

#freeze network
base_model.trainable = False
base_model.summary()

Now We Create a model by combining data augmentation, re-scaling, base_model, and feature extraction layers using the Keras Functional API. As mentioned earlier, use training = False as our model contains the BatchNormalization layer.

inputs = base_model.input
x = data_augmentation(inputs)
x = preprocess_input(x)
x = base_model(x, training=False)
global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
x=global_average_layer(x)
x = tf.keras.layers.Dropout(0.2)(x)
outputs=Dense(len(folders), activation='softmax')(x)
model = Model(inputs, outputs)
model.summary()

Let’s display the architecture of the model so far:

Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 224, 224, 3)]     0         
                                                                 
 sequential (Sequential)     (None, 224, 224, 3)       0         
                                                                 
 tf.math.truediv (TFOpLambda  (None, 224, 224, 3)      0         
 )                                                               
                                                                 
 tf.math.subtract (TFOpLambd  (None, 224, 224, 3)      0         
 a)                                                              
                                                                 
 inception_v3 (Functional)   (None, 5, 5, 2048)        21802784  
                                                                 
 global_average_pooling2d (G  (None, 2048)             0         
 lobalAveragePooling2D)                                          
                                                                 
 dropout (Dropout)           (None, 2048)              0         
                                                                 
 dense (Dense)               (None, 10)                20490     
                                                                 
=================================================================
Total params: 21,823,274
Trainable params: 20,490
Non-trainable params: 21,802,784
_________________________________________________________________

Compiling the Model

  • Select optimizer = Adam(Momentum+RMS Prop) as it is most used by experts.
  • Select loss= Sparse Categorical.
  • Select metrics= ‘accuracy’ (can choose any other metrics like precision, recall, f1-score, etc depending on the problem).
model.compile( optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False), metrics=['accuracy'] )

Fitting the Model

  • In the .fit() parameter we are passing our train dataset, validation dataset, and then passing epoch.
  • Epoch: It means several passes of the entire dataset. In our case, we are passing 10 epochs.
history = model.fit(train_dataset, epochs=10, validation_data=validation_dataset)

 

Output

The main aim is here to decrease the loss & improve the accuracy. The below code gives us information about training loss and validation loss, training accuracy, and validation accuracy for every epoch.

Epoch 1/10
574/574 [==============================] - 3030s 5s/step - loss: 1.0469 - accuracy: 0.6454 - val_loss: 0.7922 - val_accuracy: 0.7337
Epoch 2/10
574/574 [==============================] - 121s 210ms/step - loss: 0.7154 - accuracy: 0.7546 - val_loss: 0.6552 - val_accuracy: 0.7725
Epoch 3/10
574/574 [==============================] - 121s 210ms/step - loss: 0.6475 - accuracy: 0.7764 - val_loss: 0.6101 - val_accuracy: 0.7911
Epoch 4/10
574/574 [==============================] - 121s 210ms/step - loss: 0.6167 - accuracy: 0.7879 - val_loss: 0.5976 - val_accuracy: 0.7945
Epoch 5/10
574/574 [==============================] - 123s 212ms/step - loss: 0.5826 - accuracy: 0.7995 - val_loss: 0.5615 - val_accuracy: 0.8083
Epoch 6/10
574/574 [==============================] - 121s 210ms/step - loss: 0.5795 - accuracy: 0.7985 - val_loss: 0.5892 - val_accuracy: 0.8015
Epoch 7/10
574/574 [==============================] - 121s 210ms/step - loss: 0.5586 - accuracy: 0.8057 - val_loss: 0.5836 - val_accuracy: 0.7945
Epoch 8/10
574/574 [==============================] - 121s 210ms/step - loss: 0.5573 - accuracy: 0.8076 - val_loss: 0.5435 - val_accuracy: 0.8107
Epoch 9/10
574/574 [==============================] - 122s 211ms/step - loss: 0.5466 - accuracy: 0.8107 - val_loss: 0.5628 - val_accuracy: 0.7976
Epoch 10/10
574/574 [==============================] - 122s 211ms/step - loss: 0.5567 - accuracy: 0.8058 - val_loss: 0.5680 - val_accuracy: 0.8044

 Visualize Train loss, accuracy vs Valid loss, accuracy

Create plots of loss and accuracy on the training and validation sets:

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

loss = history.history['loss']
val_loss = history.history['val_loss']

plt.figure(figsize=(8, 8))
plt.subplot(2, 1, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.ylabel('Accuracy')
plt.ylim([min(plt.ylim()),1])
plt.title('Training and Validation Accuracy')

plt.subplot(2, 1, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.ylabel('Cross Entropy')
plt.ylim([0,1.0])
plt.title('Training and Validation Loss')
plt.xlabel('epoch')
plt.show()

Check Results of Actual vs Predicted

Now visualize the results that the Inception model predicted concerning Actual value.

def predict(model, img):
    img_array = tf.keras.preprocessing.image.img_to_array(images[i].numpy())
    img_array = tf.expand_dims(img_array, 0)

    predictions = model.predict(img_array)

    predicted_class = names1[np.argmax(predictions[0])]
    confidence = round(100 * (np.max(predictions[0])), 2)
    return predicted_class, confidence

plt.figure(figsize=(15, 15))
for images, labels in test_dataset.take(1):
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(images[i].numpy().astype("uint8"))
        
        predicted_class, confidence = predict(model, images[i].numpy())
        actual_class = names1[labels[i]] 
        
        plt.title(f"Actual: {actual_class},\n Predicted: {predicted_class}.\n Confidence: {confidence}%")
        
        plt.axis("off")

Output

Leave a Reply

Your email address will not be published.