February 24, 2026
Last time: ResNets solved the depth problem, augmentation filled the invariance gap, and we reframed CNNs as representation learners.
We took a cat vs. dog backbone and transferred it to CIFAR-10 — features trained on two classes helped classify ten.
Today: we go deeper on why this works and exploit it fully with pretrained backbones from ImageNet.
Last lecture’s key reframe: a trained CNN backbone is an embedding function.
\[\phi: \mathbb{R}^{H \times W \times 3} \to \mathbb{R}^{d}\]
It takes a 224×224×3 image (150,528 numbers with no useful structure) and maps it to a 512-dimensional vector.
Similar images land near each other. Different images land far apart.
A CNN backbone maps images to vectors where “Persian cat” is near “Siamese cat” and far from “Boeing 747.”
The backbone is an image embedding model.
The classification head is just a linear classifier on top of the embeddings — the least interesting part.
A good embedding has three properties:
Images that share visual concepts land near each other
Images that differ in visually meaningful ways land far apart
Downstream tasks become simple linear operations on the embedding
Our cat vs. dog backbone learned good general features — edges, textures, shapes.
But its later layers were tuned specifically for distinguishing cats from dogs.
“Pointy ears” is a highly informative dimension.
“Has wings” is not — the network never needed to detect wings.
“Has wheels” is not — the network never saw a car.
Transfer to CIFAR-10 worked because low-level features are universal. But the embedding wasn’t optimized for broader visual understanding.
If we trained on a massive, diverse dataset — thousands of categories spanning animals, vehicles, food, scenes, tools, plants — the embedding would need to capture a much richer vocabulary:
“Has wings” would matter (birds vs. dogs)
“Has wheels” would matter (trucks vs. horses)
“Is shiny” would matter (metal vs. fur)
“Has text on it” would matter (street signs vs. landscapes)
The resulting embedding would be a general-purpose visual representation — useful for almost any downstream image task.
Early-layer features are universal. Edge detectors, color gradients, Gabor-like filters. These are determined by image physics, not by the task. Every CNN trained on natural images converges to similar early features — they look like primate visual cortex (V1).
Middle-layer features are broadly useful. Textures, corners, simple shapes. These appear across many visual domains.
Late-layer features are task-specific. “Cat face detector” doesn’t help with airplanes. “Wheel detector” doesn’t help with flowers.
The further you go from the input, the more task-specific the features become.
The early-layer features of a CNN are determined by the statistical regularities of natural images, not by the downstream task.
Edges, color gradients, and Gabor-like filters emerge because all natural images share the same local structure
They’re produced by the same physics — light reflecting off surfaces, boundaries between objects and backgrounds
This is why transfer worked at all. This is why a cat vs. dog backbone has features useful for recognizing airplanes.
Our cat vs. dog backbone: 15,000 images, 2 classes, narrow domain.
What we want: a backbone trained on a dataset so broad and diverse that the resulting embeddings capture most of what matters about visual content.
That backbone exists. The dataset it was trained on is called ImageNet.
ImageNet (ILSVRC): 1.2 million training images, 1,000 categories.
Categories span a huge range: hundreds of dog breeds, hundreds of bird species, vehicles, instruments, food, scenes, household objects.
Think about what training on this forces the network to learn.
It can’t get by with “pointy ears vs. floppy ears” — it needs to distinguish 120 dog breeds from each other, AND from 1,000 other things.
It needs edges, textures, material properties, shapes, parts, spatial relationships, color patterns — a comprehensive visual vocabulary.
The resulting embedding isn’t tuned for any single task. It’s tuned for visual understanding generally.
The ImageNet Large Scale Visual Recognition Challenge drove the deep learning revolution in computer vision.
2012: AlexNet (8 layers) — first deep CNN to win, cut error from 25.8% to 16.4%
2014: VGG (19 layers) — our design template, 7.3% error
2015: ResNet (152 layers) — 3.6% error. Better than human performance on this task.
A trained ImageNet model doesn’t just get the right answer — its top-5 guesses reveal what the embedding “sees.”
Even the “wrong” guesses are visually sensible. A grille looks like a convertible. A cherry looks like a grape.
The embedding has learned a perceptual similarity space — not just classification boundaries.
PyTorch’s torchvision.models gives you access to dozens of pretrained backbones — all trained on ImageNet, all free to use.
Each model was trained for days or weeks on clusters of GPUs with 1.2 million images.
You get all of that investment — all of those learned features — in a single line of code.
Bottom layers = universal feature detectors (edges, textures).
Top layers = task-specific classifiers.
Strategy: keep the bottom, replace the top.
Take a ResNet-50 pretrained on ImageNet.
Throw away the final classification head (nn.Linear(2048, 1000)).
Attach a new head for your task (e.g., nn.Linear(2048, 7) for FER2013 emotions).
The backbone provides the embedding. The new head learns to classify using that embedding.
Simplest approach: freeze all pretrained weights, train only the new head.
The backbone is a fixed feature extractor — no gradients flow through it.
This is exactly what we did last lecture with our cat vs. dog backbone on CIFAR-10 — but now with a much better backbone.
The Razavian et al. (2014) result was stunning:
Take an ImageNet-trained CNN. Freeze it. Extract features. Train an SVM on top.
Beat the prior state of the art on Objects, Scenes, Birds, Flowers, Human Attributes, Object Attributes
No task-specific architecture. No task-specific training of the backbone. Just good embeddings + a simple classifier.
The title of the paper: “CNN Features Off-the-Shelf: An Astounding Baseline for Recognition”
They weren’t kidding.
import torchvision.models as models
# Load pretrained ResNet-50 (trained on ImageNet)
model = models.resnet50(pretrained=True)
# Freeze ALL parameters
for param in model.parameters():
param.requires_grad = False
# Replace the classification head
model.fc = nn.Linear(2048, num_classes)That’s it. Three lines. The full training pipeline is in the notebook — but the setup really is this simple.
For our datasets:
# Cat vs. Dog (binary)
model.fc = nn.Linear(2048, 2)
# FER2013 (7 emotions)
model.fc = nn.Linear(2048, 7)
# CIFAR-10 (10 classes)
model.fc = nn.Linear(2048, 10)
# Chest X-Ray Pneumonia (binary)
model.fc = nn.Linear(2048, 2)Same backbone. Different heads. Each head has only \(2048 \times K + K\) parameters.
What about FER2013 (grayscale, 48×48) or chest X-rays (grayscale)?
The ImageNet backbone expects RGB — 3 input channels. Grayscale is 1 channel. The shapes don’t match.
Solution: repeat the grayscale channel three times.
transforms.Compose([
transforms.Grayscale(num_output_channels=3), # 1ch → 3ch
transforms.Resize(224), # 48×48 → 224×224
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])We’ll use this exact transform in the notebook for both FER2013 and Chest X-Ray.
Why does this work?
The first-layer filters are looking for local spatial patterns — edges, gradients, textures.
Those patterns exist in the grayscale image just the same. An edge is an edge whether it’s in one channel or three.
Some first-layer filters are tuned to color contrasts (red vs. green). Those will see no useful signal since R=G=B everywhere. They’ll produce near-zero activations and the network learns to ignore them.
The spatial filters carry the load — and those are the majority of what layer 1 learns.
This reinforces the point: early features are universal. Edge detectors don’t care about color channels.
We don’t have to stop at a linear head.
Once we have the 2,048-dim embedding from the frozen backbone, we can feed it into a small MLP:
model.fc = nn.Sequential(
nn.Linear(2048, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, num_classes)
)This gives the classifier more capacity to learn nonlinear decision boundaries in the embedding space.
It trains in minutes because we’re only training a tiny network on 2,048-dim inputs — no convolutions, no backprop through the backbone.
Let’s go to the notebook and see how frozen ImageNet features perform across our datasets.
What to watch for:
Training time. We’re only training a tiny head — this should be fast.
Where it works great. Cat vs. Dog and CIFAR-10 are natural images, similar to ImageNet.
Where it plateaus. FER2013 and Chest X-Rays are far from ImageNet’s domain. The frozen features may not have the right vocabulary.
That gap motivates the next section: fine-tuning.
The Bayesian framing:
The pretrained weights are the prior. They encode everything the network learned about visual structure from 1.2 million images.
The new classification head is the likelihood — it maps the prior beliefs about image content to the specific task.
Feature extraction = maximum a posteriori with a very strong, very informative prior.
Feature extraction works best when:
Your target dataset is small (hundreds to low thousands of images, weak likelihood offset by strong realistic prior!)
Your target domain is similar to ImageNet (natural photos of objects, animals, scenes)
You need a quick baseline — frozen feature extraction trains in minutes, not hours
You want to compare many approaches rapidly
Feature extraction works great when the target domain is similar to ImageNet — natural photos of objects, animals, scenes.
But what about domains that look nothing like ImageNet?
Medical imaging (X-rays, histopathology slides)
Satellite imagery
Industrial defect inspection
Microscopy
The low-level features (edges, textures) still transfer. But the higher-level features (“dog face,” “sports car”) are useless for these domains.
We need to adapt the backbone.
Unfreeze some or all of the backbone layers. Train with a small learning rate.
The pretrained weights are already near a good solution — large steps would destroy the useful features we’re trying to keep.
Think of it as: the backbone starts with a good embedding and we gently adjust it to better suit our specific domain.
Imagine you hired a professional photographer who’s spent 20 years shooting wildlife.
Feature extraction = you hand them a new camera and say “shoot portraits of people, but don’t change anything about how you work.” They’ll do okay — composition skills transfer. But they won’t adjust their instincts for human faces.
Fine-tuning = you say “shoot portraits, and feel free to gently adapt your technique.” They keep their 20 years of fundamentals but adjust framing, lighting, and focus for the new subject.
Training from scratch = you hire someone who has never held a camera. Good luck.
You usually don’t need to unfreeze the whole backbone.
Early layers (edges, textures) — universal. Leave them frozen.
Later layers (object parts, compositions) — this is where ImageNet-specific features live. Unfreeze these.
New head — learning from scratch. Always unfrozen.
Only go deeper if you have a lot of target data and the domain is very different from ImageNet.
A useful mental model:
| Target domain | Target data size | Strategy |
|---|---|---|
| Similar to ImageNet | Small | Freeze everything, train head only |
| Similar to ImageNet | Large | Unfreeze last 1-2 stages + head |
| Far from ImageNet | Small | Freeze early, unfreeze last 1-2 stages + head |
| Far from ImageNet | Large | Unfreeze most/all of backbone + head |
Chest X-rays = far from ImageNet, moderate data → unfreeze later stages.
Cat vs. Dog = similar to ImageNet, decent data → frozen backbone is probably enough, but fine-tuning gives a small boost.
Use different learning rates for different parts of the network.
optimizer = optim.Adam([
{'params': model.layer1.parameters(), 'lr': 1e-6}, # early: tiny
{'params': model.layer2.parameters(), 'lr': 1e-6}, # early: tiny
{'params': model.layer3.parameters(), 'lr': 1e-5}, # mid: small
{'params': model.layer4.parameters(), 'lr': 1e-5}, # late: small
{'params': model.fc.parameters(), 'lr': 1e-3}, # head: normal
])Early features need tiny adjustments at most.
The head needs to learn its task from scratch.
The pretrained weights represent a good region of the loss landscape — the result of training on 1.2 million images.
A large learning rate would catapult the weights out of this region.
The features that took weeks to learn would be destroyed in a few gradient steps.
Fine-tuning is like nudging a well-placed boulder. You want to roll it slightly downhill, not launch it off a cliff.
Let’s go to the notebook.
We’ll fine-tune an ImageNet-pretrained ResNet:
~5,800 grayscale X-ray images, 2 classes (Normal vs. Pneumonia)
Visually very different from ImageNet — no color, no objects, completely different texture vocabulary
But edges, gradients, and intensity patterns still apply
Pay attention to:
Frozen vs. fine-tuned accuracy. The gap tells you how much the domain differs from ImageNet.
Training time. Fine-tuning is slower than frozen feature extraction — we’re backpropagating through more layers.
Overfitting risk. With only ~5,800 images and millions of unfrozen parameters, we need augmentation and early stopping. The pretrained initialization helps enormously — but it’s not a free pass.
Every student in this room has used a fine-tuned model. Every time you use ChatGPT, Claude, or any LLM chatbot:
The base model (GPT, Claude’s base) = “pretrained backbone” — trained on massive text data to learn general language representations.
Fine-tuning adapts it to follow instructions, be helpful, refuse harmful requests.
Same principle: pretrained backbone provides general representations, fine-tuning adapts to the specific task.
| Vision | Language | |
|---|---|---|
| Pretrained backbone | ResNet on ImageNet | GPT/BERT on web text |
| What it learns | Visual features (edges → objects) | Language features (syntax → semantics) |
| Frozen + head | Image classification | Text classification (sentiment, topic) |
| Fine-tuning | Medical imaging, satellite, etc. | Chatbots, code generation, etc. |
| Key insight | Early visual features are universal | Basic language structure is universal |
Transfer learning is not a vision trick. It’s a representation learning principle that applies everywhere.
Full fine-tuning of a ResNet-50: update all ~25 million parameters.
Store gradients for all of them.
Adam stores 2 extra copies of every parameter (momentum + variance).
Total memory: ~3× the model size just for optimizer state.
For ResNet-50, this is manageable (but requires essentially top-end consumer compute resources)
But the trend is toward larger models:
ViT-Large: 300M+ parameters
LLMs: billions of parameters
Multi-task deployment: 10 tasks = 10 full copies of the model
Is there a way to adapt a pretrained model without updating all the weights?
When you fine-tune a large pretrained model, measure the weight updates:
\[\Delta W = W_{\text{fine-tuned}} - W_{\text{pretrained}}\]
Empirical finding: \(\Delta W\) is low-rank.
Most of the “movement” in weight space during fine-tuning lives in a low-dimensional subspace.
The model doesn’t need to change all 25M parameters independently — the changes are highly structured and redundant.
This connects to intrinsic dimensionality: the effective number of free parameters is much smaller than the actual count.
Instead of updating the full weight matrix \(W\), decompose the update:
\[W' = W + BA\]
where \(B \in \mathbb{R}^{d \times r}\), \(A \in \mathbb{R}^{r \times d}\), and \(r \ll d\).
The original \(W\) stays frozen.
We only train \(B\) and \(A\) — two small matrices whose product is a rank-\(r\) update.
For \(r = 8\) and \(d = 512\):
Full fine-tuning: \(512 \times 512 = 262{,}144\) parameters
LoRA: \(512 \times 8 + 8 \times 512 = 8{,}192\) parameters
That’s a 32× reduction.
The adapter is a “bypass” that adds a small correction to the frozen pretrained computation.
\(W\) does the heavy lifting (frozen, no gradients)
\(BA\) learns a small task-specific adjustment (trainable)
Output = \(Wx + BAx\)
Key detail: \(B\) is initialized to zero.
\[W' = W + BA = W + 0 \cdot A = W\]
Training starts exactly at the pretrained model — no disruption.
The adapter gradually learns to add corrections as training progresses. If the pretrained model is already good enough for some layers, the adapter stays near zero for those layers.
This is a low-rank prior on the weight update.
Connection to PCA: just as PCA says “most of the variance lives in a few directions,” LoRA says “most of the useful adaptation lives in a rank-\(r\) subspace.”
The rank \(r\) is a hyperparameter:
Higher \(r\) = more capacity to adapt, more parameters
Lower \(r\) = stronger prior, fewer parameters
Typical values: \(r = 4, 8, 16\)
Even \(r = 4\) works surprisingly well — the useful adaptation really is low-dimensional.
At inference time, we can merge:
\[W' = W + BA\]
This is just a matrix addition — done once.
The resulting \(W'\) is a regular weight matrix with no special structure.
The model runs at exactly the same speed as the original.
No extra latency, no extra memory at inference.
The low-rank structure is a training trick, not a deployment cost.
| Feature Extraction | Full Fine-Tuning | LoRA | |
|---|---|---|---|
| Trainable params | Head only (~10K) | All (~25M) | Adapters (~100K-500K) |
| GPU memory | Low | High (3× model) | Moderate |
| Training time | Minutes | Hours | Tens of minutes |
| Storage per task | Head only | Full model copy | Frozen model + tiny adapter |
| Quality | Good (similar domain) | Best | Near best |
| When to use | Quick baseline, similar domain | Max accuracy, enough data | Large models, multiple tasks |
One frozen backbone + tiny adapters per task.
ResNet-50 backbone: 25M parameters (stored once)
Each LoRA adapter: ~100K–500K parameters
| Approach | 10 tasks |
|---|---|
| Full fine-tuning | 10 × 25M = 250M total |
| LoRA | 25M + 10 × 500K = 30M total |
For LLMs with billions of parameters, this difference is the difference between feasible and impossible.
We’ll apply LoRA to:
Chest X-Ray Pneumonia — compare against frozen feature extraction and full fine-tuning on the same task
HAM10000 Skin Lesion Classification — 10,000 dermoscopic images, 7 classes of skin lesions (including melanoma)
For the skin lesion dataset, 100% accuracy isn’t the goal — this is a screening tool where efficiency matters. LoRA’s speed and parameter efficiency make it the practical choice.
We’ll compare all three approaches side by side in the notebook.
To summarize everything so far:
Take a pretrained backbone (ImageNet ResNet, etc.)
Choose adaptation strategy (frozen / fine-tuned / LoRA)
Add augmentation
Train with early stopping
This covers the vast majority of practical image classification and regression tasks.
Nobody trains from scratch in 2026 unless they’re doing research or working on a fundamentally new modality.
Everything so far classifies whole images — one label per image.
But many real tasks require spatial outputs:
Object detection: where are the objects and what are they? → bounding boxes + labels
Semantic segmentation: which pixels belong to each class? → per-pixel labels
The naive approach: slide a classifier window across the image at multiple scales.
The key insight: the CNN backbone already computes a spatial grid of features.
The \(7 \times 7 \times 512\) feature map before GAP tells us where features are, not just that they’re present.
For classification, we averaged this away with GAP. For detection, we keep the spatial information and add a detection head on top.
YOLO takes a pretrained CNN backbone and replaces the single classification head with two heads:
Classification head — what’s in each grid cell?
Regression head — where exactly is the bounding box? (x, y, width, height)
One forward pass through the backbone + both heads = full detection.
That’s why it’s called “You Only Look Once” — detection as a single regression problem, not a pipeline.
The backbone is the same ResNet/VGG we’ve been studying all block.
Transfer learning applies directly — the backbone is pretrained on ImageNet.
The detection heads are trained on detection data (e.g., COCO with bounding box annotations).
Fast enough for real-time video — which is why YOLO is the standard for deployed detection systems.