TAI BUI
Back to curriculum
BuildPython

CNNs — LeNet to ResNet

Every major CNN since 1998 is the same conv-nonlinearity-downsample recipe with one new idea bolted on. Learn the ideas in order.

TIME: ~75 minutesPREREQUISITES: Phase 3 Lesson 11 (PyTorch), Phase 4 Lesson 01 (Image Fundamentals), Phase 4 Lesson 02 (Convolutions from Scratch)

Learning Objectives

  • Trace the architectural lineage LeNet-5 -> AlexNet -> VGG -> Inception -> ResNet and state the single new idea each family contributed
  • Implement LeNet-5, a VGG-style block, and a ResNet BasicBlock in PyTorch, each under 40 lines
  • Explain why residual connections turn a 1,000-layer network from untrainable into state-of-the-art
  • Read a modern backbone (ResNet-18, ResNet-50) and predict its output shape, receptive field, and parameter count before looking at the source

The Problem

In 2011, the best ImageNet classifier scored around 74% top-5 accuracy. In 2012 AlexNet scored 85%. In 2015 ResNet scored 96%. No new data. No new GPU generation. The gains came from architecture ideas. A working vision engineer needs to know which idea came from which paper, because every production backbone you ship in 2026 is a recombination of those same pieces. The ideas keep transferring too: grouped convs went from CNNs to transformers, residual connections went from ResNet to every LLM in existence, batch normalisation lives in diffusion models.

Studying these networks in order also immunises you against a common mistake: reaching for the biggest available model when a LeNet-sized network would solve the problem. MNIST does not need a ResNet. Knowing the scaling curve of each family tells you where to sit on it.

Warm-up0/3
1. A dense (fully connected) layer assigns a separate weight to every pixel position in an image. If the same object shifts a few pixels from where it appeared in training, what happens to the layer's ability to recognize it?Warm-up
Two small grids side by side. The first has a filled square near its top-left corner; the second has the identical square moved to a different position in the grid.
2. You send a signal through fifty identical stages in a row, and each stage on its own weakens the signal by a small amount. What would you expect at the far end?Warm-up
A chain of boxes: signal in, stage 1, stage 2, stage 3, an ellipsis, stage 50, then a box with a question mark for the result.
3. A model scores 99% on its training images but only 70% on images it has never seen. What is this usually called?Warm-up
Bar chart with two bars: training images at 99 percent and never-seen images at 70 percent.
Check: The Problem0/1
1. You must classify small images of handwritten digits on a tight compute budget. Which starting point fits best?Design
A small pixel grid with a simple illustrative shape filled in, captioned one small image, a tight compute budget.

The Concept

The four ideas that changed vision

Mermaid diagram

Nothing else in classical vision mattered as much as these four jumps.

LeNet-5 (1998)

Yann LeCun's digit recogniser. 60,000 parameters. Two conv-pool blocks, two fully connected layers, tanh activations. It defined the template every CNN inherits:

input (1, 32, 32)
  conv 5x5 -> (6, 28, 28)
  avg pool 2x2 -> (6, 14, 14)
  conv 5x5 -> (16, 10, 10)
  avg pool 2x2 -> (16, 5, 5)
  flatten -> 400
  dense -> 120
  dense -> 84
  dense -> 10

Everything the modern world calls a CNN (alternating convolutions and downsampling feeding a small classifier head) is LeNet with more layers, bigger channels, and better activations.

AlexNet (2012)

Three changes that together broke ImageNet:

  1. ReLU instead of tanh. Gradients stop vanishing. Training speeds up by a factor of six.
  2. Dropout in the fully connected head. Regularisation becomes a layer, not a trick.
  3. Depth and width. Five conv layers, three dense layers, 60M parameters, trained on two GPUs with the model split across them.

The paper's Figure 2 still shows the GPU split as two parallel streams. That parallelism was a hardware workaround, not an architectural insight — the three ideas above are still in every model you use.

VGG (2014)

VGG asked: what happens if you only use 3x3 convolutions and you go deep?

stack:   conv 3x3 -> conv 3x3 -> pool 2x2
repeat:  16 or 19 conv layers

Two 3x3 convs see the same 5x5 input area as one 5x5 conv but with fewer parameters (29C^2 = 18C^2 vs 25*C^2) and an extra ReLU in between. VGG turned this observation into an entire architecture. The simplicity, one block type repeated, made it the reference point for everything that came after.

Cost: 138M parameters, slow to train, expensive at inference.

Inception (2014, same year)

Google's answer to "what kernel size should I use?" was: all of them, in parallel.

Mermaid diagram

Each branch specialises: 1x1 for channel mixing, 3x3 for local texture, 5x5 for larger patterns, pooling for shift-invariant features. The concat then lets the next layer pick whichever branch is useful. Inception v1 used 1x1 convolutions inside each branch as a bottleneck to keep parameter counts sane.

The degradation problem

By 2015, VGG-19 worked and VGG-32 did not. Depth was supposed to help, but past ~20 layers both training and test loss got worse. That is not overfitting. That is the optimiser failing to find useful weights because gradients shrink multiplicatively through every layer.

Plain deep network:
  y = f_L( f_{L-1}( ... f_1(x) ... ) )

Gradient wrt early layer:
  dL/dW_1 = dL/dy * df_L/df_{L-1} * ... * df_2/df_1 * df_1/dW_1

Each multiplicative term has magnitude roughly (weight magnitude) * (activation gain).
Stack 100 of them with gains < 1 and the gradient is effectively zero.

VGG worked at 19 layers because batch norm (published simultaneously) kept activations well-scaled. But even batch norm could not rescue depth beyond 30-ish layers.

ResNet (2015)

He, Zhang, Ren, Sun proposed one change that fixed everything:

standard block:   y = F(x)
residual block:   y = F(x) + x

The + x means the layer can always choose to do nothing by driving F(x) to zero. A 1,000-layer ResNet is now at most as bad as a 1-layer network, because every extra block has a trivial escape hatch. With that guarantee, the optimiser is willing to make every block slightly useful — and slightly useful, stacked 100 times, is state-of-the-art.

Mermaid diagram

Two variants of the block show up everywhere:

  • BasicBlock (ResNet-18, ResNet-34): two 3x3 convs, skip around both.
  • Bottleneck (ResNet-50, -101, -152): 1x1 down, 3x3 middle, 1x1 up, skip around the trio. Cheaper when channel counts are high.

When the skip has to cross a downsample (stride=2), the identity path is replaced with a 1x1 stride=2 conv to match shapes.

Why residuals matter beyond vision

The idea reaches well past image classification: it turned deep networks from "cross-your-fingers and hope gradients survive" into a reliable, scalable engineering tool. Every transformer you will read about next phase carries the exact same skip connection in every block. Without ResNet, there is no GPT.

Check: The Concept0/17
1. What is the degradation problem in deep CNNs?Define
A chain of conv boxes with no skip connections, a note about adding more conv layers to the same stack, and a dashed box with a question mark for the error after training.
2. What is a residual connection?Define
A vertical diagram: a box x leads into a box labelled block, whose output meets a round node marked with a question mark; a second line carries x around the block to that same node, which then leads to a box labelled y.
3. What is a BasicBlock?Define
The lesson's TinyResNet drawn as an input, four groups of two blocks each, and the scores, with a line from one group down to a dashed box asking what is inside one BasicBlock.
4. What is a Bottleneck block?Define
A box for a 256-channel input, a dashed box with a question mark for what is inside a Bottleneck block, and a box for a 256-channel output, with a note giving the weight count of a plain 3x3 conv at that width.
5. LeNet-5 is the template every CNN inherits. How many parameters does it have, and what building blocks does it use?Define
A box for LeNet-5's 1 by 32 by 32 input, a dashed box with a question mark for its parameter count and building blocks, and a box for its 10 digit-class output.
6. Which AlexNet (2012) change is credited with stopping gradients from vanishing and speeding up training roughly sixfold?Explain
A dashed box with a question mark standing for one AlexNet change, with arrows to two result boxes: gradients stop vanishing, and training about six times faster.
7. Besides replacing tanh with ReLU, what two other changes did AlexNet contribute?Explain
A box naming AlexNet's first change, replacing tanh with ReLU, above two dashed boxes with question marks for its second and third changes, and a note giving the ImageNet top-5 jump from 74 percent in 2011 to 85 percent in 2012.
8. You train a from-scratch AlexNet-style network but swap ReLU back out for tanh, keeping everything else the same. What would you expect to change first?Explain
Two boxes: an AlexNet-style network with five conv and three dense layers trained with ReLU, and an arrow labelled swap to the same network now trained with tanh instead.
9. Why did VGG prefer stacks of 3x3 convolutions over a single larger kernel?Explain
The lesson's VGG stage listing, two 3x3 convs then a 2x2 pool repeated over 16 or 19 conv layers, next to a box for the single 5x5 alternative, with a dashed box asking why VGG preferred the stack.
10. What was Inception's answer to "what kernel size should I use?"Explain
An input box, a dashed box with a question mark for the kernel to use in one conv stage, and an output box, with a note listing the candidates the lesson names: 1x1, 3x3, 5x5 and pooling.
11. Why do gradients become effectively zero in a very deep plain network?Explain
A chain of five boxes labelled layer 1, layer 2, layer 3, an ellipsis, and layer 100, with a line noting that the gradient flows backward through all 100 layers and asking what its size is by the time it reaches layer 1.
12. ResNet introduced residual connections as y = F(x) + x. What problem does this solve?Explain
Three stacked boxes, each reading y equals block of x plus x, joined by arrows, with an ellipsis below suggesting the stack continues much further.
13. Why is a 1,000-layer ResNet at most as bad as a 1-layer network?Explain
A chain of four boxes labelled block 1, block 2, an ellipsis and block 1000, with a question asking why adding a thousand of these blocks is never worse than adding none.
14. Why is a Bottleneck block cheaper than a stack of two 3x3 convs when channel counts are high?Explain
Two boxes: a stack of two 3x3 convs at 256 channels with its weight count, and a dashed box with a question mark for what is inside a Bottleneck block.
15. For C = 64 channels, how many fewer weights does a stack of two 3x3 convs use than one 5x5 conv, ignoring biases (18C^2 versus 25C^2)?Apply
Two boxes giving the weight-count formulas 18 times C squared and 25 times C squared for two stacked 3x3 convs versus one 5x5 conv, with the difference at C = 64 left as a question mark.
16. A 34-layer plain CNN has higher training loss than an 18-layer plain CNN of the same design, and a teammate calls it overfitting. What is the right diagnosis?Analyze
Bar chart with two bars for training loss only: an 18-layer plain network and a 34-layer plain network of the same design, with the 34-layer bar taller.
17. An engineer adds batch norm to a 50-layer plain stack and training loss still gets worse with depth. What reading fits the lesson?Analyze
A training-loss curve rising with depth up to 50 layers for a plain stack that has batch norm applied throughout.

Build It

Step 1: LeNet-5

A minimal, faithful LeNet. Tanh activations, average pooling. The only concession to modernity is that we use nn.CrossEntropyLoss downstream instead of the original Gaussian connections.

python
import torch
import torch.nn as nn
import torch.nn.functional as F
 
class LeNet5(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.pool = nn.AvgPool2d(2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, num_classes)
 
    def forward(self, x):
        x = self.pool(torch.tanh(self.conv1(x)))
        x = self.pool(torch.tanh(self.conv2(x)))
        x = torch.flatten(x, 1)
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)
 
net = LeNet5()
x = torch.randn(1, 1, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Expected output: output: torch.Size([1, 10]), params: 61,706. That is the entire digit classifier that started modern vision.

Step 2: A VGG block

One reusable block: two 3x3 convs, ReLU, batch norm, max pool.

python
class VGGBlock(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_c)
        self.pool = nn.MaxPool2d(2)
 
    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return self.pool(x)
 
class MiniVGG(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stack = nn.Sequential(
            VGGBlock(3, 32),
            VGGBlock(32, 64),
            VGGBlock(64, 128),
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(128, num_classes),
        )
 
    def forward(self, x):
        return self.head(self.stack(x))
 
net = MiniVGG()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Three VGG blocks on CIFAR-sized input, an adaptive pool, one linear layer. ~290k parameters. Plenty for CIFAR-10.

Step 3: A ResNet BasicBlock

The core building block of ResNet-18 and ResNet-34.

python
class BasicBlock(nn.Module):
    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_c)
        if stride != 1 or in_c != out_c:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_c, out_c, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_c),
            )
        else:
            self.shortcut = nn.Identity()
 
    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = out + self.shortcut(x)
        return F.relu(out)

bias=False on conv layers is a batch-norm convention: BN's beta parameter already handles the bias, so carrying conv bias too would be redundant. The shortcut only needs a real conv when stride or channel count changes; otherwise it is a no-op identity.

Step 4: A tiny ResNet

Stack four groups of BasicBlocks to get a working ResNet for CIFAR-sized inputs.

python
class TinyResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
        )
        self.layer1 = self._make_group(32, 32, num_blocks=2, stride=1)
        self.layer2 = self._make_group(32, 64, num_blocks=2, stride=2)
        self.layer3 = self._make_group(64, 128, num_blocks=2, stride=2)
        self.layer4 = self._make_group(128, 256, num_blocks=2, stride=2)
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(256, num_classes),
        )
 
    def _make_group(self, in_c, out_c, num_blocks, stride):
        blocks = [BasicBlock(in_c, out_c, stride=stride)]
        for _ in range(num_blocks - 1):
            blocks.append(BasicBlock(out_c, out_c, stride=1))
        return nn.Sequential(*blocks)
 
    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        return self.head(x)
 
net = TinyResNet()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Four groups of two blocks each. Stride 2 at the start of groups 2, 3, 4. Channel count doubles at every downsample. Roughly 2.8M parameters. That is the standard recipe that scales cleanly up to ResNet-152.

Step 5: Compare parameter-to-feature efficiency

Run the same input through all three networks and compare parameter counts.

python
def summary(name, net, x):
    y = net(x)
    params = sum(p.numel() for p in net.parameters())
    print(f"{name:12s}  input {tuple(x.shape)} -> output {tuple(y.shape)}  params {params:>10,}")
 
x = torch.randn(1, 3, 32, 32)
summary("LeNet5",     LeNet5(),       torch.randn(1, 1, 32, 32))
summary("MiniVGG",    MiniVGG(),      x)
summary("TinyResNet", TinyResNet(),   x)

Three models, three eras, three orders of magnitude in parameter count. For CIFAR-10 accuracy, you need roughly: LeNet 60%, MiniVGG 89%, TinyResNet 93% after a few epochs of training.

Check: Build It0/11
1. Why do the convs in the BasicBlock code use bias=False?Explain
A Conv2d with bias set to False, followed by a BatchNorm2d on the same number of channels, with a question asking why the conv has no bias.
2. For x of shape (1, 1, 32, 32), what is the shape of self.pool(torch.tanh(self.conv1(x))) in the LeNet5 code (conv1 is a 5x5 conv from 1 to 6 channels, pool is AvgPool2d(2))?Apply
The lesson's conv1 and pool definitions plus the line pool(tanh(conv1(x))), with the input shape 1,1,32,32 given and the resulting shape left as a question mark.
3. How many parameters does LeNet5's conv1 = nn.Conv2d(1, 6, kernel_size=5) have, counting its bias?Apply
One line of code defining conv1 as a Conv2d from 1 input channel to 6 output channels with a 5 by 5 kernel, with its parameter count, including bias, left as a question mark.
4. A (1, 3, 32, 32) input goes through MiniVGG's stack of three VGGBlocks (padding=1 on the 3x3 convs, MaxPool2d(2) at the end of each). What shape enters the head?Apply
The lesson's MiniVGG stack of three VGGBlocks, each a padded 3x3 conv pair followed by a 2x2 max pool, with the input shape 1,3,32,32 given and the resulting shape into the head left as a question mark.
5. What is self.shortcut in BasicBlock(128, 128) with the default stride=1?Apply
The lesson's if/else that builds the shortcut, with the specific case BasicBlock(128, 128) at the default stride of 1 left as a question mark.
6. In a ResNet BasicBlock with in_channels=64, out_channels=128, stride=2, what is the shortcut branch?Apply
The same if/else that builds the shortcut, with the specific case BasicBlock with in_channels 64, out_channels 128 and stride 2 left as a question mark.
7. A (1, 3, 32, 32) input goes through TinyResNet's stem and four groups (stride 1, 2, 2, 2). What shape reaches the head?Apply
A table of TinyResNet's four groups with their stride and channel change, layer1 through layer4, with the resulting spatial size left as a question mark.
8. You add a BasicBlock(32, 64) with stride=1 so channels grow but resolution does not. What shortcut does it need?Apply
The same if/else that builds the shortcut, with the specific case BasicBlock with in_channels 32, out_channels 64 and stride 1 left as a question mark.
9. A hand-written block uses stride=2 in conv1 but defines no shortcut conv, and the addition line raises a shape-mismatch error. Where is the real bug?Analyze
A hand-written block with conv1 at stride 2, conv2 at stride 1, and the shortcut set to Identity, with the addition line marked as the one that raises a shape-mismatch error.
10. A hand-written residual block runs without any error, the loss drops for a few steps, but the trained network behaves strangely. Which cause fits?Analyze
A note that no error is raised, a small loss curve dropping over the first few steps, and a line stating that the trained network behaves strangely afterward.
11. You build a ResNet for 32x32 CIFAR images. Which stem fits?Design
A 32 by 32 CIFAR image box with an arrow to an unlabelled box asking what stem fits this input.

Use It

torchvision.models gives you pretrained versions of all of the above. The call signature is identical across families, which is exactly the point of the backbone abstraction.

python
from torchvision.models import resnet18, ResNet18_Weights, vgg16, VGG16_Weights
 
r18 = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
r18.eval()
 
print(f"ResNet-18 params: {sum(p.numel() for p in r18.parameters()):,}")
print(r18.layer1[0])
print()
 
v16 = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
v16.eval()
print(f"VGG-16   params: {sum(p.numel() for p in v16.parameters()):,}")

ResNet-18 has 11.7M parameters. VGG-16 has 138M. Similar ImageNet top-1 accuracy (69.8% vs 71.6%). Residual connections buy you a 12x parameter efficiency win. That is why ResNet variants dominated from 2016 until ViT arrived in 2021 — and still dominate real-world deployments where compute is the constraint.

You can predict r18's internal shapes before ever printing them, using the same (H - K + 2P) / S + 1 formula from Convolutions from Scratch. The stem is a 7x7 conv, stride 2, padding 3, followed by a 3x3 max-pool, stride 2, padding 1. On a 224x224 input: (224 - 7 + 6) / 2 + 1 = 112 out of the conv, then (112 - 3 + 2) / 2 + 1 = 56 out of the pool. layer1 runs at 56x56 before layer2, layer3, and layer4 each halve it again on the way to a 7x7 map before the final average pool. Run print(r18.layer1[0]) above and check the shapes against this by hand before you trust the printout.

For transfer learning, the recipe is always the same: load pretrained, freeze the backbone, replace the classifier head.

python
for p in r18.parameters():
    p.requires_grad = False
r18.fc = nn.Linear(r18.fc.in_features, 10)

Three lines. You now have a 10-class CIFAR classifier that inherits the representations ImageNet paid for.

Check: Use It0/9
1. In a CNN classifier, what is the backbone?Define
A horizontal pipeline of the lesson's MiniVGG: input with 3 channels, three VGGBlocks widening 3 to 32 to 64 to 128 channels, an average pool with flatten, a Linear layer from 128 to 10, and the class scores.
2. What is the stem of a ResNet?Define
A horizontal pipeline of ResNet-18's eight stages: conv 7x7, max-pool, layer1, layer2, layer3, layer4, avgpool and fc, with no stage singled out.
3. What is the head of a CNN classifier?Define
A horizontal pipeline of the lesson's TinyResNet: a conv, four layer groups, then pool, flatten and linear, with no stage singled out.
4. What is transfer learning in the lesson's recipe?Define
A box labelled pretrained backbone, trained on ImageNet, an arrow to an unlabelled box with a question mark, then an arrow to a box labelled your task, with your own classes.
5. ResNet-18 has 11.7M parameters and VGG-16 has 138M, at similar ImageNet top-1 accuracy (69.8% vs 71.6%). What does the lesson credit for ResNet's roughly 12x parameter efficiency?Explain
Bar chart of parameter counts: ResNet-18 at 11.7 million and VGG-16 at 138 million, with a note that their top-1 accuracy is 69.8 percent versus 71.6 percent.
6. After loading a pretrained ResNet-18 for a 10-class task, why is the classifier head replaced while the backbone is kept?Explain
Two boxes side by side: a pretrained head that outputs 1000 ImageNet classes, and your task, which needs 10 classes.
7. Using the (H - K + 2P) / S + 1 formula on ResNet-18's stem (7x7 conv, stride 2, padding 3, then a 3x3 max-pool, stride 2, padding 1), what spatial size does a 224x224 input have when it enters layer1?Apply
The output-size formula plus a table of the stem's two stages, a 7x7 stride-2 conv and a 3x3 stride-2 max-pool with their padding, and a 224 by 224 input, with the spatial size entering layer1 left as a question mark.
8. Compute is the main constraint on a deployment, and VGG-16 and ResNet-18 give similar ImageNet top-1 accuracy. Which is the better pick and why?Analyze
A table comparing ResNet-18 and VGG-16 on parameter count and top-1 accuracy, with a note that the deployment is compute-constrained.
9. You have a small labeled set with 10 classes and want ImageNet-quality features. Which setup follows the lesson's recipe?Design
A pretrained ResNet-18 box and an arrow to a box describing a 10-class task that wants ImageNet-quality features, with what happens to the backbone and head left as a question mark.

Ship It

This lesson produces:

  • outputs/prompt-backbone-selector.md, a prompt that picks the right CNN family (LeNet/VGG/ResNet/MobileNet/ConvNeXt) given task, dataset size, and compute budget.
  • outputs/skill-residual-block-reviewer.md, a skill that reads a PyTorch module and flags skip-connection mistakes (missing shortcut on stride change, shortcut activation order, BN placement relative to addition).
Author's Note

The bug I see most often when someone writes their own ResNet block by hand is forgetting the shortcut convolution when stride or channel count changes. Most of the time that fails loudly: the addition raises a shape-mismatch error, and the traceback points at the arithmetic line, not at the missing shortcut definition back in __init__, so it takes a minute to trace down. The quieter failure shows up when you get the shapes to line up by accident, say a stride you added to the wrong conv. Then there's no error at all: the block runs, the loss drops for a few steps, and you've trained a network that isn't the one you meant to build. If a residual block you wrote yourself refuses to run, or trains something that behaves strangely, check the shortcut branch before you check anything else.

The other habit worth building here: don't try to memorize every parameter count in this lesson. Memorize the shape of the argument at each step instead. Tanh saturates, so swap it for ReLU. Plain depth degrades past ~20 layers, so add a skip connection. Learn to recognize that shape, and the next unfamiliar architecture in a paper stops being a wall of new notation and starts being a variation you can place on this same timeline before you've finished reading the abstract.

Exercises

  1. (Easy) Count parameters by hand for TinyResNet layer by layer. Compare against sum(p.numel() for p in net.parameters()). Where does the majority of the parameter budget go: convs, BN, or the classifier head?
  2. (Medium) Implement the Bottleneck block (1x1 -> 3x3 -> 1x1 with skip) and use it to build a ResNet-50-style network for CIFAR. Compare params against TinyResNet.
  3. (Hard) Remove the skip connection from BasicBlock, train a 34-block "plain" network and a 34-block ResNet on CIFAR-10 for 10 epochs each. Plot training loss vs epoch for both. Reproduce the He et al. Figure 1 result where the plain deep network converges to higher loss than its shallower twin.

Key Terms

TermWhat people sayWhat it actually means
Backbone"The model"The stack of convolutional blocks that produces the feature map fed to the task head
Residual connection"Skip connection"y = F(x) + x; lets the optimiser learn identity by setting F to zero, which makes arbitrary depth trainable
BasicBlock"Two 3x3 convs with a skip"The ResNet-18/34 building block: conv-BN-ReLU-conv-BN-add-ReLU
Bottleneck"1x1 down, 3x3, 1x1 up"The ResNet-50/101/152 block; cheap at high channel counts because the 3x3 runs on a reduced width
Degradation problem"Deeper is worse"Past ~20 plain conv layers, both training and test error increase; solved by residual connections, not by more data
Stem"The first layer"The initial conv that converts 3-channel input into the base feature width; usually 7x7 stride 2 for ImageNet, 3x3 stride 1 for CIFAR
Head"The classifier"The layers after the final backbone block: adaptive pool, flatten, linear(s)
Transfer learning"Pretrained weights"Loading a backbone trained on ImageNet and fine-tuning only the head on your task

Further Reading