ResNet, Skip Connections, and Deep Network Design
1043 words
5 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# ResNet, Skip Connections, and Deep Network Design ## ๐ฏ Learning Objectives - Understand the vanishing gradient problem in deep networks - Explain how skip connections enable training of very deep networks - Implement a residual block with bottleneck design - Compare ResNet variants (18, 34, 50, 101, 152) ## ๐ Pr...

ResNet, Skip Connections, and Deep Network Design
๐ฏ Learning Objectives
- Understand the vanishing gradient problem in deep networks
- Explain how skip connections enable training of very deep networks
- Implement a residual block with bottleneck design
- Compare ResNet variants (18, 34, 50, 101, 152)
๐ Prerequisites
- CNN fundamentals
- Backpropagation and gradient flow
1. ๐ Core Content
1.1 The Degradation Problem
Intuitively, deeper networks should perform better. But in practice, very deep plain networks have higher training error than their shallower counterparts โ even with proper normalization and careful initialization.
This is the degradation problem: adding layers makes optimization harder, not just due to overfitting or vanishing gradients.
1.2 Residual Learning
Key insight: Instead of learning the desired mapping H(x) directly, learn the residual F(x)=H(x)โx:
If the optimal identity mapping is desired, the layers just need to learn F(x)=0 (easy), rather than F(x)=x (hard).
python# runnable import torch.nn as nn class BasicBlock(nn.Module): """Basic residual block for ResNet-18/34""" expansion = 1 def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) # Skip connection (1ร1 conv if dimensions differ) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), nn.BatchNorm2d(out_channels) ) def forward(self, x): residual = self.shortcut(x) # Skip connection out = nn.ReLU()(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += residual # Element-wise addition out = nn.ReLU()(out) return out class Bottleneck(nn.Module): """Bottleneck block for ResNet-50/101/152 (1ร1 โ 3ร3 โ 1ร1)""" expansion = 4 def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.conv3 = nn.Conv2d(out_channels, out_channels * 4, 1, bias=False) self.bn3 = nn.BatchNorm2d(out_channels * 4) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels * 4: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels * 4, 1, stride, bias=False), nn.BatchNorm2d(out_channels * 4) ) def forward(self, x): residual = self.shortcut(x) out = nn.ReLU()(self.bn1(self.conv1(x))) out = nn.ReLU()(self.bn2(self.conv2(out))) out = self.bn3(self.conv3(out)) out += residual out = nn.ReLU()(out) return out
1.3 ResNet Architectures
| Variant | Layers | Blocks | Param Count | Top-1 Error (ImageNet) |
|---|---|---|---|---|
| ResNet-18 | 18 | [2, 2, 2, 2] | 11.7M | 30.2% |
| ResNet-34 | 34 | [3, 4, 6, 3] | 21.8M | 27.9% |
| ResNet-50 | 50 | [3, 4, 6, 3] Bottleneck | 25.6M | 25.8% |
| ResNet-101 | 101 | [3, 4, 23, 3] Bottleneck | 44.5M | 24.6% |
| ResNet-152 | 152 | [3, 8, 36, 3] Bottleneck | 60.2M | 23.9% |
1.4 Why Skip Connections Work
Gradient flow: During backpropagation, the gradient at layer l is:
The "1" term ensures gradients can flow directly from the loss to early layers without passing through any weight layers. This prevents vanishing gradients even in 100+ layer networks.
Ensemble interpretation: ResNets can be viewed as an ensemble of many shallow networks (paths from input to output can skip any subset of residual blocks).
๐ Practice Questions
</details> * * * ## ๐ Cross-References - **Next**: [Object Detection](/notes/04-degree-electives-bsda5006-dl-cv-week04-04-object-detection) - **Video**: BSDA5006 Week 4-5 transcripts [Join Discord](https://discord.gg/gE2m4Qrdqv) [Previous**CNN Fundamentals**](/notes/04-degree-electives-bsda5006-dl-cv-week01-01-cnn-fundamentals)[Next**Transfer Learning**](/notes/04-degree-electives-bsda5006-dl-cv-week03-03-transfer-learning)Q1<strong>Q1</strong>: Why does the bottleneck block (1ร1 โ 3ร3 โ 1ร1) reduce computation compared to two 3ร3 layers?For input/output channels = C, bottleneck expansion = 4:Two 3ร3 layers: 2 ร (3ร3รCรC) = 18Cยฒ parametersBottleneck: (1ร1รCรC/4) + (3ร3รC/4รC/4) + (1ร1รC/4รC) = Cยฒ/4 + 9Cยฒ/16 + Cยฒ/4 = 4Cยฒ/16 + 9Cยฒ/16 + 4Cยฒ/16 = 17Cยฒ/16 โ 1.06CยฒThe bottleneck uses ~6% of the parameters of two 3ร3 layers while maintaining representational capacity.The 1ร1 layers reduce dimensionality before the expensive 3ร3 convolution, then restore it. Q2<strong>Q2<strong>Q2</strong>: In a ResNet-50, the first 1ร1 layer in a bottleneck reduces channels from 256 to 64. Why reduce channels?Reducing channels before the 3ร3 conv saves computation:
- Without reduction: 3ร3 conv on 256 channels โ 3ร3ร256ร256 = 589,824 operations
- With reduction: 1ร1 reduces 256โ64, then 3ร3 on 64 channels โ 3ร3ร64ร64 = 36,864 operations
- Total with reduction: 1ร1ร(256ร64) + 3ร3ร(64ร64) + 1ร1ร(64ร256) = 16,384 + 36,864 + 16,384 = 69,632
- Without: 589,824
The bottleneck performs ~8ร fewer operations. The receptive field is preserved because the 3ร3 conv operates on reduced but spatially meaningful features. Q3<strong>Q3<strong>Q3<strong>Q3<strong>Q3</strong>: What happens if you remove skip connections from a 152-layer ResNet and train from scratch?Without skip connections, the 152-layer network would suffer from:
- Higher training error: The degradation problem means the deep network can't even fit the training data as well as a shallow one
- Vanishing gradients: Early layers receive negligible gradient updates
- Optimization difficulty: The loss landscape becomes much harder to navigate
Empirically, a plain 152-layer CNN would achieve worse training accuracy than ResNet-34, even though it has more parameters and capacity. This is the degradation problem that ResNet was designed to solve.With skip connections, ResNet-152 uses them to create a highway for gradients and learned residual functions. Q4<strong>Q4<strong>Q4<strong>Q4<strong>Q4<strong>Q4</strong>: ResNet-18 has 11.7M parameters. If each parameter is FP32 (4 bytes), what's the memory for weights? What about activations for a 224ร224 input with batch_size=32?Weights: 11.7M ร 4 bytes = 46.8 MBActivations (approximate โ depends on feature map sizes at each layer):
- After conv1: 112ร112ร64 = 0.8M per image
- After layer1: 56ร56ร64 ร 2 blocks โ 0.4M
- After layer2: 28ร28ร128 ร 2 blocks โ 0.2M
- After layer3: 14ร14ร256 ร 2 blocks โ 0.1M
- After layer4: 7ร7ร512 ร 2 blocks โ 0.05M
- Total activations: ~1.5M per image ร batch_size=32 = 48M
In FP32: 48M ร 4 bytes = 192 MB for activationsTotal memory โ 47 MB (weights) + 192 MB (activations) + 94 MB (gradients) + 384 MB (optimizer states for Adam) โ 717 MBThis fits on most modern GPUs.