February 19, 2026
Last time: how to encode domain knowledge about images directly into the network architecture.
We formalized three beliefs about images, built them into convolutional layers, assembled those layers into VGG, and saw the dramatic payoff over fully connected networks.
Today: what happens when we push deeper, how to handle symmetries the architecture misses, and why the representation — not the classification — is the real product of a CNN.
Before we saw any data, we encoded three beliefs into our architecture.
| Belief | Meaning | Architectural Choice |
|---|---|---|
| Locality | Useful features are computed from small spatial neighborhoods | ??? |
| Translation Equivariance | The same feature can appear anywhere | ??? |
| Hierarchy / Compositionality | Complex features are built from simpler features | ??? |
What architectural choices encode each belief?
| Belief | Meaning | Architectural Choice |
|---|---|---|
| Locality | Useful features are computed from small spatial neighborhoods | Local receptive fields (small filters) |
| Translation Equivariance | The same feature can appear anywhere | Weight sharing (same filter at every position) |
| Hierarchy / Compositionality | Complex features are built from simpler features | Stacking layers with pooling |
A single convolutional layer encodes all three.
These aren’t engineering convenience — they follow from how the physical world produces images: light interacts locally, objects move, and parts compose into wholes.
The CNN doesn’t have more capacity than the MLP — the parameter counts are comparable.
The convolutional structure places a strong, correct prior on the kinds of functions the network can represent.
It restricts the universe of nonlinear mappings to those that respect locality and translation equivariance.
Think about what this eliminates.
An MLP connecting a 224×224 image to its first hidden layer has 38.5 million parameters — one for every pixel-to-unit connection. Most of these encode relationships between distant pixels that have nothing to do with each other.
A conv layer with 64 filters of size 3×3 on the same image? 1,728 parameters. Each one encodes a meaningful local relationship.
Same logic as Ridge regression:
| Unconstrained | Constrained | |
|---|---|---|
| Regression | OLS: uniform prior, high variance | Ridge: L2 prior, lower variance |
| Neural network | MLP: any pixel → any hidden unit | CNN: local, shared, hierarchical |
More bias (structural constraints) → dramatically less variance (fewer effectively free parameters) → better generalization when the prior matches the data.
When the prior is correct (images have local, translation-equivariant structure), the tradeoff is overwhelmingly favorable.
MLP on CIFAR-10: ~45% test accuracy.
CNN on CIFAR-10, same parameter budget: ~65% test accuracy.
When the prior is wrong, convolutions hurt.
Tabular data has no spatial locality.
Nobody uses CNNs on Kaggle tabular competitions. XGBoost wins there because it makes no spatial assumptions.
Architecture is a prior — match it to your data.
VGG (Simonyan & Zisserman, 2014) is the cleanest expression of the three beliefs.
Always use 3×3 filters.
Double the channels each time you halve the spatial dimensions.
The architecture is just this pattern repeated:
\[[\text{Conv}(3 \times 3) \to \text{BN} \to \text{ReLU}] \times B \to \text{MaxPool}(2 \times 2)\]
Each block detects features at one spatial scale.
Pooling compresses space.
The next block operates on the compressed representation — detecting combinations of the previous features over a larger effective region.
Why not use a single 7×7 filter to see a bigger region?
But stacked 3×3 is strictly better:
For a 224×224 input:
| Stage | Operations | Output Size | Channels | Eff. Receptive Field |
|---|---|---|---|---|
| Input | — | 224×224 | 3 | 1×1 |
| Block 1 | Conv×2, Pool | 112×112 | 64 | ~6×6 |
| Block 2 | Conv×2, Pool | 56×56 | 128 | ~16×16 |
| Block 3 | Conv×3, Pool | 28×28 | 256 | ~44×44 |
| Block 4 | Conv×3, Pool | 14×14 | 512 | ~100×100 |
| Block 5 | Conv×3, Pool | 7×7 | 512 | ~212×212 |
By the final block, each unit “sees” nearly the entire image — built entirely from 9-parameter filters.
After the final conv block: \(7 \times 7 \times 512 = 25{,}088\) values.
If we flatten this directly and connect to an FC layer with 512 hidden units:
\[25{,}088 \times 512 = 12.8 \text{ million parameters}\]
That’s more parameters than the entire conv backbone that built the features. Incredibly wasteful.
Key idea: Each of those 512 feature maps already represents “how much of feature \(k\) is present in this image.”
We don’t need 25,088 numbers to summarize that — we need one number per feature map.
\[\text{GAP}: \mathbf{F}^{H \times W \times C} \longrightarrow \bar{\mathbf{f}}^{C \times 1}, \quad \text{where } \bar{f}_k = \frac{1}{HW}\sum_{i=1}^{H}\sum_{j=1}^{W} F_{ijk}\]
The classifier becomes nn.Linear(512, K). For 10 classes: 5,120 parameters vs. 12.8 million.
Each pooling step halves the spatial dimensions while the number of channels grows:
The funnel architecture creates the conditions for hierarchical feature construction. Training fills in the content in the optimal way.
We’ll now see VGG in action.
Carefully constructed depth trumps width/dumb depth every time in terms of accuracy.
But it costs a lot. Even with GPU acceleration, VGG takes substantially more wall-clock time than a shallow CNN or MLP.
The convolution operation at each position is cheap (9 multiplies for a 3×3 filter). But we’re doing it at every spatial position, across every filter, at every layer.
Is it worth it? Almost always yes since prediction is incredibly cheap after training.
But we’ll soon see cases where we want to go even deeper — and that’s where things break.
Pets (cat vs. dog) is a relatively easy classification task.
FER2013 is much harder: 48×48 grayscale images of faces, 7 emotion categories (angry, disgust, fear, happy, sad, surprise, neutral).
The differences between emotions are semantically subtle.
The same face can look angry or disgusted based on small changes around the eyes and mouth.
A slight downturn of the lips.
A furrowing of the brow.
These are fine-grained, high-level features
Detect not just “there are eyes here”
“The eyes are narrowed in a specific way that suggests anger rather than concentration.”
VGG with a few blocks does okay. Can we do better with more depth?
Since FER2013 images are relatively small (48×48)
\[[\text{Conv}(3\times3) \to \text{BN} \to \text{ReLU}] \times 3 \to \text{MaxPool} \to \ldots\]
Adding depth improves performance — up to a point.
Training accuracy degrades
Test accuracy degrades
This is NOT overfitting.
If it were overfitting, training accuracy would be high and test accuracy would be low. That’s not what we see.
The deeper model is worse on the training set.
Deep is better
But the network can’t find that solution.
Optimization failure.
Callback to L10: gradients are a product of Jacobians through every layer.
\[\frac{\partial \mathcal{L}}{\partial \mathbf{x}} = \frac{\partial \mathcal{L}}{\partial \mathbf{h}_D} \cdot \frac{\partial \mathbf{h}_D}{\partial \mathbf{h}_{D-1}} \cdot \frac{\partial \mathbf{h}_{D-1}}{\partial \mathbf{h}_{D-2}} \cdots \frac{\partial \mathbf{h}_1}{\partial \mathbf{x}}\]
Even with BN keeping activations healthy, ill-conditioned optimization landscape.
One small value ruins the whole thing!
For FER2013, the problem is compounded:
Small spatial resolution (48×48): fewer spatial positions means less redundancy in the gradient signal. Each gradient estimate is noisier.
Subtle discriminative features: the differences between “angry” and “sad” live in tiny pixel-value variations around the eyes and mouth. Small very localized gradients in the first place!
Key idea (He et al., 2015):
Instead of asking a block to learn the desired mapping \(h(\mathbf{x})\), ask it to learn the residual — how to adjust the current representation.
\[h(\mathbf{x}) = f(\mathbf{x}) + \mathbf{x}\]
The network learns \(f(\mathbf{x}) = h(\mathbf{x}) - \mathbf{x}\): “what should I change?”
We’re not really changing what we’re doing.
The network still learns convolutions, still applies BN and ReLU, still builds hierarchical features. The representational capacity is essentially the same.
Learn corrections rather than complete transformations.
The residual block:
Input x ─────────────────────────── (+) ──→ ReLU ──→ Output
│ ↑
└──→ Conv → BN → ReLU → Conv → BN ─┘
(the residual branch f(x))
The skip connection is an identity shortcut — no learnable parameters. The block’s output is \(f(\mathbf{x}) + \mathbf{x}\).
The residual path should always start with a conv and end with a batch norm
If the residual branch started with ReLU, the output \(f(\mathbf{x})\) could only be non-negative (ReLU kills negatives).
Same deal if we end with ReLU!
A face-recognition network might need to suppress a “smiling” feature when it detects “furrowed brow” — that requires a negative residual. If we constrain residuals to be positive, we lose half the adjustment space.
\[ h = f(\mathbf{x}) + \mathbf{x} \]
The gradient through a residual block:
\[\frac{\partial h}{\partial \mathbf{x}} = \frac{\partial f(\mathbf{x})}{\partial \mathbf{x}} + \mathbf{I}\]
The identity term \(\mathbf{I}\) means gradients always have a direct path backward, regardless of what \(f\) does.
Where have you seen this idea before?
This is the “gradient highway” — information flows backward through the skip connections unimpeded by the transformations in the main path.
But the \(+ \mathbf{I}\) argument only tells us about one block. What happens when we stack many blocks?
Let’s trace the math carefully. This is where the real insight lives.
In a plain network, the forward pass is a nested composition:
\[y = f_3(f_2(f_1(\mathbf{x})))\]
The gradient is a product of Jacobians (chain rule):
\[\frac{\partial y}{\partial \mathbf{x}} = \frac{\partial f_3}{\partial f_2} \cdot \frac{\partial f_2}{\partial f_1} \cdot \frac{\partial f_1}{\partial \mathbf{x}}\]
If any single factor is small, the entire product vanishes.
Now with residual connections, each block computes:
\[h_l = f_l(h_{l-1}) + h_{l-1}\]
Let’s trace a 3-block network carefully. Write \(h_0 = \mathbf{x}\) (input).
Block 1:
\[h_1 = f_1(h_0) + h_0\]
Block 2:
\[h_2 = f_2(h_1) + h_1 = f_2(h_1) + f_1(h_0) + h_0\]
Block 3:
\[h_3 = f_3(h_2) + h_2 = f_3(h_2) + f_2(h_1) + f_1(h_0) + h_0\]
The output is no longer a nested composition — it’s the input plus a sum of residuals.
Now differentiate \(h_3\) with respect to \(\mathbf{x}\):
\[\frac{\partial h_3}{\partial \mathbf{x}} = \frac{\partial f_3}{\partial h_2}\frac{\partial h_2}{\partial \mathbf{x}} + \frac{\partial h_2}{\partial \mathbf{x}}\]
Since \(\frac{\partial h_2}{\partial \mathbf{x}} = \frac{\partial f_2}{\partial h_1}\frac{\partial h_1}{\partial \mathbf{x}} + \frac{\partial h_1}{\partial \mathbf{x}}\) and \(\frac{\partial h_1}{\partial \mathbf{x}} = \frac{\partial f_1}{\partial \mathbf{x}} + \mathbf{I}\), we can expand:
\[\frac{\partial h_3}{\partial \mathbf{x}} = \left(1 + \frac{\partial f_3}{\partial h_2}\right)\left(1 + \frac{\partial f_2}{\partial h_1}\right)\left(1 + \frac{\partial f_1}{\partial \mathbf{x}}\right)\]
Expand that product:
\[\frac{\partial h_3}{\partial \mathbf{x}} = \underbrace{1}_{\text{skip all}} + \underbrace{\frac{\partial f_1}{\partial \mathbf{x}}}_{\text{block 1 only}} + \underbrace{\frac{\partial f_2}{\partial h_1}}_{\text{block 2 only}} + \underbrace{\frac{\partial f_3}{\partial h_2}}_{\text{block 3 only}}\]
\[+ \underbrace{\frac{\partial f_2}{\partial h_1}\frac{\partial f_1}{\partial \mathbf{x}}}_{\text{blocks 1\&2}} + \underbrace{\frac{\partial f_3}{\partial h_2}\frac{\partial f_1}{\partial \mathbf{x}}}_{\text{blocks 1\&3}} + \underbrace{\frac{\partial f_3}{\partial h_2}\frac{\partial f_2}{\partial h_1}}_{\text{blocks 2\&3}}\]
\[+ \underbrace{\frac{\partial f_3}{\partial h_2}\frac{\partial f_2}{\partial h_1}\frac{\partial f_1}{\partial \mathbf{x}}}_{\text{all blocks}}\]
That’s \(2^3 = 8\) terms — one for every combination of “take the skip” or “take the residual branch” at each block.
Each of the 8 terms corresponds to a path through the network:
| Path | Blocks used | Length | Gradient term |
|---|---|---|---|
| 1 | none (all skips) | 0 | \(1\) |
| 2 | block 1 only | 1 | \(\frac{\partial f_1}{\partial \mathbf{x}}\) |
| 3 | block 2 only | 1 | \(\frac{\partial f_2}{\partial h_1}\) |
| 4 | block 3 only | 1 | \(\frac{\partial f_3}{\partial h_2}\) |
| 5 | blocks 1 & 2 | 2 | \(\frac{\partial f_2}{\partial h_1}\frac{\partial f_1}{\partial \mathbf{x}}\) |
| 6 | blocks 1 & 3 | 2 | \(\frac{\partial f_3}{\partial h_2}\frac{\partial f_1}{\partial \mathbf{x}}\) |
| 7 | blocks 2 & 3 | 2 | \(\frac{\partial f_3}{\partial h_2}\frac{\partial f_2}{\partial h_1}\) |
| 8 | all blocks | 3 | \(\frac{\partial f_3}{\partial h_2}\frac{\partial f_2}{\partial h_1}\frac{\partial f_1}{\partial \mathbf{x}}\) |
The gradient is the sum over all \(2^D\) paths that respect ordering.
| Plain Network | Residual Network | |
|---|---|---|
| Gradient structure | A single product of \(D\) Jacobians | A sum of \(2^D\) terms |
| Effect of one bad layer | Entire gradient vanishes | Only paths through that layer are affected |
| Short paths | Don’t exist — gradient must traverse all \(D\) layers | Always present — path through all skips has gradient = 1 |
| Depth | A liability for optimization | Adds paths without destroying existing ones |
The \(1\) in each factor \((1 + \frac{\partial f_l}{\partial h_{l-1}})\) is the skip connection!
Even if the residual gradients \(\frac{\partial f_l}{\partial h}\) are tiny, the short paths (including the direct path through all skips (gradient = 1)) always carry signal.
For \(D = 50\) blocks: \(2^{50} \approx 10^{15}\) paths. The chance of all of them vanishing simultaneously is essentially zero.
This is why ResNets can be 50, 100, 152 layers deep. The gradient doesn’t have to survive a single long chain — it takes all possible shortcuts.
Same funnel as VGG — spatial dims halve, channels double — but built from residual blocks instead of plain conv layers.
Global average pooling replaces the FC head (same improvement we discussed earlier).
| Model | Stages | Blocks per stage | Total layers | Parameters |
|---|---|---|---|---|
| ResNet-18 | 4 | [2, 2, 2, 2] | 18 | ~11M |
| ResNet-50 | 4 | [3, 4, 6, 3] | 50 | ~25M |
| ResNet-152 | 4 | [3, 8, 36, 3] | 152 | ~60M |
All trainable. Error keeps improving with depth — the degradation problem is solved.
The skip connection doesn’t add meaningful capacity — it enables optimization of the capacity that was already there.
The plain network had the representational power - just couldn’t find it
Skip connections make the good solutions findable.
ResNet SGD landscape is much smoother and much more easy to work with!
(Look at notebook for performance info)
(1) Gradient flow: The identity highway prevents vanishing gradients regardless of depth. Gradients always have a direct path of length zero.
(2) Implicit ensemble: A ResNet with \(D\) blocks has \(2^D\) paths of varying length. It behaves like an ensemble of shallow and deep networks. Sorta like a built in dropout in the convolutional backbone.
(3) Residual learning: If the current representation is already good, the block only needs to learn small corrections. Most blocks in a trained ResNet do learn small residuals — the skip carries most of the information. The block is an editor, not an author.
Consider detecting “a face.”
A face = two eyes above a nose above a mouth.
An eye = a circular outline with a dark center.
A circular outline = curved edges arranged in a ring.
Curved edges = specific combinations of oriented edge fragments.
A deep network mirrors this hierarchy:
Layer 1: detects edge fragments (shared across all features).
Layer 2: combines edges into curves and corners (reused for eyes, noses, mouths).
Layer 3: combines curves into parts — eyes, noses, mouths.
Layer 4: combines parts into faces.
Each layer reuses the features below it. An edge detector trained in layer 1 helps detect both eyes and mouths. The total parameter count grows polynomially with the complexity of the target function.
Recall from earlier: conv layers give us translation equivariance for free.
But conv layers are NOT equivariant to rotation, scale, or color shifts.
A rotated cat produces a different feature map
A darker photo of the same dog produces different activations
The architecture doesn’t know that these are the same animal.
Think about what this means for FER2013.
A face tilted slightly to the left and the same face straight-on express the same emotion — but the pixel patterns around the eyes and mouth shift in ways that have nothing to do with the emotional content.
A face photographed under bright studio lighting and the same face in a dim room look completely different at the pixel level. But the expression is identical.
The label and semantic meaning of the image hasn’t changed. The pixels have.
How do we get invariance to these transformations?
Apply random transformations that preserve semantics during training.
Each time the network sees a training image, randomly:
The meaning doesn’t change!
Why does FER2013 need this so badly?
~28,000 training images at 48×48 grayscale for a semantically hard task!
Without augmentation: the network memorizes and misses the nuanced variations in expression.
With augmentation: the same face appears flipped, slightly rotated, slightly cropped. The only thing consistent across all versions is the expression. The network is forced to learn what actually matters. (In theory)
Horizontal flip: The most basic augmentation. An angry face facing left and an angry face facing right express the same emotion. (Note: we don’t vertically flip — upside-down faces are rare in practice.)
Random crop + resize: Take a random sub-region and resize it back to the original dimensions. Forces the network to recognize the expression even when parts of the face are slightly cut off. Also provides slight scale variation.
Small rotation: ±15° or so. Real photos aren’t perfectly level. Faces aren’t always perfectly upright.
Color jitter: Random changes to brightness and contrast. (For grayscale FER2013, this is brightness/contrast only. For RGB datasets like our cat vs. dog data, we also jitter saturation and hue.)
The key constraint: every augmentation must preserve the label.
Horizontal flip preserves “angry” ✓
Random crop preserves “angry” (as long as the key facial features are visible) ✓
Brightness jitter preserves “angry” ✓
But:
rotating a “6” by 180° turns it into a “9” ✗
flipping a “b” horizontally turns it into a “d” ✗
The choice of augmentations is domain knowledge.
from torchvision import transforms
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomResizedCrop(48, scale=(0.85, 1.0)),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(), #Normal
transforms.Normalize(mean=[0.5], std=[0.5]), #Normal
])Critical detail: augmentation is applied only during training.
At test time, do deterministic operations (normalize, center, etc.)
We’re evaluating the model’s ability to generalize to unseen data as the world produces it.
This is a 5-line code change in the data pipeline. No changes to the model, no changes to the training loop. Massive impact.
The standard view: a CNN takes an image and outputs a class label.
This is correct but incomplete. It misses the most important thing the CNN does.
The better view: the real output of a CNN is the representation in the penultimate layer — the vector right before the final classifier.
Cat photo → Conv backbone → [0.82, -0.14, 1.37, …, 0.56] → Linear → “cat”
That 512-dimensional vector (after GAP) is a summary of the image in terms of learned visual concepts.
A 512 number representation of the image, not the class label
The conv backbone learns a nonlinear feature transform:
\[\phi: \mathbb{R}^{H \times W \times 3} \to \mathbb{R}^{d}\]
that maps raw pixels into a space where classes become linearly separable.
For VGG/ResNet with global average pooling: \(d = 512\).
This 512-dimensional vector summarizes the entire image.
Each dimension corresponds to “how much of learned concept \(k\) is present in this image.”
One dimension might encode “amount of fur texture.” Another might encode “presence of pointed ears.” Another might encode “orange coloring.” The network figured out which concepts matter — we never told it.
Here’s the deeper connection to the entire course:
All of classical ML — logistic regression, SVM, random forests — works well when given good features. The hard part was always feature engineering. Practitioners spent months hand-crafting features for each domain.
CNNs automate feature engineering. The conv backbone learns \(\phi\) — the feature transform — end-to-end from data. The classification head is just logistic regression on top.
This is why deep learning took over computer vision.
Incomparably better.
Our ResNet was trained on 7,500 cat and dog images at 224×224. It achieved ~93% test accuracy with VGG, and 96% with ResNet + augmentation.
Along the way, it learned to detect:
Edges and gradients (layer 1)
Textures (layers 2–3)
Parts (layers 3–4)
Compositions (layer 5) — “pointy ears + narrow face = cat face”, “floppy ears + wide snout = dog face”
But edges aren’t specific to cats and dogs. Textures aren’t specific to cats and dogs. Shapes aren’t specific to cats and dogs.
Here’s the payoff of thinking about CNNs as representation learners.
Take our ResNet trained on cat vs. dog (7,500 images, 224×224, binary classification with augmentation).
Can we use those features on a completely different task?
CIFAR-10: 10 classes — airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck.
Our backbone has never seen an airplane. It has never seen a frog. It was trained entirely on cats and dogs.
There’s also a practical problem: our backbone expects 224×224 inputs, but CIFAR-10 images are 32×32.
Solution: upscale the CIFAR-10 images from 32×32 to 224×224.
cifar_transfer_transform = transforms.Compose([
transforms.Resize(224), ## 32×32 → 224×224
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])Yes, we’re blowing up a tiny image to 7× its original size. The upscaled image looks blocky and blurry to us.
But the conv backbone doesn’t care about that…
Freeze the entire cat vs. dog backbone. No gradients flow through it — we treat it as a fixed feature extractor.
nn.Linear(512, 10).\[\mathbf{x}^{\text{CIFAR}} \xrightarrow{\text{resize to 224}} \mathbf{x}' \xrightarrow{\text{frozen } \phi_{\text{CatDog}}} \mathbf{z} \in \mathbb{R}^{512} \xrightarrow{\text{new Linear}} \hat{y} \in \mathbb{R}^{10}\]
The backbone extracts features. The new head learns to classify using those features. That’s it.
Think about what the cat vs. dog backbone learned — and what transfers.
Edges and gradients — every image has edges. A vertical edge in a cat photo = vertical edge in an airplane photo.
Textures and patterns — fur texture is specific to animals, rough textures appear everywhere, trucks have smooth metal, frogs have bumpy skin. Nothing unique about textures!
Shape primitives — curved edges, sharp corners, elongated regions. Cat ears and airplane tails are different shapes, but the ability to detect shapes partially transfers.
“Cat face” and “dog face” detectors — these are specific to the training task. Get rid of it and use the new features we learned for any task!
This is the key insight:
Early-layer features are universal. They’re determined by the physics of images, not by the classification task.
Middle-layer features are broadly useful. Textures, corners, and simple shapes appear across many visual domains.
Late-layer features are task-specific. “Cat face detector” doesn’t help with airplanes.
The further you go from the input, the more task-specific the features become.
This has a direct implication:
If someone trained a CNN on a massive, diverse dataset — say, 1.2 million images across 1,000 categories covering animals, vehicles, food, scenes, objects — the features would be even more universal.
That dataset exists. It’s called ImageNet.
If our cat vs. dog backbone — trained on just 15,000 images of two classes — can transfer to CIFAR-10, imagine what an ImageNet backbone can do.
If you need to classify images, the question isn’t “how do I build a classifier?”
The question is “where do I get a good representation?”
Option 1: Learn \(\phi\) from scratch on your data. Requires tens of thousands of images, a lot of compute, and a lot of patience. This is what we’ve been doing all lecture.
Option 2: Use a \(\phi\) that someone else already learned on a massive dataset. Reuse their backbone. Train only a small head on your task. This works surprisingly well!
That’s transfer learning — and it’s the entire subject of next lecture.
If someone already learned a good \(\phi\) on a massive dataset, can we reuse it?
Three adaptation modes:
Feature extraction — freeze the backbone, train only a new head. The simplest approach. (We just did this with our cat vs. dog backbone.)
Fine-tuning — unfreeze some layers, train with a small learning rate. The backbone adjusts to your domain.
LoRA — learn a small, low-rank correction to the pretrained weights. The efficient approach for large models.
Nobody trains CNNs from scratch for regression, classification, object detection, etc. in 2026! Next lecture, we’ll see why.