Part 5 — Classification & Neural Architectures Lesson 27 / 56
Estimated Reading: 22–25 mins Level: Intermediate

Transfer Learning & Pretrained CNNs: ResNet, Fine-Tuning & Image Classification

In the previous lesson, we built a Convolutional Neural Network from scratch to classify handwritten digits. However, in real-world engineering, training modern deep vision models from scratch requires millions of annotated images, multi-GPU clusters, and weeks of compute. In this lesson, we master Transfer Learning: taking vision backbones pretrained on massive visual corpuses (like ImageNet) and adapting them to custom downstream tasks using Feature Extraction, Fine-Tuning, and Residual Networks (ResNet).

What You Will Master

  • The Transfer Learning Paradigm: Why visual representations generalize and how to overcome data scarcity.
  • Pretrained Model Zoo & ImageNet: Architectural trade-offs across ResNet, VGG, MobileNet, and EfficientNet.
  • Feature Extraction vs Fine-Tuning: Mathematical formulation, parameter dynamics, and staged warmup protocols.
  • ResNet & Skip Connections: The degradation problem and mathematical formulation of residual learning ($H(x) = F(x) + x$).
  • Torchvision Modern Weights API: Loading backbones, inspecting feature dimensions, and replacing classification heads.
  • Layer Freezing & Differential Learning Rates: Selectively unfreezing blocks and configuring multi-parameter group optimizers.
  • Production Image Preprocessing: Matching ImageNet normalization statistics and applying training augmentations.
  • End-to-End Flower Classifier: Training, validation checkpointing, and evaluation on Oxford 102 Flowers dataset.

1. The Cold Start Problem: Why Train from Scratch Fails

Imagine you are tasked with building a computer vision system to detect rare agricultural plant diseases, identify industrial circuit board defects, or classify cancer cells in histopathology slides. In these real-world domains, you rarely possess millions of neatly labeled images. More likely, you have only $200$ to $2,000$ training images.

If you initialize a deep CNN with random Gaussian weights and train it on such a small dataset, two catastrophic failure modes occur:

1. Severe Overfitting & Memorization

A standard vision model like ResNet50 contains over $25.5$ million learnable parameters. With only a few hundred training samples, the network has vastly more degrees of freedom than constraints. It trivially memorizes background noise and individual training images, yielding $100\%$ training accuracy and near-random test accuracy.

2. Astronomical Computational Barrier

Training deep networks from random initialization to high accuracy on large datasets requires days or weeks on multi-GPU clusters, consuming thousands of dollars in cloud electricity. Small teams and edge developers cannot afford to train foundational models from scratch for every new project.

The Core Insight of Transfer Learning

Human vision does not relearn what an edge, corner, shadow, or texture is every time we see a new species of animal. Visual features are fundamentally hierarchical and compositional. Transfer Learning leverages this insight: we take a network that spent weeks mastering visual representations on millions of images (such as ImageNet) and transfer that rich visual knowledge to our custom target task.

2. Pretrained Models & The ImageNet Benchmark

A pretrained model is a neural network whose weights have already been optimized by training on a large, diverse dataset. Instead of starting gradient descent from random noise, we initialize our network at a highly refined point in parameter space where universal visual features are already encoded.

In computer vision, the canonical source dataset is ImageNet (ILSVRC), comprising over $1.28$ million training images across $1,000$ distinct object classes (ranging from animals and plants to everyday tools, vehicles, and structures).

Architecture Parameters Top-1 Acc (ImageNet) GFLOPs Key Design Characteristic & Best Use Case
ResNet18 11.7M 69.8% 1.82 Lightweight residual network; rapid prototyping, edge deployment, fast baselines.
ResNet50 25.6M 80.8% 4.12 Industry workhorse with $1\times 1$ bottleneck blocks; optimal balance of speed and accuracy.
VGG16 138.4M 71.6% 15.50 Uniform $3\times 3$ conv stacks; excellent for perceptual loss / style transfer, but massive memory footprint.
MobileNetV3-S 2.5M 67.7% 0.06 Depthwise separable convolutions + inverted bottlenecks; sub-millisecond mobile and IoT inference.
EfficientNet-B0 5.3M 77.1% 0.39 Compound scaling of depth, width, and resolution; state-of-the-art parameter efficiency.

3. Hierarchical Feature Learning & The Anatomy of Transfer

Why are features learned on ImageNet (recognizing dogs, trucks, and teapots) capable of classifying medical X-rays or rare flower species? The answer lies in the depth hierarchy of convolutional representations:

Layer 1–2 (Early) Universal Low-Level Primitives

Gabor-like directional edge filters, oriented gradients, color opponency, corners. These exist identically in natural scenes, medical radiology, and text documents.

Layer 3 (Middle) Mid-Level Geometric Motifs

Textures, curves, honeycomb lattices, intersecting lines, circular contours, and surface patterns. Shared widely across visual domains.

Layer 4 + Head (Deep) High-Level Task Semantics

Domain-specific object parts: dog muzzles, car headlights, flower petals. The final head projects these to the original 1,000 ImageNet logit categories.

When applying transfer learning to a new target dataset (e.g. 5 flower species), we keep the feature extractor backbone (which already knows edges, textures, and curves) and replace only the final classification head with a new linear projection layer tailored to our $5$ target classes.

4. Feature Extraction vs. Fine-Tuning: Deep Comparative Analysis

There are two primary paradigms for adapting a pretrained network to a new task:

Strategy A: Feature Extraction

We treat the pretrained backbone as an immutable, deterministic feature extractor. All convolutional parameters are frozen (requires_grad = False). Only the newly initialized linear classification head is updated via backpropagation.

Backbone Weights: FROZEN (requires_grad = False)
Classification Head: TRAINABLE (requires_grad = True)
Optimizer: Updates only head parameters
  • ✓ Lightning Fast: Only $\approx 50\text{k}$ weights updated instead of millions.
  • ✓ Zero Overfitting: Highly robust even with tiny datasets ($N \lt 500$).
  • ✗ Rigid Features: Cannot adapt low-level representations to strange domains.
Strategy B: Fine-Tuning

We unfreeze some or all layers of the pretrained backbone, allowing their weights to subtly adapt to the new target distribution. Because pretrained weights already reside in a high-performing valley, we use an ultra-small learning rate ($\eta \approx 10^{-4} \text{ or } 10^{-5}$) to prevent shattering the learned representations.

Early Backbone: FROZEN (Layers 1–3)
Late Backbone: UNFROZEN (Layer 4, Small lr)
Classification Head: TRAINABLE (10x larger lr)
  • ✓ Maximum Accuracy: Features adapt directly to nuanced target motifs.
  • ✗ Catastrophic Forgetting Risk: High learning rate will ruin pretrained weights.
  • ✗ Compute Intensive: Requires full backpropagation passes through deep conv layers.
Property Feature Extraction Fine-Tuning (Partial / Full)
Pretrained Weights State 100% Frozen (requires_grad = False) Selected blocks or all unfrozen (requires_grad = True)
Trainable Parameters (ResNet18) $\approx 2,560$ (for 5 classes) $\approx 8.4\text{M}$ (layer4 + fc) up to $11.2\text{M}$
Optimal Dataset Size Small to medium ($100 \le N \le 2,000$) Medium to large ($N \ge 2,000$)
Learning Rate Configuration Single standard lr ($\eta = 10^{-3}$) on head Differential lr: $\eta_{\text{backbone}} \approx 10^{-5}, \eta_{\text{head}} \approx 10^{-3}$
Training Time & Memory Minimal (no conv backward passes) Substantial ($3\times$ to $5\times$ more VRAM and time)
Industry Best Practice: The 2-Stage Warm-Up Protocol

Never begin fine-tuning on day one with an unfrozen backbone and a randomly initialized classification head. In epoch 1, the raw random head outputs garbage logits, producing massive loss gradients that immediately shatter the fragile pretrained backbone weights.

Stage 1 (Warmup): Freeze the backbone. Train only the new classification head for 3–5 epochs until head loss stabilizes.
Stage 2 (Fine-Tune): Unfreeze deeper blocks (e.g. layer4) and train jointly using a $10\times$ smaller learning rate.

5. Understanding ResNet & The Residual Shortcut Breakthrough

Before Kaiming He et al. introduced ResNet (Residual Networks) in 2015, deep learning suffered from a mystifying barrier: plain neural networks with 56 layers performed significantly worse than 20-layer networks on both training and test data!

The Degradation Paradox

This was not standard overfitting (where training error drops while test error spikes). Here, training error itself degraded. Theoretically, a 56-layer network should easily match a 20-layer network by simply learning identity mappings ($f(x) = x$) for the additional 36 layers. Yet stochastic gradient descent failed to optimize plain stacked layers because gradients vanished across continuous matrix multiplications.

The Residual Formulation:

Instead of expecting stacked layers to learn a direct non-linear mapping $H(x)$, ResNet reformulates the objective so that the convolutional layers approximate a residual mapping $F(x) = H(x) - x$. The original input $x$ is reintroduced via an additive identity skip connection:

$$H(x) = F(x) + x$$

Why is this formulation mathematically revolutionary during backpropagation? Consider computing the gradient of the loss $\mathcal{L}$ with respect to the input $x$:

$$\frac{\partial \mathcal{L}}{\partial x} = \frac{\partial \mathcal{L}}{\partial H} \cdot \frac{\partial H}{\partial x} = \frac{\partial \mathcal{L}}{\partial H} \cdot \left(\frac{\partial F(x)}{\partial x} + 1\right) = \frac{\partial \mathcal{L}}{\partial H} \cdot \frac{\partial F(x)}{\partial x} + \frac{\partial \mathcal{L}}{\partial H}$$

Notice the term $\mathbf{+ \frac{\partial \mathcal{L}}{\partial H}}$! Even if the convolutional path $\frac{\partial F(x)}{\partial x}$ approaches zero (due to vanishing gradients or tiny weights), the gradient flows directly backward through the $+1$ identity shortcut unaltered. The identity connection forms an uninterrupted gradient superhighway, allowing networks with 152 or even 1,000 layers to train smoothly.

BasicBlock (ResNet18 / ResNet34)

Consists of two consecutive $3 \times 3$ convolutional layers with Batch Normalization and ReLU:

x → [Conv 3×3 → BN → ReLU → Conv 3×3 → BN] + x → ReLU
BottleneckBlock (ResNet50 / 101 / 152)

To prevent computational explosion in very deep networks, uses three layers:

x → [Conv 1×1 (compress) → Conv 3×3 → Conv 1×1 (expand)] + x
Compresses channel depth by $4\times$, performs conv, then expands channels back.

6. Torchvision Modern Weights API & Head Replacement

In modern PyTorch (Torchvision $\ge 0.13$), pretrained models are loaded using the Weights Enum API rather than the deprecated boolean flag (pretrained=True):

import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights, resnet50, ResNet50_Weights

# 1. Load ResNet18 with recommended default ImageNet weights
weights = ResNet18_Weights.DEFAULT
model = resnet18(weights=weights)

# 2. Inspect the original classification head
print(model.fc) # Linear(in_features=512, out_features=1000, bias=True)
in_features = model.fc.in_features # 512 for ResNet18/34, 2048 for ResNet50

# 3. Replace classification head for a 5-class target task
num_target_classes = 5
model.fc = nn.Linear(in_features, num_target_classes)
# The new head is automatically initialized with random weights and requires_grad=True!
Replacing Heads Across Different Architectures

Different model families name their final classification layer differently:

  • ResNet / ShuffleNet: model.fc = nn.Linear(model.fc.in_features, num_classes)
  • VGG / AlexNet: model.classifier[6] = nn.Linear(4096, num_classes)
  • MobileNet / EfficientNet: model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes)
  • DenseNet: model.classifier = nn.Linear(model.classifier.in_features, num_classes)

7. Layer Freezing Mechanics & Differential Learning Rates

In PyTorch, whether a parameter receives gradient updates during backpropagation is governed by the boolean property param.requires_grad.

# Step 1: Freeze all parameters in the entire network
for param in model.parameters():
    param.requires_grad = False

# Step 2: Unfreeze only the final residual block (layer4) for partial fine-tuning
for param in model.layer4.parameters():
    param.requires_grad = True

# Step 3: Replace classification head (new layer has requires_grad=True by default)
model.fc = nn.Linear(model.fc.in_features, 5)

# Step 4: Configure Differential Learning Rates in Optimizer
optimizer = torch.optim.Adam([
    {"params": model.layer4.parameters(), "lr": 1e-4}, # Gentle update for pretrained convs
    {"params": model.fc.parameters(), "lr": 1e-3} # Aggressive learning for randomly initialized head
], weight_decay=1e-4)
The Batch Normalization Nuance in Transfer Learning

Standard nn.BatchNorm2d layers maintain non-parametric running mean and running variance statistics that update during model.train() even when requires_grad = False. If your target dataset has small batch sizes ($N=8$ or $16$), noisy batch statistics will drift and corrupt the pretrained feature representations!

Production Fix: When doing feature extraction, keep the frozen backbone explicitly in evaluation mode (backbone.eval()) while training only the head (model.fc.train()).

8. Image Preprocessing & Augmentation Pipelines

Pretrained models expect incoming images to match the exact mathematical distribution of the source dataset. ImageNet models require RGB images standardized with canonical channel statistics:

$$\mu = [0.485, 0.456, 0.406], \quad \sigma = [0.229, 0.224, 0.225]$$
Training Transform (Stochastic Augmentation)
transforms.Compose([
  transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
  transforms.RandomHorizontalFlip(),
  transforms.RandomRotation(15),
  transforms.ColorJitter(brightness=0.2, contrast=0.2),
  transforms.ToTensor(),
  transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
])
Validation/Test Transform (Deterministic)
transforms.Compose([
  transforms.Resize(256),
  transforms.CenterCrop(224),
  transforms.ToTensor(),
  transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
])

9. End-to-End Implementation: Oxford 102 Flowers Classifier

Let us construct an end-to-end, production-grade transfer learning pipeline to classify Oxford 102 Flowers using a pretrained ResNet18 model:

import copy
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from torchvision.models import resnet18, ResNet18_Weights

# 1. Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 2. Preprocessing pipelines
weights = ResNet18_Weights.DEFAULT
val_transform = weights.transforms()
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 3. Load Oxford 102 Flowers Datasets
train_ds = datasets.Flowers102(root="data", split="train", download=True, transform=train_transform)
val_ds = datasets.Flowers102(root="data", split="val", download=True, transform=val_transform)
test_ds = datasets.Flowers102(root="data", split="test", download=True, transform=val_transform)

train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=0)
val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=0)
test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=0)

# 4. Instantiate Model & Freeze Backbone
model = resnet18(weights=weights)
for param in model.parameters():
    param.requires_grad = False

# Replace classification head with 102 classes
model.fc = nn.Linear(model.fc.in_features, 102)
model = model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)

# 5. Training Loop with Validation Checkpointing
best_acc = 0.0
best_state = copy.deepcopy(model.state_dict())

for epoch in range(5):
    model.train()
    model.eval() # Keeps backbone BatchNorm running stats frozen
    model.fc.train() # Enables training for classifier head
    running_loss, correct, total = 0.0, 0, 0
    for imgs, targets in train_loader:
        imgs, targets = imgs.to(device), targets.to(device)
        optimizer.zero_grad()
        logits = model(imgs)
        loss = criterion(logits, targets)
        loss.backward()
        optimizer.step()
        running_loss += loss.item() * imgs.size(0)
        correct += (logits.argmax(dim=1) == targets).sum().item()
        total += targets.size(0)

    # Validation
    model.eval()
    v_corr, v_tot = 0, 0
    with torch.no_grad():
        for v_imgs, v_targs in val_loader:
            v_imgs, v_targs = v_imgs.to(device), v_targs.to(device)
            preds = model(v_imgs).argmax(dim=1)
            v_corr += (preds == v_targs).sum().item()
            v_tot += v_targs.size(0)
    val_acc = v_corr / v_tot
    print(f"Epoch {epoch+1}/5 | Train Acc: {correct/total*100:.2f}% | Val Acc: {val_acc*100:.2f}%")
    if val_acc > best_acc:
        best_acc = val_acc
        best_state = copy.deepcopy(model.state_dict())

# 6. Restore Best Weights and Evaluate on Held-Out Test Set
model.load_state_dict(best_state)
model.eval()
t_corr, t_tot = 0, 0
with torch.no_grad():
    for t_imgs, t_targs in test_loader:
        t_imgs, t_targs = t_imgs.to(device), t_targs.to(device)
        t_corr += (model(t_imgs).argmax(dim=1) == t_targs).sum().item()
        t_tot += t_targs.size(0)
print(f"Final Test Accuracy: {t_corr / t_tot * 100:.2f}%") # ~92%+ on 102 flower categories!

10. The 2×2 Transfer Learning Decision Framework

Which transfer learning strategy should you adopt for your custom problem? The decision is dictated by two independent variables: Dataset Size and Domain Similarity relative to ImageNet:

Quadrant 1: Small Dataset + High Domain Similarity

Examples: 200 dog breeds, flowers, everyday kitchen objects.
Recommended Strategy: Pure Feature Extraction. Freeze the entire backbone, train only the linear classification head. Prevents catastrophic overfitting while leveraging pre-existing high-level object representations.

Quadrant 2: Large Dataset + High Domain Similarity

Examples: 100,000 eCommerce product catalog images, wildlife trap imagery.
Recommended Strategy: 2-Stage Fine-Tuning. Warm up the classifier head first, then unfreeze all deeper residual blocks (layer3 and layer4) with a small learning rate.

Quadrant 3: Small Dataset + Low Domain Similarity

Examples: 400 medical CT scans, industrial thermal infrared, satellite SAR.
Recommended Strategy: Extract Features from Earlier Layers (layer2 or layer3) or train an SVM/Linear classifier on mid-level representations. Deep ImageNet semantic features are irrelevant, but early edge and texture filters transfer reliably.

Quadrant 4: Large Dataset + Low Domain Similarity

Examples: 200,000 multi-spectral satellite imagery tiles, whole slide histology scans.
Recommended Strategy: Full Network Fine-Tuning or Pretrained Initialization. Use pretrained weights merely as a smart starting point, then train all layers end-to-end.

11. Top 10 Common Transfer Learning Pitfalls & Debugging Matrix

# Frequent Pitfall Root Cause & Consequence Correct Architectural Fix
1 Unreplaced Head Predicting 1,000 ImageNet logits on a 5-class problem. Loss crashes or targets out of range. Replace model.fc = nn.Linear(in_features, num_classes).
2 Forgotten Backbone Freeze Accidentally backpropagating through 25M weights with large initial lr. Set param.requires_grad = False on all backbone parameters.
3 Uniform High Learning Rate Using $\eta = 10^{-3}$ on unfrozen conv layers shatters pretrained representations in 1 epoch. Use differential lr: $\eta_{\text{conv}} \le 10^{-4}, \eta_{\text{head}} \approx 10^{-3}$.
4 Normalization Inversion Omitting ImageNet mean/std causes severe input distribution shift. Apply transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]).
5 BatchNorm Statistics Drift Running stats update on small mini-batches ($B \le 16$), degrading feature quality. Keep backbone in eval mode (model.eval()) during feature extraction.
6 Premature Fine-Tuning Fine-tuning before new head has converged sends chaotic gradients backward. Warm up head for 3–5 epochs before unfreezing deeper layers.
7 Augmenting Validation Set Random crops on validation data introduce stochastic evaluation jitter. Keep validation transforms strictly deterministic (Resize → CenterCrop).
8 Optimizer Memory Bloat Passing model.parameters() tracks Adam momentum states for frozen weights. Pass only filter(lambda p: p.requires_grad, model.parameters()).
9 Selecting Final Checkpoint Overfitting in later epochs produces sub-optimal final weights. Save and reload the model checkpoint with highest validation accuracy.
10 Test Leakage During Tuning Using the test set to decide when to stop training biases benchmark results. Evaluate test set strictly once after all modeling decisions are complete.

Practical Assignment: Fine-Tuning ResNet18 on Custom Vision Data

Select an image classification dataset with at least 5 distinct categories (e.g. Oxford Flowers, Oxford-IIIT Pets, or Stanford Dogs) and complete the full transfer learning evaluation:

1 Pipeline Setup: Ingest dataset with 70/15/15 train/val/test splits. Configure stochastic training transforms and deterministic validation transforms with ImageNet normalization.
2 Phase 1 (Feature Extraction): Freeze backbone, replace classification head, and train for 5 epochs using Adam(lr=0.001). Record best validation accuracy.
3 Phase 2 (Partial Fine-Tuning): Unfreeze layer4. Apply differential learning rates ($\eta_{\text{layer4}} = 10^{-4}, \eta_{\text{fc}} = 10^{-3}$) and train for 5 additional epochs.
4 Comparative Analysis: Plot training/validation loss curves for both phases, document the accuracy gap, and compute parameter reduction vs training from scratch.
5 Model Persistence: Save the best validation checkpoint using torch.save(model.state_dict(), ...) and verify clean reloading.

Transfer Learning & Pretrained Vision Workbench

Explore layer freezing dynamics, residual skip connections, strategy selection matrices, and model zoo architectures in real time.

Interactive Labs

Interactive Layer Freezing & Parameter Flow

Toggle transfer strategies to observe layer freezing states, gradient flow, and parameter count dynamics.

Target Classes:
Trainable Params 2,565 Params
Frozen Params 11,176,512 Params
Est. VRAM Footprint ~45 MB
Overfitting Risk Very Low

Core Lesson Takeaways

  • Visual Feature Transfer: Pretrained backbones already recognize low-level edges, textures, and motifs, eliminating the need for massive datasets.
  • Strategy Selection: Use Feature Extraction for small datasets ($N \lt 2,000$) and Fine-Tuning for larger datasets or divergent domains.
  • Residual Superhighway: The identity connection $H(x) = F(x) + x$ ensures gradients flow backward with a $+1$ term, solving the vanishing gradient and degradation problems.
  • Differential Learning Rates: Update unfrozen convolutional layers with small rates ($\eta \approx 10^{-5}$) and the newly initialized classification head with $10\times$ larger rates.
  • BatchNorm Stability: Keep frozen backbone BatchNorm layers in evaluation mode during training to prevent running statistic corruption.
  • Strict Preprocessing Match: Always normalize target images with canonical ImageNet mean $[0.485, 0.456, 0.406]$ and std $[0.229, 0.224, 0.225]$.