Train a 10-Class Animal Classifier
Train and test a CNN that classifies ten animal categories from images.
Introduction
30 Second Summary
A photo of a tiger is obvious to you. To a computer, it begins as a grid of coloured pixels with no animal name attached.
In this project, you will train a ten-class animal image classifier in Google Colab. The finished notebook uses a convolutional neural network to rank its top three known-animal predictions for a photo from your Mac.
What You'll Build
You upload a photo from your Mac to see it displayed beside three ranked probabilities for bird, cat, deer, dog, frog, horse, bear, elephant, lion, or tiger.
By the end of this project, you'll have:
- A ten-animal data gallery that shows every category the model knows. Count charts expose the original 10-to-1 class imbalance before confirming balanced training, validation, and test splits.
- Training diagnostics that let you inspect learning curves plus a 10-by-10 confusion matrix. Labeled test predictions reveal which animals the model gets right.
- An upload-driven classifier that displays your photo with three ranked known-class probabilities. Its downloaded animal_classifier.keras file preserves the model for later use.
- Secret Mission: A closed-set classifier stress test using a supported animal, an unsupported animal, plus a non-animal image. Your notes identify which confident result should not be trusted.
Are there any prerequisites?
You only need a Google account, Chrome on your Mac, basic Python knowledge, plus a reliable internet connection. The pinned Colab runtime includes every package used in the notebook.
Before We Start
Before the hands-on work begins, lock in the ten animals your classifier will recognize. This fixed label set gives the model a clear learning goal.
Set Up the Colab Environment
Your classifier depends on a specific Google Colab runtime. Pinning that environment keeps local Mac setup from consuming the project.
In this step, you create the notebook. You also confirm that Python, Keras, and TensorFlow are using the verified package stack.
In this step, get ready to:
- Create a Google Colab notebook named animal_classifier.ipynb.
- Select the Colab 2026.07 runtime.
- Set the TensorFlow backend and verify every required package version.
Create and name the notebook
A Colab notebook stores executable code in cells. Your browser sends those cells to a cloud runtime for execution.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Google Chrome and press Enter to open Google Chrome.
- Go to colab.research.google.com in Chrome.
- Sign in with your Google account if Google asks you to authenticate.
- Click New Notebook on the Colab welcome screen.
- Click the current notebook name in the top-left corner.
- Enter animal_classifier.ipynb and press Enter.
You now have a blank notebook named animal_classifier.ipynb.
Select the pinned runtime
A runtime provides the Python process and installed packages behind your notebook. The 2026.07 runtime gives every later cell the package versions expected by this project.
- Click Runtime in the notebook menu.
- Select Change runtime type.
- Open the Runtime Version dropdown.
- Choose 2026.07.
- Confirm the runtime choice at the bottom of the dialog.
- Click Connect in the top-right corner.
The first connection can take a couple of minutes while Colab allocates the runtime. You will see the RAM and disk indicators when the connection is ready.
Why Pin the Runtime?
Cloud notebook packages change over time. Pinning the runtime keeps the code tied to one tested combination of Python and machine-learning libraries.
The version check below stops immediately if that combination changes. This turns a hidden compatibility problem into a clear setup signal.
Verify the package stack
Keras reads its backend choice during import. Setting KERAS_BACKEND first ensures that every Keras operation uses TensorFlow.
- Click inside the first code cell.
- Paste the import block below into the first code cell:
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
What Do These Imports Prepare?
- The os module sets KERAS_BACKEND before Keras loads.
- The metadata helper reads the installed version of each package.
- TensorFlow Datasets supplies the dataset-loading tools used in the next step.
- NumPy handles image arrays.
- Matplotlib creates the gallery and charts.
- The Colab files helper supports local uploads and downloads later in the project.
- Run the import cell by clicking its triangular run control.
The cell finishes with an execution count beside it. That completed state confirms every import loaded without an exception.
Did an Import Fail?
Confirm that the notebook shows an active runtime connection. An inactive runtime cannot execute the imports.
Check that KERAS_BACKEND is set before import keras.
Ask for help with the failing import: Help me diagnose an import failure in my Colab environment cell.
The next cell compares the installed stack with the versions expected by the classifier. A mismatch raises an error before any dataset work begins.
- Add a new code cell below the import cell using the notebook's code-cell control.
- Paste the version-check code into the new cell:
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
How Does the Check Work?
- EXPECTED_VERSIONS stores the tested version for each required package.
- metadata.version() reads the version installed in the connected runtime.
- The loop prints each installed version before comparing it with the expected value.
- The final line prints the active Keras backend so you can confirm it is tensorflow.
Before you run the check, do you expect every installed version to match the notebook's required stack?
- Run the version-check cell using its triangular run control.
You should see Python 3.12.13. The package lines should show TensorFlow 2.20.0, Keras 3.13.2, TensorFlow Datasets 4.9.10, NumPy 2.0.2, Matplotlib 3.10.0, and Google Colab 1.0.0.
The last line should report tensorflow as the Keras backend.
✔️ I See Every Expected Version
Your pinned runtime is ready. Continue to the shared classifier settings below.
ⓧ I See a Different Version
The notebook is connected to a different package stack. Return it to the pinned runtime before continuing.
- Return to Runtime in the notebook menu.
- Select Change runtime type.
- Choose 2026.07 from the Runtime Version dropdown.
- Confirm the runtime choice at the bottom of the dialog.
- Click Connect in the top-right corner.
- Rerun the import cell.
- Rerun the version-check cell.
You should now see each expected version without a runtime error.
ⓧ The Cell Does Not Finish
The runtime connection needs attention before the check can complete.
- Wait for any connection activity in the top-right corner to finish.
- Click Connect if the notebook shows no active runtime.
- Rerun the import cell after the connection completes.
- Rerun the version-check cell.
You should see the Python line followed by the package-version lines.
Does the Version Check Still Stop?
Confirm that 2026.07 remains selected in the runtime dialog. Reconnecting without changing the runtime can preserve the mismatched stack.
Ask for help with the mismatch: Why does my Colab version check still fail?
The environment now needs the shared labels used by both source datasets. A fixed seed also makes later shuffling repeatable.
- Add a new code cell below the version-check cell using the notebook's code-cell control.
- Paste the shared classifier settings into the new cell:
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
What Do These Settings Control?
- SEED stores 42 so later shuffles can be reproduced.
- CLASS_NAMES defines the shared output order for all ten animals.
- CIFAR10_CLASSES identifies the six labels that come from CIFAR-10.
- CIFAR100_CLASSES identifies the four labels that come from CIFAR-100.
Before the final check, do you expect all three cells to finish cleanly on the pinned runtime?
- Run the import cell using its triangular run control.
- Run the version-check cell using its triangular run control.
- Run the shared-settings cell using its triangular run control.
You should see the full version report again. The final line should show the tensorflow backend.
That locks down the foundation for your classifier. Your notebook now has the verified environment and shared ten-label vocabulary.
Does the Settings Cell Stop?
Check each class name against the code block. A missing quote or comma prevents the list from loading.
Ask for help with the settings cell: Help me find the syntax problem in my classifier settings.
✔️ Awesome, I've Got Everything
Great. Confirm that all three cells have execution counts and that animal_classifier.ipynb shows the expected version output.
ⓧ I'd Like to Double Check the Full Code
Compare your three cells with the complete setup code below.
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
How to Compare the Setup
The backend assignment appears before the Keras import. The version check follows the imports.
The shared seed and class collections come last. This order leaves the notebook ready for dataset loading.
Your cloud environment and label vocabulary are ready. Next, you will load both CIFAR datasets and expose the imbalance hidden inside their combined training examples.
Reveal the Dataset Imbalance
Your pinned Google Colab environment is ready. Now you can bring real image data into the notebook.
Combining every matching image can create class imbalance because source datasets contribute different amounts. This step keeps that first combination untouched so the count chart can reveal what the model would receive.
In this step, get ready to:
- Load the two image datasets with their label metadata.
- Map ten animal categories into one shared label vocabulary.
- Compare the naive class counts in a chart.
Load both CIFAR datasets
The CIFAR-10 and CIFAR-100 datasets store their labels as numbers. Their metadata lets you recover the animal names without hard-coding source label positions.
- Add the dataset-loading code to the next empty cell beneath the class constants:
def load_dataset(name):
splits, info = tfds.load(
name,
split=["train", "test"],
as_supervised=True,
batch_size=-1,
with_info=True,
)
train_data = tfds.as_numpy(splits[0])
test_data = tfds.as_numpy(splits[1])
source_names = info.features["label"].names
return train_data, test_data, source_names
c10_train, c10_test, c10_names = load_dataset("cifar10:3.0.2")
c100_train, c100_test, c100_names = load_dataset("cifar100:3.0.2")
What does this code do?
- The load_dataset() function loads the training split plus the test split from TensorFlow Datasets.
- The batch_size=-1 setting loads each complete split into memory.
- The as_supervised=True setting returns each split as image-label pairs.
- The source_names variable reads the ordered label names from the dataset metadata.
The first dataset load can pause while Colab prepares the files. A busy cell during this stage is expected.
- Run the cell.
When the cell finishes, c10_train, c10_test, c100_train, and c100_test are available in notebook memory. The matching c10_names and c100_names variables contain the source label names.
Does the dataset cell stop early?
- Confirm that your internet connection is stable.
- Confirm that the notebook is still connected to the 2026.07 runtime.
- Rerun the cell after the connection is restored.
Ask for help diagnosing the loading stage: Why did my CIFAR dataset-loading cell stop before both datasets were available?
Map the ten animal labels
Each source dataset has its own numeric label system. The shared position of each animal in CLASS_NAMES becomes the classifier label from 0 through 9.
- Add the class-selection function to a new code cell beneath the dataset-loading cell:
def select_classes(data, source_names, selected_names, start=0, count=None):
images, labels = data
images = np.asarray(images)
labels = np.asarray(labels).reshape(-1)
image_parts = []
label_parts = []
for target_label, class_name in enumerate(CLASS_NAMES):
if class_name not in selected_names:
continue
source_label = source_names.index(class_name)
matching_indices = np.flatnonzero(labels == source_label)
if count is None:
matching_indices = matching_indices[start:]
else:
matching_indices = matching_indices[start : start + count]
image_parts.append(images[matching_indices])
label_parts.append(
np.full(len(matching_indices), target_label, dtype=np.int64)
)
return np.concatenate(image_parts), np.concatenate(label_parts)
How does the label mapping work?
- The source_names.index(class_name) lookup finds each animal inside its original dataset metadata.
- The np.flatnonzero() call collects the image positions carrying that source label.
- The target_label value comes from the animal's position in CLASS_NAMES.
- The final concatenation returns one image array plus one shared-label array.
- Run the cell to define select_classes().
The cell should finish without an exception. The selection helper is now ready to extract matching images by animal name.
Does the selection cell fail?
- Check that every indented line remains inside select_classes().
- Check that CLASS_NAMES matches the constant from the previous step.
- Rerun the dataset-loading cell if the source-name variables are missing.
Ask for help checking the mapping logic: Why is my select_classes function failing to map the CIFAR labels?
The next helper applies that mapping to both datasets. It then combines the selected images under the shared labels.
- Add the source-combination code to the next empty cell:
def combine_sources(c10_data, c100_data, start=0, count=None):
c10_images, c10_labels = select_classes(
c10_data, c10_names, CIFAR10_CLASSES, start=start, count=count
)
c100_images, c100_labels = select_classes(
c100_data, c100_names, CIFAR100_CLASSES, start=start, count=count
)
return (
np.concatenate([c10_images, c100_images]),
np.concatenate([c10_labels, c100_labels]),
)
naive_images, naive_labels = combine_sources(c10_train, c100_train)
naive_counts = np.bincount(naive_labels, minlength=len(CLASS_NAMES))
What does the combination create?
- The combine_sources() function selects the requested animals from each source.
- The naive_images array contains every matching training image.
- The naive_labels array assigns each image its shared label.
- The naive_counts array measures how many images belong to each shared label.
- Run the cell to create the naive combined arrays.
The cell should finish without an exception. The untouched source counts are now stored in naive_counts.
Are the combined arrays missing?
- Run the select_classes() cell before this cell.
- Check that CIFAR10_CLASSES and CIFAR100_CLASSES retain their original names.
- Check that both dataset-loading calls completed successfully.
Ask for help tracing the arrays: Why did combine_sources fail to create naive_images and naive_labels?
Expose the count gap
A gallery checks that every shared label points to the intended animal. This gives you a visual check before you compare the class totals.
- Add the ten-image gallery code to the next empty cell:
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for class_id, axis in enumerate(axes.flat):
example_index = np.flatnonzero(naive_labels == class_id)[0]
axis.imshow(naive_images[example_index], interpolation="nearest")
axis.set_title(CLASS_NAMES[class_id])
axis.axis("off")
fig.suptitle("One training image from each animal class")
fig.tight_layout()
plt.show()
What does the gallery check?
- The loop visits each shared class label once.
- The example_index value selects the first matching image for that label.
- Each subplot uses the matching entry from CLASS_NAMES as its title.
- Run the gallery cell.
You should see two rows containing one image each for bird, cat, deer, dog, frog, horse, bear, elephant, lion, and tiger.
Is an animal missing from the gallery?
- Confirm that CLASS_NAMES contains all ten animal names in the original order.
- Rerun the combination cell to recreate naive_images and naive_labels.
- Rerun the gallery cell after the arrays are available.
Ask for help checking the gallery: Why does my animal gallery have a missing or incorrect class?
The gallery proves that all ten labels are present. The count chart now tests whether those labels receive equal representation.
- Add the naive-count chart code to the next empty cell:
fig, axis = plt.subplots(figsize=(11, 4))
axis.bar(CLASS_NAMES, naive_counts)
axis.set_title("Naive combined training counts")
axis.set_ylabel("Images")
axis.tick_params(axis="x", rotation=45)
fig.tight_layout()
plt.show()
print(dict(zip(CLASS_NAMES, naive_counts.tolist())))
How does the chart reveal imbalance?
- Each bar uses one animal name from CLASS_NAMES.
- Each bar height comes from the matching value in naive_counts.
- The final printout gives you the exact count behind every bar.
Before you run the count cell, ask yourself whether all ten bars will reach the same height.
- Run the count-chart cell.
You should see 5,000 images each for bird, cat, deer, dog, frog, and horse. You should see 500 images each for bear, elephant, lion, and tiger.
That ten-to-one gap is the intended shortfall. The naive dataset gives the first six labels ten times as many training examples as the final four.
Do the counts look different?
- Confirm that the naive call uses combine_sources(c10_train, c100_train) with its default values.
- Confirm that CIFAR10_CLASSES contains six animal names.
- Confirm that CIFAR100_CLASSES contains four animal names.
Ask for help comparing the totals: Why does my naive class-count chart differ from the expected CIFAR counts?
You caught the hidden bias before it reached training. Your notebook now shows exactly how the two source datasets produce unequal practice across the ten animals.
✔️ Awesome, I've got everything!
Great. Confirm that the gallery shows all ten animals and the count chart shows the ten-to-one gap.
ⓧ I'd like to double check the full code
Compare your notebook with the complete code through this point:
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
def load_dataset(name):
splits, info = tfds.load(
name,
split=["train", "test"],
as_supervised=True,
batch_size=-1,
with_info=True,
)
train_data = tfds.as_numpy(splits[0])
test_data = tfds.as_numpy(splits[1])
source_names = info.features["label"].names
return train_data, test_data, source_names
c10_train, c10_test, c10_names = load_dataset("cifar10:3.0.2")
c100_train, c100_test, c100_names = load_dataset("cifar100:3.0.2")
def select_classes(data, source_names, selected_names, start=0, count=None):
images, labels = data
images = np.asarray(images)
labels = np.asarray(labels).reshape(-1)
image_parts = []
label_parts = []
for target_label, class_name in enumerate(CLASS_NAMES):
if class_name not in selected_names:
continue
source_label = source_names.index(class_name)
matching_indices = np.flatnonzero(labels == source_label)
if count is None:
matching_indices = matching_indices[start:]
else:
matching_indices = matching_indices[start : start + count]
image_parts.append(images[matching_indices])
label_parts.append(
np.full(len(matching_indices), target_label, dtype=np.int64)
)
return np.concatenate(image_parts), np.concatenate(label_parts)
def combine_sources(c10_data, c100_data, start=0, count=None):
c10_images, c10_labels = select_classes(
c10_data, c10_names, CIFAR10_CLASSES, start=start, count=count
)
c100_images, c100_labels = select_classes(
c100_data, c100_names, CIFAR100_CLASSES, start=start, count=count
)
return (
np.concatenate([c10_images, c100_images]),
np.concatenate([c10_labels, c100_labels]),
)
naive_images, naive_labels = combine_sources(c10_train, c100_train)
naive_counts = np.bincount(naive_labels, minlength=len(CLASS_NAMES))
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for class_id, axis in enumerate(axes.flat):
example_index = np.flatnonzero(naive_labels == class_id)[0]
axis.imshow(naive_images[example_index], interpolation="nearest")
axis.set_title(CLASS_NAMES[class_id])
axis.axis("off")
fig.suptitle("One training image from each animal class")
fig.tight_layout()
plt.show()
fig, axis = plt.subplots(figsize=(11, 4))
axis.bar(CLASS_NAMES, naive_counts)
axis.set_title("Naive combined training counts")
axis.set_ylabel("Images")
axis.tick_params(axis="x", rotation=45)
fig.tight_layout()
plt.show()
print(dict(zip(CLASS_NAMES, naive_counts.tolist())))
The imbalance is now visible instead of hidden inside the arrays. Next, you will build training, validation, and test splits that give every animal equal representation.
Build Balanced Animal Splits
Your gallery proved that the shared label mapping works. It also exposed a ten-to-one class imbalance between the two source datasets.
That imbalance would let source dataset size steer the model. This step gives every animal equal representation in the training, validation, and test data.
In this step, get ready to:
- Reserve equal numbers of images for every animal class.
- Shuffle each split reproducibly while keeping its images paired with the correct labels.
- Confirm the split shapes and class counts with printed summaries and a chart.
Create a reproducible shuffle helper
The selected images arrive grouped by class. A seeded NumPy shuffle mixes their order while preserving the connection between each image and its label.
- Add a code cell beneath the cell that displayed the naive count chart.
- Paste this helper into the new cell:
def shuffle_pair(images, labels, seed):
generator = np.random.default_rng(seed)
order = generator.permutation(len(labels))
return images[order], labels[order]
What does this helper do?
- The `seed` value makes the shuffled order reproducible across matching runs.
- The `order` array stores one seeded arrangement of every position in the split.
- The return line applies the same order to `images` and `labels`, so every image keeps its correct label.
- Run the cell to register `shuffle_pair()` in the connected runtime.
The cell's execution indicator should complete without an error. That confirms the helper is available for the next cell.
Does the helper cell fail?
Check that each indented line sits inside `shuffle_pair()`. Confirm that `np` is still available from the verified imports.
Ask for help with the shuffle helper.
Assemble and verify the balanced splits
Each class contributes its first `400` original training images to `x_train` and `y_train`.
The next `100` original training images per class become the validation split.
A separate `100` original test images per class become the untouched test split.
- Add another code cell beneath the shuffle helper.
- Paste the split-building code into the new cell:
x_train, y_train = combine_sources(c10_train, c100_train, start=0, count=400)
x_val, y_val = combine_sources(c10_train, c100_train, start=400, count=100)
x_test, y_test = combine_sources(c10_test, c100_test, start=0, count=100)
x_train, y_train = shuffle_pair(x_train, y_train, SEED)
x_val, y_val = shuffle_pair(x_val, y_val, SEED + 1)
x_test, y_test = shuffle_pair(x_test, y_test, SEED + 2)
print("Training shape:", x_train.shape)
print("Validation shape:", x_val.shape)
print("Test shape:", x_test.shape)
print("Training counts:", np.bincount(y_train, minlength=10).tolist())
print("Validation counts:", np.bincount(y_val, minlength=10).tolist())
print("Test counts:", np.bincount(y_test, minlength=10).tolist())
How are the splits built?
- The three `combine_sources()` calls select the same number of examples from both CIFAR sources.
- The `start` values keep the training and validation selections separate.
- The three `shuffle_pair()` calls use related fixed seeds to produce reproducible split orders.
- The shape lines report the number and dimensions of the image arrays.
- The count lines measure how many labels belong to each of the ten classes.
Before you run this cell, do you think any class will still have more examples than another?
- Run the cell to create the three balanced splits and print their summaries.
What should you see?
- The training shape should be `(4000, 32, 32, 3)`.
- The validation shape should be `(1000, 32, 32, 3)`.
- The test shape should be `(1000, 32, 32, 3)`.
- The training count line should contain ten values of `400`.
- The validation count line should contain ten values of `100`.
- The test count line should contain ten values of `100`.
That is the balancing fix working. Every animal now contributes equally within each split.
Do the shapes or counts differ?
Check the `start` and `count` values against the cell above. Confirm that the test split uses `c10_test` and `c100_test`.
Ask for help with the split summary.
Plot the balanced class counts
Printed totals confirm the exact values. A grouped chart makes the full train-validation-test split visible across all ten animals.
- Add a code cell beneath the split-summary cell.
- Paste the balanced count chart into the new cell:
fig, axis = plt.subplots(figsize=(11, 4))
positions = np.arange(len(CLASS_NAMES))
axis.bar(positions - 0.25, np.bincount(y_train, minlength=10), width=0.25, label="train")
axis.bar(positions, np.bincount(y_val, minlength=10), width=0.25, label="validation")
axis.bar(positions + 0.25, np.bincount(y_test, minlength=10), width=0.25, label="test")
axis.set_xticks(positions, CLASS_NAMES, rotation=45)
axis.set_ylabel("Images")
axis.set_title("Balanced class counts")
axis.legend()
fig.tight_layout()
plt.show()
What does this chart show?
- The `positions` array gives every animal one location along the horizontal axis.
- The three bar calls offset the training, validation, and test counts so you can compare them side by side.
- The legend identifies which bars belong to each split.
- The final layout call keeps the rotated animal names readable.
Before you run this cell, what pattern do you expect the three bar series to form?
- Run the cell to display the balanced class-count chart.
You'll see every training bar reach `400`.
You'll see every validation bar reach `100`. Every test bar also reaches `100`.
The flat profiles prove that source dataset size no longer controls class representation.
Is the chart missing or uneven?
Run the split-building cell again before running the chart cell. Check that the chart uses `y_train`, `y_val`, and `y_test` from the balanced splits.
Ask for help with the balanced chart.
✔️ Awesome, I've got everything!
Great. Your notebook now contains reproducibly shuffled splits with equal representation for all ten animals.
ⓧ I'd like to double check the full code
Compare your notebook with the complete code through the balanced class-count chart.
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
def load_dataset(name):
splits, info = tfds.load(
name,
split=["train", "test"],
as_supervised=True,
batch_size=-1,
with_info=True,
)
train_data = tfds.as_numpy(splits[0])
test_data = tfds.as_numpy(splits[1])
source_names = info.features["label"].names
return train_data, test_data, source_names
c10_train, c10_test, c10_names = load_dataset("cifar10:3.0.2")
c100_train, c100_test, c100_names = load_dataset("cifar100:3.0.2")
def select_classes(data, source_names, selected_names, start=0, count=None):
images, labels = data
images = np.asarray(images)
labels = np.asarray(labels).reshape(-1)
image_parts = []
label_parts = []
for target_label, class_name in enumerate(CLASS_NAMES):
if class_name not in selected_names:
continue
source_label = source_names.index(class_name)
matching_indices = np.flatnonzero(labels == source_label)
if count is None:
matching_indices = matching_indices[start:]
else:
matching_indices = matching_indices[start : start + count]
image_parts.append(images[matching_indices])
label_parts.append(
np.full(len(matching_indices), target_label, dtype=np.int64)
)
return np.concatenate(image_parts), np.concatenate(label_parts)
def combine_sources(c10_data, c100_data, start=0, count=None):
c10_images, c10_labels = select_classes(
c10_data, c10_names, CIFAR10_CLASSES, start=start, count=count
)
c100_images, c100_labels = select_classes(
c100_data, c100_names, CIFAR100_CLASSES, start=start, count=count
)
return (
np.concatenate([c10_images, c100_images]),
np.concatenate([c10_labels, c100_labels]),
)
naive_images, naive_labels = combine_sources(c10_train, c100_train)
naive_counts = np.bincount(naive_labels, minlength=len(CLASS_NAMES))
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for class_id, axis in enumerate(axes.flat):
example_index = np.flatnonzero(naive_labels == class_id)[0]
axis.imshow(naive_images[example_index], interpolation="nearest")
axis.set_title(CLASS_NAMES[class_id])
axis.axis("off")
fig.suptitle("One training image from each animal class")
fig.tight_layout()
plt.show()
fig, axis = plt.subplots(figsize=(11, 4))
axis.bar(CLASS_NAMES, naive_counts)
axis.set_title("Naive combined training counts")
axis.set_ylabel("Images")
axis.tick_params(axis="x", rotation=45)
fig.tight_layout()
plt.show()
print(dict(zip(CLASS_NAMES, naive_counts.tolist())))
def shuffle_pair(images, labels, seed):
generator = np.random.default_rng(seed)
order = generator.permutation(len(labels))
return images[order], labels[order]
x_train, y_train = combine_sources(c10_train, c100_train, start=0, count=400)
x_val, y_val = combine_sources(c10_train, c100_train, start=400, count=100)
x_test, y_test = combine_sources(c10_test, c100_test, start=0, count=100)
x_train, y_train = shuffle_pair(x_train, y_train, SEED)
x_val, y_val = shuffle_pair(x_val, y_val, SEED + 1)
x_test, y_test = shuffle_pair(x_test, y_test, SEED + 2)
print("Training shape:", x_train.shape)
print("Validation shape:", x_val.shape)
print("Test shape:", x_test.shape)
print("Training counts:", np.bincount(y_train, minlength=10).tolist())
print("Validation counts:", np.bincount(y_val, minlength=10).tolist())
print("Test counts:", np.bincount(y_test, minlength=10).tolist())
fig, axis = plt.subplots(figsize=(11, 4))
positions = np.arange(len(CLASS_NAMES))
axis.bar(positions - 0.25, np.bincount(y_train, minlength=10), width=0.25, label="train")
axis.bar(positions, np.bincount(y_val, minlength=10), width=0.25, label="validation")
axis.bar(positions + 0.25, np.bincount(y_test, minlength=10), width=0.25, label="test")
axis.set_xticks(positions, CLASS_NAMES, rotation=45)
axis.set_ylabel("Images")
axis.set_title("Balanced class counts")
axis.legend()
fig.tight_layout()
plt.show()
Your dataset now gives every animal an equal chance to influence learning. Next, you'll train the convolutional neural network and diagnose how well it performs.
Train and Diagnose the CNN
Your balanced splits now give every animal the same amount of training practice. Those labeled pixels still need a model that can learn visual features.
In this step, you will build a convolutional neural network with Keras. The network will learn patterns such as edges, textures, and shapes.
Seeded data augmentation gives the model varied views of its training images. Dropout reduces its dependence on individual examples.
You will then compare training behavior with held-out results. Learning curves, a confusion matrix, and labeled predictions reveal where the classifier succeeds or struggles.
In this step, get ready to:
- Build and compile a ten-class convolutional model.
- Train the network for 12 epochs.
- Evaluate and diagnose predictions on the untouched test split.
Build the convolutional model
Each convolutional layer searches for increasingly complex visual features. Pooling compresses those features so the later dense layers can turn them into ten class probabilities.
- Add a new code cell directly below the balanced class-count cell.
- Paste the model definition into the new cell:
model = keras.Sequential(
[
keras.Input(shape=(32, 32, 3)),
keras.layers.Rescaling(1.0 / 255.0),
keras.layers.RandomFlip(mode="horizontal", seed=SEED),
keras.layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(128, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Flatten(),
keras.layers.Dropout(0.3, seed=SEED),
keras.layers.Dense(128, activation="relu"),
keras.layers.Dropout(0.3, seed=SEED + 1),
keras.layers.Dense(len(CLASS_NAMES), activation="softmax"),
],
name="animal_classifier",
)
What does this model do?
- The input layer accepts images with a height of 32 pixels, a width of 32 pixels, and three color channels.
- Rescaling converts pixel values from their original range to values between zero and one.
- Random horizontal flips create varied training examples while preserving recognizable animal features.
- Three convolution-and-pooling blocks learn visual patterns at increasing levels of complexity.
- The final softmax layer returns one probability for each name in CLASS_NAMES.
- Run the model-definition cell.
The cell should finish without an exception. That is the architecture locked in: the model variable now holds the full layer stack.
Model cell raising an exception?
Confirm that the earlier environment cells still define keras, SEED, and CLASS_NAMES. A disconnected runtime removes those values from memory.
Compare every parenthesis in the model definition with the code above.
Help me diagnose the model-definition exception.
Compilation connects the network to the Adam optimizer. It also defines how incorrect predictions are measured and how accuracy is reported.
- Add a new code cell beneath the model-definition cell.
- Paste the compilation settings into the new cell:
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy(name="accuracy")],
)
model.summary()
How is the model prepared for training?
- Adam adjusts the model weights with a learning rate of 0.001.
- Sparse categorical cross-entropy measures the error between each integer label and the model's class probabilities.
- Sparse categorical accuracy checks whether the highest-scoring class matches the integer label.
- model.summary() prints the architecture so you can confirm every layer is connected.
- Run the compilation cell.
You should see a model summary listing the layers and their output shapes. The summary should end with the model's parameter totals.
Model summary missing?
Run the model-definition cell before the compilation cell. The compilation cell depends on the model variable created there.
Check that model.summary() sits outside the closing parenthesis for model.compile().
Help me restore the model summary.
Train and evaluate the network
Training repeatedly compares the model's predictions with y_train. The explicit validation data measures progress on images that never update the model's weights.
- Add a new code cell beneath the model summary.
- Paste the training loop into the new cell:
history = model.fit(
x_train,
y_train,
validation_data=(x_val, y_val),
epochs=12,
batch_size=64,
verbose=2,
)
What controls this training run?
- x_train supplies the balanced training images.
- y_train supplies the shared integer labels.
- validation_data measures performance on the separate validation split after each epoch.
- epochs=12 gives the network twelve complete passes over the training split.
- batch_size=64 updates the model after each group of 64 images.
Training is the slowest part of this step, so a busy cell for a while is expected. Before you run it, do you think training accuracy and validation accuracy will move in exactly the same way?
- Run the training cell.
You should see one progress line for each of the 12 epochs. Each line reports training loss, training accuracy, validation loss, and validation accuracy.
Training stopped early?
Confirm that the balanced arrays from the previous step are still available. The training cell requires x_train, y_train, x_val, and y_val.
If the runtime disconnected, run the earlier notebook cells in order before starting training again.
Help me diagnose the interrupted training run.
Learning curves show how the model changes across epochs. The untouched test split then measures performance on images excluded from both training and validation.
- Add another code cell beneath the training output.
- Paste the learning-curve and test-evaluation code into the new cell:
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(history.history["accuracy"], label="train")
axes[0].plot(history.history["val_accuracy"], label="validation")
axes[0].set_title("Accuracy")
axes[0].set_xlabel("Epoch")
axes[0].legend()
axes[1].plot(history.history["loss"], label="train")
axes[1].plot(history.history["val_loss"], label="validation")
axes[1].set_title("Loss")
axes[1].set_xlabel("Epoch")
axes[1].legend()
fig.tight_layout()
plt.show()
test_results = model.evaluate(x_test, y_test, verbose=0, return_dict=True)
print("Test results:", test_results)
How do these checks work?
- The accuracy chart compares correct classifications across the training and validation splits.
- The loss chart compares prediction error across those same splits.
- model.evaluate() calculates the final metrics from x_test and y_test.
- return_dict=True stores the named test metrics in test_results.
Before you run this cell, do you expect the validation curves to follow the training curves closely or separate from them?
- Run the learning-curve and evaluation cell.
You should see separate Accuracy and Loss charts. Beneath the charts, Test results: should report the held-out loss and accuracy.
That is the full training loop complete. Your classifier has now learned from balanced examples and faced a separate test split.
Learning curves not appearing?
Run the training cell first so history contains the epoch metrics. Confirm that the earlier import cell still defines plt.
Help me restore the learning curves.
Diagnose predictions on the test set
A single accuracy score hides which labels the network mixes up. The confusion matrix counts every actual class against every predicted class.
- Add a new code cell beneath the test results.
- Paste the confusion-matrix code into the new cell:
probabilities = model.predict(x_test, batch_size=64, verbose=0)
predicted_labels = np.argmax(probabilities, axis=1)
confusion_matrix = np.zeros((len(CLASS_NAMES), len(CLASS_NAMES)), dtype=np.int64)
for actual, predicted in zip(y_test, predicted_labels):
confusion_matrix[int(actual), int(predicted)] += 1
fig, axis = plt.subplots(figsize=(8, 7))
matrix_image = axis.imshow(confusion_matrix, cmap="Blues")
axis.set_xticks(range(len(CLASS_NAMES)), CLASS_NAMES, rotation=45, ha="right")
axis.set_yticks(range(len(CLASS_NAMES)), CLASS_NAMES)
axis.set_xlabel("Predicted label")
axis.set_ylabel("Actual label")
axis.set_title("Test confusion matrix")
fig.colorbar(matrix_image, ax=axis)
fig.tight_layout()
plt.show()
How is the matrix assembled?
- model.predict() returns ten known-class probabilities for every test image.
- np.argmax() selects the highest-scoring class for each image.
- confusion_matrix starts as a ten-by-ten grid of zeros.
- The loop increments the cell matching each actual label and predicted label.
- The image plot turns larger counts into darker blue cells.
Before you run this cell, where do you expect correct predictions to appear in the matrix?
- Run the confusion-matrix cell.
You should see a ten-by-ten grid with actual labels on the vertical axis and predicted labels on the horizontal axis. Correct predictions sit on the main diagonal.
Confusion matrix looks empty?
Run the training and evaluation cells before this cell. The prediction step requires a trained model plus the untouched x_test array.
Check that the loop body remains indented beneath the for line.
Help me fix the confusion matrix.
Individual examples connect those counts back to visible images. Each title compares the model's prediction with the known test label.
- Add one final code cell beneath the confusion matrix.
- Paste the labeled-prediction grid into the new cell:
fig, axes = plt.subplots(2, 5, figsize=(13, 6))
for index, axis in enumerate(axes.flat):
actual_name = CLASS_NAMES[int(y_test[index])]
predicted_name = CLASS_NAMES[int(predicted_labels[index])]
confidence = float(probabilities[index, predicted_labels[index]])
axis.imshow(x_test[index], interpolation="nearest")
axis.set_title(
f"Pred: {predicted_name}\nActual: {actual_name}\n{confidence:.1%}"
)
axis.axis("off")
fig.tight_layout()
plt.show()
What does each example reveal?
- actual_name reads the known label stored in y_test.
- predicted_name reads the model's highest-scoring class.
- confidence stores the probability assigned to that selected class.
- The two-row grid displays ten test images with predicted labels, actual labels, and confidence percentages.
Before you run this final check, do you expect every predicted label to match its actual label?
- Run the labeled-prediction cell.
You should see ten test images arranged in two rows. Each image should show a predicted label, an actual label, and a confidence percentage.
Prediction grid not rendering?
Run the confusion-matrix cell first so probabilities and predicted_labels exist. Confirm that x_test and y_test still contain the balanced test split.
Check that every line inside the loop remains indented consistently.
Help me fix the labeled-prediction grid.
✔️ Awesome, I've got everything!
Your notebook now contains the compiled model, training history, held-out test metrics, learning curves, confusion matrix, and labeled test predictions.
ⓧ I'd like to double check the full code
Compare your notebook cells with the cumulative code through this step:
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
def load_dataset(name):
splits, info = tfds.load(
name,
split=["train", "test"],
as_supervised=True,
batch_size=-1,
with_info=True,
)
train_data = tfds.as_numpy(splits[0])
test_data = tfds.as_numpy(splits[1])
source_names = info.features["label"].names
return train_data, test_data, source_names
c10_train, c10_test, c10_names = load_dataset("cifar10:3.0.2")
c100_train, c100_test, c100_names = load_dataset("cifar100:3.0.2")
def select_classes(data, source_names, selected_names, start=0, count=None):
images, labels = data
images = np.asarray(images)
labels = np.asarray(labels).reshape(-1)
image_parts = []
label_parts = []
for target_label, class_name in enumerate(CLASS_NAMES):
if class_name not in selected_names:
continue
source_label = source_names.index(class_name)
matching_indices = np.flatnonzero(labels == source_label)
if count is None:
matching_indices = matching_indices[start:]
else:
matching_indices = matching_indices[start : start + count]
image_parts.append(images[matching_indices])
label_parts.append(
np.full(len(matching_indices), target_label, dtype=np.int64)
)
return np.concatenate(image_parts), np.concatenate(label_parts)
def combine_sources(c10_data, c100_data, start=0, count=None):
c10_images, c10_labels = select_classes(
c10_data, c10_names, CIFAR10_CLASSES, start=start, count=count
)
c100_images, c100_labels = select_classes(
c100_data, c100_names, CIFAR100_CLASSES, start=start, count=count
)
return (
np.concatenate([c10_images, c100_images]),
np.concatenate([c10_labels, c100_labels]),
)
naive_images, naive_labels = combine_sources(c10_train, c100_train)
naive_counts = np.bincount(naive_labels, minlength=len(CLASS_NAMES))
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for class_id, axis in enumerate(axes.flat):
example_index = np.flatnonzero(naive_labels == class_id)[0]
axis.imshow(naive_images[example_index], interpolation="nearest")
axis.set_title(CLASS_NAMES[class_id])
axis.axis("off")
fig.suptitle("One training image from each animal class")
fig.tight_layout()
plt.show()
fig, axis = plt.subplots(figsize=(11, 4))
axis.bar(CLASS_NAMES, naive_counts)
axis.set_title("Naive combined training counts")
axis.set_ylabel("Images")
axis.tick_params(axis="x", rotation=45)
fig.tight_layout()
plt.show()
print(dict(zip(CLASS_NAMES, naive_counts.tolist())))
def shuffle_pair(images, labels, seed):
generator = np.random.default_rng(seed)
order = generator.permutation(len(labels))
return images[order], labels[order]
x_train, y_train = combine_sources(c10_train, c100_train, start=0, count=400)
x_val, y_val = combine_sources(c10_train, c100_train, start=400, count=100)
x_test, y_test = combine_sources(c10_test, c100_test, start=0, count=100)
x_train, y_train = shuffle_pair(x_train, y_train, SEED)
x_val, y_val = shuffle_pair(x_val, y_val, SEED + 1)
x_test, y_test = shuffle_pair(x_test, y_test, SEED + 2)
print("Training shape:", x_train.shape)
print("Validation shape:", x_val.shape)
print("Test shape:", x_test.shape)
print("Training counts:", np.bincount(y_train, minlength=10).tolist())
print("Validation counts:", np.bincount(y_val, minlength=10).tolist())
print("Test counts:", np.bincount(y_test, minlength=10).tolist())
fig, axis = plt.subplots(figsize=(11, 4))
positions = np.arange(len(CLASS_NAMES))
axis.bar(positions - 0.25, np.bincount(y_train, minlength=10), width=0.25, label="train")
axis.bar(positions, np.bincount(y_val, minlength=10), width=0.25, label="validation")
axis.bar(positions + 0.25, np.bincount(y_test, minlength=10), width=0.25, label="test")
axis.set_xticks(positions, CLASS_NAMES, rotation=45)
axis.set_ylabel("Images")
axis.set_title("Balanced class counts")
axis.legend()
fig.tight_layout()
plt.show()
model = keras.Sequential(
[
keras.Input(shape=(32, 32, 3)),
keras.layers.Rescaling(1.0 / 255.0),
keras.layers.RandomFlip(mode="horizontal", seed=SEED),
keras.layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(128, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Flatten(),
keras.layers.Dropout(0.3, seed=SEED),
keras.layers.Dense(128, activation="relu"),
keras.layers.Dropout(0.3, seed=SEED + 1),
keras.layers.Dense(len(CLASS_NAMES), activation="softmax"),
],
name="animal_classifier",
)
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy(name="accuracy")],
)
model.summary()
history = model.fit(
x_train,
y_train,
validation_data=(x_val, y_val),
epochs=12,
batch_size=64,
verbose=2,
)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(history.history["accuracy"], label="train")
axes[0].plot(history.history["val_accuracy"], label="validation")
axes[0].set_title("Accuracy")
axes[0].set_xlabel("Epoch")
axes[0].legend()
axes[1].plot(history.history["loss"], label="train")
axes[1].plot(history.history["val_loss"], label="validation")
axes[1].set_title("Loss")
axes[1].set_xlabel("Epoch")
axes[1].legend()
fig.tight_layout()
plt.show()
test_results = model.evaluate(x_test, y_test, verbose=0, return_dict=True)
print("Test results:", test_results)
probabilities = model.predict(x_test, batch_size=64, verbose=0)
predicted_labels = np.argmax(probabilities, axis=1)
confusion_matrix = np.zeros((len(CLASS_NAMES), len(CLASS_NAMES)), dtype=np.int64)
for actual, predicted in zip(y_test, predicted_labels):
confusion_matrix[int(actual), int(predicted)] += 1
fig, axis = plt.subplots(figsize=(8, 7))
matrix_image = axis.imshow(confusion_matrix, cmap="Blues")
axis.set_xticks(range(len(CLASS_NAMES)), CLASS_NAMES, rotation=45, ha="right")
axis.set_yticks(range(len(CLASS_NAMES)), CLASS_NAMES)
axis.set_xlabel("Predicted label")
axis.set_ylabel("Actual label")
axis.set_title("Test confusion matrix")
fig.colorbar(matrix_image, ax=axis)
fig.tight_layout()
plt.show()
fig, axes = plt.subplots(2, 5, figsize=(13, 6))
for index, axis in enumerate(axes.flat):
actual_name = CLASS_NAMES[int(y_test[index])]
predicted_name = CLASS_NAMES[int(predicted_labels[index])]
confidence = float(probabilities[index, predicted_labels[index]])
axis.imshow(x_test[index], interpolation="nearest")
axis.set_title(
f"Pred: {predicted_name}\nActual: {actual_name}\n{confidence:.1%}"
)
axis.axis("off")
fig.tight_layout()
plt.show()
Your classifier is trained and its weaknesses are visible. Next up, you will upload your own animal photo and preserve the model beyond the temporary Colab runtime.
Upload a Photo and Save the Model
Your balanced data has produced a trained Keras model with test results and visual diagnostics. You can now turn those results into a demo that accepts one of your own photos.
Test metrics disappear when a temporary Google Colab runtime ends. This step adds an upload workflow and preserves the complete model on your Mac.
In this step, get ready to:
- Create an upload workflow that displays an image with its top three predictions.
- Classify a clear photo of one supported animal.
- Preserve the trained model on your Mac.
Create the upload workflow
The upload workflow converts a local photo into the same input shape the model learned from. It also turns the model's ten softmax scores into a ranked result you can read.
- Add a code cell beneath your sample-prediction cell.
- Create the image-loading part of classify_uploaded_image() by pasting this code into the new cell:
def classify_uploaded_image(trained_model):
uploaded = files.upload()
if not uploaded:
print("No image was uploaded.")
return None
image_path = next(iter(uploaded))
display_image = keras.utils.load_img(image_path, color_mode="rgb")
model_image = keras.utils.load_img(
image_path,
color_mode="rgb",
target_size=(32, 32),
interpolation="bilinear",
)
image_array = keras.utils.img_to_array(model_image)
image_batch = np.expand_dims(image_array, axis=0)
scores = np.asarray(trained_model.predict(image_batch, verbose=0)[0])
top_three = np.argsort(scores)[-3:][::-1]
fig, axis = plt.subplots(figsize=(6, 5))
axis.imshow(display_image)
axis.axis("off")
axis.set_title(
f"Top prediction: {CLASS_NAMES[int(top_three[0])]} "
f"({float(scores[top_three[0]]):.1%})"
)
plt.show()
What does this code do?
- The upload widget transfers one local file into the current Colab runtime.
- The first loaded image keeps its original dimensions for display.
- The second loaded image becomes a 32-by-32 RGB input for the model.
- The model produces ten scores before np.argsort selects the three highest positions.
- Matplotlib displays the original image with the highest-ranked label in its title.
- Run the function-definition cell.
The cell should finish without an exception. The completed execution indicator confirms that Colab has stored the function definition.
Does the function cell stop with an error?
Check that every line beneath classify_uploaded_image() keeps the same indentation shown above. Also confirm that the closing parentheses below axis.set_title() are present.
Ask for help with the function structure.
The image title gives you the highest-ranked class. The function also needs to print all three leading scores and return them for later analysis.
- Place your cursor on a new indented line directly beneath plt.show() in the same function cell.
- Complete the reporting part of the function by pasting this code:
print("Top three known-class predictions:")
for class_id in top_three:
print(f"{CLASS_NAMES[int(class_id)]}: {float(scores[class_id]):.1%}")
print(
"Closed-set warning: this model always ranks its ten known labels, "
"even when the image contains something else."
)
return image_path, scores
How are the predictions reported?
- The loop converts each selected score position into its matching name from CLASS_NAMES.
- Each score is formatted as a percentage so the ranking is easy to compare.
- The warning describes the boundary of a closed-set classifier. Every uploaded image receives one of the ten known labels.
- The return value preserves the uploaded path and all ten scores for further checks.
- Run the completed function-definition cell again.
The cell should finish without an exception. Your upload workflow is now ready to call.
Is the reporting code outside the function?
Make sure each new line begins with four spaces. The reporting code must align with fig, axis = plt.subplots(...) inside the function.
Ask for help checking the indentation.
Classify a supported photo
A clear image from the trained label set gives you the fairest first test. The uploaded photo is resized to the model's 32-by-32 input shape before prediction.
- Add a code cell beneath the completed function.
Before you run this, which of the ten labels do you expect the model to rank first for your photo?
- Start the upload workflow by running this code:
classify_uploaded_image(model)
What does this call do?
The call passes your trained model into the upload function. The function pauses at the browser widget until you provide an image.
- Use the upload widget to select a clear photo of a bird, cat, deer, dog, frog, horse, bear, elephant, lion, or tiger from your Mac.
- Wait for the prediction cell to finish.
You should see your image with its highest-ranked known class in the title. Beneath it, you should see three ranked labels with percentages and a closed-set warning.
That is your classifier working on a photo from outside its test arrays. You can now compare the first prediction with the subject you uploaded.
Does the upload finish without a prediction?
Confirm that you ran the completed function-definition cell before calling it. Choose a standard image file that Keras can load.
Ask for help diagnosing the upload result.
Save, reload, and download the model
A saved Keras model preserves its architecture, learned weights, and optimizer state. Reloading it before download proves that the file can reproduce the trained model's prediction.
- Add a code cell beneath your upload test.
Before you run this, do you expect the original model and the reloaded model to choose the same label for the first test image?
- Save the model and test the reloaded copy by running this code:
MODEL_PATH = "animal_classifier.keras"
model.save(MODEL_PATH)
reloaded_model = keras.saving.load_model(MODEL_PATH)
original_label = int(np.argmax(model.predict(x_test[:1], verbose=0)[0]))
reloaded_label = int(np.argmax(reloaded_model.predict(x_test[:1], verbose=0)[0]))
print("Original model prediction:", CLASS_NAMES[original_label])
print("Reloaded model prediction:", CLASS_NAMES[reloaded_label])
print("Predictions match:", original_label == reloaded_label)
How does the reload check work?
- MODEL_PATH gives the saved file its reusable .keras format.
- keras.saving.load_model() reconstructs the saved model as reloaded_model.
- Both models predict the first image in x_test.
- The final comparison checks whether both models select the same class position.
You should see the same animal name for both predictions. The final line should report that the predictions match.
Do the saved predictions fail to match?
Run the model-saving cell again without changing x_test[:1] between predictions. Confirm that both calls use the same first test image.
Ask for help comparing the saved model.
- Download the saved model to your Mac by running this code:
files.download(MODEL_PATH)
What does this download preserve?
The download copies animal_classifier.keras from the temporary Colab runtime to your Mac. The local file remains available after the runtime disconnects.
- Allow the download if Chrome asks for permission.
- Open the Downloads folder in Finder.
- Confirm that animal_classifier.keras appears in the folder.
Your trained classifier now survives beyond the Colab session. The downloaded model is ready for later reloading or continued training.
Can’t find the downloaded model?
Check Chrome's recent downloads for animal_classifier.keras. Run the download cell again if the browser blocked the first attempt.
Ask for help locating the file.
Use these tabs to confirm that your notebook matches the reusable animal_classifier.py source.
✔️ Awesome, I've got everything!
Your upload function, model reload check, and downloaded animal_classifier.keras file are complete.
ⓧ I'd like to double check the full code
Compare your notebook cells with this complete animal_classifier.py reference:
import os
import sys
from importlib import metadata
os.environ["KERAS_BACKEND"] = "tensorflow"
import keras
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import tensorflow_datasets as tfds
from google.colab import files
EXPECTED_VERSIONS = {
"tensorflow": "2.20.0",
"keras": "3.13.2",
"tensorflow-datasets": "4.9.10",
"numpy": "2.0.2",
"matplotlib": "3.10.0",
"google-colab": "1.0.0",
}
print("Python:", sys.version.split()[0])
for package_name, expected_version in EXPECTED_VERSIONS.items():
installed_version = metadata.version(package_name)
print(f"{package_name}: {installed_version}")
if installed_version != expected_version:
raise RuntimeError(
f"Expected {package_name}=={expected_version}, "
f"but found {installed_version}. Select Colab runtime 2026.07."
)
print("Keras backend:", keras.backend.backend())
SEED = 42
CLASS_NAMES = [
"bird",
"cat",
"deer",
"dog",
"frog",
"horse",
"bear",
"elephant",
"lion",
"tiger",
]
CIFAR10_CLASSES = {"bird", "cat", "deer", "dog", "frog", "horse"}
CIFAR100_CLASSES = {"bear", "elephant", "lion", "tiger"}
def load_dataset(name):
splits, info = tfds.load(
name,
split=["train", "test"],
as_supervised=True,
batch_size=-1,
with_info=True,
)
train_data = tfds.as_numpy(splits[0])
test_data = tfds.as_numpy(splits[1])
source_names = info.features["label"].names
return train_data, test_data, source_names
c10_train, c10_test, c10_names = load_dataset("cifar10:3.0.2")
c100_train, c100_test, c100_names = load_dataset("cifar100:3.0.2")
def select_classes(data, source_names, selected_names, start=0, count=None):
images, labels = data
images = np.asarray(images)
labels = np.asarray(labels).reshape(-1)
image_parts = []
label_parts = []
for target_label, class_name in enumerate(CLASS_NAMES):
if class_name not in selected_names:
continue
source_label = source_names.index(class_name)
matching_indices = np.flatnonzero(labels == source_label)
if count is None:
matching_indices = matching_indices[start:]
else:
matching_indices = matching_indices[start : start + count]
image_parts.append(images[matching_indices])
label_parts.append(
np.full(len(matching_indices), target_label, dtype=np.int64)
)
return np.concatenate(image_parts), np.concatenate(label_parts)
def combine_sources(c10_data, c100_data, start=0, count=None):
c10_images, c10_labels = select_classes(
c10_data, c10_names, CIFAR10_CLASSES, start=start, count=count
)
c100_images, c100_labels = select_classes(
c100_data, c100_names, CIFAR100_CLASSES, start=start, count=count
)
return (
np.concatenate([c10_images, c100_images]),
np.concatenate([c10_labels, c100_labels]),
)
naive_images, naive_labels = combine_sources(c10_train, c100_train)
naive_counts = np.bincount(naive_labels, minlength=len(CLASS_NAMES))
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for class_id, axis in enumerate(axes.flat):
example_index = np.flatnonzero(naive_labels == class_id)[0]
axis.imshow(naive_images[example_index], interpolation="nearest")
axis.set_title(CLASS_NAMES[class_id])
axis.axis("off")
fig.suptitle("One training image from each animal class")
fig.tight_layout()
plt.show()
fig, axis = plt.subplots(figsize=(11, 4))
axis.bar(CLASS_NAMES, naive_counts)
axis.set_title("Naive combined training counts")
axis.set_ylabel("Images")
axis.tick_params(axis="x", rotation=45)
fig.tight_layout()
plt.show()
print(dict(zip(CLASS_NAMES, naive_counts.tolist())))
def shuffle_pair(images, labels, seed):
generator = np.random.default_rng(seed)
order = generator.permutation(len(labels))
return images[order], labels[order]
x_train, y_train = combine_sources(c10_train, c100_train, start=0, count=400)
x_val, y_val = combine_sources(c10_train, c100_train, start=400, count=100)
x_test, y_test = combine_sources(c10_test, c100_test, start=0, count=100)
x_train, y_train = shuffle_pair(x_train, y_train, SEED)
x_val, y_val = shuffle_pair(x_val, y_val, SEED + 1)
x_test, y_test = shuffle_pair(x_test, y_test, SEED + 2)
print("Training shape:", x_train.shape)
print("Validation shape:", x_val.shape)
print("Test shape:", x_test.shape)
print("Training counts:", np.bincount(y_train, minlength=10).tolist())
print("Validation counts:", np.bincount(y_val, minlength=10).tolist())
print("Test counts:", np.bincount(y_test, minlength=10).tolist())
fig, axis = plt.subplots(figsize=(11, 4))
positions = np.arange(len(CLASS_NAMES))
axis.bar(positions - 0.25, np.bincount(y_train, minlength=10), width=0.25, label="train")
axis.bar(positions, np.bincount(y_val, minlength=10), width=0.25, label="validation")
axis.bar(positions + 0.25, np.bincount(y_test, minlength=10), width=0.25, label="test")
axis.set_xticks(positions, CLASS_NAMES, rotation=45)
axis.set_ylabel("Images")
axis.set_title("Balanced class counts")
axis.legend()
fig.tight_layout()
plt.show()
model = keras.Sequential(
[
keras.Input(shape=(32, 32, 3)),
keras.layers.Rescaling(1.0 / 255.0),
keras.layers.RandomFlip(mode="horizontal", seed=SEED),
keras.layers.Conv2D(32, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(64, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Conv2D(128, (3, 3), padding="same", activation="relu"),
keras.layers.MaxPooling2D(),
keras.layers.Flatten(),
keras.layers.Dropout(0.3, seed=SEED),
keras.layers.Dense(128, activation="relu"),
keras.layers.Dropout(0.3, seed=SEED + 1),
keras.layers.Dense(len(CLASS_NAMES), activation="softmax"),
],
name="animal_classifier",
)
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy(name="accuracy")],
)
model.summary()
history = model.fit(
x_train,
y_train,
validation_data=(x_val, y_val),
epochs=12,
batch_size=64,
verbose=2,
)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(history.history["accuracy"], label="train")
axes[0].plot(history.history["val_accuracy"], label="validation")
axes[0].set_title("Accuracy")
axes[0].set_xlabel("Epoch")
axes[0].legend()
axes[1].plot(history.history["loss"], label="train")
axes[1].plot(history.history["val_loss"], label="validation")
axes[1].set_title("Loss")
axes[1].set_xlabel("Epoch")
axes[1].legend()
fig.tight_layout()
plt.show()
test_results = model.evaluate(x_test, y_test, verbose=0, return_dict=True)
print("Test results:", test_results)
probabilities = model.predict(x_test, batch_size=64, verbose=0)
predicted_labels = np.argmax(probabilities, axis=1)
confusion_matrix = np.zeros((len(CLASS_NAMES), len(CLASS_NAMES)), dtype=np.int64)
for actual, predicted in zip(y_test, predicted_labels):
confusion_matrix[int(actual), int(predicted)] += 1
fig, axis = plt.subplots(figsize=(8, 7))
matrix_image = axis.imshow(confusion_matrix, cmap="Blues")
axis.set_xticks(range(len(CLASS_NAMES)), CLASS_NAMES, rotation=45, ha="right")
axis.set_yticks(range(len(CLASS_NAMES)), CLASS_NAMES)
axis.set_xlabel("Predicted label")
axis.set_ylabel("Actual label")
axis.set_title("Test confusion matrix")
fig.colorbar(matrix_image, ax=axis)
fig.tight_layout()
plt.show()
fig, axes = plt.subplots(2, 5, figsize=(13, 6))
for index, axis in enumerate(axes.flat):
actual_name = CLASS_NAMES[int(y_test[index])]
predicted_name = CLASS_NAMES[int(predicted_labels[index])]
confidence = float(probabilities[index, predicted_labels[index]])
axis.imshow(x_test[index], interpolation="nearest")
axis.set_title(
f"Pred: {predicted_name}\nActual: {actual_name}\n{confidence:.1%}"
)
axis.axis("off")
fig.tight_layout()
plt.show()
def classify_uploaded_image(trained_model):
uploaded = files.upload()
if not uploaded:
print("No image was uploaded.")
return None
image_path = next(iter(uploaded))
display_image = keras.utils.load_img(image_path, color_mode="rgb")
model_image = keras.utils.load_img(
image_path,
color_mode="rgb",
target_size=(32, 32),
interpolation="bilinear",
)
image_array = keras.utils.img_to_array(model_image)
image_batch = np.expand_dims(image_array, axis=0)
scores = np.asarray(trained_model.predict(image_batch, verbose=0)[0])
top_three = np.argsort(scores)[-3:][::-1]
fig, axis = plt.subplots(figsize=(6, 5))
axis.imshow(display_image)
axis.axis("off")
axis.set_title(
f"Top prediction: {CLASS_NAMES[int(top_three[0])]} "
f"({float(scores[top_three[0]]):.1%})"
)
plt.show()
print("Top three known-class predictions:")
for class_id in top_three:
print(f"{CLASS_NAMES[int(class_id)]}: {float(scores[class_id]):.1%}")
print(
"Closed-set warning: this model always ranks its ten known labels, "
"even when the image contains something else."
)
return image_path, scores
classify_uploaded_image(model)
MODEL_PATH = "animal_classifier.keras"
model.save(MODEL_PATH)
reloaded_model = keras.saving.load_model(MODEL_PATH)
original_label = int(np.argmax(model.predict(x_test[:1], verbose=0)[0]))
reloaded_label = int(np.argmax(reloaded_model.predict(x_test[:1], verbose=0)[0]))
print("Original model prediction:", CLASS_NAMES[original_label])
print("Reloaded model prediction:", CLASS_NAMES[reloaded_label])
print("Predictions match:", original_label == reloaded_label)
files.download(MODEL_PATH)
Secret mission
Challenge the Closed-Set Classifier
Test your classifier with a supported animal, an unsupported animal, and a non-animal image. You will document how confident known-class predictions can become misleading when an image falls outside the model's training scope.
Clean Up Your Resources
Clean Up Your Resources
This project used Google Colab's free compute, so the main path has no ongoing charges. Choose whether to keep your work, pause the runtime, or delete the project files.
Resources you used:
- The animal_classifier.ipynb notebook in Google Colab.
- A connected Colab runtime containing the in-memory datasets, trained model state, and uploaded test images.
- Temporary runtime files, including animal_classifier.py and animal_classifier.keras.
- The downloaded animal_classifier.keras model file on your Mac.
Keep everything running
No action is needed. Choose this if you are still testing images or refining the classifier.
- Keep animal_classifier.ipynb as the editable record of your data preparation and training work.
- Keep animal_classifier.keras on your Mac for future predictions or continued training.
- Continue using the connected Colab runtime while it remains available.
Pause - I'll come back to this later
Disconnect the runtime to release its compute. Your saved notebook and downloaded model remain available.
- Save animal_classifier.ipynb using the notebook's save control.
- Use Google Colab's runtime controls to disconnect the current session.
- Confirm the connection indicator shows that the runtime is disconnected.
- Keep animal_classifier.keras on your Mac for your next session.
Delete - I don't want to use this again
Remove the notebook, release its compute, and delete the downloaded model. This leaves no project files running or stored for later use.
- Use Google Colab's runtime controls to disconnect the current session.
- Use the same controls to delete the temporary runtime.
- Confirm the connection indicator shows no active runtime.
Deleting the runtime clears its in-memory datasets. It also clears uploaded images and temporary files.
- Use the open notebook's file controls to move animal_classifier.ipynb to trash.
- Confirm animal_classifier.ipynb no longer appears in your Colab notebook list.
- Press Cmd+Space to open Spotlight on your Mac.
- Type Finder into Spotlight.
- Press Enter to open Finder.
- Use Finder's search field to locate animal_classifier.keras.
- Move animal_classifier.keras to Trash.
- Empty Trash to remove the model file from your Mac.
Nice Work!
Nice Work!
You did it! Your Google Colab notebook now trains a ten-class convolutional neural network that classifies uploaded animal photos.
You've learned how to:
- Expose the original 10-to-1 class imbalance created by combining CIFAR-10 with CIFAR-100. Build balanced dataset splits with equal representation for every animal.
- Train the network with data augmentation plus dropout to reduce dependence on individual examples. Read learning curves to compare training behavior with validation behavior. Use a confusion matrix to locate label mix-ups.
- Turn classify_uploaded_image(model) into an upload-driven demo that displays three ranked known-class probabilities. Save the complete network as animal_classifier.keras. Reload the file to verify its prediction. Download the reusable model to your Mac.
- Secret Mission: Challenge the closed-set classifier with a supported animal. Repeat the test with an unsupported animal. Probe it once more with a non-animal image. Document why high confidence does not prove membership in a known class.
Ready to quiz yourself?