CNN Architectures Explained: From LeNet to ResNet
In this series (30 parts)
- Computer Vision Roadmap: From Basics to Real Projects
- What is Computer Vision? From Pixels to Decisions
- A Short History of Computer Vision: 1545 to Now
- Math for CV Beginners: Vectors, Matrices, Convolutions
- Image Fundamentals: Color, Histograms, Noise, Filtering
- How Images and Video Are Stored: Colour, JPEG, Frames
- OpenCV Setup + First 10 Tasks in Python
- Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
- CV Project Workflow: Dataset, Baseline, Error Iteration
- Intensity Transforms and Frequency-Domain Filtering
- Edge Detection and Thresholding That Actually Work
- Morphology, Contours, and Shape Analysis for Real Images
- Feature Matching (SIFT/ORB) and Image Stitching
- Camera Calibration and Perspective Correction
- Epipolar Geometry, Stereo Vision, and Depth Estimation
- Optical Flow and Motion Tracking in Video
- HOG, HOF and MBH: Descriptors Before Deep Learning
- Your First Image Classifier (PyTorch + Transfer Learning)
- Data Pipelines and Augmentation for Vision Models
- CNN Architectures Explained: From LeNet to ResNet
- YOLO Detection Pipeline: Data to Inference
- Semantic and Instance Segmentation: U-Net to Mask R-CNN
- Vision Transformers (ViT) and When to Use Them
- CV Explainability: Grad-CAM, Failures, Bias Checks
- 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
- Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
- Video Understanding: Flow Networks, Interpolation, Stabilisation
- Real-Time CV Systems: Tracking, Latency, Streaming
- Deploying CV Models: ONNX, TensorRT, Edge, and APIs
- Responsible CV: Privacy, Fairness, Security, Governance
You have used ResNet-18 twice now without knowing what is inside it. This post opens it up — not as history, but as a sequence of specific problems and the specific fixes that solved them. Every claim here comes with the arithmetic.
Prerequisites: Your first image classifier and convolution basics.
Why convolution at all
A 224×224 colour image has 150,528 numbers. Connect that to even 1,000 hidden units with a fully connected layer:
150 million parameters for one layer, and it would still be a bad model: a cat shifted ten pixels right activates completely different weights, so the network has to learn “cat” separately at every position.
A convolution layer with 64 filters of size 3×3 on 3 input channels:
weight sharing The same small filter is applied at every position in the image, so a feature learned in one place is detected everywhere. It is what makes a conv layer's parameter count independent of image size.
84,000 times fewer parameters, and the feature is detected wherever it appears. Those two properties — weight sharing and translation equivariance — are the entire argument for convolution.
Step the filter through the grid above. The same nine weights are reused at every position; only the patch underneath changes.
The parameter formula
| # | Layer | Calculation | Parameters target | |||
|---|---|---|---|---|---|---|
| 1 | conv1 | 7 | 3 | 64 | 7×7×3×64 + 64 | 9,472 |
| 2 | 3×3 early | 3 | 64 | 64 | 3×3×64×64 + 64 | 36,928 |
| 3 | 3×3 mid | 3 | 64 | 128 | 3×3×64×128 + 128 | 73,856 |
| 4 | 3×3 deep | 3 | 256 | 512 | 3×3×256×512 + 512 | 1,180,160 |
| 5 | 1×1 reduce | 1 | 256 | 64 | 1×1×256×64 + 64 | 16,448 |
Conv layer parameters. Note how deep layers with many channels dominate the total.
Nothing in that formula mentions image size. A 3×3 conv from 64 to 128 channels costs 73,856 parameters whether the input is 8×8 or 800×800.
The output size formula
| # | Input | Padding | Stride | Calculation | Output target | |
|---|---|---|---|---|---|---|
| 1 | 224 | 3 | 1 | 1 | (224−3+2)/1 + 1 | 224 |
| 2 | 224 | 3 | 0 | 1 | (224−3+0)/1 + 1 | 222 |
| 3 | 224 | 3 | 1 | 2 | (224−3+2)/2 + 1 | 112 |
| 4 | 224 | 7 | 3 | 2 | (224−7+6)/2 + 1 | 112 |
| 5 | 56 | 1 | 0 | 2 | (56−1+0)/2 + 1 | 28 |
Output sizes. Padding K//2 with stride 1 keeps the size unchanged — that is why 3×3 pairs with padding 1.
Receptive field: how much each unit sees
receptive field The region of the original input image that influences one output unit. It grows with depth, which is how small filters eventually see whole objects.A single 3×3 conv sees a 3×3 patch. Stack another and each unit of the second layer sees a 3×3 region of the first layer, which itself spans 3×3 of the input — so it sees 5×5.
Each 3×3 layer adds 2 to the receptive field. With stride, growth is multiplicative:
| # | After layer | Kernel | Stride | Cumulative stride | Receptive field target |
|---|---|---|---|---|---|
| 1 | conv1 | 7 | 2 | 2 | 7 |
| 2 | maxpool | 3 | 2 | 4 | 11 |
| 3 | layer1 (2 blocks) | 3 | 1 | 4 | 43 |
| 4 | layer2 (2 blocks) | 3 | 2 | 8 | 99 |
| 5 | layer3 (2 blocks) | 3 | 2 | 16 | 211 |
| 6 | layer4 (2 blocks) | 3 | 2 | 32 | 435 |
ResNet-18 receptive field growth. By layer4 each unit sees more than the whole 224×224 input.
That final row explains something practical. By layer4, a unit’s receptive field exceeds the input, which is why fine-tuning only layer4 adapts whole-object reasoning while leaving edge and texture detectors alone.
Why 3×3 beat 5×5
VGG’s contribution was noticing that two 3×3 convs reach the same 5×5 receptive field as one 5×5 conv, and cost less. With channels in and out:
18 versus 25 is 28% fewer parameters, and there is a ReLU between the two 3×3 layers where the 5×5 has none — more nonlinearity for less cost.
With : one 5×5 costs 409,600 weights, two 3×3 cost 294,912.
Try the filter selector above. Edge, blur and Sobel are the kinds of low-level detectors a trained network’s first layer converges to on its own.
The architectures, and what each one fixed
| # | Model | Year | Layers | Parameters | ImageNet top-1 target | The problem it fixed |
|---|---|---|---|---|---|---|
| 1 | LeNet-5 | 1998 | 7 | 60K | — | Proved conv + pool works, on digits |
| 2 | AlexNet | 2012 | 8 | 60M | 57.1% | ReLU, dropout, GPUs — scale was possible |
| 3 | VGG-16 | 2014 | 16 | 138M | 71.6% | Depth via uniform 3×3 stacks |
| 4 | GoogLeNet | 2014 | 22 | 6.8M | 69.8% | 1×1 bottlenecks cut cost massively |
| 5 | ResNet-50 | 2015 | 50 | 25.6M | 76.1% | Skip connections made real depth trainable |
| 6 | ResNet-152 | 2015 | 152 | 60M | 78.3% | Depth kept paying off |
| 7 | EfficientNet-B0 | 2019 | 82 | 5.3M | 77.7% | Balanced scaling of depth/width/resolution |
Seven architectures. Notice GoogLeNet: 22 layers in 6.8M parameters against VGG's 16 layers in 138M.
The competition that drove all of this
Those architecture numbers came out of one benchmark, and seeing its full trajectory makes the pace of change concrete.
The ImageNet Large Scale Visual Recognition Challenge ran annually on 1.2 million training images across 1,000 classes. The headline metric was top-5 error: the fraction of images where the correct label was not among the model’s five best guesses.
Two things in that chart are worth sitting with. The 2011-to-2012 drop of 9.4 points is larger than the total improvement of the preceding two years, and it is the single event that redirected the field. And the crossing of the human line in 2015 is why the competition eventually stopped being interesting — once error is a third of a human’s, the remaining errors are mostly label ambiguity.
Top-1 accuracy tells the longer story, because it kept moving after top-5 saturated:
For the wider arc this benchmark sits inside — including what came before SIFT+FV and why the datasets themselves mattered as much as the models — see the history of computer vision.
The depth problem
By 2015 the obvious move was to stack more layers. It did not work.
| # | Plain network depth | Training error target | Test error | Diagnosis |
|---|---|---|---|---|
| 1 | 20 layers | 7.5% | 8.8% | fine |
| 2 | 32 layers | 8.9% | 10.2% | worse |
| 3 | 56 layers | 10.8% | 12.1% | much worse |
Plain deep networks on CIFAR-10. Training error rises with depth, which rules out overfitting.
Read the training error column carefully. It goes up. If this were overfitting, training error would fall while test error rose. Instead the deeper network cannot even fit the training data — an optimisation failure, not a generalisation one.
That is a mathematical absurdity on its face. A 56-layer network can represent everything a 20-layer network can: set the extra 36 layers to the identity. So the extra capacity is there and gradient descent simply cannot find it.
Why: gradients shrink multiplicatively
Backpropagation multiplies a gradient by each layer’s Jacobian on the way back. If each layer scales it by an average factor of 0.9:
| # | Depth | Gradient factor target | Effective learning rate at layer 1 |
|---|---|---|---|
| 1 | 10 layers | 0.349 | 35% of nominal |
| 2 | 20 layers | 0.122 | 12% |
| 3 | 30 layers | 0.042 | 4% |
| 4 | 50 layers | 0.005 | 0.5% |
| 5 | 100 layers | 0.000027 | essentially zero |
Multiplicative gradient decay. At 50 layers the early layers receive 0.5% of the signal.
. The first layers barely move, so they stay near their random initialisation, and everything built on top of random features is handicapped.
The residual block
He and colleagues changed one thing. Instead of a block computing directly, have it compute a correction to its input:
The visualization above shows the two paths. Now differentiate:
That is the entire fix. Even when shrinks toward zero, the gradient reaching is at least 1. Stack fifty of these and the gradient still arrives at the first layer intact, because along the skip path it is multiplied by 1 fifty times instead of by 0.9.
There is a second reading that is just as useful. If a block is not needed, makes it the identity, and driving weights toward zero is exactly what weight decay does anyway. The identity is now the easy default rather than something the network must learn.
graph TD
X["x, 64 channels"] --> C1["3×3 conv 64→64"]
C1 --> B1["BatchNorm"]
B1 --> R1["ReLU"]
R1 --> C2["3×3 conv 64→64"]
C2 --> B2["BatchNorm"]
B2 --> ADD(("+"))
X -->|"identity skip"| ADD
ADD --> R2["ReLU"]
R2 --> Y["y, 64 channels"]
class BasicBlock(nn.Module):
def __init__(self, cin, cout, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(cin, cout, 3, stride, 1, bias=False)
self.bn1 = nn.BatchNorm2d(cout)
self.conv2 = nn.Conv2d(cout, cout, 3, 1, 1, bias=False)
self.bn2 = nn.BatchNorm2d(cout)
# projection only when shape changes
self.skip = nn.Sequential()
if stride != 1 or cin != cout:
self.skip = nn.Sequential(
nn.Conv2d(cin, cout, 1, stride, bias=False),
nn.BatchNorm2d(cout))
def forward(self, x):
out = torch.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.relu(out + self.skip(x))
Note bias=False on every conv. BatchNorm immediately subtracts the mean, which cancels any bias term, so it would be dead weight.
Bottleneck blocks
For deeper networks, ResNet uses a three-layer block that is far cheaper. The trick is 1×1 convolutions that squeeze the channel count before the expensive 3×3.
A plain block at 256 channels:
The bottleneck version — reduce to 64, do the 3×3 there, expand back:
| # | Block type | Layers | Parameters at 256 ch target | Used in |
|---|---|---|---|---|
| 1 | Basic | 3×3, 3×3 | 1,179,648 | ResNet-18, ResNet-34 |
| 2 | Bottleneck | 1×1, 3×3, 1×1 | 69,632 | ResNet-50, 101, 152 |
Basic versus bottleneck. The 17x saving is why ResNet-50 has fewer parameters than VGG-16 despite being three times deeper.
Choosing a backbone
| Question | ResNet-18 | ResNet-50 | EfficientNet-B0 | MobileNetV3 |
|---|---|---|---|---|
| Parameters | 11.7M | 25.6M | 5.3M | 5.4M |
| ImageNet top-1 | 69.8% | 76.1% | 77.7% | 75.2% |
| FLOPs per image | 1.8G | 4.1G | 0.39G | 0.22G |
| CPU inference | ~35 ms | ~95 ms | ~45 ms | ~18 ms |
| Fine-tunes on 1k images | very well | well | well | well |
| Best for | Fast iteration, baselines | Accuracy on a server | Accuracy per FLOP | Phones and edge |
Compare EfficientNet-B0 and ResNet-50: nearly the same accuracy from a tenth of the FLOPs. But EfficientNet’s depthwise separable convolutions are memory-bandwidth heavy, so its real CPU latency is only about twice as fast, not ten times. FLOPs are a poor predictor of latency. Always measure on your target hardware.
- The confusion matrix shows errors spread evenly rather than concentrated in one pair
- You already have plenty of data and good augmentation
- Your smaller model underfits — training accuracy is also low
- Latency budget has clear headroom
- Errors concentrate in one class pair — collect data for that pair instead
- You are already overfitting — a bigger model overfits harder
- You have fewer than a few thousand images
- You have not yet tuned augmentation and learning rate
Practice task
Using the classifier from post 17.
- Print
sum(p.numel() for p in model.parameters())for ResNet-18, ResNet-50, and MobileNetV3-Small. - For ResNet-18’s
layer1[0].conv1, verify the parameter count against by hand. - Pass a 1×3×224×224 tensor through and print the shape after each stage. Check each against the output-size formula.
- Time 100 forward passes on CPU for each backbone. Plot latency against parameter count.
- Fine-tune all three on your dataset for the same epochs. Plot accuracy against latency.
- Take one
BasicBlockand delete the skip connection. Retrain and compare. - Build a 40-layer plain conv network with no skips and train it. Record the training accuracy.
Steps 6 and 7 are the ones that make it concrete. The 40-layer plain network will train worse than a 20-layer one on the same data, and watching that happen on your own machine turns the vanishing-gradient argument from a claim into something you have seen.
Summary
Convolution replaced 150 million fully connected weights with 1,792, because the same filter is reused at every position. Parameters are and do not depend on image size at all.
Receptive field grows with depth — 3, then 5, then 7 for stacked 3×3 layers, reaching 435 pixels by ResNet-18’s layer4, which is larger than the input. Two 3×3 convs match a 5×5’s field for instead of , a 28% saving plus an extra ReLU.
Depth then hit a wall. A 56-layer plain network had higher training error than a 20-layer one, which is an optimisation failure rather than overfitting: at , early layers received half a percent of the gradient. The residual block’s makes , and that keeps the gradient alive through any depth.
Bottleneck blocks squeeze 256 channels to 64 for the expensive 3×3, cutting a block from 1,179,648 to 69,632 parameters — 17× — which is how ResNet-50 is both deeper and smaller than VGG-16.
- Conv parameters are K²·Cin·Cout + Cout, and do not depend on image size.
- The same layer as a fully connected one would be 150 million weights instead of 1,792.
- Set bias=False on any conv followed by BatchNorm — the bias is cancelled anyway.
- Each 3×3 layer adds 2 to the receptive field; strides multiply the growth.
- Two 3×3 convs match one 5×5 for 18C² instead of 25C², plus an extra nonlinearity.
- Plain networks past ~20 layers had higher training error — an optimisation failure, not overfitting.
- A 0.9 per-layer gradient factor leaves 0.5% of the signal after 50 layers.
- y = F(x) + x gives ∂y/∂x = ∂F/∂x + 1, and that +1 is why depth became trainable.
- Bottleneck blocks use 1×1 convs to cut a 256-channel block by 17x.
- FLOPs predict latency poorly. Benchmark on your target hardware.
What comes next
Classification names one thing per image. Most real problems need to know where things are, and how many. The YOLO object detection pipeline builds on everything here — a backbone extracts features, then a detection head predicts boxes and classes at many positions at once, scored with the IoU and mAP metrics from earlier in the series.