Deep Dive: Fire Classification with CNNs & MobileNetV2 - Code Walkthrough & Expected Outputs
This blog post walks through a Python script designed for fire classification using Convolutional Neural Networks (CNNs) and transfer learning with MobileNetV2. We'll break down each section of the code and discuss the outputs you would typically expect if you were to run this in an environment like Google Colab or a local setup with the necessary dependencies and data.
Note: Actually running this script requires a specific dataset, GPU resources for efficient training, and an environment where shell commands and plotting work interactively. This post describes the process and anticipated results.
1. Initial Kaggle Data Import
The script begins by attempting to download the dataset from Kaggle using the `kagglehub` library.
# IMPORTANT: RUN THIS CELL IN ORDER TO IMPORT YOUR KAGGLE DATA SOURCES,
# THEN FEEL FREE TO DELETE THIS CELL.
# NOTE: THIS NOTEBOOK ENVIRONMENT DIFFERS FROM KAGGLE'S PYTHON
# ENVIRONMENT SO THERE MAY BE MISSING LIBRARIES USED BY YOUR
# NOTEBOOK.
import kagglehub
phylake1337_fire_dataset_path = kagglehub.dataset_download('phylake1337/fire-dataset')
print('Data source import complete.')
Expected Output:
- Download progress messages for the dataset 'phylake1337/fire-dataset'.
- Finally, the line:
Data source import complete.
2. Importing Necessary Libraries
This section imports all the Python libraries required for data manipulation, plotting, deep learning, and system interactions.
# Import Data Science Libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from sklearn.model_selection import train_test_split
from PIL import Image
# Tensorflow Libraries
from tensorflow import keras
from tensorflow.keras import layers,models
from keras_preprocessing.image import ImageDataGenerator
from keras.layers import Dense, Dropout
from tensorflow.keras.callbacks import Callback, EarlyStopping,ModelCheckpoint
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras import Model
from tensorflow.keras.layers.experimental import preprocessing
# System libraries
from pathlib import Path
import os.path
# Metrics
from sklearn.metrics import classification_report, confusion_matrix
import itertools
Expected Output: No direct visible output, unless a library is not installed, in which case an `ImportError` would occur.
3. Downloading and Importing Helper Functions
The script uses a set of helper functions from a remote source, downloaded via `wget`.
!wget https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/extras/helper_functions.py
# Import series of helper functions for our notebook
from helper_functions import create_tensorboard_callback, plot_loss_curves, unzip_data, compare_historys, walk_through_dir, pred_and_plot
Expected Output from !wget
:
--YYYY-MM-DD HH:MM:SS-- https://raw.githubusercontent.com/mrdbourke/tensorflow-deep-learning/main/extras/helper_functions.py Resolving raw.githubusercontent.com (raw.githubusercontent.com)... XXX.XXX.XXX.XXX, ... Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|XXX.XXX.XXX.XXX|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 10246 (10K) [text/plain] Saving to: 'helper_functions.py' helper_functions.py 100%[===================>] 10.01K --.-KB/s in 0s YYYY-MM-DD HH:MM:SS (XXX MB/s) - 'helper_functions.py' saved [10246/10246]
The import statement that follows will produce no output unless the file download failed or the file has issues.
4. Loading and Transforming Data: Initial Setup
Constants for batch size and image size are defined. The `walk_through_dir` helper function is used to inspect the dataset directory.
BATCH_SIZE = 32
IMAGE_SIZE = (320, 320) # Note: Later, 224x224 is used for MobileNetV2
# Walk through each directory
dataset = "../input/fire-dataset/fire_dataset" # Path depends on where kagglehub downloads
walk_through_dir(dataset)
Expected Output from walk_through_dir(dataset)
:
There are 2 directories and 0 images in '../input/fire-dataset/fire_dataset'. There are 0 directories and XXX images in '../input/fire-dataset/fire_dataset/fire_images'. There are 0 directories and YYY images in '../input/fire-dataset/fire_dataset/non_fire_images'.
(XXX and YYY are image counts in subfolders, assuming 'fire_images' and 'non_fire_images' are class folders. The exact path might differ.)
5. Structuring Data with Pandas DataFrame
File paths and labels are extracted and organized into a Pandas DataFrame.
image_dir = Path(dataset)
# Get filepaths and labels
filepaths = list(image_dir.glob(r'**/*.JPG')) + list(image_dir.glob(r'**/*.jpg')) + list(image_dir.glob(r'**/*.png'))
labels = list(map(lambda x: os.path.split(os.path.split(x)[0])[1], filepaths))
filepaths = pd.Series(filepaths, name='Filepath').astype(str)
labels = pd.Series(labels, name='Label')
# Concatenate filepaths and labels
image_df = pd.concat([filepaths, labels], axis=1)
print(f"Number of PNG files found: {len(list(image_dir.glob(r'**/*.png')))}")
print("\\nDataFrame Head:")
print(image_df.head())
print("\\nDataFrame Tail:")
print(image_df.tail())
print(f"\\nDataFrame Shape: {image_df.shape}")
Expected Output:
Number of PNG files found: ZZZ DataFrame Head: Filepath Label 0 ../input/fire-dataset/fire_dataset/fire_images... fire_images 1 ../input/fire-dataset/fire_dataset/fire_images... fire_images 2 ../input/fire-dataset/fire_dataset/fire_images... fire_images 3 ../input/fire-dataset/fire_dataset/fire_images... fire_images 4 ../input/fire-dataset/fire_dataset/fire_images... fire_images DataFrame Tail: Filepath Label NNN-5 ../input/fire-dataset/fire_dataset/non_fire_images... non_fire_images NNN-4 ../input/fire-dataset/fire_dataset/non_fire_images... non_fire_images NNN-3 ../input/fire-dataset/fire_dataset/non_fire_images... non_fire_images NNN-2 ../input/fire-dataset/fire_dataset/non_fire_images... non_fire_images NNN-1 ../input/fire-dataset/fire_dataset/non_fire_images... non_fire_images DataFrame Shape: (NNN, 2)
(ZZZ is the count of .png files, NNN is the total number of images. Paths and labels will be specific.)
6. Visualizing Sample Images
A random selection of images from the dataset is plotted to provide a visual check.
import matplotlib.image as mpimg
# Display 16 picture of the dataset with their labels
random_index = np.random.randint(0, len(image_df), 16)
fig, axes = plt.subplots(nrows=4, ncols=4, figsize=(10, 10),
subplot_kw={'xticks': [], 'yticks': []})
for i, ax in enumerate(axes.flat):
image = Image.open(image_df.Filepath[random_index[i]])
ax.imshow(image)
ax.set_title(image_df.Label[random_index[i]])
plt.tight_layout()
plt.show()
Expected Output:
A 4x4 grid of 16 randomly selected images from the dataset would be displayed. Each sub-plot shows an image, with its corresponding label (e.g., "fire_images" or "non_fire_images") as its title.
7. Data Preprocessing for the Model
The data is split into training and testing sets. TensorFlow's `ImageDataGenerator` is used to load, preprocess, and augment images in batches.
# Separate in train and test data
train_df, test_df = train_test_split(image_df, test_size=0.2, shuffle=True, random_state=42)
train_generator = ImageDataGenerator(
preprocessing_function=tf.keras.applications.mobilenet_v2.preprocess_input,
validation_split=0.2
)
test_generator = ImageDataGenerator(
preprocessing_function=tf.keras.applications.mobilenet_v2.preprocess_input
)
# Split the data into three categories.
train_images = train_generator.flow_from_dataframe(
dataframe=train_df,
x_col='Filepath',
y_col='Label',
target_size=(224, 224),
color_mode='rgb',
class_mode='categorical',
batch_size=32,
shuffle=True,
seed=42,
subset='training'
)
val_images = train_generator.flow_from_dataframe(
dataframe=train_df,
x_col='Filepath',
y_col='Label',
target_size=(224, 224),
color_mode='rgb',
class_mode='categorical',
batch_size=32,
shuffle=True,
seed=42,
subset='validation'
)
test_images = test_generator.flow_from_dataframe(
dataframe=test_df,
x_col='Filepath',
y_col='Label',
target_size=(224, 224),
color_mode='rgb',
class_mode='categorical',
batch_size=32,
shuffle=False
)
# Resize Layer (defined but its application point is key)
resize_and_rescale = tf.keras.Sequential([
layers.experimental.preprocessing.Resizing(224,224),
layers.experimental.preprocessing.Rescaling(1./255),
])
Expected Output from .flow_from_dataframe(...)
calls:
Found XXXX validated image filenames belonging to 2 classes. (For train_images) Found YYYY validated image filenames belonging to 2 classes. (For val_images) Found ZZZZ validated image filenames belonging to 2 classes. (For test_images)
(XXXX, YYYY, ZZZZ are the respective image counts per data split.)
8. Setting Up the Pre-trained Model (MobileNetV2)
MobileNetV2 is loaded with pre-trained ImageNet weights, excluding its top classification layer. Callbacks for training are also defined.
# Load the pretained model
pretrained_model = tf.keras.applications.MobileNetV2(
input_shape=(224, 224, 3),
include_top=False,
weights='imagenet',
pooling='avg'
)
pretrained_model.trainable = False
# Create checkpoint callback
checkpoint_path = "fires_classification_model_checkpoint"
checkpoint_callback = ModelCheckpoint(checkpoint_path,
save_weights_only=True,
monitor="val_accuracy",
save_best_only=True)
# Setup EarlyStopping callback
early_stopping = EarlyStopping(monitor = "val_loss",
patience = 5,
restore_best_weights = True)
Expected Output:
- If MobileNetV2 weights are not cached, TensorFlow might print download progress.
- Defining callbacks produces no direct output.
9. Building and Compiling the Custom Model
Custom classification layers are added on top of MobileNetV2. The final model is then compiled with an optimizer, loss function, and metrics.
inputs = pretrained_model.input
# The resize_and_rescale layer is applied to the input of the pretrained model in the notebook
x = resize_and_rescale(inputs) # Note: This step applies resizing and rescaling BEFORE MobileNetV2 base which also has its own preprocessing.
# The script then uses pretrained_model.output (which is derived from the original inputs, not necessarily the rescaled 'x' above directly in the graph, depending on how MobileNetV2 is called with 'x')
# Corrected logic based on typical transfer learning:
# If 'x' from resize_and_rescale is meant to be the input to MobileNetV2, the pretrained_model should be called with 'x'.
# However, the original notebook code is:
# x = Dense(256, activation='relu')(pretrained_model.output)
# This implies the 'resize_and_rescale' might be an alternative preprocessing not directly fed to this instance of pretrained_model if 'inputs' is directly from pretrained_model.input.
# For clarity, let's assume 'inputs' is the original placeholder and the custom head is added to pretrained_model.output.
x_head = Dense(256, activation='relu')(pretrained_model.output)
x_head = Dropout(0.2)(x_head)
x_head = Dense(256, activation='relu')(x_head)
x_head = Dropout(0.2)(x_head)
outputs = Dense(2, activation='softmax')(x_head)
# The model uses 'inputs' which is pretrained_model.input.
# The resize_and_rescale layer as used in the notebook `x = resize_and_rescale(inputs)`
# effectively means there's an initial preprocessing step, and then these processed `inputs`
# are what `pretrained_model` implicitly uses because `pretrained_model.output` is derived from `pretrained_model.input`.
# The most straightforward interpretation of the notebook is that `resize_and_rescale` is applied, then `MobileNetV2` processes these, then the custom head.
model = Model(inputs=inputs, outputs=outputs) # inputs = pretrained_model.input
model.compile(
optimizer=Adam(0.0001),
loss='categorical_crossentropy',
metrics=['accuracy']
)
# To see the architecture, one would call:
# model.summary()
Expected Output: No direct visible output. If `model.summary()` were called, it would print a textual summary of the model's architecture, including layers, output shapes, and parameter counts.
10. Training the Model
The `model.fit()` method starts the training process, iterating over the data for a specified number of epochs.
history = model.fit(
train_images,
steps_per_epoch=len(train_images),
validation_data=val_images,
validation_steps=len(val_images),
epochs=100,
callbacks=[
early_stopping,
# create_tensorboard_callback("training_logs", "fire_classification"), # Assuming helper_functions are available
checkpoint_callback,
]
)
Expected Output (Training Loop):
Epoch 1/100 XX/XX [==============================] - YYs ZZZms/step - loss: A.AAAA - accuracy: B.BBBB - val_loss: C.CCCC - val_accuracy: D.DDDD Epoch 2/100 XX/XX [==============================] - YYs ZZZms/step - loss: E.EEEE - accuracy: F.FFFF - val_loss: G.GGGG - val_accuracy: H.HHHH ... (This continues for up to 100 epochs or until EarlyStopping criteria are met) ...
(XX: steps_per_epoch, YY: time per epoch, ZZZ: time per step. A-H represent changing loss and accuracy values.)
11. Evaluating Model Performance
The trained model is evaluated on the unseen test dataset to assess its generalization performance.
results = model.evaluate(test_images, verbose=0)
print(" Test Loss: {:.5f}".format(results[0]))
print("Test Accuracy: {:.2f}%".format(results[1] * 100))
Expected Output:
Test Loss: X.XXXXX Test Accuracy: YY.YY%
(X.XXXXX is the calculated loss, and YY.YY is the percentage accuracy on the test set.)
12. Visualizing Training Loss and Accuracy Curves
The `plot_loss_curves` helper function visualizes the training and validation loss and accuracy over epochs.
# from helper_functions import plot_loss_curves # Ensure this is imported
plot_loss_curves(history)
Expected Output:
One or two plots would be displayed:
- Training Loss and Validation Loss vs. Epochs.
- Training Accuracy and Validation Accuracy vs. Epochs.
13. Making Predictions on the Test Data
The model makes predictions on the test images, and a sample of these predictions is displayed.
# Predict the label of the test_images
pred = model.predict(test_images)
pred_classes = np.argmax(pred,axis=1)
# Map the label
idx_to_class_labels = {v:k for k,v in train_images.class_indices.items()}
pred_labels = [idx_to_class_labels[k] for k in pred_classes]
print(f'The first 5 predictions (labels): {pred_labels[:5]}')
# Display 15 random pictures from the test set with their labels
random_index = np.random.randint(0, len(test_df) - 1, 15)
fig, axes = plt.subplots(nrows=3, ncols=5, figsize=(25, 15),
subplot_kw={'xticks': [], 'yticks': []})
for i, ax in enumerate(axes.flat):
image_path = test_df.Filepath.iloc[random_index[i]]
true_label = test_df.Label.iloc[random_index[i]]
predicted_label_for_image = pred_labels[random_index[i]] # Use the mapped prediction
image = Image.open(image_path)
ax.imshow(image)
color = "green" if true_label == predicted_label_for_image else "red"
ax.set_title(f"True: {true_label}\nPredicted: {predicted_label_for_image}", color=color)
plt.tight_layout()
plt.show()
Expected Output from print statement:
The first 5 predictions (labels): ['fire_images', 'non_fire_images', 'fire_images', 'fire_images', 'non_fire_images']
(Actual labels depend on model predictions.)
Expected Output from plt.show()
:
A 3x5 grid of 15 randomly selected test images. Each image is titled with its "True" label and "Predicted" label. The title text is colored green for correct predictions and red for incorrect ones.
14. Generating Classification Report and Confusion Matrix
Detailed performance metrics are calculated and displayed, including a classification report and a confusion matrix.
y_test_labels = list(test_df.Label) # True labels from the test dataframe
print("Classification Report:")
print(classification_report(y_test_labels, pred_labels))
print("\\nClassification Report as DataFrame:")
report_dict = classification_report(y_test_labels, pred_labels, output_dict=True)
df_report = pd.DataFrame(report_dict).transpose()
print(df_report)
# from helper_functions import make_confusion_matrix # Ensure this is imported if not part of the notebook directly
# Definition for make_confusion_matrix from the notebook:
def make_confusion_matrix(y_true, y_pred, classes=None, figsize=(15, 7), text_size=10, norm=False, savefig=False):
cm = confusion_matrix(y_true, y_pred)
cm_norm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
n_classes = cm.shape[0]
fig, ax = plt.subplots(figsize=figsize)
cax = ax.matshow(cm, cmap=plt.cm.Blues)
fig.colorbar(cax)
if classes:
labels_for_matrix = classes
else:
labels_for_matrix = np.arange(cm.shape[0])
ax.set(title="Confusion Matrix",
xlabel="Predicted label",
ylabel="True label",
xticks=np.arange(n_classes),
yticks=np.arange(n_classes),
xticklabels=labels_for_matrix,
yticklabels=labels_for_matrix)
ax.xaxis.set_label_position("bottom")
ax.xaxis.tick_bottom()
plt.xticks(rotation=90, fontsize=text_size) # Rotation was 90 in notebook
plt.yticks(fontsize=text_size)
threshold = (cm.max() + cm.min()) / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
if norm:
plt.text(j, i, f"{cm[i, j]} ({cm_norm[i, j]*100:.1f}%)",
horizontalalignment="center",
color="white" if cm[i, j] > threshold else "black",
size=text_size)
else:
plt.text(j, i, f"{cm[i, j]}",
horizontalalignment="center",
color="white" if cm[i, j] > threshold else "black",
size=text_size)
if savefig:
fig.savefig("confusion_matrix.png")
plt.show()
make_confusion_matrix(y_test_labels, pred_labels, classes=list(idx_to_class_labels.values()))
Expected Output from print(classification_report(...))
:
Classification Report: precision recall f1-score support fire_images X.XX X.XX X.XX AAA non_fire_images Y.YY Y.YY Y.YY BBB accuracy Z.ZZ CCC macro avg A.AA A.AA A.AA CCC weighted avg B.BB B.BB B.BB CCC
(X.XX, Y.YY, etc., are metric scores; AAA, BBB, CCC are support counts.)
Expected Output from printing `df_report`:
Classification Report as DataFrame: precision recall f1-score support fire_images ... ... ... ... non_fire_images ... ... ... ... accuracy ... ... ... ... macro avg ... ... ... ... weighted avg ... ... ... ...
(A more structured table format of the classification report.)
Expected Output from make_confusion_matrix(...)
:
A 2x2 confusion matrix plot. The cells show counts of True Positives, True Negatives, False Positives, and False Negatives. Axes are labeled "True label" and "Predicted label" with class names. Cells may be color-coded and include numbers and/or percentages.
Conclusion
This walkthrough has detailed the steps involved in the provided Python script for fire classification, from data loading and preprocessing to model training, evaluation, and prediction analysis. The expected outputs give an idea of what to look for at each stage of executing such a deep learning pipeline. The model aims for high accuracy in distinguishing fire from non-fire images, which can be crucial for various safety applications.