Plant Disease Detection Using ResNet
By: Atharva Ingle
Tags: Deep Learning, Convolutional Network, Resnet, Pytorch, Computer Vision

Photo by Conscious Design on Unsplash

⚠️⚠️️️DISCLAIMER⚠️️⚠️️

This tutorial is beginner friendly, so don’t worry if you don’t know much about CNNs and Pytorch. Even if you have used TensorFlow in the past and are new to PyTorch, hang in there, everything is explained clearly and concisely. You will get a good overview of how to use PyTorch for image classification problems.

Motivation

Precision Agriculture is in trend nowadays which includes recommending personalized crops and fertilizers based on soil, weather and various other parameters to maximize the yield.

Getting affected by a disease is very common in plants due to various factors such as wrong choice of fertilizers, environmental conditions, pest attacks, etc. These diseases affect agricultural yield adversely and eventually economy based on it.

Predicting disease early and getting a warning before the plants are affected more adversely would aid farmers to efficiently cultivate crops or plants, both qualitatively and quantitatively. Thus, disease detection in plants plays a very important role in agriculture.

So without further ado, let’s dive in ……

Dataset

I used a publicly available dataset from Kaggle. The dataset can be downloaded from here . This dataset is created using offline augmentation from the original dataset. The original PlantVillage Dataset can be found here. This dataset consists of about 87K RGB images of healthy and diseased crop leaves which is categorized into 38 different classes. The total dataset is divided into 80/20 ratio of training and validation set preserving the directory structure. A new directory containing 33 test images is created later for prediction purpose.

Goal🎯

Goal is clear and simple. We need to build a model, which can classify between healthy and diseased crop leaves and also if the crop have any disease, predict which disease is it.

Let’s get started …….

Note: The following code is implemented in a Kaggle notebook having a P100 GPU. You can checkout the notebook here.

Importing necessary libraries

Importing necessary libraries

Importing data

Importing the data

Exploring the data 🧭

The dataset has 38 different classes in total. We have images of leaves of 14 types of plants and 26 types of images that show a particular disease in a particular plant.

The plants are Tomato, Grape, Orange, Soybean, Squash, Potato, Corn_(maize), Strawberry, Peach, Apple, Blueberry, Cherry_(including_sour), Pepper_bell, Raspberry.

The following image shows total images available per class:

No. of images per class

There are 70,295 images available for training. Rest of the images are used for validation set.

More analysis and insights on the data can be found in my Kaggle notebook here

Data Preparation for Training 🍳

torchvision.datasets is a class which helps in loading all common and famous datasets. It also helps in loading custom datasets. We will use a subclass torchvision.datasets.ImageFolder which helps in loading the image data when the data is arranged in this way:

You can read more about ImageFolder here

Next, after loading the data, we need to transform the pixel values of each image (0–255) to 0–1 as neural networks works quite good with normalized data. The entire array of pixel values is converted to torch tensor and then divided by 255. If you are not familiar why normalizing inputs help neural network, read this post.

Next, we define batch_size as 32.

batch_size is the total number of images given as input at once in forward propagation of the CNN. Basically, batch size defines the number of samples that will be propagated through the network.

For instance, let’s say you have 1050 training samples and you want to set up a batch_size equal to 100. The algorithm takes the first 100 samples (from 1st to 100th) from the training dataset and trains the network. Next, it takes the second 100 samples (from 101st to 200th) and trains the network again. We can keep doing this procedure until we have propagated all samples through of the network, which eventually makes a one epoch of training.

You can even increase batch_size if your GPU has more VRAM.

Next, we load the data using DataLoader:

DataLoader is a subclass which comes from torch.utils.data . It helps in loading large and memory consuming datasets. It takes in batch_size which denotes the number of samples contained in each generated batch.

is a subclass which comes from . It helps in loading large and memory consuming datasets. It takes in which denotes the number of samples contained in each generated batch. Setting shuffle=True shuffles the dataset. It is helpful so that batches between epochs do not look alike. Doing so will eventually make our model more robust.

shuffles the dataset. It is helpful so that batches between epochs do not look alike. Doing so will eventually make our model more robust. num_workers , denotes the number of processes that generate batches in parallel. If you have more cores in your CPU, you can set it to number of cores in your CPU. Since, Kaggle provides a 2 core CPU, I have set it to 2.

Seeing a single batch of training data

One batch of training instance

🏗️ Modelling 🏗️

It is advisable to use GPU instead of CPU when dealing with images dataset because CPUs are generalized for general purpose and GPUs are optimized for training deep learning models as they can process multiple computations simultaneously. They have a large number of cores, which allows for better computation of multiple parallel processes. Additionally, computations in deep learning need to handle huge amounts of data — this makes a GPU’s memory bandwidth most suitable. To seamlessly use a GPU, if one is available, we define a couple of helper functions ( get_default_device and to_device ) and a helper class DeviceDataLoader to move our model and data to the GPU as required.

Now, we need to wrap our training and validation data loaders using DeviceDataLoader for automatically transferring batches of data to the GPU (if available)

👷 Building the model architecture 👷

We are going to use ResNet, which have been one of the major breakthroughs in computer vision when they were introduced in 2015. We will use ResNet-9 architecture to train our model. We will implement the architecture from scratch.

In ResNets, unlike in traditional neural networks, each layer feeds into the next layer, we use a network with residual blocks, each layer feeds into the next layer and directly into the layers about 2–3 hops away, to avoid over-fitting (a situation when validation loss stop decreasing at a point and then keeps increasing while training loss still decreases). This also helps in preventing vanishing gradient problem and allow us to train deep neural networks. Here is a simple residual block:

A single residual block

If you want to learn more about ResNets read the following articles:

Residual block code implementation

Next, we define our ImageClassificationBase class whose functions are:

training_step - To figure out how “wrong” the model is going after training or validation step. We are using this function other than just an accuracy metric that is likely not going to be differentiable (this would mean that the gradient can’t be determined, which is necessary for the model to improve during training)

A quick look at the PyTorch docs that yields the cost function: cross_entropy.

validation_step - Because an accuracy metric can’t be used while training the model, doesn’t mean it shouldn’t be implemented! Accuracy in this case would be measured by a threshold, and counted if the difference between the model’s prediction and the actual label is lower than that threshold.

- Because an accuracy metric can’t be used while training the model, doesn’t mean it shouldn’t be implemented! Accuracy in this case would be measured by a threshold, and counted if the difference between the model’s prediction and the actual label is lower than that threshold. validation_epoch_end - We want to track the validation losses/accuracies and train losses after each epoch, and every time we do so we have to make sure the gradient is not being tracked.

- We want to track the validation losses/accuracies and train losses after each epoch, and every time we do so we have to make sure the gradient is not being tracked. epoch_end - We also want to print validation losses/accuracies, train losses and learning rate too because we are using learning rate scheduler (which will change the learning rate after every batch of training) after each epoch.

We also define an accuracy function which calculates the overall accuracy of the model on an entire batch of outputs, so that we can use it as a metric in fit_one_cycle .

👷 Defining the final architecture of our model 👷

Now, we define a model object and transfer it into the device with which we are working…

Here’s a summary of our model:

Summary of our model

As we can see here, there are around 6.5 million trainable parameters in our model.

Here’s a graph of our model architecture:

Model architecture (drawn using Netron)

Finally, we are ready to train our model !!!!!

Training the model 🏋️

Before we train the model, Let’s define a utility function evaluate , which will perform the validation phase, and a fit_one_cycle function which will perform the entire training process. In fit_one_cycle , we have use some techniques:

Learning Rate Scheduling : Instead of using a fixed learning rate, we will use a learning rate scheduler, which will change the learning rate after every batch of training. There are many strategies for varying the learning rate during training, and the one we’ll use is called the “One Cycle Learning Rate Policy”, which involves starting with a low learning rate, gradually increasing it batch-by-batch to a high learning rate for about 30% of epochs, then gradually decreasing it to a very low value for the remaining epochs.

: Instead of using a fixed learning rate, we will use a learning rate scheduler, which will change the learning rate after every batch of training. There are many strategies for varying the learning rate during training, and the one we’ll use is called the “One Cycle Learning Rate Policy”, which involves starting with a low learning rate, gradually increasing it batch-by-batch to a high learning rate for about 30% of epochs, then gradually decreasing it to a very low value for the remaining epochs. Weight Decay : We also use weight decay, which is a regularization technique which prevents the weights from becoming too large by adding an additional term to the loss function.

: We also use weight decay, which is a regularization technique which prevents the weights from becoming too large by adding an additional term to the loss function. Gradient Clipping: Apart from the layer weights and outputs, it also helpful to limit the values of gradients to a small range to prevent undesirable changes in parameters due to large gradient values. This simple yet effective technique is called gradient clipping.

We’ll also record the learning rate used for each batch.

Now, let’s declare some hyperparameters and start training the model…

Training might take 20 mins to 45 mins depending upon your GPU.

Train and validation loss/accuracy

As we can see here, we got an validation accuracy of 99.23 %. When the model was tested on test set, it was able to predict each image perfectly without any errors. You can check out the test set predictions here .

Plotting some stuffs

Accuracy vs. No. of epochs

Loss vs. No. of epochs

Learning Rate vs. Batch no.

The code for plotting above plots can be accessed here

Saving the model

There are several ways to save the model in Pytorch, following are the two most common ways:

Save/Load state_dict (Recommended)

When saving a model for inference, it is only necessary to save the trained model’s learned parameters. Saving the model’s state_dict with the torch.save() function will give you the most flexibility for restoring the model later, which is why it is the recommended method for saving models.

A common PyTorch convention is to save models using either a .pt or .pth file extension.

Remember that you must call model.eval() to set dropout and batch normalization layers to evaluation mode before running inference. Failing to do this will yield inconsistent inference results.

2. Save/Load Entire Model

This save/load process uses the most intuitive syntax and involves the least amount of code. Saving a model in this way will save the entire module using Python’s pickle module. The disadvantage of this approach is that the serialized data is bound to the specific classes and the exact directory structure used when the model is saved. The reason for this is because pickle does not save the model class itself. Rather, it saves a path to the file containing the class, which is used during load time. Because of this, your code can break in various ways when used in other projects or after refactors.

Conclusion

ResNets perform significantly well for image classification when some of the parameters are tweaked and techniques like scheduling learning rate, gradient clipping and weight decay are applied. The model is able to predict every image in test set perfectly without any errors !!!!

References

Ongoing work

I have also integrated this model into a web application using Flask as a backend. This ongoing project also recommends crop based on various parameters like soil N-P-K value, temperature, humidity, etc. It also suggests the type of fertilizer to use and how to prevent/cure the disease of plants.

This is a work in progress and soon will be deployed to cloud. You can checkout the GitHub repo here . I am also planning to make an app and publish the same on Google Play.

Thanks for hanging in and reading this comprehensive article.

You can access the complete code of this article and more insights here.

Feel free to fork and upvote my Kaggle notebook which will motivate me to bring more such tutorials.

Happy learning !!!!

Catch you guys on the next one …

Peace ✌️

Get in touch:

LinkedIn

Twitter