Registry Synced

ResNet, Skip Connections, and Deep Network Design

1043 words
5 min read

Reading compass

Now ยท ๐ŸŽฏ Learning Objectives

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)H(x) directly, learn the residual F(x)=H(x)โˆ’xF(x) = H(x) - x:
H(x)=F(x)+xH(x) = F(x) + x
If the optimal identity mapping is desired, the layers just need to learn F(x)=0F(x) = 0 (easy), rather than F(x)=xF(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

VariantLayersBlocksParam CountTop-1 Error (ImageNet)
ResNet-1818[2, 2, 2, 2]11.7M30.2%
ResNet-3434[3, 4, 6, 3]21.8M27.9%
ResNet-5050[3, 4, 6, 3] Bottleneck25.6M25.8%
ResNet-101101[3, 4, 23, 3] Bottleneck44.5M24.6%
ResNet-152152[3, 8, 36, 3] Bottleneck60.2M23.9%

1.4 Why Skip Connections Work

Gradient flow: During backpropagation, the gradient at layer l is:
โˆ‚Lโˆ‚xl=โˆ‚Lโˆ‚xL(1+โˆ‘i=lLโˆ’1โˆ‚F(xi,Wi)โˆ‚xi)\frac{\partial L}{\partial x_l} = \frac{\partial L}{\partial x_L} \left(1 + \sum_{i=l}^{L-1} \frac{\partial F(x_i, W_i)}{\partial x_i}\right)
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

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ยฒ parameters
Bottleneck: (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:
  1. Higher training error: The degradation problem means the deep network can't even fit the training data as well as a shallow one
  2. Vanishing gradients: Early layers receive negligible gradient updates
  3. 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 MB
Activations (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 activations
Total memory โ‰ˆ 47 MB (weights) + 192 MB (activations) + 94 MB (gradients) + 384 MB (optimizer states for Adam) โ‰ˆ 717 MB
This fits on most modern GPUs.
</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)
Document outline

Keep your place and jump directly to a heading.

Table of contents

No note selected

Open a chapter to see its outline, backlinks, and sources here.