Build a Music Genre Classifier

Train and evaluate a music genre classifier with FMA audio features.

Introduction

30 Second Summary

Genre labels make large music libraries easier to search. Songs that blend styles make those labels difficult to assign consistently.

In this project, you will build a music genre classifier in Google Colab using the Free Music Archive's precomputed audio-derived feature vectors. The finished pipeline compares a linear baseline with a neural model before exporting confidence-scored predictions.

What You'll Build

Run your notebook to see the genre balance, compare two trained models, inspect an eight-class confusion matrix, and review held-out track predictions with confidence scores.

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

  • A reproducible data pipeline that turns 8,000 labeled tracks into standardized training, validation, and test arrays while keeping artists separated across splits.
  • A model benchmark that compares logistic regression with a neural classifier using test accuracy and macro F1.
  • A reusable prediction workflow that displays class-level mistakes, predicts held-out tracks in batches, and exports the trained model with its supporting metadata.
  • Secret Mission: Add an abstention rule that sends low-confidence predictions to human review.

Are there any prerequisites?

You need a Google account with Google Colab access plus prior deep learning experience. No local installation is required.

Before We Start

This is your moment to commit to a feature-based FMA classifier that predicts one of eight top-level genres from precomputed audio-derived feature vectors. Artist-separated splits prevent artist leakage from inflating the evaluation.

Set Up the Colab Workspace

A classifier built on precomputed features still depends on a predictable software environment. A changed library version can turn working notebook code into an API mismatch.

In this step, you will prepare a pinned Google Colab CPU runtime. You will also download the 342 MiB metadata archive from the Free Music Archive instead of downloading several gigabytes of raw audio.

In this step, get ready to:
  • Configure runtime version 2026.07 with no hardware accelerator.
  • Install the pinned project packages.
  • Prepare the verified FMA metadata archive for use.
Configure the Colab runtime

A pinned runtime gives every later script the same Python foundation. The CPU setting is enough because this project trains on feature rows instead of decoded audio.

  • Create a new notebook in Google Colab.

You will see an empty notebook with a code cell ready for the project.

  • Click Runtime in the top menu.
  • Select Change runtime type.

The runtime dialog gives you separate controls for the software image and computing hardware.

  • Choose 2026.07 in the Runtime version field.
  • Set Hardware accelerator to None.

Connecting to a pinned runtime can take 2-3 minutes while Colab loads its image. A quiet notebook during this wait is expected.

  • Apply the selections using the confirmation control at the bottom of the dialog.

You will see the notebook reconnect. It now uses runtime 2026.07 with Python 3.12.13 on a standard CPU environment.

Why Use a CPU Runtime?

A CPU can process the tabular feature data used in this project. This keeps the workflow independent of variable free-accelerator availability.

The selected runtime also fixes the Python version beneath your installed packages. That stable base makes the notebook reproducible.

Runtime Taking Too Long to Connect?

  • Allow a few minutes for the pinned runtime image to load.
  • Reopen Change runtime type if the notebook reconnects to a different runtime.
  • Confirm that Runtime version still shows 2026.07.
  • Still stuck? Help me configure the pinned Google Colab runtime.
Install and verify the project packages

The classifier uses a specific combination of data-processing libraries and machine-learning libraries. Installing exact versions prevents differences in APIs or saved-model behavior.

  • Install the exact package versions in the current notebook runtime by running this cell:
%pip install numpy==2.5.4 pandas==3.0.6 scikit-learn==1.9.1 matplotlib==3.11.2 tensorflow==2.21.0 keras==3.15.1

What Does This Install Do?

The %pip command installs packages into the notebook's current kernel. Each version pin locks one dependency to the release used by this project.

  • NumPy provides numerical arrays for prepared features.
  • pandas loads the multi-header FMA metadata files.
  • scikit-learn supplies feature scaling and baseline evaluation tools.
  • TensorFlow and Keras train the neural classifier.
  • Matplotlib displays the confusion matrix.

The first installation can take several minutes because the runtime downloads the pinned machine-learning packages.

Before you check the environment, do you expect every imported package to report the requested version?

  • Verify the installed versions by running this cell:
import keras
import matplotlib
import numpy
import pandas
import sklearn
import tensorflow

print("NumPy:", numpy.__version__)
print("pandas:", pandas.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)
print("TensorFlow:", tensorflow.__version__)
print("Keras:", keras.__version__)

What Should I See?

  • NumPy reports 2.5.4.
  • Pandas reports 3.0.6.
  • Scikit-learn reports 1.9.1.
  • Matplotlib reports 3.11.2.
  • TensorFlow reports 2.21.0.
  • Keras reports 3.15.1.

That is the software environment locked in. Every modeling step now runs against the expected package APIs.

Versions Do Not Match?

  • Confirm that the installation cell includes every version pin shown above.
  • Rerun the installation cell if one package reports a different version.
  • Rerun the version cell after the installation finishes.
  • Still stuck? Help me fix the package versions in my Colab runtime.
Download and verify the FMA metadata

The metadata archive contains the track labels and precomputed audio features used by the classifier. A SHA-1 checksum confirms that the downloaded archive matches the official file before you extract it.

  • Download fma_metadata.zip into the Colab runtime by running this cell:
!curl -O https://os.unil.cloud.switch.ch/fma/fma_metadata.zip

What Does This Command Do?

The command downloads the official FMA metadata archive into the notebook's current storage. The archive contains metadata and precomputed features instead of raw music files.

  • Confirm that the download reaches completion by reading the cell output.

You will see download progress for the 342 MiB archive. The completed file is named fma_metadata.zip.

Download Interrupted?

  • Check that the Colab runtime still shows a connected status.
  • Rerun the download cell if the transfer stops before completion.
  • Still stuck? Help me download the FMA metadata archive in Colab.

Before you run the integrity check, do you expect the downloaded archive to match the official checksum?

  • Verify the archive against the official SHA-1 value by running this cell:
!echo "f0df49ffe5f2a6008d7dc83c6915b31835dfe733  fma_metadata.zip" | sha1sum -c -

What Does the Checksum Prove?

The command calculates a fingerprint for the downloaded archive. It compares that fingerprint with the official value before any files are extracted.

A valid result proves that the archive arrived intact. It also protects the next step from using a partial download.

Good work. The checksum reports the archive as valid, so the file is ready to extract.

Checksum Does Not Validate?

  • Remove the incomplete archive from the Colab file browser.
  • Rerun the download cell to fetch a fresh copy.
  • Rerun the checksum cell before extracting the archive.
  • Still stuck? Help me resolve an invalid FMA archive checksum.
  • Extract the verified metadata archive by running this cell:
!unzip fma_metadata.zip

What Does This Command Extract?

The archive expands into the fma_metadata folder. Its CSV files contain track metadata and precomputed feature columns.

The next step reads tracks.csv for labels. It reads features.csv for model inputs.

Before you inspect the extracted folder, what evidence would prove that both labels and features are available?

  • Select the Colab file browser from the left sidebar.
  • Expand the fma_metadata folder.
  • Confirm that tracks.csv is listed.
  • Confirm that features.csv is listed.

Your verified FMA inputs are in place. The notebook can now reach both the track metadata and the precomputed feature table.

Extracted Files Missing?

  • Confirm that the checksum reported the archive as valid before extraction.
  • Rerun the extraction cell if the fma_metadata folder is missing.
  • Refresh the Colab file browser if the folder exists but its contents are not visible.
  • Still stuck? Help me find the extracted FMA metadata files.

✔️ Awesome, I've got everything!

Your runtime, package versions, verified archive, and extracted FMA files are ready for the data-preparation step.

ⓧ I'd like to double check the full code

This setup step creates data files instead of project Python scripts. Double-check that your workspace contains these exact artifacts:

  • fma_metadata.zip: Downloaded official FMA metadata archive with SHA-1 checksum verified as f0df49ffe5f2a6008d7dc83c6915b31835dfe733.
  • fma_metadata/features.csv: Extracted FMA three-header precomputed audio-feature CSV.
  • fma_metadata/tracks.csv: Extracted FMA multi-header track metadata CSV.

Your Colab workspace now has a reproducible runtime and verified FMA inputs. Next, you will turn those multi-header files into standardized training, validation, and test arrays.

Prepare Artist-Separated Data Splits

Your verified Free Music Archive (FMA) files are ready in Google Colab. The next goal is to turn those large tables into arrays that a classifier can train on.

A random track split can place songs from the same artist on both sides of an evaluation. The supplied FMA assignments keep each artist within one partition.

Preserving those assignments protects the evaluation from artist leakage. The printed split sizes plus genre bars make this first version of the pipeline visible before training starts.

In this step, get ready to:
  • Build prepare_data.py from the extracted metadata.
  • Preserve the supplied artist-separated split assignments.
  • Export standardized arrays for model training.
Load the FMA tables

FMA stores track labels in tracks.csv. It stores precomputed audio summaries in features.csv.

Both files use several header rows. pandas reads those rows as nested column labels so the script can select fields precisely.

  • Add a new code cell below your earlier setup cells.
  • Create prepare_data.py with its imports plus file paths by running:
%%writefile prepare_data.py
import json
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

DATA_DIR = Path("fma_metadata")
ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
METADATA_PATH = ARTIFACT_DIR / "metadata.json"

TRACKS_PATH = DATA_DIR / "tracks.csv"
FEATURES_PATH = DATA_DIR / "features.csv"

if not TRACKS_PATH.exists() or not FEATURES_PATH.exists():
    raise FileNotFoundError("Extract fma_metadata.zip before running this script.")

ARTIFACT_DIR.mkdir(exist_ok=True)

What does this setup do?

  • The path constants give every input plus output one reusable location.
  • The file check stops the script if either extracted CSV is missing.
  • The artifacts directory holds the prepared arrays plus their metadata.
  • Confirm the cell output names prepare_data.py as the written file.

File not created?

Make sure %%writefile prepare_data.py is the first line in the cell. Remove any blank line above it before running the cell again.

Still stuck? Help me create prepare_data.py with the Colab writefile cell.

  • Add another code cell below the file-creation cell.
  • Append the CSV loading plus small-subset selection by running:
%%writefile -a prepare_data.py

tracks = pd.read_csv(
    TRACKS_PATH,
    index_col=0,
    header=[0, 1],
    low_memory=False,
)
features = pd.read_csv(
    FEATURES_PATH,
    index_col=0,
    header=[0, 1, 2],
)

small_mask = tracks[("set", "subset")].eq("small")
small_ids = tracks.index[small_mask]
small_tracks = tracks.loc[small_ids]
small_features = features.loc[small_ids]

raw_labels = small_tracks[("track", "genre_top")]
if raw_labels.isna().any():
    raise ValueError("The selected rows contain missing top-level genre labels.")

How are the source rows selected?

  • The two header settings match the nested columns in the official FMA CSV files.
  • The small-subset mask selects the 8,000 track IDs used throughout this project.
  • The label check prevents a track without a top-level genre from reaching model training.
  • Confirm the cell output reports that it appended to prepare_data.py.

Append cell failing?

Check that the first file-creation cell completed before this append cell. The append cell depends on prepare_data.py already existing.

Need a hand? Help me append the FMA table-loading code to prepare_data.py.

  • Add a new code cell below the table-loading cell.
  • Append label encoding plus split extraction by running:
%%writefile -a prepare_data.py

labels = raw_labels.astype(str)
class_names = np.array(sorted(labels.unique()), dtype=str)
class_to_id = {name: index for index, name in enumerate(class_names)}

clean_features = small_features.replace([np.inf, -np.inf], np.nan)
clean_features = clean_features.dropna(axis=1, how="any").astype("float32")

split_labels = small_tracks[("set", "split")].astype(str)


def extract_split(split_name):
    split_ids = small_tracks.index[split_labels.eq(split_name)]
    x_values = clean_features.loc[split_ids].to_numpy(dtype=np.float32)
    y_values = labels.loc[split_ids].map(class_to_id).to_numpy(dtype=np.int32)
    return split_ids.to_numpy(dtype=np.int64), x_values, y_values


train_ids, x_train, y_train = extract_split("training")
val_ids, x_val, y_val = extract_split("validation")
test_ids, x_test, y_test = extract_split("test")

if min(len(train_ids), len(val_ids), len(test_ids)) == 0:
    raise ValueError("At least one supplied FMA split is empty.")

How are the splits preserved?

  • The sorted genre names create a stable mapping from class names to integer labels.
  • Columns containing infinite or missing values are removed before array conversion.
  • The extract_split() function selects tracks from each supplied FMA assignment.
  • The final check catches any unexpectedly empty partition.
  • Confirm the cell output reports another append to prepare_data.py.

Split section not appended?

Check that the append flag remains on the first line. Without it, the cell overwrites the earlier script sections.

Want help checking the file order? Help me verify the split-extraction sections in prepare_data.py.

Scale and save the prepared arrays

Feature columns have different numeric ranges. scikit-learn's StandardScaler learns a mean plus scale for each column.

Fitting the scaler on training data only prevents validation or test statistics from leaking into training. The same learned transformation then applies to every partition.

  • Add a new code cell below the split-extraction cell.
  • Append training-only scaling plus NumPy archive export by running:
%%writefile -a prepare_data.py

scaler = StandardScaler()
x_train = scaler.fit_transform(x_train).astype(np.float32)
x_val = scaler.transform(x_val).astype(np.float32)
x_test = scaler.transform(x_test).astype(np.float32)

np.savez(
    PREPARED_PATH,
    train_ids=train_ids,
    val_ids=val_ids,
    test_ids=test_ids,
    X_train=x_train,
    y_train=y_train,
    X_val=x_val,
    y_val=y_val,
    X_test=x_test,
    y_test=y_test,
    scaler_mean=scaler.mean_.astype(np.float32),
    scaler_scale=scaler.scale_.astype(np.float32),
)

What gets saved in the array archive?

  • The training call learns scaling statistics from x_train only.
  • The validation plus test calls reuse those training statistics.
  • The .npz archive stores track IDs plus features plus integer labels.
  • The saved mean plus scale allow later inference data to receive the same transformation.
  • Confirm the cell output reports another append to prepare_data.py.

Scaling section missing?

Check that this cell appears after the split-extraction cell. The scaler requires x_train plus the other two feature arrays to exist when the completed script runs.

Need help tracing the dependency? Help me check the scaling section in prepare_data.py.

  • Add another code cell below the scaling cell.
  • Append the reusable JSON metadata export by running:
%%writefile -a prepare_data.py

project_metadata = {
    "dataset": "FMA small",
    "class_names": class_names.tolist(),
    "feature_columns": [list(map(str, column)) for column in clean_features.columns],
}
with METADATA_PATH.open("w", encoding="utf-8") as file:
    json.dump(project_metadata, file, indent=2)

Why save metadata separately?

The metadata file preserves the ordered genre names plus feature columns. Later scripts can translate numeric predictions without rebuilding the mapping.

  • Confirm the cell output reports another append to prepare_data.py.

Metadata section not saved?

Check that the opening plus closing braces match the reference. A missing brace prevents the completed script from creating metadata.json.

Still blocked? Help me inspect the metadata export in prepare_data.py.

Print and verify the genre distribution

A data pipeline needs a visible check before model training. Track counts plus text bars expose missing genres or empty partitions immediately.

  • Add one final code cell below the metadata cell.
  • Append the distribution summary plus saved-file confirmation by running:
%%writefile -a prepare_data.py

genre_counts = labels.value_counts().reindex(class_names)
bar_unit = max(1, int(genre_counts.max() // 40))

print(f"Tracks: {len(small_ids):,}")
print(f"Genres: {len(class_names)}")
print(f"Usable features: {clean_features.shape[1]}")
print(
    "Split sizes:",
    {"training": len(train_ids), "validation": len(val_ids), "test": len(test_ids)},
)
print("\nGenre distribution")
for genre in class_names:
    count = int(genre_counts.loc[genre])
    print(f"{genre:15} {'#' * max(1, count // bar_unit)} {count}")

print(f"\nSaved {PREPARED_PATH} and {METADATA_PATH}")

What does the summary reveal?

  • The first lines report the track total plus genre count plus usable feature count.
  • The split dictionary proves that training plus validation plus test arrays all contain tracks.
  • Each text bar makes the genre balance visible in the notebook output.
  • The final line confirms the two artifact paths written by the script.
  • Confirm the cell output reports the final append to prepare_data.py.

Final section not appended?

Check the indentation inside the genre loop. The count plus print lines must remain inside the loop.

Need help with the final section? Help me check the genre-distribution code in prepare_data.py.

Use the reference below to confirm that every script section landed in the correct order.

✔️ Awesome, I've got everything!

Your prepare_data.py chunks are assembled into one data-preparation script.

ⓧ I'd like to double check the full code

  • Compare your complete prepare_data.py file with this reference.
import json
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

DATA_DIR = Path("fma_metadata")
ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
METADATA_PATH = ARTIFACT_DIR / "metadata.json"

TRACKS_PATH = DATA_DIR / "tracks.csv"
FEATURES_PATH = DATA_DIR / "features.csv"

if not TRACKS_PATH.exists() or not FEATURES_PATH.exists():
    raise FileNotFoundError("Extract fma_metadata.zip before running this script.")

ARTIFACT_DIR.mkdir(exist_ok=True)

tracks = pd.read_csv(
    TRACKS_PATH,
    index_col=0,
    header=[0, 1],
    low_memory=False,
)
features = pd.read_csv(
    FEATURES_PATH,
    index_col=0,
    header=[0, 1, 2],
)

small_mask = tracks[("set", "subset")].eq("small")
small_ids = tracks.index[small_mask]
small_tracks = tracks.loc[small_ids]
small_features = features.loc[small_ids]

raw_labels = small_tracks[("track", "genre_top")]
if raw_labels.isna().any():
    raise ValueError("The selected rows contain missing top-level genre labels.")

labels = raw_labels.astype(str)
class_names = np.array(sorted(labels.unique()), dtype=str)
class_to_id = {name: index for index, name in enumerate(class_names)}

clean_features = small_features.replace([np.inf, -np.inf], np.nan)
clean_features = clean_features.dropna(axis=1, how="any").astype("float32")

split_labels = small_tracks[("set", "split")].astype(str)


def extract_split(split_name):
    split_ids = small_tracks.index[split_labels.eq(split_name)]
    x_values = clean_features.loc[split_ids].to_numpy(dtype=np.float32)
    y_values = labels.loc[split_ids].map(class_to_id).to_numpy(dtype=np.int32)
    return split_ids.to_numpy(dtype=np.int64), x_values, y_values


train_ids, x_train, y_train = extract_split("training")
val_ids, x_val, y_val = extract_split("validation")
test_ids, x_test, y_test = extract_split("test")

if min(len(train_ids), len(val_ids), len(test_ids)) == 0:
    raise ValueError("At least one supplied FMA split is empty.")

scaler = StandardScaler()
x_train = scaler.fit_transform(x_train).astype(np.float32)
x_val = scaler.transform(x_val).astype(np.float32)
x_test = scaler.transform(x_test).astype(np.float32)

np.savez(
    PREPARED_PATH,
    train_ids=train_ids,
    val_ids=val_ids,
    test_ids=test_ids,
    X_train=x_train,
    y_train=y_train,
    X_val=x_val,
    y_val=y_val,
    X_test=x_test,
    y_test=y_test,
    scaler_mean=scaler.mean_.astype(np.float32),
    scaler_scale=scaler.scale_.astype(np.float32),
)

project_metadata = {
    "dataset": "FMA small",
    "class_names": class_names.tolist(),
    "feature_columns": [list(map(str, column)) for column in clean_features.columns],
}
with METADATA_PATH.open("w", encoding="utf-8") as file:
    json.dump(project_metadata, file, indent=2)

genre_counts = labels.value_counts().reindex(class_names)
bar_unit = max(1, int(genre_counts.max() // 40))

print(f"Tracks: {len(small_ids):,}")
print(f"Genres: {len(class_names)}")
print(f"Usable features: {clean_features.shape[1]}")
print(
    "Split sizes:",
    {"training": len(train_ids), "validation": len(val_ids), "test": len(test_ids)},
)
print("\nGenre distribution")
for genre in class_names:
    count = int(genre_counts.loc[genre])
    print(f"{genre:15} {'#' * max(1, count // bar_unit)} {count}")

print(f"\nSaved {PREPARED_PATH} and {METADATA_PATH}")

Why this reference matters

This is the cumulative script produced by the append cells. Matching its order ensures every variable exists before the next section uses it.

Before you run the script, do you expect every supplied split to contain tracks?

  • Run the completed data-preparation script in a new code cell with:
%run prepare_data.py

What should you see?

  • The output reports Tracks: 8,000.
  • The output reports Genres: 8.
  • The split-size dictionary contains non-zero training plus validation plus test counts.
  • Eight genre rows appear with text bars plus counts.
  • The final line confirms artifacts/prepared_fma_small.npz plus artifacts/metadata.json were saved.

Script did not finish?

If a source-file check fails, confirm fma_metadata/tracks.csv plus fma_metadata/features.csv still appear in Colab's file panel.

If the failure points to an undefined name, compare the append-cell order with the full-file reference. One earlier section may have been overwritten.

Still stuck? Help me diagnose my prepare_data.py run in Colab.

That is the data foundation in place. Your 8,000 tracks now form standardized artist-separated arrays with reusable labels plus feature ordering.

Your model-ready arrays are saved. Next, you will train a linear baseline that gives the neural classifier a meaningful score to beat.

Establish a Linear Baseline

Your FMA data pipeline now produces standardized arrays with artist-separated training data. A neural model only earns its extra complexity when it improves on a simpler comparison.

Logistic regression gives you a linear benchmark for the same eight-genre task. You will report accuracy plus macro F1 because accuracy alone can hide class-level failures.

In this step, get ready to:
  • Load the standardized training data from the prepared archive.
  • Fit a logistic-regression baseline on the eight genre classes.
  • Report test accuracy plus macro F1.
Create the baseline script

A Google Colab cell can save the complete baseline as a reusable Python file. The script reads the prepared archive from the previous step.

  • Add a new code cell beneath your data-preparation output.
  • Create train_baseline.py by running this cell:
%%writefile train_baseline.py
from pathlib import Path

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score

PREPARED_PATH = Path("artifacts/prepared_fma_small.npz")

with np.load(PREPARED_PATH) as data:
    x_train = data["X_train"]
    y_train = data["y_train"]
    x_test = data["X_test"]
    y_test = data["y_test"]

baseline = LogisticRegression(
    C=1.0,
    l1_ratio=0.0,
    solver="lbfgs",
    max_iter=500,
)
baseline.fit(x_train, y_train)
baseline_predictions = baseline.predict(x_test)

baseline_accuracy = accuracy_score(y_test, baseline_predictions)
baseline_macro_f1 = f1_score(y_test, baseline_predictions, average="macro")

print(f"Baseline test accuracy: {baseline_accuracy:.4f}")
print(f"Baseline test macro F1: {baseline_macro_f1:.4f}")

What does this baseline do?

  • The NumPy archive supplies the standardized training features plus the untouched test features.
  • The LogisticRegression configuration creates a repeatable linear benchmark.
  • The baseline_predictions array stores one predicted genre ID for each test track.
  • The accuracy_score calculation measures overall correctness.
  • The f1_score calculation uses average="macro" to give every genre equal weight.
  • Confirm the cell finishes with a message showing that train_baseline.py was written.

Baseline file not written?

  • Check that %%writefile train_baseline.py is the first line in the cell.
  • Remove any blank line above the notebook command.
  • Run the complete cell again after fixing the first line.
  • Still stuck? Help me create the baseline script in Google Colab.

✔️ Awesome, I've got everything!

Great. Your saved train_baseline.py is ready to train the comparison model.

ⓧ I'd like to double check the full code

  • Compare your saved train_baseline.py against this complete reference file.
from pathlib import Path

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score

PREPARED_PATH = Path("artifacts/prepared_fma_small.npz")

with np.load(PREPARED_PATH) as data:
    x_train = data["X_train"]
    y_train = data["y_train"]
    x_test = data["X_test"]
    y_test = data["y_test"]

baseline = LogisticRegression(
    C=1.0,
    l1_ratio=0.0,
    solver="lbfgs",
    max_iter=500,
)
baseline.fit(x_train, y_train)
baseline_predictions = baseline.predict(x_test)

baseline_accuracy = accuracy_score(y_test, baseline_predictions)
baseline_macro_f1 = f1_score(y_test, baseline_predictions, average="macro")

print(f"Baseline test accuracy: {baseline_accuracy:.4f}")
print(f"Baseline test macro F1: {baseline_macro_f1:.4f}")

What should match?

This reference loads four prepared arrays. It fits the baseline before calculating both required test metrics.

Run the baseline and inspect both metrics

The test split measures performance on tracks excluded from fitting. Its two metrics give you the benchmark that the neural classifier must justify beating.

  • Pause for a prediction about whether accuracy plus macro F1 will match.
  • Run the saved baseline by executing this notebook command:
%run train_baseline.py

What should you see?

  • You will see a line beginning with Baseline test accuracy.
  • You will see a second line beginning with Baseline test macro F1.
  • Each score is a value between 0 plus 1.
  • A lower macro F1 points to uneven performance across genres.

That is your first model benchmark. The neural classifier now has a clear score to beat.

Metrics not appearing?

  • Return to the data-preparation cell from the previous step if the prepared archive is missing.
  • Rerun that cell to recreate artifacts/prepared_fma_small.npz.
  • Check that max_iter=500 appears inside the LogisticRegression configuration if fitting stops early.
  • Still stuck? Help me diagnose the missing baseline metrics.

Your linear benchmark is in place. Next, you will train a Keras multilayer perceptron to test whether nonlinear feature interactions improve the genre predictions.

Train the Neural Genre Classifier

Your logistic regression baseline now gives you a score that every more complex model must justify beating. The next goal is to test whether a nonlinear model can improve on that reference point.

A compact multilayer perceptron can learn nonlinear interactions among MFCC, chroma, rhythm, and spectral summaries. You will build it with Keras on the TensorFlow backend while keeping raw-audio processing outside this project.

In this step, get ready to:
  • Load the prepared training data into train_neural.py.
  • Build the eight-class Keras network.
  • Produce saved model and training-history artifacts through validation-based training.
Create the training script

The neural model needs the standardized training arrays from earlier. It also needs the validation split so it can measure performance on data that does not update the weights.

  • Add a new code cell below the baseline cell.
  • Type %%writefile train_neural.py on the first line.
  • Paste the code below it:
import json
import os
from pathlib import Path

os.environ.setdefault("KERAS_BACKEND", "tensorflow")

import keras
import numpy as np

SEED = 42
ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
MODEL_PATH = ARTIFACT_DIR / "genre_mlp.keras"
HISTORY_PATH = ARTIFACT_DIR / "training_history.json"

keras.utils.set_random_seed(SEED)

with np.load(PREPARED_PATH) as data:
    x_train = data["X_train"]
    y_train = data["y_train"]
    x_val = data["X_val"]
    y_val = data["y_val"]

with (ARTIFACT_DIR / "metadata.json").open("r", encoding="utf-8") as file:
    metadata = json.load(file)

What does this foundation do?

  • The KERAS_BACKEND setting selects TensorFlow before Keras is imported.
  • The fixed SEED makes the project more reproducible.
  • The NumPy archive supplies the standardized training arrays.
  • The same archive supplies the untouched validation arrays.
  • The metadata preserves the ordered list of eight genre names.
  • Run the cell to write the data-loading foundation into train_neural.py.

Good progress. Colab reports that it wrote train_neural.py, so your training script now has its data foundation.

Was the script file not written?

Confirm that %%writefile train_neural.py is the first line in the cell. Check that no blank line appears above the notebook magic.

Still stuck? Help me troubleshoot why my Colab writefile cell is not creating train_neural.py.

Build and compile the model

The hidden layers give the network capacity to learn nonlinear feature combinations. Dropout reduces reliance on individual hidden units during training.

The final softmax layer produces one probability for each genre. Early stopping watches validation loss so the model can keep the best weights from the training run.

  • Return to the train_neural.py writefile cell from earlier.
  • Place your cursor below metadata = json.load(file).
  • Paste the model definition below that line:
model = keras.Sequential(
    [
        keras.Input(shape=(x_train.shape[1],)),
        keras.layers.Dense(256, activation="relu"),
        keras.layers.Dropout(0.30, seed=SEED),
        keras.layers.Dense(64, activation="relu"),
        keras.layers.Dense(len(metadata["class_names"]), activation="softmax"),
    ],
    name="fma_genre_mlp",
)

model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

early_stopping = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=4,
    restore_best_weights=True,
)

How does this network learn?

  • The input shape comes directly from the number of standardized FMA features.
  • The first dense layer learns 256 hidden representations from those features.
  • The dropout layer temporarily removes 30% of its inputs during training.
  • The second dense layer compresses the learned representation to 64 values.
  • The softmax output contains one probability for every class in metadata["class_names"].
  • The Adam optimizer updates the model using a learning rate of 0.001.
  • The early-stopping callback restores the weights from the lowest observed validation loss.
  • Run the writefile cell again to update train_neural.py with the model definition.

Colab reports that it overwrote train_neural.py. The file now contains the network architecture plus its validation-based stopping rule.

Was the model code written in the wrong place?

Keep the model definition below the metadata-loading block. Check that it remains inside the same writefile cell.

Need another pair of eyes? Help me check the model section in train_neural.py.

Train and save the classifier

Training uses the supplied validation split after every epoch. The callback can stop the run when validation loss has stopped improving for four epochs.

The saved history turns every completed epoch into reusable JSON values. The saved Keras model carries the restored weights from the best validation point.

  • Return to the train_neural.py writefile cell from earlier.
  • Place your cursor below the early_stopping block.
  • Paste the training and saving code below that block:
history = model.fit(
    x_train,
    y_train,
    validation_data=(x_val, y_val),
    epochs=30,
    batch_size=128,
    callbacks=[early_stopping],
    verbose=2,
)

model.save(MODEL_PATH)
serializable_history = {
    key: [float(value) for value in values]
    for key, values in history.history.items()
}
with HISTORY_PATH.open("w", encoding="utf-8") as file:
    json.dump(serializable_history, file, indent=2)

best_val_accuracy = max(serializable_history["val_accuracy"])
print(f"Best validation accuracy: {best_val_accuracy:.4f}")
print(f"Epochs completed: {len(serializable_history['loss'])}")
print(f"Saved {MODEL_PATH} and {HISTORY_PATH}")

What does the training workflow save?

  • The model trains for up to 30 epochs using batches of 128 feature rows.
  • The validation arrays provide val_loss and val_accuracy after every epoch.
  • The restored model is saved to artifacts/genre_mlp.keras.
  • The completed metric sequences are converted to plain numbers for JSON storage.
  • The converted history is saved to artifacts/training_history.json.
  • Run the writefile cell again to save the complete train_neural.py file.

Colab reports that it overwrote train_neural.py. The complete training workflow is now ready to run.

Did the writefile cell stop with a syntax problem?

Check that the training code sits below the closing parenthesis of the early_stopping block. Confirm that every opening parenthesis has its matching closing parenthesis.

Need help locating the mismatch? Help me find the syntax problem in train_neural.py.

✔️ Awesome, I've got everything!

Your train_neural.py file now contains the complete model-training workflow.

ⓧ I'd like to double check the full code

import json
import os
from pathlib import Path

os.environ.setdefault("KERAS_BACKEND", "tensorflow")

import keras
import numpy as np

SEED = 42
ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
MODEL_PATH = ARTIFACT_DIR / "genre_mlp.keras"
HISTORY_PATH = ARTIFACT_DIR / "training_history.json"

keras.utils.set_random_seed(SEED)

with np.load(PREPARED_PATH) as data:
    x_train = data["X_train"]
    y_train = data["y_train"]
    x_val = data["X_val"]
    y_val = data["y_val"]

with (ARTIFACT_DIR / "metadata.json").open("r", encoding="utf-8") as file:
    metadata = json.load(file)

model = keras.Sequential(
    [
        keras.Input(shape=(x_train.shape[1],)),
        keras.layers.Dense(256, activation="relu"),
        keras.layers.Dropout(0.30, seed=SEED),
        keras.layers.Dense(64, activation="relu"),
        keras.layers.Dense(len(metadata["class_names"]), activation="softmax"),
    ],
    name="fma_genre_mlp",
)

model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)

early_stopping = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=4,
    restore_best_weights=True,
)

history = model.fit(
    x_train,
    y_train,
    validation_data=(x_val, y_val),
    epochs=30,
    batch_size=128,
    callbacks=[early_stopping],
    verbose=2,
)

model.save(MODEL_PATH)
serializable_history = {
    key: [float(value) for value in values]
    for key, values in history.history.items()
}
with HISTORY_PATH.open("w", encoding="utf-8") as file:
    json.dump(serializable_history, file, indent=2)

best_val_accuracy = max(serializable_history["val_accuracy"])
print(f"Best validation accuracy: {best_val_accuracy:.4f}")
print(f"Epochs completed: {len(serializable_history['loss'])}")
print(f"Saved {MODEL_PATH} and {HISTORY_PATH}")

How to compare the file

  • Compare your file from the first import to the final saved-path message.
  • Check the indentation inside both with blocks.
  • Check that the model layers appear in the same order.
  • Add a new code cell below the writefile cell.

Training runs across multiple epochs on the CPU runtime. Expect several minutes of output while the model updates its weights.

  • Before you run this, predict which metrics each epoch will report for both splits.
  • Start the neural training workflow by running this command:
%run train_neural.py

What should you see?

  • Each epoch prints training loss plus training accuracy.
  • Each epoch also prints val_loss plus val_accuracy.
  • Early stopping can finish the run before all 30 epochs when validation loss stops improving.
  • The final output prints Best validation accuracy as a value between 0 and 1.
  • The final saved-path message confirms artifacts/genre_mlp.keras.
  • The same message confirms artifacts/training_history.json.

That is the nonlinear model trained. You now have the best restored network plus a complete record of its learning curve.

Did training stop before saving the artifacts?

Confirm that artifacts/prepared_fma_small.npz still exists from the data-preparation step. Confirm that artifacts/metadata.json remains in the same folder.

If the runtime disconnected, rerun the complete train_neural.py script after the prepared artifacts are available.

Still stuck? Help me diagnose why train_neural.py did not finish training or save its artifacts.

Your neural classifier is trained and saved. Next, you will measure its test performance, expose its genre-level mistakes, and inspect confidence-scored predictions.

Diagnose, Predict, and Export

Your Keras model is trained on standardized FMA features. Its saved best weights are ready for a held-out evaluation.

A single accuracy score hides the genre pairs the model gets wrong. It also hides whether those failures affect every class equally.

A row-normalized confusion matrix exposes those class-level mistakes. Batched inference adds confidence scores to the held-out predictions.

In this step, get ready to:
  • Evaluate the neural model with accuracy plus macro F1.
  • Display a normalized confusion matrix for all eight genres.
  • Produce confidence-scored predictions with a reusable artifact inventory.
Load the held-out test artifacts

The held-out test split gives the model data it never used for training or early stopping. The evaluator also restores the saved class order so every numeric prediction maps back to the correct genre.

  • Create evaluate.py in a new Colab code cell by pasting this code:
%%writefile evaluate.py
import json
import os
from pathlib import Path

os.environ.setdefault("KERAS_BACKEND", "tensorflow")

import keras
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, f1_score

ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
MODEL_PATH = ARTIFACT_DIR / "genre_mlp.keras"
METADATA_PATH = ARTIFACT_DIR / "metadata.json"

with np.load(PREPARED_PATH) as data:
    test_ids = data["test_ids"]
    x_test = data["X_test"]
    y_test = data["y_test"]

with METADATA_PATH.open("r", encoding="utf-8") as file:
    metadata = json.load(file)

class_names = np.array(metadata["class_names"], dtype=str)
model = keras.saving.load_model(MODEL_PATH)

What does this setup load?

  • The TensorFlow backend restores the saved neural model before any predictions run.
  • NumPy loads the held-out track IDs plus their standardized features and labels.
  • The metadata file restores the ordered genre names used during data preparation.
  • Matplotlib renders the confusion matrix.
  • scikit-learn calculates the evaluation metrics.
  • Run the Colab cell to write evaluate.py.
  • Confirm evaluate.py appears in the notebook's file list.

Good progress. Your evaluator can now load the untouched test arrays with the trained model.

Evaluator file missing?

  • Check that %%writefile evaluate.py is the first line in the cell.
  • Run the complete cell again if the file list still shows the older notebook state.
  • Still stuck? Help me create evaluate.py in Google Colab.
Measure class-level performance

Accuracy measures the total share of correct predictions. Macro F1 gives every genre equal weight before averaging the class scores.

  • Return to the %%writefile evaluate.py cell from earlier.
  • Paste this inference block beneath the model-loading line:
probabilities = model.predict(x_test, batch_size=256, verbose=0)
predictions = probabilities.argmax(axis=1)
confidences = probabilities.max(axis=1)

neural_accuracy = accuracy_score(y_test, predictions)
neural_macro_f1 = f1_score(y_test, predictions, average="macro")

print(f"Neural test accuracy: {neural_accuracy:.4f}")
print(f"Neural test macro F1: {neural_macro_f1:.4f}")

How are the scores calculated?

  • The model processes the test features in batches of 256 rows.
  • The argmax operation selects the most probable genre for each track.
  • The maximum probability becomes that prediction's confidence value.
  • Accuracy measures the overall proportion of correct predictions.
  • Macro F1 averages the genre-level F1 scores without favoring larger classes.
  • Run the %%writefile cell to update evaluate.py.
  • Confirm Colab reports that the file was written.

Before you run the evaluator, what range do you expect both test metrics to fall within?

  • Execute the evaluator by running this command:
%run evaluate.py

What should the metrics show?

You should see Neural test accuracy plus Neural test macro F1. Each value should fall between 0 and 1.

Evaluation not running?

  • Rerun the write cell if the evaluator finishes without printing both metrics.
  • Rerun train_neural.py if the saved model file is missing.
  • Still stuck? Help me run the neural evaluation metrics.

The two scores summarize performance across the full test set. The confusion matrix now needs to reveal where those errors occur.

  • Return to the %%writefile evaluate.py cell.
  • Paste this visualization block beneath the metric print statements:
ConfusionMatrixDisplay.from_predictions(
    y_test,
    predictions,
    labels=np.arange(len(class_names)),
    display_labels=class_names,
    normalize="true",
    values_format=".2f",
    xticks_rotation=45,
)
plt.savefig(
    ARTIFACT_DIR / "confusion_matrix.png",
    dpi=150,
    bbox_inches="tight",
)
plt.show()

How does the matrix expose mistakes?

  • Each row represents the true genre for a group of test tracks.
  • Each column represents the genre predicted by the neural model.
  • Row normalization converts each true genre's results into proportions.
  • The saved confusion_matrix.png image uses 150 DPI for reuse outside the notebook.
  • Run the %%writefile cell to save the visualization code.
  • Confirm Colab reports that evaluate.py was updated.

Before you run the updated evaluator, which genre pairs do you expect to show the strongest off-diagonal values?

  • Display the normalized confusion matrix by running:
%run evaluate.py

What should the matrix show?

You should see eight genre labels across both axes. Every row shows how one true genre is distributed across the model's predictions.

Values on the diagonal represent correct classifications. Off-diagonal values reveal the genre pairs the model confuses.

Confusion matrix missing?

  • Check that the visualization block sits below the lines that create predictions.
  • Rerun the complete write cell if the script still contains only the metric block.
  • Still stuck? Help me display and save the confusion matrix.

That matrix turns a single score into visible evidence of class-level behavior.

Preview predictions and exports

A batch preview connects aggregate metrics to individual tracks. Each row shows the model's chosen genre beside its confidence.

  • Return to the %%writefile evaluate.py cell.
  • Paste this prediction preview beneath plt.show():
print("\nConfidence-scored batch preview")
print("track_id | true genre | predicted genre | confidence")
for track_id, true_id, predicted_id, confidence in zip(
    test_ids[:12],
    y_test[:12],
    predictions[:12],
    confidences[:12],
):
    print(
        f"{int(track_id):06d} | "
        f"{class_names[int(true_id)]} | "
        f"{class_names[int(predicted_id)]} | "
        f"{float(confidence):.3f}"
    )

What does each preview row contain?

  • The first field identifies the held-out FMA track.
  • The second field shows the true top-level genre.
  • The third field shows the model's predicted genre.
  • The final field shows the largest softmax probability as a three-decimal confidence value.
  • Run the %%writefile cell to save the preview code.
  • Execute the updated evaluator by running:
%run evaluate.py

What should the preview show?

You should see 12 rows beneath Confidence-scored batch preview. Each row should contain a track ID followed by two genre names plus a confidence value.

Prediction rows incomplete?

  • Check that the preview loop uses the first 12 values from every test array.
  • Check that class_names converts both numeric genre IDs into labels.
  • Still stuck? Help me fix the confidence-scored prediction preview.

The final output should also prove that every reusable file is present. Listing the artifact directory gives you that checkpoint.

  • Return to the %%writefile evaluate.py cell.
  • Paste this artifact inventory beneath the prediction loop:
print("\nReusable artifacts")
for path in sorted(ARTIFACT_DIR.iterdir()):
    print(path.name)

Why print the artifact inventory?

The inventory confirms that evaluation used the same saved model inputs produced earlier. It also verifies that the confusion matrix image joined those reusable files.

  • Run the %%writefile cell to save the completed evaluator.
  • Confirm Colab reports that evaluate.py was updated.

Before you run the finished evaluator, which part of its output should prove that the model dependencies are ready for reuse?

  • Run the completed evaluation pipeline with this command:
%run evaluate.py

What should the finished evaluator produce?

  • You should see neural test accuracy plus macro F1 as values between 0 and 1.
  • You should see a row-normalized confusion matrix with eight genre labels.
  • You should see 12 confidence-scored test predictions.
  • You should see confusion_matrix.png listed with the model and data artifacts.

Final output missing a section?

  • Rerun the complete %%writefile evaluate.py cell if the script stops after an earlier output section.
  • Check that every added block sits inside the same write cell in the displayed order.
  • Still stuck? Help me verify the completed evaluation pipeline.

✔️ Awesome, I've got everything!

Your completed evaluator is saved. The final run now produces every metric, visualization, prediction row, and artifact check.

ⓧ I'd like to double check the full code

The complete contents of evaluate.py should match this reference.

import json
import os
from pathlib import Path

os.environ.setdefault("KERAS_BACKEND", "tensorflow")

import keras
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, f1_score

ARTIFACT_DIR = Path("artifacts")
PREPARED_PATH = ARTIFACT_DIR / "prepared_fma_small.npz"
MODEL_PATH = ARTIFACT_DIR / "genre_mlp.keras"
METADATA_PATH = ARTIFACT_DIR / "metadata.json"

with np.load(PREPARED_PATH) as data:
    test_ids = data["test_ids"]
    x_test = data["X_test"]
    y_test = data["y_test"]

with METADATA_PATH.open("r", encoding="utf-8") as file:
    metadata = json.load(file)

class_names = np.array(metadata["class_names"], dtype=str)
model = keras.saving.load_model(MODEL_PATH)

probabilities = model.predict(x_test, batch_size=256, verbose=0)
predictions = probabilities.argmax(axis=1)
confidences = probabilities.max(axis=1)

neural_accuracy = accuracy_score(y_test, predictions)
neural_macro_f1 = f1_score(y_test, predictions, average="macro")

print(f"Neural test accuracy: {neural_accuracy:.4f}")
print(f"Neural test macro F1: {neural_macro_f1:.4f}")

ConfusionMatrixDisplay.from_predictions(
    y_test,
    predictions,
    labels=np.arange(len(class_names)),
    display_labels=class_names,
    normalize="true",
    values_format=".2f",
    xticks_rotation=45,
)
plt.savefig(
    ARTIFACT_DIR / "confusion_matrix.png",
    dpi=150,
    bbox_inches="tight",
)
plt.show()

print("\nConfidence-scored batch preview")
print("track_id | true genre | predicted genre | confidence")
for track_id, true_id, predicted_id, confidence in zip(
    test_ids[:12],
    y_test[:12],
    predictions[:12],
    confidences[:12],
):
    print(
        f"{int(track_id):06d} | "
        f"{class_names[int(true_id)]} | "
        f"{class_names[int(predicted_id)]} | "
        f"{float(confidence):.3f}"
    )

print("\nReusable artifacts")
for path in sorted(ARTIFACT_DIR.iterdir()):
    print(path.name)

Strong finish. Your classifier now exposes class-level failures through visible evidence.

Each previewed track carries a confidence score. The model inputs plus evaluation image are saved for reuse.

Secret mission

Add a Confidence Gate

Your classifier currently assigns a genre to every track. Some of those predictions carry weak confidence. In this secret mission, you will route uncertain tracks to human review while preserving stronger predictions.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab's free CPU runtime, so these session files create no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The verified FMA archive at fma_metadata.zip plus the extracted fma_metadata/ directory.
  • The project scripts prepare_data.py, train_baseline.py, train_neural.py, evaluate.py, and confidence_gate.py.
  • The prepared arrays at artifacts/prepared_fma_small.npz plus the reusable metadata at artifacts/metadata.json.
  • The trained model at artifacts/genre_mlp.keras plus the learning history at artifacts/training_history.json.
  • The saved evaluation image at artifacts/confusion_matrix.png.

Keep everything running

No action is needed. Choose this if you are still comparing confidence thresholds or inspecting the classifier's predictions.

  • Leave the project files in place while the current Colab runtime remains active.
  • Revisit the existing notebook cells whenever you want to retrain the model or test another confidence threshold.
  • Preserve important outputs outside the runtime because session storage is temporary.

The free CPU runtime creates no ongoing project charge.

Pause - I'll come back to this later

A disconnected runtime releases its compute while your notebook remains available for a later session. Session storage is temporary, so preserve the files you want before disconnecting.

  • Download the five project scripts through the notebook's file browser.
  • Download every file inside artifacts/ through the notebook's file browser.
  • Disconnect the current Colab runtime using the runtime status menu in the top-right corner.
  • Reconnect the notebook when you return.
  • Rerun the Step 1 setup cells to restore the pinned packages plus the extracted FMA metadata.

Your saved copies preserve the model outputs even if Colab replaces the runtime.

Delete - I don't want to use this again

Deleting these files is permanent within this runtime. The command targets only this project's named paths.

  • Delete the extracted data plus every generated project file by running this command in a new Colab code cell:
!rm -rf artifacts fma_metadata fma_metadata.zip
!rm -f prepare_data.py train_baseline.py train_neural.py evaluate.py confidence_gate.py

What does this command remove?

  • The rm -rf command removes the artifacts directory plus the downloaded and extracted FMA metadata.
  • The rm -f command removes the five project scripts.
  • Confirm that the project paths are gone by running this command:
!ls

What should I see?

The output should no longer include artifacts, fma_metadata, fma_metadata.zip, or any of the five project scripts.

Still see a project path?

Check that its spelling matches the resource list above. Run the deletion cell again if one of the named paths remains.

Help me work out why these FMA project paths still appear after I ran the Colab cleanup commands.

Nice Work!

Nice Work!

You made it! Your Google Colab pipeline now turns Free Music Archive feature rows into confidence-scored genre predictions.

You've learned how to:

  • Prepare leakage-aware data from FMA's artist-separated splits. Fit feature scaling from training rows. Apply those statistics to validation rows. Apply them again to test rows.
  • Benchmark a Keras neural classifier against logistic regression. Compare both models with accuracy plus macro F1.
  • Diagnose class-level errors with a normalized confusion matrix. Export reusable inference artifacts for confidence-scored batch predictions.
  • Complete the optional Secret Mission by adding a confidence gate. Route uncertain predictions to Needs review using a threshold selected from validation coverage plus accepted-prediction accuracy.

Ready to quiz yourself?