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
Jr Codex Deep Learning Notes
Level: Intermediate Prerequisites: Chapter 2: CNN Architectures Time to complete: ~30 minutes
Table of Contents
- The Full Practical Workflow
- Loading and Preparing Image Data
- Building the Model
- The Training Loop
- Evaluating the Model
- Visualizing What the Network Learned
- Common Pitfalls
- 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.44. 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 andmodel.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
- Why must image normalization statistics come from the training set, not the test set?
- What two behaviors change when switching a model between
.train()and.eval()mode? - 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