Plant Disease Classifier with CNN

Train a MobileNetV2 model to classify 38 plant diseases from leaf images.

Introduction

30 Second Summary

Imagine a farmer spotting a strange discoloration on their tomato leaves. Is it early blight, late blight, or just sunburn? Identifying plant diseases by sight takes years of expertise that most growers simply do not have.

In this project, you will build a machine learning model that looks at a photo of any plant leaf and predicts which of 38 diseases it has. You will train two models side by side in Google Colab: a simple CNN that struggles, and a transfer learning model that achieves over 90% accuracy.

What You'll Build

Point your model at a leaf photo, and it instantly returns the disease name and a confidence percentage telling you how sure it is about that diagnosis.

By the end of this project, you'll have:

  • A working classifier where you upload a leaf photo and get back a disease prediction with confidence percentage.
  • A visual comparison showing why MobileNetV2 transfer learning dramatically outperforms a from-scratch CNN on real image data.
  • A confusion matrix and classification report proving your model works across all 38 disease classes covering 14 crop species.
  • A saved .keras model file on Google Drive, ready for deployment in a Java/Spring Boot backend.
  • Secret Mission: Fine-tune MobileNetV2 by unfreezing layers to push accuracy past 97%.

Are there any prerequisites?

You will need a Google account (for Colab and Drive) and a Kaggle account (to download the PlantVillage dataset). Basic Python knowledge is helpful, but all code is provided step by step.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the goal before any hands-on work begins.

Configure Colab and Load Dataset

Training a neural network on thousands of images requires serious computing power. A regular CPU would take hours to process the 54,000+ leaf images in the PlantVillage dataset.

In this step, you will open a GPU-powered notebook in Google Colab, connect it to the dataset on your Google Drive, and load the images into training and validation splits ready for model training.

In this step, get ready to:
  • Set up Google Colab with a T4 GPU and mount Google Drive.
  • Verify the PlantVillage dataset has all 38 disease class folders.
  • Load the dataset into training and validation splits.
Open Colab and enable GPU

Google Colab gives you a free cloud notebook with access to a GPU. You need a GPU because training on images is parallelizable work that GPUs handle orders of magnitude faster than CPUs.

  • Open colab.research.google.com in your browser and sign in with your Google account.
  • Click New notebook to create a fresh notebook.
  • In the top menu, click Runtime.
  • Click Change runtime type.
  • In the Hardware accelerator dropdown, select T4 GPU.
  • Click Save.

Why T4 GPU?

The T4 is a free GPU available in Colab that handles the matrix math behind neural networks. Without it, training on 54,000 images would take hours instead of minutes.

Now mount Google Drive and verify your environment by pasting this code into the first cell:

  • Click inside the first code cell in your notebook and paste the following code:
# Cell 1: Mount Drive and verify environment
from google.colab import drive
drive.mount('/content/drive')

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import os

print(f"TensorFlow version: {tf.__version__}")
print(f"GPU available: {tf.config.list_physical_devices('GPU')}")

What does this code do?

drive.mount('/content/drive') connects your Google Drive to the Colab notebook so you can access your uploaded dataset files.

The tf.config.list_physical_devices('GPU') line checks whether Colab successfully assigned a GPU to your session.

  • Run the cell by pressing Shift+Enter.
  • Colab will ask you to authorize access to Google Drive. Click Allow when the popup appears.

You should see output showing a TensorFlow version number and a list containing PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU').

GPU not showing up?

  • If the GPU list is empty [], go back to Runtime > Change runtime type and confirm T4 GPU is selected. Then click Runtime > Restart runtime and re-run the cell.
  • If you see a message about GPU availability, Colab free-tier GPUs depend on demand. Try again in a few minutes or use a different browser session.
  • help me troubleshoot why my Colab GPU is not showing up
Verify the dataset path

Your PlantVillage dataset should already be on Google Drive from the prerequisites. Now you need to tell the notebook exactly where to find those 38 class folders.

  • Create a new code cell by clicking the + Code button at the top of your notebook.
  • Paste the following code into the new cell:
# Cell 2: Set dataset path and verify
DATASET_DIR = '/content/drive/MyDrive/PlantVillage'

class_folders = sorted([
    d for d in os.listdir(DATASET_DIR)
    if os.path.isdir(os.path.join(DATASET_DIR, d))
])
print(f"Number of classes found: {len(class_folders)}")
print(f"First 5 classes: {class_folders[:5]}")

What does this code do?

DATASET_DIR stores the path to your PlantVillage folder on Drive. The list comprehension filters for only subdirectories (the class folders), ignoring any stray files.

Each subdirectory name is a class label like Apple___Apple_scab or Tomato___Late_blight. The format is PlantName___DiseaseName with triple underscores.

Uploaded to a different folder?

If you saved the PlantVillage dataset in a subfolder on Drive (e.g., MyDrive/datasets/PlantVillage), update the DATASET_DIR path to match your actual location.

  • Run the cell with Shift+Enter.

You should see Number of classes found: 38 followed by the first five class names in alphabetical order.

Seeing a FileNotFoundError?

  • Double-check that you uploaded the contents of the PlantVillage color/ directory (the 38 class folders themselves) directly into your Drive folder, not the parent color/ folder itself.
  • Navigate to the Files panel in Colab's left sidebar. Expand drive > MyDrive and confirm the PlantVillage folder exists at the path you specified.
  • help me fix a FileNotFoundError when loading my PlantVillage dataset
Load the dataset into training and validation splits

With the dataset path confirmed, you can now load the images into two groups: one for training the model and one for testing how well it performs on images it has not seen before.

  • Create a new code cell and paste the following code:
# Cell 3: Load datasets with train/validation split
IMG_SIZE = (224, 224)
BATCH_SIZE = 32

train_ds = tf.keras.utils.image_dataset_from_directory(
    DATASET_DIR,
    validation_split=0.2,
    subset="training",
    seed=42,
    image_size=IMG_SIZE,
    batch_size=BATCH_SIZE
)

val_ds = tf.keras.utils.image_dataset_from_directory(
    DATASET_DIR,
    validation_split=0.2,
    subset="validation",
    seed=42,
    image_size=IMG_SIZE,
    batch_size=BATCH_SIZE
)

class_names = train_ds.class_names
num_classes = len(class_names)
print(f"Number of classes: {num_classes}")
print(f"Class names: {class_names}")

What does this code do?

image_dataset_from_directory reads images from the folder structure and automatically assigns labels based on subfolder names. The validation_split=0.2 reserves 20% of images for testing and uses 80% for training.

IMG_SIZE = (224, 224) resizes every image to 224x224 pixels. This is the input size that MobileNetV2 expects (you will use this model in a later step).

BATCH_SIZE = 32 means the model processes 32 images at a time during training. This balances memory usage and training speed.

Before you run this cell, what do you think the output will confirm about the number of classes?

  • Run the cell with Shift+Enter.

You should see Number of classes: 38 followed by a full list of all class names. TensorFlow also prints how many images it found in each split.

Seeing fewer than 38 classes?

  • If you see fewer classes, some subdirectories may be missing from your Drive upload. Go back to Kaggle and confirm you downloaded the color images folder, which should contain exactly 38 subdirectories.
  • If you see hidden system files (like .DS_Store) counted as classes, re-check that Cell 2 correctly filters with os.path.isdir().
  • help me debug why my dataset has the wrong number of classes

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook (Ctrl+S or Cmd+S) and that all three cells ran without errors.

ⓧ I'd like to double check the full code

Scroll up through your notebook and confirm you have these three cells in order, each having run successfully:

  • Cell 1: Mount Drive and verify environment (imports TensorFlow, mounts Drive, prints GPU info).
  • Cell 2: Set dataset path and verify (sets DATASET_DIR, lists class folders, prints 38).
  • Cell 3: Load datasets with train/validation split (creates train_ds and val_ds, prints 38 class names).

Each cell should show a green checkmark and output matching the descriptions above.

Your environment is ready and your data is loaded. Next up, you will explore the images and check for patterns before training any models.

Explore the Data

Your environment is live and your dataset is loaded. You have 38 class folders and over 54,000 leaf images sitting in Google Drive, split into training and validation sets.

Before training any model, you need to actually look at the data. What do the leaves look like? Are some diseases overrepresented? Spotting issues like class imbalance now saves you from confusing results later.

In this step, get ready to:
  • Visualize sample leaf images to see what healthy vs. diseased leaves look like.
  • Plot the class distribution to check for imbalance across all 38 categories.
  • Optimize the data pipeline for faster training in future steps.
Visualize sample images from the training set

The best way to understand image data is to look at it. You will pull a batch of 9 images from the training set and display them in a grid with their disease labels.

  • Create a new code cell in your Colab notebook below the existing cells.
  • Display a 3x3 grid of sample training images by pasting this code and running the cell:
# Cell 4: Visualize sample images
plt.figure(figsize=(15, 10))
for images, labels in train_ds.take(1):
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(images[i].numpy().astype("uint8"))
        plt.title(class_names[labels[i]], fontsize=8)
        plt.axis("off")
plt.tight_layout()
plt.show()

What does this code do?

  • train_ds.take(1) grabs a single batch of 32 images from the training set.
  • The loop picks the first 9 images and plots each one in a 3x3 subplot grid.
  • plt.title(class_names[labels[i]]) labels each image with its disease class name so you can see what category it belongs to.

Before you run this cell, take a guess. Do you think diseased leaves will look visually different from healthy ones to the naked eye?

  • Run the cell by pressing the play button or hitting Shift+Enter.

You should see a 3x3 grid of leaf photos. Each image has a title like Apple___Apple_scab or Tomato___Late_blight. Notice how diseased leaves show visible spots, discoloration, or wilting compared to healthy ones.

Not seeing any images?

If you see an empty plot or an error, make sure you ran all the cells from Step 1 first. The train_ds variable must already exist.

If you see a Drive timeout error, your Google Drive connection may have dropped. Re-run the drive.mount('/content/drive') cell at the top.

Help me debug my image visualization in Colab

Plot class distribution and optimize the pipeline

A model trained on imbalanced data will perform well on common classes and poorly on rare ones. Plotting how many images each class has reveals whether some diseases are underrepresented.

  • Create a new code cell below Cell 4.
  • Count images per class and plot the distribution by pasting this code:
# Cell 5: Plot class distribution
import pathlib

data_dir = pathlib.Path(DATASET_DIR)
class_counts = {}
for class_dir in sorted(data_dir.iterdir()):
    if class_dir.is_dir():
        count = len(list(class_dir.glob('*')))
        class_counts[class_dir.name] = count

print(f"Total images: {sum(class_counts.values())}")

plt.figure(figsize=(20, 6))
plt.bar(range(len(class_counts)), list(class_counts.values()))
plt.xticks(range(len(class_counts)), list(class_counts.keys()), rotation=90, fontsize=7)
plt.xlabel('Class')
plt.ylabel('Number of Images')
plt.title('Class Distribution in PlantVillage Dataset')
plt.tight_layout()
plt.show()

What does this code do?

  • pathlib.Path(DATASET_DIR).iterdir() walks through each subdirectory (one per class) in your dataset folder.
  • For each class folder, it counts every file inside using glob('*') and stores the result in a dictionary.
  • The bar chart then visualizes all 38 class counts side by side so you can spot any imbalance at a glance.

Do you think all 38 classes will have roughly the same number of images, or will some diseases have far more samples than others?

  • Run the cell.

You should see the total image count printed (around 54,000) followed by a wide bar chart with 38 bars. Some classes have more images than others, but the PlantVillage dataset is relatively balanced overall.

Seeing a FileNotFoundError?

This means your DATASET_DIR path does not match where you uploaded the PlantVillage color images on Google Drive. Double check that the path points to the folder containing the 38 class subdirectories (like Apple___Apple_scab), not a parent folder.

Help me fix my dataset path in Google Colab

Now that you have confirmed your data looks good, the last thing to do is optimize how TensorFlow loads it during training. Caching and prefetching keep the GPU fed with data instead of waiting for disk reads.

  • Create a new code cell below Cell 5.
  • Optimize the data pipeline for faster training by pasting this code and running the cell:
# Cell 6: Optimize data pipeline
AUTOTUNE = tf.data.AUTOTUNE
train_ds_cached = train_ds.cache().prefetch(buffer_size=AUTOTUNE)
val_ds_cached = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

What do cache and prefetch do?

.cache() stores the loaded images in memory after the first epoch so subsequent epochs skip the disk read entirely.

.prefetch(buffer_size=AUTOTUNE) loads the next batch of images while the GPU is still processing the current one. TensorFlow automatically tunes how many batches to prefetch for optimal throughput.

The cell completes without printed output. The new train_ds_cached and val_ds_cached variables are what you will pass to model training in the next step.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook (Ctrl+S) so all your outputs are preserved.

ⓧ I'd like to double check the full code

Your notebook now has 6 cells total (3 from Step 1 and 3 from this step). Scroll through and confirm each one is present in order:

  • Cell 1: Mount Drive and verify environment.
  • Cell 2: Set dataset path and verify.
  • Cell 3: Load datasets with train/validation split.
  • Cell 4: Visualize sample images (3x3 grid).
  • Cell 5: Plot class distribution (import pathlib, count images, bar chart).
  • Cell 6: Optimize data pipeline (AUTOTUNE, cache, prefetch).

You have seen your data up close and confirmed the class distribution across all 38 categories. Next up, you will train a simple CNN from scratch and watch it struggle with this complexity.

Train a Simple CNN from Scratch

You've explored the PlantVillage dataset and seen the variety of leaf images across 38 disease classes. Now it's time to see if a neural network can learn to tell them apart.

The most direct approach is to build a Convolutional Neural Network from scratch and train it on your images. You'll construct a simple 3-layer CNN and find out how well it handles this classification challenge.

In this step, get ready to:
  • Build a 3-layer CNN architecture with convolutional, pooling, and dense layers.
  • Train the CNN on the PlantVillage dataset for 5 epochs.
  • Evaluate the baseline accuracy for comparison.
Build a simple CNN architecture

A CNN learns to recognize visual patterns by stacking convolutional layers that detect increasingly complex features. Early layers find edges, middle layers find textures, and deeper layers find shapes.

  • Create a new code cell in your Colab notebook and build the CNN model by pasting this code:
# --- STEP 3: TRAIN A SIMPLE CNN FROM SCRATCH ---

# Cell 7: Build simple CNN
simple_cnn = tf.keras.Sequential([
    tf.keras.layers.Rescaling(1./255, input_shape=(224, 224, 3)),
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(),
    tf.keras.layers.Conv2D(128, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D(),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(num_classes)
])

simple_cnn.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

simple_cnn.summary()

What does this code do?

This builds a small CNN with three detection stages. Each stage has a Conv2D layer that scans for patterns, followed by MaxPooling2D that shrinks the image to focus on the strongest signals.

Rescaling(1./255) normalizes pixel values from [0, 255] to [0, 1] so the network trains efficiently. The final Dense(num_classes) layer outputs a raw score for each of the 38 disease classes.

Dropout(0.5) randomly disables half the neurons during training to prevent overfitting. SparseCategoricalCrossentropy(from_logits=True) is the loss function for multi-class classification when labels are integers (not one-hot encoded).

  • Run the cell.
  • You should see a model summary table printed below the cell. It shows each layer, its output shape, and the total parameter count (around 3.5 million parameters).

Not seeing the model summary?

Make sure you ran the previous cells first. The variable num_classes must already exist from Step 1's Cell 3.

If you see a NameError, go to **Runtime** > **Run all** to execute all cells from the top in order.

Help me fix my CNN model summary error

Train the model for 5 epochs

Your CNN architecture is ready. Time to train it on the 54,000+ leaf images and see what accuracy it achieves on 38 disease classes.

  • Create a new code cell and paste the training code:
# Cell 8: Train simple CNN
print("\nTraining Simple CNN (5 epochs)...")
simple_history = simple_cnn.fit(
    train_ds_cached,
    validation_data=val_ds_cached,
    epochs=5
)

print(f"\nSimple CNN Results:")
print(f"  Training accuracy:   {simple_history.history['accuracy'][-1]:.4f}")
print(f"  Validation accuracy: {simple_history.history['val_accuracy'][-1]:.4f}")

What does this code do?

simple_cnn.fit() feeds the training images through the network 5 times (epochs). Each epoch, the model adjusts its internal weights to reduce prediction errors.

The validation_data parameter tests the model on images it has never trained on after each epoch. This reveals whether the model is truly learning disease patterns or just memorizing the training set.

Before you run this cell, what accuracy do you think a 3-layer CNN can achieve on 38 different disease classes? Take a guess.

  • Run the cell. Training takes approximately 5-10 minutes on the T4 GPU. You'll see progress bars for each epoch.

When training finishes, look at the final validation accuracy. It plateaus around 50-70%.

That is intentional. This small CNN is too shallow to learn the complex visual features needed to distinguish 38 disease categories from leaf images alone. This is the exact gap that transfer learning solves in the next step.

Why is the accuracy so low?

Three convolutional layers and 128 dense neurons are simply not enough firepower for this problem. Distinguishing Apple___Apple_scab from Apple___Black_rot requires recognizing subtle texture differences that take many more layers to detect.

Training a deeper network from scratch would need far more data, more epochs, and more GPU time. Transfer learning sidesteps all of that by starting with features already learned from millions of images.

Training seems stuck or extremely slow?

Confirm your runtime is set to T4 GPU. Go to **Runtime** > **Change runtime type** and check that **T4 GPU** is selected. Without a GPU, each epoch takes significantly longer.

If you see an out-of-memory error, restart the runtime with **Runtime** > **Restart runtime**, then re-run all cells from the top.

Help me fix my CNN training issue in Colab

  • Note down your validation accuracy number. You'll compare it against the transfer learning model in the next step.

✔️ Awesome, I've got everything!

Great. Make sure both cells (Cell 7 and Cell 8) ran successfully and you can see the final accuracy printout.

ⓧ I'd like to double check the full code

Scroll up through your notebook and confirm these two cells are present in order:

  • Cell 7: Builds simple_cnn as a Sequential model with Rescaling, three Conv2D+MaxPooling2D blocks, Flatten, Dense(128), Dropout(0.5), Dense(num_classes). Compiles with Adam and SparseCategoricalCrossentropy. Ends with simple_cnn.summary().
  • Cell 8: Calls simple_cnn.fit() on train_ds_cached with val_ds_cached for 5 epochs. Stores result in simple_history. Prints final training and validation accuracy.

Your baseline is set. A from-scratch CNN tops out around 50-70% on this problem. In the next step, you'll see what happens when you start with a model that already knows how to see.

Apply Transfer Learning with MobileNetV2

Your simple CNN topped out around 50-70% accuracy. That is not production-ready for a tool that farmers would rely on to identify crop diseases. The model is too small to learn the complex visual features needed to distinguish 38 disease categories from scratch.

This is exactly the problem transfer learning solves. MobileNetV2 has already learned to recognize edges, textures, and shapes from training on 1.4 million ImageNet images. Instead of learning all of that from scratch, you just teach it what plant diseases look like.

In this step, get ready to:
  • Load MobileNetV2 pre-trained on ImageNet and freeze its layers.
  • Build a transfer learning model and train it for 5 epochs.
  • Compare MobileNetV2 accuracy against your simple CNN.
Load MobileNetV2 and build the transfer model

MobileNetV2 is a lightweight CNN with approximately 3.5 million parameters. It was trained on the ImageNet dataset, so it already knows how to detect visual features like edges, textures, colours, and shapes. You will load those pre-trained weights and freeze them so they do not change during your training.

  • Create a new code cell in your Colab notebook and load the MobileNetV2 base model by pasting this code:
# Cell 9: Build transfer learning model
base_model = tf.keras.applications.MobileNetV2(
    input_shape=(224, 224, 3),
    include_top=False,
    weights='imagenet'
)
base_model.trainable = False

What does this code do?

  • include_top=False removes MobileNetV2's original 1000-class ImageNet classifier. You will add your own 38-class output layer instead.
  • weights='imagenet' loads the pre-trained weights that encode all the visual features MobileNetV2 learned from 1.4 million images.
  • base_model.trainable = False freezes every layer so training only updates your new output layer, not the pre-learned features.

Why MobileNetV2 instead of ResNet50?

MobileNetV2 has approximately 3.5 million parameters compared to ResNet50's 25 million. That means it trains faster per epoch, uses less GPU memory, and still achieves comparable accuracy on plant disease classification.

It is also designed for mobile deployment, which makes it a better fit for a production model that could eventually run on a farmer's phone.

  • Run the cell. You should see TensorFlow load the MobileNetV2 weights (it may print a download progress bar the first time).

Now you need to build your custom model on top of the frozen MobileNetV2 base. This model will rescale pixel values to the range MobileNetV2 expects, pass images through the frozen base, then classify them into 38 disease categories.

  • In the same cell (below the code you just pasted), add the following code to build, compile, and display the transfer model:
transfer_model = tf.keras.Sequential([
    tf.keras.layers.Rescaling(1./127.5, offset=-1, input_shape=(224, 224, 3)),
    base_model,
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(num_classes)
])

transfer_model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

transfer_model.summary()

What does this code do?

  • Rescaling(1./127.5, offset=-1) maps pixel values from [0, 255] to [-1, 1]. This is the specific range MobileNetV2 was trained on.
  • GlobalAveragePooling2D() condenses the spatial feature maps from MobileNetV2 into a single vector per image.
  • Dropout(0.2) randomly drops 20% of connections during training to prevent overfitting.
  • Dense(num_classes) is your 38-class output layer. This is the only part that learns plant disease features.
  • Run the cell. The model summary prints, showing the total parameter count and confirming most parameters are non-trainable (frozen).

Not seeing the model summary?

  • Make sure you ran the base_model loading code and the transfer_model building code in the same cell or in order.
  • Check that num_classes is defined. It was set in Step 1 when you loaded the dataset.

Why isn't my transfer model building correctly?

Train the transfer model and compare results

Your transfer model is built. Now it is time to train it on the same data your simple CNN struggled with. Only the final Dense layer and GlobalAveragePooling2D weights will update during training.

  • Create a new code cell and train the transfer model for 5 epochs by pasting this code:
# Cell 10: Train transfer learning model
print("\nTraining MobileNetV2 Transfer Learning (5 epochs)...")
transfer_history = transfer_model.fit(
    train_ds_cached,
    validation_data=val_ds_cached,
    epochs=5
)

What does this code do?

  • transfer_model.fit() trains your model on the cached training data for 5 epochs, evaluating against the validation set after each epoch.
  • transfer_history stores the accuracy and loss values from each epoch so you can compare them to your simple CNN later.
  • Run the cell. Training takes approximately 5-10 minutes on a T4 GPU. Watch the validation accuracy climb each epoch.

Training is extremely slow?

  • Confirm you are using a GPU runtime. Go to **Runtime** then **Change runtime type** and verify **T4 GPU** is selected.
  • Make sure you are using train_ds_cached (with caching and prefetch) and not the raw train_ds.

Why is my MobileNetV2 training so slow in Colab?

Now for the moment of truth. Before you run the comparison, think about this: your simple CNN hit around 50-70% accuracy after 5 epochs of learning everything from scratch. What do you think MobileNetV2 achieved with its pre-trained features?

  • Create a new code cell and print the side-by-side comparison by pasting this code:
# Cell 11: Compare results
print(f"\n{'='*50}")
print(f"  COMPARISON")
print(f"{'='*50}")
print(f"  Simple CNN    - Val Accuracy: {simple_history.history['val_accuracy'][-1]:.4f}")
print(f"  MobileNetV2   - Val Accuracy: {transfer_history.history['val_accuracy'][-1]:.4f}")
print(f"{'='*50}")
  • Run the cell. You should see MobileNetV2 validation accuracy at 90% or higher, dramatically outperforming the simple CNN.

Why is the difference so dramatic?

Your simple CNN had to learn every visual feature from nothing. Edges, textures, colour patterns, leaf shapes. Five epochs was nowhere near enough time for a small network to figure all of that out across 38 classes.

MobileNetV2 already knew all of those features from ImageNet. It only needed to learn the final mapping from "these are the features I see" to "this is the disease". That is a much simpler problem, which is why accuracy jumped to 90%+ in the same number of epochs.

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook in Colab (it auto-saves, but you can press **Ctrl+S** to be sure).

ⓧ I'd like to double check the full code

Here is the complete code you should have in your notebook for this step. Scroll up and confirm you have Cell 9, Cell 10, and Cell 11 with the code matching below.

# --- STEP 4: TRANSFER LEARNING WITH MOBILENETV2 ---

# Cell 9: Build transfer learning model
base_model = tf.keras.applications.MobileNetV2(
    input_shape=(224, 224, 3),
    include_top=False,
    weights='imagenet'
)
base_model.trainable = False

transfer_model = tf.keras.Sequential([
    tf.keras.layers.Rescaling(1./127.5, offset=-1, input_shape=(224, 224, 3)),
    base_model,
    tf.keras.layers.GlobalAveragePooling2D(),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(num_classes)
])

transfer_model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

transfer_model.summary()
# Cell 10: Train transfer learning model
print("\nTraining MobileNetV2 Transfer Learning (5 epochs)...")
transfer_history = transfer_model.fit(
    train_ds_cached,
    validation_data=val_ds_cached,
    epochs=5
)
# Cell 11: Compare results
print(f"\n{'='*50}")
print(f"  COMPARISON")
print(f"{'='*50}")
print(f"  Simple CNN    - Val Accuracy: {simple_history.history['val_accuracy'][-1]:.4f}")
print(f"  MobileNetV2   - Val Accuracy: {transfer_history.history['val_accuracy'][-1]:.4f}")
print(f"{'='*50}")

Your transfer learning model is classifying plant diseases at 90%+ accuracy. Next up, you will evaluate exactly which diseases the model gets right and wrong, then save it to Google Drive for future deployment.

Evaluate and Export the Model

Your MobileNetV2 model hit 90%+ validation accuracy. That sounds impressive, but a single number hides important details.

Which diseases does the model confuse with each other? Does it struggle on any specific crop? In this step, you will generate per-class metrics, visualize a confusion matrix, compare both training histories, and save the finished model to Google Drive for future deployment.

In this step, get ready to:
  • Generate a classification report and confusion matrix heatmap.
  • Plot training history comparison charts for both models.
  • Save the trained model to Google Drive as a .keras file.
Generate the classification report

A classification report breaks down precision, recall, and F1-score for every single class. This tells you exactly which diseases the model nails and which ones it confuses.

  • Create a new code cell in your Colab notebook and generate predictions across the full validation set by running this code:
# Cell 12: Classification report
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns

val_labels = []
val_predictions = []

for images, labels in val_ds:
    preds = transfer_model.predict(images, verbose=0)
    val_predictions.extend(np.argmax(preds, axis=1))
    val_labels.extend(labels.numpy())

val_labels = np.array(val_labels)
val_predictions = np.array(val_predictions)

print("Classification Report:")
print(classification_report(
    val_labels, val_predictions,
    target_names=class_names,
    zero_division=0
))

What does this code do?

The loop iterates over every batch in val_ds, runs predictions through transfer_model, and collects both the true labels and predicted labels into arrays.

The classification_report function from scikit-learn then computes precision, recall, and F1-score for each of the 38 classes. This reveals whether the model is biased toward certain diseases or struggles with visually similar ones.

  • Run the cell. This takes a minute or two as the model predicts on all validation images.
  • You should see a full table with precision, recall, and F1-score printed for each of the 38 disease classes.

Not seeing output?

If the cell seems stuck, check that you are using val_ds (not val_ds_cached) in the loop. The cached version can cause issues when iterated multiple times in separate cells.

Help me debug my classification report cell.

Display the confusion matrix heatmap

A confusion matrix shows you exactly where mistakes happen. Dark squares along the diagonal mean correct predictions. Off-diagonal color means the model is confusing two classes.

  • Create a new code cell and display the confusion matrix as a heatmap by running this code:
# Cell 13: Confusion matrix
cm = confusion_matrix(val_labels, val_predictions)
plt.figure(figsize=(20, 16))
sns.heatmap(cm, annot=False, cmap='Blues',
            xticklabels=class_names, yticklabels=class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix - MobileNetV2 Transfer Learning')
plt.xticks(rotation=90, fontsize=6)
plt.yticks(fontsize=6)
plt.tight_layout()
plt.show()

What does this code do?

The confusion_matrix function builds a 38x38 grid. Each cell (i, j) counts how many images of class i got predicted as class j.

The seaborn heatmap visualizes this grid with color intensity. A strong diagonal means the model classifies most diseases correctly.

  • Run the cell.
  • You should see a large blue heatmap with a strong diagonal line running from top-left to bottom-right. Most of the color is concentrated along this diagonal, showing the model predicts correctly for the vast majority of classes.

Heatmap labels overlapping?

With 38 class names, labels can overlap. Make sure you have fontsize=6 on both plt.xticks and plt.yticks. The plt.tight_layout() call prevents clipping.

Help me fix my confusion matrix display.

Plot training history and test a single prediction

Training curves show how each model learned over time. Comparing both models on the same chart makes the transfer learning advantage impossible to miss.

  • Create a new code cell and plot the accuracy and loss curves for both models side by side by running this code:
# Cell 14: Training history comparison
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(simple_history.history['accuracy'], label='Simple CNN - Train')
axes[0].plot(simple_history.history['val_accuracy'], label='Simple CNN - Val')
axes[0].plot(transfer_history.history['accuracy'], label='MobileNetV2 - Train')
axes[0].plot(transfer_history.history['val_accuracy'], label='MobileNetV2 - Val')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Accuracy')
axes[0].set_title('Model Accuracy Comparison')
axes[0].legend()

axes[1].plot(simple_history.history['loss'], label='Simple CNN - Train')
axes[1].plot(simple_history.history['val_loss'], label='Simple CNN - Val')
axes[1].plot(transfer_history.history['loss'], label='MobileNetV2 - Train')
axes[1].plot(transfer_history.history['val_loss'], label='MobileNetV2 - Val')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].set_title('Model Loss Comparison')
axes[1].legend()

plt.tight_layout()
plt.show()

What does this code do?

This creates two side-by-side plots. The left chart shows training and validation accuracy over epochs for both models. The right chart shows the same for loss.

You will see MobileNetV2's curves start high and stay high, while the simple CNN curves stay much lower. The gap between them is the value of transfer learning.

  • Run the cell.
  • You should see two charts. On the accuracy chart, MobileNetV2 lines sit near the top while the simple CNN lines are noticeably lower. On the loss chart, MobileNetV2 lines drop to near zero while the simple CNN stays elevated.

Now test the model on a single leaf image to see a real prediction in action.

  • Create a new code cell and display a single image with its prediction by running this code:
# Cell 15: Test on single image and save model
plt.figure(figsize=(6, 6))
for images, labels in val_ds.take(1):
    img = images[0]
    true_label = class_names[labels[0]]

    prediction = transfer_model.predict(tf.expand_dims(img, 0), verbose=0)
    predicted_class = np.argmax(prediction[0])
    predicted_label = class_names[predicted_class]
    confidence = tf.nn.softmax(prediction[0])[predicted_class].numpy() * 100

    plt.imshow(img.numpy().astype("uint8"))
    plt.title(f"True: {true_label}\nPredicted: {predicted_label} ({confidence:.1f}%)")
    plt.axis("off")
    break
plt.show()

What does this code do?

This grabs one image from the validation set, runs it through the model, and applies softmax to convert raw logits into a confidence percentage.

The title shows both the true label and the predicted label with confidence. If your model is working well, these should match with high confidence (typically 90%+).

  • Run the cell.
  • You should see a leaf image displayed with the true disease label on top and the model's predicted label with a confidence percentage below it.

Prediction doesn't match the true label?

This can happen. No model is 100% accurate. Run the cell again to test on a different batch, or change images[0] to images[2] to pick a different sample. Most predictions should be correct with 90%+ confidence.

Why is my single image prediction incorrect?

Save the model to Google Drive

The trained model exists only in Colab's memory right now. Saving it to Google Drive means you can reuse it later without retraining.

  • In the same Cell 15, add these lines below plt.show() to save the model and confirm the file size:
# Save model to Google Drive
transfer_model.save('/content/drive/MyDrive/plant_disease_model.keras')
print("\nModel saved to Google Drive: plant_disease_model.keras")
print(f"File size: {os.path.getsize('/content/drive/MyDrive/plant_disease_model.keras') / (1024*1024):.1f} MB")
print("\nProject complete! Your model is ready for deployment.")

What does this code do?

The .save() method exports the entire model (architecture, weights, and optimizer state) to a single .keras file on your Google Drive.

The .keras format is the recommended save format in TensorFlow 2.16+. It replaces the older .h5 format and bundles everything needed to reload the model later.

Before you run this, what do you think the file size will be for a MobileNetV2-based model with 38 output classes?

  • Run the cell (or re-run the full Cell 15 if you added the save code at the bottom).
  • You should see the model saved confirmation message and a file size printed in MB.

Seeing a permission error?

Make sure Google Drive is still mounted. If your Colab session disconnected, re-run the Drive mount cell (Cell 1) first. Then re-run the save cell.

Help me fix a Drive permission error when saving my model.

  • Confirm the file exists on your Drive by running:
os.path.exists('/content/drive/MyDrive/plant_disease_model.keras')
  • This should print True.

✔️ Awesome, I've got everything!

Great. Double check you have saved all cells in your notebook.

ⓧ I'd like to double check the full code

Your notebook should now have the following cells from this step, in order:

  • Cell 12: Imports classification_report, confusion_matrix, and seaborn. Loops over val_ds to generate predictions. Prints the classification report.
  • Cell 13: Builds the confusion matrix and displays it as a seaborn heatmap with class labels on both axes.
  • Cell 14: Plots accuracy and loss curves for both simple_history and transfer_history side by side.
  • Cell 15: Tests a single image prediction with confidence, then saves transfer_model to /content/drive/MyDrive/plant_disease_model.keras and prints the file size.

Scroll through your notebook and confirm each cell is present in order.

Your model is trained, evaluated, and saved. You now have a production-ready plant disease classifier sitting on your Google Drive, ready to serve predictions from a backend API in a future project.

Secret mission

Fine-Tune MobileNetV2

Your frozen MobileNetV2 hit 90%+ accuracy using generic ImageNet features. But those features were trained on cats, cars, and coffee mugs. What if you could teach those final layers to specifically recognize the subtle textures of plant diseases? Unfreeze the top layers and push past 97% accuracy.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs on Google Colab's free GPU tier with data stored on Google Drive, so there are no ongoing financial costs.

Resources you used:

  • Google Colab notebook with T4 GPU runtime.
  • PlantVillage dataset folder on Google Drive (~828 MB).
  • Saved model file plant_disease_model.keras on Google Drive.
  • Saved model file plant_disease_model_finetuned.keras on Google Drive (if you completed the Secret Mission).

Keep everything running

No action needed. Choose this if you plan to continue building on this model in Project 2 (the Java/Spring Boot backend).

  • Your Colab runtime will automatically disconnect after a period of inactivity. No GPU charges accumulate while disconnected.
  • Your model files and dataset remain on Google Drive, ready for future use.
  • The PlantVillage dataset takes approximately 828 MB of your Google Drive storage. The model files add roughly 10-30 MB depending on whether you completed the Secret Mission.

Pause - I'll come back to this later

Shut down the Colab runtime to free up GPU resources. Your notebook and Drive files stay intact for when you return.

  • In your Colab notebook, click **Runtime** in the top menu bar.
  • Click **Disconnect and delete runtime**.
  • Your notebook code cells remain saved. When you return, you will need to re-run the cells to reload the dataset and models.

Delete - I don't want to use this again

Remove all project files from Google Drive and delete the Colab notebook to start fresh.

  • In your Colab notebook, click **Runtime** in the top menu, then **Disconnect and delete runtime** to shut down the GPU session.
  • Open Google Drive in your browser.
  • Navigate to the PlantVillage folder you uploaded. Right-click it and select **Move to trash**.
  • Find plant_disease_model.keras in your Drive root. Right-click it and select **Move to trash**.
  • Find plant_disease_model_finetuned.keras (if you completed the Secret Mission). Right-click it and select **Move to trash**.
  • Find your Colab notebook file in Google Drive (under the **Colab Notebooks** folder). Right-click it and select **Move to trash**.
  • Click **Trash** in the left sidebar of Google Drive, then click **Empty trash** to permanently free the storage space.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete machine learning pipeline that takes a photo of a plant leaf and identifies which of 38 diseases it has.

You've learned how to:

  • Built a full ML pipeline in Google Colab from raw images to a saved model, covering data loading, exploration, preprocessing, training, and evaluation.
  • Trained a simple CNN from scratch to experience firsthand why it struggles, then applied transfer learning with MobileNetV2 to achieve 90%+ accuracy on 38 plant disease classes.
  • Evaluated the model with classification reports and a confusion matrix, then exported a production-ready .keras model file to Google Drive for future API deployment.
  • Secret Mission: Fine-tuned MobileNetV2 by unfreezing the top layers to push accuracy past 97%.

Ready to quiz yourself?