Build a Forest Fire CNN
Train and evaluate a three-class forest fire CNN from scratch.
Introduction
30 Second Summary
Bright flames can resemble a sunset in a single photo. Pale smoke can fade into cloud or haze.
In this project, you will build a three-class convolutional neural network from random initialization with TensorFlow. You will prove how its pixels become predictions through layer calculations.
What You'll Build
You will run the finished model on forest images to see its class-level mistakes laid out in a confusion matrix.
By the end of this project, you'll have:
- A hand-checkable convolution where a 3 by 3 kernel produces the same feature map in your calculation and TensorFlow.
- A model summary that exposes how a Flatten bottleneck spends 221,187 parameters in one Dense layer.
- A compact CNN trained from scratch that you can save for class-by-class testing.
- Secret Mission: Add a deeper convolution block, derive its extra parameters, and compare one training epoch with the baseline.
Are there any prerequisites?
You need basic Python plus a forest fire dataset with train, val, and test splits. Each split needs fire, nofire, and smoke folders.
Before We Start
Before the hands-on work begins, commit to building a fire, nofire, and smoke CNN from random initialization. This gives you a clear view of convolution mathematics, tensor shapes, parameters, logits, and predictions before later Intel Arc acceleration.
Prepare a Compatible TensorFlow Environment
Your CNN mathematics depends on a stable software baseline. A newer TensorFlow release could break compatibility with the environment used for the later Intel Arc work.
This step gives the project an isolated Python 3.11 virtual environment with TensorFlow 2.15.1. You will use Windows PowerShell to prepare the files. The final check confirms the expected native Windows CPU path.
In this step, get ready to:
- Confirm Python 3.11 in Windows PowerShell.
- Prepare an activated .venv with TensorFlow 2.15.1 installed from requirements.txt.
- Verify the three dataset splits and the native Windows CPU baseline.
Confirm Python 3.11
TensorFlow 2.15.1 works with Python 3.11. Confirming the interpreter first prevents the virtual environment from inheriting an incompatible Python version.
- Press the Windows key to open Windows search.
- Type Windows PowerShell and press Enter.
You will see a Windows PowerShell window with a command prompt.
- Check the active Python version by running this command:
python --version
What does this command do?
The python command starts your current Python installation. The --version option prints its version without opening the interpreter.
✔️ I see Python 3.11
Your output begins with Python 3.11. Good, the required interpreter is ready to create this project's environment.
ⓧ I see an older Python version
The current Python installation is below the project's required 3.11 version. Install the pinned version before creating the environment.
- Open the official Python guide for Windows.
- Follow the Windows installation guidance for Python 3.11.
- Close the current Windows PowerShell window.
- Reopen Windows PowerShell through Windows search.
- Run the version command above again.
ⓧ Command not found
Windows cannot find a Python installation from the current command prompt. Install Python 3.11 before continuing.
- Open the official Python guide for Windows.
- Follow the Windows installation guidance for Python 3.11.
- Close the current Windows PowerShell window after installation.
- Reopen Windows PowerShell through Windows search.
- Run the version command above again.
Create the isolated environment
The project folder keeps the environment beside the scripts that use it. PowerShell also treats this folder as the current location for every setup command that follows.
- Choose a short folder name for this project: your project folder name.
- Create the folder on your Desktop by running these commands:
Set-Location -Path ~/Desktop
New-Item -Path '[[PROJECT_FOLDER="your project folder name"]]' -ItemType Directory
Set-Location -Path '[[PROJECT_FOLDER="your project folder name"]]'
What do these commands do?
- The first line points PowerShell at your Desktop.
- The middle line creates your named project folder.
- The final line makes that folder the current PowerShell location.
PowerShell prints a directory record when it creates the folder. The prompt now ends with your chosen project folder name.
Folder already exists?
Choose a different folder name if PowerShell reports that the path already exists. Keep the new value consistent wherever this step shows your project folder name.
Ask for help if PowerShell cannot create or enter the folder: help me fix my PowerShell project folder command.
You will edit the dependency file in Visual Studio Code. Opening the project folder there gives you a clear view of every file created during setup.
- Press the Windows key to open Windows search.
- Type Visual Studio Code and press Enter.
You will see Visual Studio Code open.
- Click File in the top menu bar.
- Click Open Folder.
You will see a folder picker for choosing the project that appears in the editor.
- Select your project folder name from your Desktop.
- Click Select Folder.
The Explorer sidebar now shows your empty project folder.
- Right-click your project folder name in the Explorer sidebar.
- Select New File.
A filename field appears under the project folder.
- Type requirements.txt and press Enter.
- Pin the TensorFlow dependency by entering this line:
tensorflow==2.15.1
Why pin this dependency?
The line tells Python to install TensorFlow version 2.15.1 exactly. The pin keeps this CPU baseline compatible with the TensorFlow release family used by the later Intel Arc environment.
- Press Ctrl+S to save requirements.txt.
- Confirm requirements.txt remains visible in the Explorer sidebar.
File missing from Explorer?
Check that you created requirements.txt directly inside the project folder. Make sure the filename does not end with an extra .txt extension.
Ask for help checking the file location: help me find my requirements file in Visual Studio Code.
✔️ Awesome, I've got everything!
Your dependency file is saved inside the project folder with the required TensorFlow pin.
ⓧ I'd like to double check the full code
Compare your complete requirements.txt file with this reference.
tensorflow==2.15.1
This single dependency line is the complete file for this step.
The .venv directory isolates this project's packages from other Python projects on your computer. Creating it with the confirmed interpreter gives the environment its Python 3.11 foundation.
Environment creation can take up to a minute while Python prepares its local files.
- Create the virtual environment from the project folder by running this command in Windows PowerShell:
python -m venv .venv
What does this command do?
Python's venv module creates an isolated environment in .venv. Packages installed after activation stay local to this project.
- Confirm .venv appears in the Visual Studio Code Explorer sidebar.
Virtual environment not created?
Check that PowerShell still points to your project folder. Confirm the earlier version check reported Python 3.11 before trying the command again.
Ask for help with the environment creation error: help me create my Python virtual environment.
- Activate the new environment by running this command:
.venv\Scripts\Activate.ps1
What does activation change?
Activation makes the environment's Python interpreter the default for this PowerShell session. Future package installations now go into .venv.
You should see .venv at the start of the PowerShell prompt. That prefix confirms the isolated environment is active.
PowerShell blocked activation?
PowerShell may block local scripts under your account's current execution policy. Use the activation policy guidance in the official Python virtual environment documentation before running the activation command again.
Ask for help without weakening system-wide security: help me activate my virtual environment safely.
The first TensorFlow installation can take several minutes while Python downloads the Windows packages. A long stream of package activity is expected during this step.
- Install the pinned dependency from requirements.txt by running this command:
python -m pip install -r requirements.txt
What does this command do?
Python runs its active pip installer against the dependency list in requirements.txt. The active environment keeps the installed packages inside .venv.
When the installation finishes successfully, the PowerShell prompt returns without an installation error. The final import check will prove that TensorFlow is usable.
TensorFlow installation failed?
Confirm the prompt still starts with .venv. Check that requirements.txt contains the exact dependency line from the reference tab.
Ask for help with the package output: help me fix my TensorFlow installation.
Complete the readiness checks
TensorFlow will load images from the dataset's directory structure in later steps. Each split needs the same fire, nofire, and smoke class folders.
- Right-click your project folder name in the Visual Studio Code Explorer sidebar.
- Select New Folder.
A folder name field appears beneath the project folder.
- Type data and press Enter.
The data folder now appears in the project.
- Press the Windows key to open Windows search.
- Type File Explorer and press Enter.
You will see File Explorer open for locating the provided dataset.
- Locate the provided forestfire-classifier-dataset folder in File Explorer.
- Drag forestfire-classifier-dataset onto the data folder in the Visual Studio Code Explorer sidebar.
The dataset now sits at data/forestfire-classifier-dataset inside the project.
Before you check the directory tree, which split names and class names do you expect PowerShell to print?
- Print the dataset's directory names from the active PowerShell session by running this command:
Get-ChildItem -Path data\forestfire-classifier-dataset -Directory -Recurse -Name
What does this command show?
The Get-ChildItem command reads the dataset directory tree. Its options keep the output focused on folder names across every nested level.
The output should show train, val, and test. Under each split, you should see fire, nofire, and smoke.
Dataset folders missing?
Check that the dataset folder is directly inside data. Remove any accidental extra nesting caused by extracting or copying the dataset into another folder with the same name.
Ask for help checking the directory tree: help me fix my forest fire dataset layout.
Before you run the final check, do you expect native Windows TensorFlow to report an available GPU?
- Verify the TensorFlow import and device baseline by running this command:
python -c "import tensorflow as tf; print(tf.config.list_physical_devices('GPU'))"
What does this check prove?
The command imports TensorFlow inside the active environment. It then asks the runtime for visible physical GPU devices.
A successful import proves the installation works. An empty device list confirms the expected native Windows CPU baseline for this project.
That's the baseline ready. TensorFlow imports from .venv, while native Windows reports no supported TensorFlow GPU.
TensorFlow import failed?
Confirm the PowerShell prompt starts with .venv. Return to the installation check if the command reports a missing TensorFlow package.
Ask for help interpreting the import failure: help me fix my TensorFlow import check.
Why these versions?
TensorFlow 2.15.1 requires Python 3.9 or newer. On Windows, that TensorFlow package resolves to tensorflow-intel==2.15.1 for the CPU path.
Python 3.11 also stays inside the Python 3.9 to 3.11 range documented for the later Intel Extension for TensorFlow Arc path. This keeps the model code portable into the follow-on project.
Your Python environment, TensorFlow installation, and dataset are ready. Next up, you will make one convolution visible as a hand-checked feature map.
Make One Convolution Visible
Your environment can now run TensorFlow on the CPU. The next challenge is understanding what a convolution actually computes.
Large neural networks hide this arithmetic inside layers. A small tensor lets you inspect how a kernel turns pixels into a feature map.
In this step, get ready to:
- Define a 5 by 5 single-channel image.
- Calculate one output value with a 3 by 3 vertical-edge kernel.
- Apply the kernel with stride 1 plus VALID padding.
Build the image and edge kernel
A convolution needs an image plus a set of weights. This experiment uses zeros around a square of ones so the vertical changes are easy to see.
- Create inspect_convolution.py from the activated Windows PowerShell session by running this command:
code inspect_convolution.py
What Does This Command Do?
The code command asks Visual Studio Code to open the named file. Visual Studio Code prepares the new file inside the current project folder.
You should see an empty inspect_convolution.py editor tab.
File Did Not Open?
- Confirm that the PowerShell prompt still points to the project folder containing requirements.txt.
- Check that Visual Studio Code is installed on your Windows computer.
- Help me open inspect_convolution.py from my current PowerShell folder.
- Build the convolution experiment by pasting this code into inspect_convolution.py:
import tensorflow as tf
pixels = tf.constant(
[[0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0]],
dtype=tf.float32,
)
kernel_values = tf.constant(
[[-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]],
dtype=tf.float32,
)
image = tf.reshape(pixels, (1, 5, 5, 1))
kernel = tf.reshape(kernel_values, (3, 3, 1, 1))
feature_map = tf.nn.conv2d(image, kernel, strides=1, padding="VALID")
print("Input shape:", image.shape)
print("Kernel shape:", kernel.shape)
print("Expected output width: (5 - 3) / 1 + 1 = 3")
print("Top-left hand calculation: 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 1 = 2")
print("Feature map:")
print(tf.squeeze(feature_map).numpy())
What Does This Code Do?
- The pixels tensor stores the 5 by 5 image values.
- The kernel_values tensor stores the 3 by 3 vertical-edge weights.
- The image reshape produces the batch-height-width-channels shape (1, 5, 5, 1).
- The kernel reshape produces the kernel-height-kernel-width-input-channels-output-channels shape (3, 3, 1, 1).
- The feature_map tensor stores the result of moving the kernel across the image.
- Save inspect_convolution.py by pressing Ctrl+S.
- Confirm that inspect_convolution.py appears beside requirements.txt in the Visual Studio Code Explorer.
File Saved in the Wrong Folder?
- Check whether the Explorer also lists requirements.txt.
- Return to the PowerShell project folder before reopening inspect_convolution.py.
- Help me verify where inspect_convolution.py was saved.
Calculate one output by hand
Each feature-map value comes from one 3 by 3 image patch. The first value uses the patch in the image's top-left corner.
- Read the first three values from each of the image's first three rows.
- Pair each image value with the kernel weight in the same position.
- Multiply the nine matching pairs.
- Add the nine products.
The calculation is 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 1 = 2. TensorFlow should place 2.0 in the feature map's top-left cell.
Why Does This Kernel Find Vertical Changes?
The negative weights subtract values from the patch's left column. The positive weights add values from its right column.
A difference between those columns creates a nonzero result. The sign shows the direction of that change.
Run and compare the feature map
With VALID padding, the kernel visits only positions where the entire 3 by 3 window fits. The output width is (5 - 3) / 1 + 1 = 3.
How Does TensorFlow Read These Shapes?
TensorFlow orders the image dimensions as batch, height, width, then channels. One 5 by 5 single-channel image therefore has shape (1, 5, 5, 1).
TensorFlow orders the kernel dimensions as height, width, input channels, then output channels. One 3 by 3 kernel for one channel therefore has shape (3, 3, 1, 1).
Before you run the script, do you expect TensorFlow's top-left value to match your hand calculation?
- Switch back to the activated Windows PowerShell session from earlier.
- Run the convolution experiment by entering this command:
python inspect_convolution.py
What Does This Command Do?
Python executes inspect_convolution.py inside the activated .venv environment. The script applies the kernel before printing the tensor shapes plus the feature map.
You should see the input shape (1, 5, 5, 1) plus the kernel shape (3, 3, 1, 1).
You should also see a 3 by 3 feature map with 2.0 in its top-left cell. Your hand calculation now matches TensorFlow's result.
Feature Map Does Not Match?
- Confirm that the PowerShell prompt still shows the activated .venv environment.
- Compare the pixels values with the full-file reference below.
- Check that the convolution uses strides=1.
- Check that the convolution uses padding="VALID".
- Help me debug the output from inspect_convolution.py.
✔️ Awesome, I've got everything!
That is convolution made visible. Your saved script produces a feature map that agrees with the weighted sum you calculated.
ⓧ I'd like to double check the full code
Compare your saved inspect_convolution.py file with this complete reference.
import tensorflow as tf
pixels = tf.constant(
[[0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0]],
dtype=tf.float32,
)
kernel_values = tf.constant(
[[-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]],
dtype=tf.float32,
)
image = tf.reshape(pixels, (1, 5, 5, 1))
kernel = tf.reshape(kernel_values, (3, 3, 1, 1))
feature_map = tf.nn.conv2d(image, kernel, strides=1, padding="VALID")
print("Input shape:", image.shape)
print("Kernel shape:", kernel.shape)
print("Expected output width: (5 - 3) / 1 + 1 = 3")
print("Top-left hand calculation: 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 1 = 2")
print("Feature map:")
print(tf.squeeze(feature_map).numpy())
You can now trace one feature-map value back to its pixels plus kernel weights. Next, you will scale this operation into a three-class classifier and expose the parameter cost of flattening its feature map.
Build the Naive Classifier
Your convolution experiment showed how one kernel creates a feature map from pixels. Now you can scale that idea into a model that accepts forest fire images.
A simple CNN gives you a baseline to inspect in TensorFlow. The key question is whether its parameters support spatial feature extraction or the final three-class guess.
In this step, get ready to:
- Load one training batch with a fixed class order.
- Assemble a convolutional classifier with three output logits.
- Inspect its parameter count before producing an untrained prediction.
Load one training batch
The classifier needs images with consistent dimensions. It also needs a fixed class order so each integer label always maps to the same class name.
- Press the Windows key to open your search bar.
- Type Visual Studio Code into the search bar.
- Press Enter to open Visual Studio Code.
- Use the folder picker to select the folder that contains inspect_convolution.py.
- Click the new-file icon in the Explorer sidebar.
- Enter naive_classifier.py as the file name.
- Press Enter to create the file.
- Load the training images by pasting this code into naive_classifier.py:
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TRAIN_DIR = DATA_ROOT / "train"
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
if not TRAIN_DIR.exists():
raise FileNotFoundError(f"Training directory not found: {TRAIN_DIR.resolve()}")
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=True, seed=123,
)
What does this code do?
- The DATA_ROOT path points to the verified forest fire dataset.
- The CLASS_NAMES list fixes the label order as fire, nofire, then smoke.
- The IMAGE_SIZE value resizes every image to 96 by 96 pixels.
- The BATCH_SIZE value groups up to 32 images for each pass through the dataset.
- The directory check stops the script with the resolved missing path when the training split cannot be found.
- The image_dataset_from_directory call creates a shuffled training dataset with RGB images and integer labels.
- Save naive_classifier.py.
- Return to the activated Windows PowerShell from earlier.
- Load the training dataset by running this command:
python naive_classifier.py
What does this command do?
Python executes naive_classifier.py inside your activated environment. The script checks the training path before TensorFlow scans its three class folders.
You should see TensorFlow report that it found image files belonging to three classes. Good progress. Your fixed fire, nofire, and smoke label order is now ready for the model.
Training images not found?
- Confirm that naive_classifier.py sits beside the data folder.
- Check that the dataset folder is named forestfire-classifier-dataset.
- Check that the training split contains fire, nofire, and smoke folders.
- Help me diagnose my forest fire dataset path.
Assemble the baseline model
The baseline follows one learned convolution with a direct path to three output values. Its summary lets you see how each layer changes the shape and parameter count.
- Switch back to naive_classifier.py in Visual Studio Code.
- Add the model below the existing dataset code by pasting:
model = tf.keras.Sequential([
tf.keras.Input(shape=(96, 96, 3)),
tf.keras.layers.Rescaling(1.0 / 255.0),
tf.keras.layers.Conv2D(8, kernel_size=(3, 3), padding="same", activation="relu"),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(3),
], name="naive_forest_fire_cnn")
model.summary()
print("Flattened features: 96 * 96 * 8 = 73,728")
print("Dense parameters: (73,728 + 1) * 3 = 221,187")
print("Total parameters: 224 convolution parameters + 221,187 = 221,411")
How does the model flow?
- The input expects a batch of 96 by 96 RGB images.
- The rescaling layer converts pixel values from the original range into smaller values for the model.
- The convolution layer learns eight 3 by 3 filters while preserving the spatial dimensions.
- The ReLU activation keeps positive responses from those filters.
- The Flatten layer converts the complete feature map into one long vector.
- The Dense layer converts that vector into three logits.
- Save naive_classifier.py.
Before you run the model, which layer do you think contains most of its parameters?
- Inspect the model summary by running this command in Windows PowerShell:
python naive_classifier.py
What does this run reveal?
The command rebuilds the dataset before creating naive_forest_fire_cnn. The summary reports each output shape and its parameter count.
You will see 221,187 parameters in the Dense layer and 221,411 parameters in total. The convolution layer uses only 224 parameters.
Where did the parameters go?
The eight 96 by 96 feature maps become a vector with 73,728 values. Connecting every value to three outputs requires (73,728 + 1) × 3 = 221,187 Dense parameters.
This parameter distribution leaves almost all of the model's capacity in the final classifier. The summary has exposed the Flatten bottleneck through arithmetic you can verify.
This shortfall is intentional. The model can produce three logits even though its parameter budget is concentrated after spatial feature extraction.
Summary missing or incomplete?
- Confirm that model.summary() appears after the closing model definition.
- Check that the convolution uses padding="same" so its output remains 96 by 96.
- Check that Flatten() appears before Dense(3).
- Help me fix my naive CNN model summary.
Produce one untrained prediction
The model has random weights because it has not been trained. One forward pass still proves that an image can travel through every layer and emerge as three class scores.
- Switch back to naive_classifier.py in Visual Studio Code.
- Add the prediction code below the parameter print statements by pasting:
images, labels = next(iter(train_ds))
logits = model(images[:1], training=False)
predicted_index = int(tf.argmax(logits[0]).numpy())
actual_index = int(labels[0].numpy())
print("Actual class:", CLASS_NAMES[actual_index])
print("Untrained prediction:", CLASS_NAMES[predicted_index])
print("Raw logits:", logits[0].numpy())
How is the prediction produced?
- The dataset iterator returns one batch of images with its integer labels.
- The images[:1] slice selects one image while preserving its batch dimension.
- The model returns three raw logits for the selected image.
- The tf.argmax call finds the index of the largest logit.
- The fixed class list converts each integer index into a readable class name.
- Save naive_classifier.py.
Before you run the complete classifier, do you expect its untrained prediction to match the actual class reliably?
- Verify the complete classifier by running this command in Windows PowerShell:
python naive_classifier.py
What does the final run check?
The command loads the dataset before rebuilding the model. It prints the summary before sending one image through the untrained network.
You should see 221,187 Dense parameters and 221,411 total parameters. You should also see one actual class, one clearly labeled untrained prediction, and three raw logits.
Your predicted class can vary because the weights are random. That result proves the forward pass works without claiming that the classifier is accurate.
Prediction not printing?
- Confirm that the prediction code sits below the model definition.
- Check that CLASS_NAMES contains fire, nofire, and smoke in that order.
- Check that tf.argmax(logits[0]) uses the first image's three logits.
- Help me debug my untrained TensorFlow prediction.
✔️ Awesome, I've got everything!
- Save naive_classifier.py before continuing.
- Keep the activated Windows PowerShell available for the next step.
ⓧ I'd like to double check the full code
Your complete naive_classifier.py file should match this reference.
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TRAIN_DIR = DATA_ROOT / "train"
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
if not TRAIN_DIR.exists():
raise FileNotFoundError(f"Training directory not found: {TRAIN_DIR.resolve()}")
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=True, seed=123,
)
model = tf.keras.Sequential([
tf.keras.Input(shape=(96, 96, 3)),
tf.keras.layers.Rescaling(1.0 / 255.0),
tf.keras.layers.Conv2D(8, kernel_size=(3, 3), padding="same", activation="relu"),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(3),
], name="naive_forest_fire_cnn")
model.summary()
print("Flattened features: 96 * 96 * 8 = 73,728")
print("Dense parameters: (73,728 + 1) * 3 = 221,187")
print("Total parameters: 224 convolution parameters + 221,187 = 221,411")
images, labels = next(iter(train_ds))
logits = model(images[:1], training=False)
predicted_index = int(tf.argmax(logits[0]).numpy())
actual_index = int(labels[0].numpy())
print("Actual class:", CLASS_NAMES[actual_index])
print("Untrained prediction:", CLASS_NAMES[predicted_index])
print("Raw logits:", logits[0].numpy())
Your baseline classifier now turns a forest fire image into three logits. Next, you will replace its parameter-heavy ending with a compact architecture that can train from scratch.
Train the Compact CNN
Your naive classifier produced three logits. Its Flatten layer forced 221,187 parameters into the final Dense layer.
This step replaces that bottleneck with max pooling plus global average pooling. TensorFlow can then train a compact CNN from random initialization with 23,779 parameters.
In this step, get ready to:
- Load capped training and validation datasets.
- Replace the Flatten bottleneck with a compact feature extractor.
- Train the compact CNN for three epochs. Save its model and metrics.
Load capped training and validation data
Training needs separate image streams for learning and validation. The fixed class order keeps the integer labels aligned with fire, nofire, and smoke.
The take(30) and take(10) calls cap the number of batches processed during each epoch. This keeps the training run focused while preserving separate validation data.
- Return to Visual Studio Code.
- In the Explorer sidebar, click the New File icon.
- Enter train_cnn.py beside naive_classifier.py.
You now have an empty train_cnn.py file in the same folder as your earlier scripts.
- Load the capped datasets by pasting this code into train_cnn.py:
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TRAIN_DIR = DATA_ROOT / "train"
VAL_DIR = DATA_ROOT / "val"
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
EPOCHS = 3
for required_dir in (TRAIN_DIR, VAL_DIR):
if not required_dir.exists():
raise FileNotFoundError(f"Dataset directory not found: {required_dir.resolve()}")
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=True, seed=123,
)
val_ds = tf.keras.utils.image_dataset_from_directory(
VAL_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=False,
)
train_subset = train_ds.take(30)
val_subset = val_ds.take(10)
What does this code do?
- The path constants point to the verified train and val dataset splits.
- The explicit CLASS_NAMES list fixes the label order as fire, nofire, and smoke.
- The train_ds dataset shuffles resized RGB images in batches of 32.
- The val_ds dataset keeps validation images in a stable order.
- The capped subsets process up to 30 training batches plus 10 validation batches per epoch.
- Save train_cnn.py.
Before you run this first slice, do you expect TensorFlow to recognize all three classes in both dataset splits?
- Initialize both dataset streams by running this command in the activated PowerShell window:
python train_cnn.py
What should I see?
TensorFlow scans both directories. You will see one report for the training split plus another report for the validation split.
Each report states how many image files were found. Each report also confirms that the files belong to three classes.
Good progress. Both dataset streams now load with the fixed class order.
Dataset not loading?
- Check that PowerShell is still inside the folder containing train_cnn.py.
- Confirm that data/forestfire-classifier-dataset contains the train plus val directories.
- Confirm that both splits contain the three class folders named in CLASS_NAMES.
- Help me troubleshoot the dataset paths in train_cnn.py.
Build the compact feature extractor
Each pooling layer halves the height plus width of its incoming feature maps. GlobalAveragePooling2D compresses every final feature map to one value.
The three convolution layers learn increasingly rich spatial patterns. Their filter counts grow from 16 to 32 to 64.
- Build the compact model by pasting this code at the end of train_cnn.py:
model = tf.keras.Sequential([
tf.keras.Input(shape=(96, 96, 3)),
tf.keras.layers.Rescaling(1.0 / 255.0),
tf.keras.layers.Conv2D(16, (3, 3), padding="same", activation="relu"),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(3),
], name="forest_fire_cnn")
model.summary()
print("Expected parameter total: 23,779")
How do the shapes and parameters change?
- The first convolution has 448 parameters from (3 × 3 × 3 + 1) × 16. Its output shape is 96 × 96 × 16.
- The first max-pooling layer reduces 96 × 96 × 16 to 48 × 48 × 16.
- The second convolution has 4,640 parameters from (3 × 3 × 16 + 1) × 32.
- The second max-pooling layer reduces 48 × 48 × 32 to 24 × 24 × 32.
- The third convolution has 18,496 parameters from (3 × 3 × 32 + 1) × 64.
- Each convolution uses a ReLU activation to preserve positive learned responses.
- Global average pooling reduces 24 × 24 × 64 to a vector of length 64.
- The output layer has 195 parameters from (64 + 1) × 3.
- The complete model has 23,779 trainable parameters.
- Save train_cnn.py.
Before you run this version, do you expect the parameter total to stay close to the naive model or drop sharply?
- Print the compact model summary by running this command:
python train_cnn.py
What should I see?
The model summary shows the three convolution layers plus two pooling layers. It ends with global average pooling plus a three-unit output layer.
The total parameter count is 23,779. The final printed line confirms the same expected total.
That is the bottleneck removed. The whole model now uses about one tenth of the naive classifier's parameters.
Parameter total does not match?
- Remove any Flatten layer copied from naive_classifier.py.
- Check that the convolution filter counts are 16, 32, and 64.
- Check that GlobalAveragePooling2D appears immediately before Dense(3).
- Help me find why my compact CNN parameter count is different.
Compile, train, and save the model
The Adam optimizer updates the model's weights after each training batch. Sparse categorical cross-entropy compares integer class labels with the model's logits.
The from_logits=True setting matches the output layer because Dense(3) has no probability activation. The model begins from random initialization because no pretrained network is loaded.
- Add training plus file saving by pasting this code at the end of train_cnn.py:
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=["accuracy"],
)
history = model.fit(train_subset, validation_data=val_subset, epochs=EPOCHS, verbose=2)
model.save("forest_fire_cnn.keras")
history_lines = ["epoch,loss,accuracy,val_loss,val_accuracy"]
for epoch_index in range(EPOCHS):
history_lines.append(",".join([
str(epoch_index + 1), str(history.history["loss"][epoch_index]),
str(history.history["accuracy"][epoch_index]),
str(history.history["val_loss"][epoch_index]),
str(history.history["val_accuracy"][epoch_index]),
]))
Path("training_history.txt").write_text("\n".join(history_lines), encoding="utf-8")
print("Saved model: forest_fire_cnn.keras")
print("Saved metrics: training_history.txt")
What does this code do?
- The compile configuration uses an Adam learning rate of 0.001.
- The model.fit call trains on train_subset for three epochs. It measures each epoch against val_subset.
- The model.save call stores the trained architecture plus weights in forest_fire_cnn.keras.
- The history loop records epoch loss plus accuracy in training_history.txt.
- Save train_cnn.py.
This is the longest run in the step because TensorFlow updates weights across three epochs. A pause between epoch summaries is normal on the CPU baseline.
Before you run the final script, which loss and accuracy values do you expect to change from one epoch summary to the next?
- Train the compact CNN plus save its artifacts by running this command:
python train_cnn.py
What should I see?
Three epoch summaries appear with training loss plus accuracy. Each summary also includes validation loss plus validation accuracy.
Your exact metric values depend on your images. The final lines include Saved model: forest_fire_cnn.keras plus Saved metrics: training_history.txt.
- Return to Visual Studio Code.
- In the Explorer sidebar, confirm that forest_fire_cnn.keras plus training_history.txt now appear beside train_cnn.py.
Those two files prove that the trained model plus its three-epoch metric record were saved successfully.
Training or saving failed?
- Keep the activated .venv visible at the start of the PowerShell prompt.
- Check that the dataset still contains readable images inside every training plus validation class folder.
- Compare the compile call with the full code below if the loss rejects the integer labels or logits.
- Help me troubleshoot my compact CNN training run.
✔️ Awesome, I've got everything!
Your compact training script now matches the planned architecture. Its trained model plus epoch metrics are saved locally.
ⓧ I'd like to double check the full code
Compare your train_cnn.py file with this complete version.
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TRAIN_DIR = DATA_ROOT / "train"
VAL_DIR = DATA_ROOT / "val"
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
EPOCHS = 3
for required_dir in (TRAIN_DIR, VAL_DIR):
if not required_dir.exists():
raise FileNotFoundError(f"Dataset directory not found: {required_dir.resolve()}")
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=True, seed=123,
)
val_ds = tf.keras.utils.image_dataset_from_directory(
VAL_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=False,
)
train_subset = train_ds.take(30)
val_subset = val_ds.take(10)
model = tf.keras.Sequential([
tf.keras.Input(shape=(96, 96, 3)),
tf.keras.layers.Rescaling(1.0 / 255.0),
tf.keras.layers.Conv2D(16, (3, 3), padding="same", activation="relu"),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
tf.keras.layers.MaxPooling2D((2, 2)),
tf.keras.layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(3),
], name="forest_fire_cnn")
model.summary()
print("Expected parameter total: 23,779")
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=["accuracy"],
)
history = model.fit(train_subset, validation_data=val_subset, epochs=EPOCHS, verbose=2)
model.save("forest_fire_cnn.keras")
history_lines = ["epoch,loss,accuracy,val_loss,val_accuracy"]
for epoch_index in range(EPOCHS):
history_lines.append(",".join([
str(epoch_index + 1), str(history.history["loss"][epoch_index]),
str(history.history["accuracy"][epoch_index]),
str(history.history["val_loss"][epoch_index]),
str(history.history["val_accuracy"][epoch_index]),
]))
Path("training_history.txt").write_text("\n".join(history_lines), encoding="utf-8")
print("Saved model: forest_fire_cnn.keras")
print("Saved metrics: training_history.txt")
That is the hard part complete: your forest_fire_cnn.keras model is trained from random initialization. Next, you will reload it to test predictions plus read its confusion matrix.
Evaluate Predictions and Confusion Matrix
Your compact CNN is trained from random initialization. That is a major milestone because the saved TensorFlow model can now classify images it did not use for training.
Overall accuracy can hide which classes the model confuses. A confusion matrix exposes those mistakes by comparing every actual label with its predicted label.
In this step, get ready to:
- Reload the saved model and measure its test loss and accuracy.
- Generate labeled predictions for six test images.
- Build a three-by-three confusion matrix in the fixed class order.
Load the test split and evaluate the saved model
Evaluation passes unseen test images through the trained model. The resulting loss and accuracy provide a baseline for the class-level analysis that follows.
- Switch back to your forest fire project in Visual Studio Code.
- Create an evaluate_cnn.py file beside train_cnn.py from the Explorer sidebar.
- Add the test loader and evaluation setup by pasting this code into evaluate_cnn.py:
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TEST_DIR = DATA_ROOT / "test"
MODEL_PATH = Path("forest_fire_cnn.keras")
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
if not TEST_DIR.exists():
raise FileNotFoundError(f"Test directory not found: {TEST_DIR.resolve()}")
if not MODEL_PATH.exists():
raise FileNotFoundError(f"Model not found: {MODEL_PATH.resolve()}")
test_ds = tf.keras.utils.image_dataset_from_directory(
TEST_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=False,
)
test_subset = test_ds.take(10)
model = tf.keras.models.load_model(MODEL_PATH)
results = model.evaluate(test_subset, verbose=2, return_dict=True)
print("Test loss:", results["loss"])
print("Test accuracy:", results["accuracy"])
all_labels = []
all_predictions = []
first_images = None
first_labels = None
first_predictions = None
What does this code do?
- The path constants point to the existing test split and the saved forest_fire_cnn.keras model.
- The existence checks stop the evaluation with a useful path if either required artifact is missing.
- The test loader preserves the explicit fire, nofire, and smoke order.
- Setting shuffle=False keeps the test examples in a stable order for reporting.
- The take(10) call limits evaluation to at most ten batches.
- The evaluation dictionary provides the test loss and test accuracy by name.
- Save evaluate_cnn.py by pressing Ctrl+S.
Before you run it, consider whether the saved model can be evaluated without rebuilding its layers.
- Evaluate the saved model by running this command in the activated PowerShell terminal:
python evaluate_cnn.py
What should you see?
You should see the test dataset load before the model evaluates up to ten batches. The output then includes labeled lines for Test loss: and Test accuracy:.
The exact values depend on your images and training run. A successful result proves that forest_fire_cnn.keras reloads with its compiled evaluation settings.
Evaluation stopped before the metrics?
- Confirm that evaluate_cnn.py sits in the same project folder as forest_fire_cnn.keras.
- Confirm that the test images remain under data/forestfire-classifier-dataset/test.
- Confirm that your PowerShell prompt still shows the activated .venv environment.
- Help me diagnose why my saved CNN does not evaluate.
Collect predictions and build the matrix
The model returns one logit for each class. Taking the index of the largest logit converts each output into a predicted class index.
The evaluator must collect every actual index and predicted index from the same batches. TensorFlow can then count each pairing in a three-by-three matrix.
- Return to evaluate_cnn.py.
- Place your cursor below first_predictions = None.
- Add the prediction and reporting logic by pasting this code:
for batch_index, (images, labels) in enumerate(test_subset):
logits = model(images, training=False)
predictions = tf.argmax(logits, axis=1, output_type=tf.int32)
all_labels.append(labels)
all_predictions.append(predictions)
if batch_index == 0:
first_images = images
first_labels = labels
first_predictions = predictions
labels_tensor = tf.concat(all_labels, axis=0)
predictions_tensor = tf.concat(all_predictions, axis=0)
confusion = tf.math.confusion_matrix(
labels_tensor, predictions_tensor, num_classes=len(CLASS_NAMES)
)
print("Class order:", CLASS_NAMES)
print("Rows are actual classes; columns are predicted classes.")
print(confusion.numpy())
print("Sample predictions:")
for index in range(min(6, int(first_images.shape[0]))):
actual = CLASS_NAMES[int(first_labels[index].numpy())]
predicted = CLASS_NAMES[int(first_predictions[index].numpy())]
print(f"{index + 1}. actual={actual}, predicted={predicted}")
How does the report come together?
- The loop sends each test batch through the model with training behavior disabled.
- The tf.argmax call selects the class index with the largest logit for each image.
- The two lists keep the actual labels aligned with the predictions from every processed batch.
- The first batch is retained so the report can print six actual and predicted class names.
- The tf.concat calls combine the batch tensors into full test-subset tensors.
- The tf.math.confusion_matrix call counts each actual and predicted pairing across three classes.
- Save evaluate_cnn.py by pressing Ctrl+S.
Before you run the completed evaluator, consider which pair of classes you expect it to confuse most often.
- Generate the complete evaluation report by running:
python evaluate_cnn.py
What does the completed report show?
You should see the test loss and test accuracy first. You should then see the class order followed by a matrix with three rows and three columns.
The final section contains six numbered lines. Each line shows an actual= class and a predicted= class.
Matrix or predictions missing?
- Check that both all_labels.append(labels) and all_predictions.append(predictions) remain inside the batch loop.
- Check that the three first_ assignments remain inside the first-batch condition.
- Check that each class folder in the test split contains at least one readable image.
- Help me debug my prediction collection or confusion matrix.
✔️ Awesome, I've got everything!
Your saved evaluator now reloads the model and reports its performance on the test split. Make sure evaluate_cnn.py is saved before interpreting the results.
ⓧ I'd like to double check the full code
Compare your evaluate_cnn.py file with this complete reference. Every class index and matrix position depends on the fixed class order shown here.
from pathlib import Path
import tensorflow as tf
DATA_ROOT = Path("data") / "forestfire-classifier-dataset"
TEST_DIR = DATA_ROOT / "test"
MODEL_PATH = Path("forest_fire_cnn.keras")
CLASS_NAMES = ["fire", "nofire", "smoke"]
IMAGE_SIZE = (96, 96)
BATCH_SIZE = 32
if not TEST_DIR.exists():
raise FileNotFoundError(f"Test directory not found: {TEST_DIR.resolve()}")
if not MODEL_PATH.exists():
raise FileNotFoundError(f"Model not found: {MODEL_PATH.resolve()}")
test_ds = tf.keras.utils.image_dataset_from_directory(
TEST_DIR, labels="inferred", label_mode="int", class_names=CLASS_NAMES,
color_mode="rgb", batch_size=BATCH_SIZE, image_size=IMAGE_SIZE,
shuffle=False,
)
test_subset = test_ds.take(10)
model = tf.keras.models.load_model(MODEL_PATH)
results = model.evaluate(test_subset, verbose=2, return_dict=True)
print("Test loss:", results["loss"])
print("Test accuracy:", results["accuracy"])
all_labels = []
all_predictions = []
first_images = None
first_labels = None
first_predictions = None
for batch_index, (images, labels) in enumerate(test_subset):
logits = model(images, training=False)
predictions = tf.argmax(logits, axis=1, output_type=tf.int32)
all_labels.append(labels)
all_predictions.append(predictions)
if batch_index == 0:
first_images = images
first_labels = labels
first_predictions = predictions
labels_tensor = tf.concat(all_labels, axis=0)
predictions_tensor = tf.concat(all_predictions, axis=0)
confusion = tf.math.confusion_matrix(
labels_tensor, predictions_tensor, num_classes=len(CLASS_NAMES)
)
print("Class order:", CLASS_NAMES)
print("Rows are actual classes; columns are predicted classes.")
print(confusion.numpy())
print("Sample predictions:")
for index in range(min(6, int(first_images.shape[0]))):
actual = CLASS_NAMES[int(first_labels[index].numpy())]
predicted = CLASS_NAMES[int(first_predictions[index].numpy())]
print(f"{index + 1}. actual={actual}, predicted={predicted}")
This file loads the test data before evaluating the saved model. It then converts logits into class indices before printing the matrix and six labeled examples.
Read the confusion matrix
Each matrix row represents an actual class. Each column represents the class predicted by the model.
Diagonal cells count correct predictions because the row class matches the column class. Off-diagonal cells reveal the direction of each mistake.
- Use the printed Class order: line to map index 0 to fire.
- Map index 1 to nofire.
- Map index 2 to smoke.
- Read the diagonal cells to find the correct count for each class.
- Read the off-diagonal cells to identify the strongest direction of confusion.
- Compare the six labeled examples with the matrix pattern.
How do you read one mistake?
A count in the smoke row and fire column means that actual smoke images were predicted as fire. Reversing the row and column describes a different mistake.
This direction matters because two classes can have similar accuracy while failing in different ways. The matrix makes that difference visible.
Before this final check, predict whether the largest counts should appear on the diagonal.
- Run the final evaluation check with:
python evaluate_cnn.py
What confirms success?
You should see a test loss and a test accuracy. You should also see six labeled predictions followed by a three-by-three matrix in fire, nofire, and smoke order.
That closes the testing loop. Your saved model now has both an overall score and a class-by-class error report.
Secret mission
Test a Deeper Feature Extractor
Extend the baseline with another pooling stage plus a 128-filter convolution. You will calculate the added capacity before training the deeper model for one comparison epoch.
Clean Up Your Resources
Clean Up Your Resources
Everything you created stays on your computer at a cost of $0. Choose whether to keep the files ready, pause by deactivating the environment, or delete selected resources.
Resources you used:
- A local Python 3.11 virtual environment in .venv containing TensorFlow 2.15.1.
- The dependency file requirements.txt.
- The baseline scripts inspect_convolution.py, naive_classifier.py, train_cnn.py, and evaluate_cnn.py.
- The deeper-model experiment in deeper_cnn_experiment.py.
- The saved model files forest_fire_cnn.keras and forest_fire_cnn_deeper.keras.
- The baseline training metrics in training_history.txt.
Keep everything running
No action needed. Choose this if you plan to compare the two models or continue into the Intel Arc follow-on project.
- Keep .venv ready with the pinned dependencies installed.
- Keep the five Python scripts available for future experiments.
- Keep both .keras models available for comparison.
- Keep training_history.txt as the baseline record for later training runs.
Pause - I'll come back to this later
Deactivate the virtual environment to end this PowerShell session's isolated Python context. Every project file remains ready for your next session.
- Return to the Windows PowerShell session from earlier.
- Deactivate the environment by entering deactivate.
- Close the PowerShell window.
Your scripts, dataset, metrics, and saved models remain unchanged on disk.
Delete - I don't want to use this again
Deleting project work can feel drastic. Your verified dataset remains untouched in data\forestfire-classifier-dataset.
- Return to the Windows PowerShell session from earlier.
- Deactivate the environment by entering deactivate.
- Use your Windows file manager to return to the project folder from earlier.
- Delete the .venv folder.
- Delete forest_fire_cnn.keras.
- Delete forest_fire_cnn_deeper.keras.
- Delete training_history.txt.
- Delete requirements.txt, inspect_convolution.py, naive_classifier.py, train_cnn.py, evaluate_cnn.py, and deeper_cnn_experiment.py only if you want a complete reset.
- Confirm the deleted resources no longer appear in the project folder.
Nice Work!
Nice Work!
Brilliant work! You built a three-class forest fire CNN from random initialization with TensorFlow.
You've learned how to:
- Made convolution arithmetic visible with a 3 by 3 kernel. You matched the top-left weighted sum to 2.0 in the printed feature map.
- Exposed the 221,187-parameter Dense bottleneck caused by Flatten. You replaced it with max pooling plus global average pooling. The compact CNN trained from scratch with 23,779 parameters.
- Saved the trained baseline as forest_fire_cnn.keras. You reloaded it to print test loss and accuracy. You also produced six labeled predictions plus a three-class confusion matrix.
- Secret Mission: Derived the deeper feature extractor's 97,827-parameter total before training it for one comparison epoch. You saved the variant as forest_fire_cnn_deeper.keras without replacing the baseline model.
Ready to quiz yourself?