Background
Sections
IntroductionModule 01 β€” Tensors 🧊01 Β· Creating Tensors 🧊02 Β· Indexing & Reshaping πŸ”ͺ03 Β· Tensor Math βž—04 Β· Device Placement πŸ–₯️⚑Module 02 β€” Autograd βš™οΈ01 Β· The Computational Graph πŸ•ΈοΈ02 Β· Backward Pass & Gradients ⬅️03 Β· Turning Autograd Off πŸ›‘04 Β· Gradient Gotchas πŸͺ€Module 03 β€” Neural Networks01 Β· The `nn.Module` Basics 🧱02 Β· Common Layers 🧩03 Β· Building a Network πŸ—οΈ04 Β· Inspecting Models πŸ”Module 04 β€” Data Handling πŸ—‚οΈ01 Β· Dataset Basics πŸ“‡02 Β· The DataLoader 🚚03 Β· Transforms 🎨04 Β· Splits & Built-in Datasets βœ‚οΈModule 05 β€” The Training Loop πŸ”01 Β· Loss Functions 🎯02 Β· Optimizers βš™οΈ03 Β· The Training Loop πŸ”04 Β· Evaluation & Metrics πŸ“ŠModule 06 β€” Saving & Loading πŸ’Ύ01 Β· `state_dict` Basics πŸ’Ύ02 Β· Checkpoints & Resuming ⏸️03 Β· Loading for Inference πŸš€04 Β· Best Model & Early Stopping πŸ…Module 07 β€” Computer Vision πŸ‘οΈ01 Β· Convolutions πŸ”²02 Β· CNN Architecture πŸ›οΈ03 Β· Transfer Learning πŸ”04 Β· Image Classification Project πŸ§ͺModule 08 β€” NLP & Transformers πŸ’¬01 Β· Text Data & Tokenization πŸ”€02 Β· Embeddings 🧭03 Β· Recurrent Layers & LSTMs πŸ”„04 Β· Intro to Transformers ⚑Module 09 β€” Ecosystem: PyTorch Lightning ⚑01 Β· Why Lightning? πŸ€”02 Β· The LightningModule 🧩03 Β· The Trainer πŸŽ›οΈ04 Β· DataModules & Callbacks 🧰Module 10 β€” Deployment & Optimization πŸš€01 Β· Exporting Models πŸ“¦02 Β· `torch.compile` ⚑03 Β· Inference Optimization πŸͺΆ04 Β· Serving & Next Steps πŸŽ“

04 Β· Image Classification Project πŸ§ͺ

5 min read

Time to put it all together. This is a complete, runnable image classifier that uses every skill from Phases 1–3 β€” your first real portfolio piece.


1. The 'Why'

Concepts learned in isolation don't stick until you've wired them together into something that actually runs. Every sub-module so far has handed you one puzzle piece β€” tensors, a model, a data pipeline, a training loop, checkpointing, convolutions β€” but the understanding that lasts comes from assembling those pieces into a working whole and watching a model learn end to end. This capstone does exactly that: we build a complete FashionMNIST classifier from data loading to final evaluation, and every step is something you've already met. Seeing the full pipeline in one place is what turns a collection of techniques into a mental template you can reach for on your next project.

We use FashionMNIST β€” 70,000 grayscale 28Γ—28 images of clothing in 10 categories β€” because it's a built-in dataset (Module 04), trains in minutes on a laptop, yet is hard enough to be interesting (unlike plain digit MNIST). The plan is the standard workflow you'll reuse forever: load and split the data, build the CNN from Module 07, train it with the loop from Module 05, save the best model per Module 06, and finally report test accuracy with the metrics from Module 05. Follow it once here and you'll have both a runnable project to point to and a repeatable recipe for any classification task you meet in the wild.


2. Core Concepts

The end-to-end recipe. Data (Dataset/DataLoader + transforms) β†’ model (CNN) β†’ loss + optimizer β†’ training loop with validation β†’ best-model checkpoint β†’ final test evaluation. Nothing new β€” just orchestration.

Device-agnostic code. Define one device and move the model and every batch to it (Modules 01, 05). The same script then runs on CPU, CUDA, or MPS.

Train/validation/test roles. Train fits the weights; validation picks the best epoch and guards against overfitting; test is the one-time honest score (Modules 04–05).

Reusing your helpers. The train_one_epoch and evaluate functions from Module 05 drop straight in β€” a sign your earlier code was structured well.


3. Code in Action

Data: load, split, batch

import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split

device = "cuda" if torch.cuda.is_available() else "cpu"

tfms = transforms.Compose([
    transforms.ToTensor(),                          # -> (1, 28, 28) float in [0,1]
    transforms.Normalize((0.2860,), (0.3530,)),     # FashionMNIST mean/std
])

full_train = datasets.FashionMNIST("./data", train=True,  download=True, transform=tfms)
test_ds    = datasets.FashionMNIST("./data", train=False, download=True, transform=tfms)

# Carve a validation set out of the training data (reproducibly)
gen = torch.Generator().manual_seed(42)
train_ds, val_ds = random_split(full_train, [55000, 5000], generator=gen)

train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
val_loader   = DataLoader(val_ds,   batch_size=64, shuffle=False)
test_loader  = DataLoader(test_ds,  batch_size=64, shuffle=False)

Model, loss, optimizer

import torch.nn as nn

class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),  # -> 32x14x14
            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # -> 64x7x7
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Dropout(0.25),
            nn.Linear(128, num_classes),
        )
    def forward(self, x):
        return self.classifier(self.features(x))

model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

Train with validation + best-model saving, then test

import torch

def train_one_epoch(model, loader):
    model.train(); total = 0.0
    for X, y in loader:
        X, y = X.to(device), y.to(device)
        optimizer.zero_grad()
        loss = criterion(model(X), y)
        loss.backward(); optimizer.step()
        total += loss.item()
    return total / len(loader)

@torch.no_grad()
def evaluate(model, loader):
    model.eval(); correct = total = 0
    for X, y in loader:
        X, y = X.to(device), y.to(device)
        correct += (model(X).argmax(1) == y).sum().item()
        total += y.size(0)
    return correct / total

best_acc = 0.0
for epoch in range(10):
    tr_loss = train_one_epoch(model, train_loader)
    val_acc = evaluate(model, val_loader)
    if val_acc > best_acc:                         # keep the best (Module 06)
        best_acc = val_acc
        torch.save(model.state_dict(), "best_fashion_cnn.pt")
    print(f"epoch {epoch}: train_loss {tr_loss:.3f} | val_acc {val_acc:.3f}")

# Final, one-time test on the BEST model
model.load_state_dict(torch.load("best_fashion_cnn.pt", map_location=device))
print(f"Test accuracy: {evaluate(model, test_loader):.3f}")   # expect ~0.90+

4. Common Pitfalls

Pitfall 1 β€” Wrong normalization constants. FashionMNIST has its own mean/std (~0.286 / 0.353), not digit-MNIST's. Copy-pasting the wrong constants trains a slightly worse model. Use the dataset's own statistics (and the same ones at test time).

Pitfall 2 β€” Evaluating on the training set. It's tempting to report the high training accuracy, but that measures memorization. Report test accuracy from the held-out set, and only after model selection is done on validation.

Pitfall 3 β€” Leaving the model in train() mode for testing. With Dropout active, your test numbers become noisy and pessimistic. The evaluate helper calls model.eval() under @torch.no_grad() β€” keep that discipline (Modules 03, 05).


5. Further Reading & Watch List


⬅️ Prev: 03 Β· Transfer Learning Β· 🏁 Module complete! Back to the module index or on to Module 08 β€” NLP & Transformers.