Deep Learning

Convolutional Neural Networks

Image Classification in Practice

This chapter assembles every piece from Modules 1-4 into one complete, runnable image classification pipeline — directly mirroring the ML Notes' end-to-end work

JrCodex·6 min read

Jr Codex Deep Learning Notes

Level: Intermediate Prerequisites: Chapter 2: CNN Architectures Time to complete: ~30 minutes


Table of Contents

  1. The Full Practical Workflow
  2. Loading and Preparing Image Data
  3. Building the Model
  4. The Training Loop
  5. Evaluating the Model
  6. Visualizing What the Network Learned
  7. Common Pitfalls
  8. Summary & Next Steps

1. The Full Practical Workflow

This chapter assembles every piece from Modules 1-4 into one complete, runnable image classification pipeline — directly mirroring the ML Notes' end-to-end workflow discipline, applied specifically to images.

The Pipeline, End to End
─────────────────────────────────────────
  1. Load & prepare data (Section 2)
  2. Build the model (Section 3) — reusing Chapter 2's
       architecture patterns
  3. Train (Section 4) — reusing Module 3's optimizers,
       normalization, regularization
  4. Evaluate (Section 5) — reusing the ML Notes' evaluation
       discipline
  5. Inspect what was learned (Section 6)
─────────────────────────────────────────

2. Loading and Preparing Image Data

import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
 
# Normalization values are DATASET-SPECIFIC — these are CIFAR-10's known statistics
transform = transforms.Compose([
    transforms.ToTensor(),                                     # converts image to a tensor, scales to [0, 1]
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)),      # zero-mean, unit-variance per channel
])
 
train_dataset = datasets.CIFAR10(root="./data", train=True, download=True, transform=transform)
test_dataset = datasets.CIFAR10(root="./data", train=False, download=True, transform=transform)
 
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)      # Module 2, Ch.5's shuffling
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)      # NEVER shuffle test data
 
print(f"Training examples: {len(train_dataset)}")
print(f"Classes: {train_dataset.classes}")
Why Normalize With the DATASET'S OWN Statistics
─────────────────────────────────────────
  This directly parallels the ML Notes' feature scaling
  chapter — StandardScaler is fit on TRAINING data only, then
  applied to test data using the SAME fitted statistics.
  Normalizing images works identically: mean/std are computed
  ONCE, from training data, and applied consistently to both
  train and test data.
─────────────────────────────────────────

3. Building the Model

import torch.nn as nn
 
class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),
            nn.MaxPool2d(2),                                                                  # 32x32 -> 16x16
            nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),
            nn.MaxPool2d(2),                                                                  # 16x16 -> 8x8
            nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(),
            nn.MaxPool2d(2),                                                                  # 8x8 -> 4x4
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Dropout(0.5),                        # Module 3, Ch.4
            nn.Linear(128 * 4 * 4, 256), nn.ReLU(),
            nn.Linear(256, num_classes),
        )
 
    def forward(self, x):
        x = self.features(x)
        return self.classifier(x)
 
model = SimpleCNN(num_classes=10)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = model.to(device)      # Module 1, Ch.4

4. The Training Loop

import torch.optim as optim
 
criterion = nn.CrossEntropyLoss()      # Module 2, Ch.3 — expects raw logits
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-4)      # Module 3, Ch.2/4
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)            # Module 3, Ch.5
 
num_epochs = 20
for epoch in range(num_epochs):
    model.train()      # Module 3, Ch.3 — activates dropout, uses BATCH statistics for BatchNorm
    running_loss = 0.0
 
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
 
        optimizer.zero_grad()                # Module 2, Ch.6 — clear PREVIOUS gradients
        outputs = model(images)                 # forward pass
        loss = criterion(outputs, labels)          # compute loss
        loss.backward()                               # Module 2, Ch.4 — backpropagation
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)      # Module 3, Ch.6
        optimizer.step()                                    # Module 2, Ch.5 — weight update
 
        running_loss += loss.item()
 
    scheduler.step()      # update learning rate for next epoch
    print(f"Epoch {epoch+1}/{num_epochs}, Loss: {running_loss/len(train_loader):.4f}")

5. Evaluating the Model

model.eval()      # Module 3, Ch.3 — disables dropout, uses RUNNING AVERAGE statistics for BatchNorm
 
correct, total = 0, 0
all_predictions, all_labels = [], []
 
with torch.no_grad():      # disable gradient tracking — saves memory during evaluation (no backward pass needed)
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, dim=1)
 
        correct += (predicted == labels).sum().item()
        total += labels.size(0)
        all_predictions.extend(predicted.cpu().numpy())
        all_labels.extend(labels.cpu().numpy())
 
print(f"Test accuracy: {100 * correct / total:.2f}%")
 
# Reusing the ML Notes' full classification evaluation toolkit directly
from sklearn.metrics import classification_report, confusion_matrix
print(classification_report(all_labels, all_predictions, target_names=train_dataset.classes))

Every evaluation concept from the ML Notes, Module 4, Chapter 6 — precision, recall, F1, confusion matrices — applies completely unchanged here. A CNN's output is still just a classifier; only the model producing that output has changed.


6. Visualizing What the Network Learned

import matplotlib.pyplot as plt
 
# Visualize the FIRST convolutional layer's learned filters directly
first_conv_layer = model.features[0]
filters = first_conv_layer.weight.data.cpu().clone()
 
fig, axes = plt.subplots(4, 8, figsize=(12, 6))
for i, ax in enumerate(axes.flat):
    if i < filters.shape[0]:
        filter_img = filters[i].permute(1, 2, 0)              # rearrange to (H, W, channels) for plotting
        filter_img = (filter_img - filter_img.min()) / (filter_img.max() - filter_img.min())
        ax.imshow(filter_img)
    ax.axis("off")
plt.suptitle("Learned First-Layer Filters")
plt.savefig("learned_filters.png")

Early-layer filters typically resemble simple edge or color detectors — directly confirming Chapter 1, Section 3's claim about what early convolutional layers learn, made visible and concrete.


7. Common Pitfalls

Frequent Beginner Mistakes
─────────────────────────────────────────
  Forgetting model.eval() before          BatchNorm/Dropout
  evaluation:                                behave incorrectly,
                                                producing
                                                inconsistent or
                                                degraded results
                                                (Module 3, Ch.3-4)
  Forgetting optimizer.zero_grad():           gradients ACCUMULATE
                                                 across steps
                                                 instead of being
                                                 recomputed fresh,
                                                 corrupting training
  Normalizing test data with its              causes DATA LEAKAGE
  OWN statistics instead of                     (ML Notes concept)
  training data's:                               — test statistics
                                                    shouldn't influence
                                                    preprocessing
  Shuffling the TEST set:                            unnecessary,
                                                        and can make
                                                        debugging/
                                                        comparing
                                                        runs harder
─────────────────────────────────────────

8. Summary & Next Steps

Key Takeaways

  • A complete image classification pipeline combines Chapter 1-2's architecture concepts with Module 3's training techniques, evaluated with the same tools already covered in the ML Notes.
  • Image normalization statistics must be computed from training data only and applied consistently to test data, exactly like the ML Notes' feature scaling discipline.
  • Always call model.train() before training and model.eval() before evaluation — this single detail governs both dropout and batch normalization's correct behavior.
  • Visualizing learned filters provides direct, concrete evidence for the "learned feature hierarchy" concept introduced in Module 1.

Concept Check

  1. Why must image normalization statistics come from the training set, not the test set?
  2. What two behaviors change when switching a model between .train() and .eval() mode?
  3. What would happen to training if optimizer.zero_grad() were accidentally omitted from the training loop?

Next Chapter

Chapter 4: Object Detection & Segmentation


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DL Index